From 993ecdae45ed1c5d23af6399cded22ef9fbb393a Mon Sep 17 00:00:00 2001 From: BuildTools Date: Mon, 20 Apr 2026 19:37:29 +0200 Subject: [PATCH] Task 6 feat: implement login module --- .../controllers/v1/auth/AuthController.java | 14 ++++++ .../handlers/GlobalExceptionHandler.java | 7 +++ .../users/UserAppService.java | 14 +++++- .../users/UsersService.java | 3 ++ .../wrapper/UserRepositoryWrapper.java | 3 ++ .../v1/auth/AuthControllerE2ETest.java | 44 ++++++++++++++++ .../v1/auth/AuthSpecificationTest.java | 30 +++++++++++ .../UserAlreadyExistsExceptionTest.java | 27 ++++++++++ .../PasswordEncoderBasicServiceTest.java | 24 ++++++--- .../users/UserAppServiceTest.java | 50 +++++++++++++++++++ 10 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 src/test/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsExceptionTest.java diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java index 5c4b17b..c29f426 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthController.java @@ -35,4 +35,18 @@ public ResponseEntity register(@RequestBody UserRequest userReque final String token = this.usersService.registerUser(userRequest.getUsername(), userRequest.getPassword()); return ResponseEntity.ok(new TokenResponse(token)); } + + @PostMapping("/login") + @Operation( + summary = "Login user", + description = "Authenticates a user and returns a JWT token if credentials are valid." + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Successfully authenticated. Returns JWT token."), + }) + public ResponseEntity login(@RequestBody UserRequest userRequest) { + final String token = this.usersService.loginUser(userRequest.getUsername(), userRequest.getPassword()); + return ResponseEntity.ok(new TokenResponse(token)); + } + } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java index eb387bd..e870c85 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/handlers/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -29,4 +30,10 @@ public ResponseEntity handleUserAlreadyExistsException(UserAlreadyEx .status(HttpStatus.CONFLICT) .body(new HandlerDTO(exception.getMessage())); } + + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ResponseEntity handleBadCredentials(BadCredentialsException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); + } } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java index 6bbfbb8..02f57e5 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UserAppService.java @@ -1,6 +1,8 @@ package pl.milosnicyit.codewarehousebackend.users; +import lombok.NonNull; import org.commons.login.Password; +import org.springframework.security.authentication.BadCredentialsException; import pl.milosnicyit.codewarehousebackend.exeptions.UserAlreadyExistsException; import pl.milosnicyit.codewarehousebackend.jwt.JWTService; import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; @@ -19,7 +21,7 @@ public UserAppService(UserRepositoryWrapper userRepositoryWrapper, PasswordEncod this.jwtService = jwtService; } - public String registerUser(String username, String rawPassword) { + public String registerUser(@NonNull final String username, @NonNull final String rawPassword) { if (userRepositoryWrapper.existsByUsername(username)) { throw new UserAlreadyExistsException(username); } @@ -34,4 +36,14 @@ public String registerUser(String username, String rawPassword) { } return null; } + + public String loginUser(@NonNull final String username, @NonNull final String rawPassword) { + final Password password = new Password(rawPassword); + final UserDTO userDTO = this.userRepositoryWrapper.findByUsername(username); + if (!this.passwordEncoderService.matches(password, userDTO.getPassword())) { + throw new BadCredentialsException("Bad credentials"); + } + + return this.jwtService.generateToken(userDTO.getUsername()); + } } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java index 82fab57..e5b1bf9 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/UsersService.java @@ -1,5 +1,8 @@ package pl.milosnicyit.codewarehousebackend.users; +import lombok.NonNull; + public interface UsersService { String registerUser(String username, String rawPassword); + String loginUser(@NonNull final String username, @NonNull final String rawPassword); } diff --git a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java index bc80d7a..34b1f82 100644 --- a/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java +++ b/src/main/java/pl/milosnicyit/codewarehousebackend/users/database/wrapper/UserRepositoryWrapper.java @@ -1,12 +1,15 @@ package pl.milosnicyit.codewarehousebackend.users.database.wrapper; +import jakarta.validation.constraints.NotNull; import lombok.NonNull; public interface UserRepositoryWrapper { boolean save(@NonNull final UserDTO user); + @NotNull UserDTO findByUsername(@NonNull final String username); + @NotNull UserDTO findById(@NonNull final Long id); boolean existsByUsername(@NonNull final String username); diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java index e4a1414..b7e7c14 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthControllerE2ETest.java @@ -20,6 +20,7 @@ @ActiveProfiles("test") class AuthControllerE2ETest { private static final String REGISTER_ENDPOINT = PATH + "auth/register"; + private static final String LOGIN_ENDPOINT = PATH + "auth/login"; @Autowired private MockMvc mockMvc; @@ -56,6 +57,49 @@ void shouldReturnEmptyTokenWhenUserAlreadyExistsEndToEnd() throws Exception { .andExpect(jsonPath("$.error").isNotEmpty()); } + @Test + void shouldLoginSuccessfullyEndToEnd() throws Exception { + String username = "login_e2e_user"; + String password = "securePassword123"; + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(username, password))) + .andExpect(status().isOk()); + + mockMvc.perform(post(LOGIN_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(username, password))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").isNotEmpty()); + } + + @Test + void shouldFailLoginWhenPasswordIsIncorrectEndToEnd() throws Exception { + String username = "wrong_pass_user"; + String password = "correctPassword"; + + mockMvc.perform(post(REGISTER_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(username, password))) + .andExpect(status().isOk()); + + mockMvc.perform(post(LOGIN_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(username, "securePassword123"))) + .andExpect(status().isUnauthorized()); + } + + @Test + void shouldFailLoginWhenUserDoesNotExistEndToEnd() throws Exception { + String unknownUser = "i_dont_exist_in_db"; + + mockMvc.perform(post(LOGIN_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson(unknownUser, "securePassword123"))) + .andExpect(status().isNotFound()); + } + private String getUserRequestJson(String username, String password) throws JsonProcessingException { UserRequest userRequest = new UserRequest(); userRequest.setUsername(username); diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java index ab84fa3..1fa554d 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/controllers/v1/auth/AuthSpecificationTest.java @@ -22,6 +22,7 @@ @AutoConfigureMockMvc(addFilters = false) class AuthSpecificationTest { private static final String REGISTER_ENDPOINT = PATH + "auth/register"; + private static final String LOGIN_ENDPOINT = PATH + "auth/login"; @Autowired private MockMvc mockMvc; @@ -59,6 +60,35 @@ void shouldReturnBadRequestWhenRegistrationFails() throws Exception { .andExpect(jsonPath("$.token").isEmpty()); } + @Test + void shouldLoginUserAndReturnOkWithToken() throws Exception { + String username = "testUser"; + String password = "testPassword"; + String generatedToken = "mocked.jwt.token"; + + when(usersService.loginUser(username, password)).thenReturn(generatedToken); + + mockMvc.perform(post(LOGIN_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").value(generatedToken)); + } + + @Test + void shouldReturnUnauthorizedWhenLoginFails() throws Exception { + String username = "testUser"; + String password = "testPassword"; + + when(usersService.loginUser(username, password)) + .thenThrow(new org.springframework.security.authentication.BadCredentialsException("Bad credentials")); + + mockMvc.perform(post(LOGIN_ENDPOINT) + .contentType(MediaType.APPLICATION_JSON) + .content(getUserRequestJson())) + .andExpect(status().isUnauthorized()); + } + private String getUserRequestJson() throws JsonProcessingException { UserRequest userRequest = new UserRequest(); userRequest.setUsername("testUser"); diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsExceptionTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsExceptionTest.java new file mode 100644 index 0000000..77ec16f --- /dev/null +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/exeptions/UserAlreadyExistsExceptionTest.java @@ -0,0 +1,27 @@ +package pl.milosnicyit.codewarehousebackend.exeptions; + +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; + +class UserAlreadyExistsExceptionTest { + + @Test + void shouldDisplayCorrectMessageWithUsername() { + String username = "testowyUser"; + + assertThatThrownBy(() -> { + throw new UserAlreadyExistsException(username); + }) + .isInstanceOf(UserAlreadyExistsException.class) + .hasMessage("Username testowyUser already exists"); + } + + @Test + void shouldThrowNullPointerExceptionWhenUsernameIsNull() { + assertThatThrownBy(() -> { + new UserAlreadyExistsException(null); + }) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("username is marked non-null but is null"); + } +} \ No newline at end of file diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java index 2d6211c..aa70208 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/password/PasswordEncoderBasicServiceTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import static org.junit.jupiter.api.Assertions.*; @@ -26,7 +27,6 @@ void setUp() { @Test void shouldEncodePassword() { - // given String rawPasswordString = "mojeSuperHaslo123"; String encodedPassword = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; @@ -35,17 +35,29 @@ void shouldEncodePassword() { when(passwordEncoder.encode(rawPasswordString)).thenReturn(encodedPassword); - // when String result = passwordEncoderService.encode(mockPassword); - // then assertEquals(encodedPassword, result); verify(passwordEncoder, times(1)).encode(rawPasswordString); } + @Test + void shouldPasswordAreEqual() { + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + PasswordEncoderService passwordEncoderService = new PasswordEncoderBasicService(passwordEncoder); + + String rawPasswordString = "mojeSuperHaslo123"; + Password password = new Password(rawPasswordString); + Password wrongPassword = new Password(rawPasswordString+"0"); + + String encodedPassword = passwordEncoderService.encode(password); + + assertTrue(passwordEncoderService.matches(password, encodedPassword)); + assertFalse(passwordEncoderService.matches(wrongPassword, encodedPassword)); + } + @Test void shouldReturnTrueWhenPasswordsMatch() { - // given String rawPasswordString = "mojeSuperHaslo123"; String encodedPasswordFromDb = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; @@ -56,14 +68,12 @@ void shouldReturnTrueWhenPasswordsMatch() { boolean isMatch = passwordEncoderService.matches(mockPassword, encodedPasswordFromDb); - // then assertTrue(isMatch); verify(passwordEncoder, times(1)).matches(rawPasswordString, encodedPasswordFromDb); } @Test void shouldReturnFalseWhenPasswordsDoNotMatch() { - // given String rawPasswordString = "zleHaslo"; String encodedPasswordFromDb = "$2a$10$wypVjTq...ZaszyfrowaneHaslo"; @@ -72,10 +82,8 @@ void shouldReturnFalseWhenPasswordsDoNotMatch() { when(passwordEncoder.matches(rawPasswordString, encodedPasswordFromDb)).thenReturn(false); - // when boolean isMatch = passwordEncoderService.matches(mockPassword, encodedPasswordFromDb); - // then assertFalse(isMatch); verify(passwordEncoder, times(1)).matches(rawPasswordString, encodedPasswordFromDb); } diff --git a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java index da0eb83..f16979c 100644 --- a/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java +++ b/src/test/java/pl/milosnicyit/codewarehousebackend/users/UserAppServiceTest.java @@ -7,6 +7,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.BadCredentialsException; import pl.milosnicyit.codewarehousebackend.exeptions.UserAlreadyExistsException; import pl.milosnicyit.codewarehousebackend.jwt.JWTService; import pl.milosnicyit.codewarehousebackend.password.PasswordEncoderService; @@ -88,4 +89,53 @@ void shouldReturnNullWhenDatabaseSaveFails() { verify(jwtService, never()).generateToken(anyString()); } + + @Test + void shouldLoginUserAndReturnTokenWhenCredentialsAreCorrect() { + String username = "existingUser"; + String rawPassword = "correctPassword123"; + String encodedPasswordInDb = "encodedHash"; + String expectedToken = "valid.jwt.token"; + + UserDTO userDTO = new UserDTO(); + userDTO.setUsername(username); + userDTO.setPassword(encodedPasswordInDb); + + when(userRepositoryWrapper.findByUsername(username)).thenReturn(userDTO); + when(passwordEncoderService.matches(any(Password.class), eq(encodedPasswordInDb))).thenReturn(true); + when(jwtService.generateToken(username)).thenReturn(expectedToken); + + String result = userAppService.loginUser(username, rawPassword); + + assertEquals(expectedToken, result); + verify(passwordEncoderService).matches(any(Password.class), eq(encodedPasswordInDb)); + verify(jwtService).generateToken(username); + } + + @Test + void shouldThrowExceptionWhenPasswordDoesNotMatch() { + String username = "existingUser"; + String rawPassword = "wrongPasswosssssssssssssssssssssrd"; + String encodedPasswordInDb = "encssssssssssssodedHash"; + + UserDTO userDTO = new UserDTO(); + userDTO.setUsername(username); + userDTO.setPassword(encodedPasswordInDb); + + when(userRepositoryWrapper.findByUsername(username)).thenReturn(userDTO); + when(passwordEncoderService.matches(any(Password.class), eq(encodedPasswordInDb))).thenReturn(false); + + assertThrows( + BadCredentialsException.class, + () -> userAppService.loginUser(username, rawPassword) + ); + + verify(jwtService, never()).generateToken(anyString()); + } + + @Test + void shouldThrowNullPointerExceptionWhenLoginArgumentsAreNull() { + assertThrows(NullPointerException.class, () -> userAppService.loginUser(null, "pass")); + assertThrows(NullPointerException.class, () -> userAppService.loginUser("user", null)); + } } \ No newline at end of file