Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,18 @@ public ResponseEntity<TokenResponse> 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<TokenResponse> login(@RequestBody UserRequest userRequest) {
final String token = this.usersService.loginUser(userRequest.getUsername(), userRequest.getPassword());
return ResponseEntity.ok(new TokenResponse(token));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -29,4 +30,10 @@ public ResponseEntity<HandlerDTO> handleUserAlreadyExistsException(UserAlreadyEx
.status(HttpStatus.CONFLICT)
.body(new HandlerDTO(exception.getMessage()));
}

@ExceptionHandler(BadCredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<String> handleBadCredentials(BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -26,7 +27,6 @@ void setUp() {

@Test
void shouldEncodePassword() {
// given
String rawPasswordString = "mojeSuperHaslo123";
String encodedPassword = "$2a$10$wypVjTq...ZaszyfrowaneHaslo";

Expand All @@ -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";

Expand All @@ -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";

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}
Loading