diff --git a/api/pom.xml b/api/pom.xml index c16846b8..b5801582 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -43,6 +43,11 @@ postgresql runtime + + org.springframework.boot + spring-boot-starter-validation + 3.2.2 + org.projectlombok lombok diff --git a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java index 888d946d..704149b9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/AuthService.java @@ -21,9 +21,9 @@ @Service @RequiredArgsConstructor public class AuthService { - private final JwtUtils jwtUtils; private final AuthenticationManager authenticationManager; private final UserService userService; + private final JwtUtils jwtUtils; /** * Creates a session for a user. Throws an 401 unauthorized exception otherwise * @param loginRequest the request containing the login info diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java index d50c5f83..6e3c4792 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/JwtResponseDto.java @@ -1,15 +1,15 @@ package lab.en2b.quizapi.auth.dtos; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.List; @AllArgsConstructor @NoArgsConstructor @Getter +@Builder +@EqualsAndHashCode public class JwtResponseDto { private String token; @JsonProperty("refresh_token") diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java index 5e7a0ec5..15a639e9 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/LoginDto.java @@ -1,5 +1,7 @@ package lab.en2b.quizapi.auth.dtos; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,7 +12,10 @@ @Data public class LoginDto { @NonNull + @NotBlank + @Email private String email; @NonNull + @NotBlank private String password; } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java index f327ae8a..da978fb4 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenDto.java @@ -1,6 +1,7 @@ package lab.en2b.quizapi.auth.dtos; import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,7 +11,8 @@ @NoArgsConstructor @Data public class RefreshTokenDto { - @NonNull @JsonProperty("refresh_token") + @NonNull + @NotEmpty private String refreshToken; } diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java index e87000b7..1035ad5d 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RefreshTokenResponseDto.java @@ -1,15 +1,13 @@ package lab.en2b.quizapi.auth.dtos; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Getter @Setter @AllArgsConstructor @NoArgsConstructor +@EqualsAndHashCode public class RefreshTokenResponseDto { private String token; diff --git a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java index 389b8583..94e474ef 100644 --- a/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java +++ b/api/src/main/java/lab/en2b/quizapi/auth/dtos/RegisterDto.java @@ -1,5 +1,7 @@ package lab.en2b.quizapi.auth.dtos; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -9,10 +11,14 @@ @NoArgsConstructor @Data public class RegisterDto { + @NotBlank @NonNull + @Email private String email; @NonNull + @NotBlank private String username; @NonNull + @NotBlank private String password; } diff --git a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java index d6ed64d9..59535985 100644 --- a/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java +++ b/api/src/main/java/lab/en2b/quizapi/commons/user/UserService.java @@ -23,7 +23,7 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final RoleRepository roleRepository; @Value("${REFRESH_TOKEN_DURATION_MS}") - private Long REFRESH_TOKEN_DURATION_MS; + private long REFRESH_TOKEN_DURATION_MS; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return UserDetailsImpl.build(userRepository.findByEmail(email).orElseThrow()); diff --git a/api/src/main/java/lab/en2b/quizapi/commons/utils/TestUtils.java b/api/src/main/java/lab/en2b/quizapi/commons/utils/TestUtils.java new file mode 100644 index 00000000..db67613e --- /dev/null +++ b/api/src/main/java/lab/en2b/quizapi/commons/utils/TestUtils.java @@ -0,0 +1,13 @@ +package lab.en2b.quizapi.commons.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestUtils { + public static String asJsonString(final Object obj) { + try { + return new ObjectMapper().writeValueAsString(obj); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/api/src/test/java/lab/en2b/quizapi/auth/AuthControllerTest.java b/api/src/test/java/lab/en2b/quizapi/auth/AuthControllerTest.java new file mode 100644 index 00000000..aec0af96 --- /dev/null +++ b/api/src/test/java/lab/en2b/quizapi/auth/AuthControllerTest.java @@ -0,0 +1,137 @@ +package lab.en2b.quizapi.auth; + +import lab.en2b.quizapi.auth.config.SecurityConfig; +import lab.en2b.quizapi.auth.dtos.LoginDto; +import lab.en2b.quizapi.auth.dtos.RefreshTokenDto; +import lab.en2b.quizapi.auth.dtos.RegisterDto; +import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.user.UserService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.ResponseEntity; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import static lab.en2b.quizapi.commons.utils.TestUtils.asJsonString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(AuthController.class) +@AutoConfigureMockMvc +@Import(SecurityConfig.class) +public class AuthControllerTest { + @Autowired + MockMvc mockMvc; + @MockBean + AuthService authService; + @MockBean + JwtUtils jwtUtils; + @MockBean + UserService userService; + @Test + void registerUserShouldReturn200() throws Exception { + when(authService.register(any())).thenReturn(ResponseEntity.ok().build()); + testRegister(asJsonString( new RegisterDto("test@email.com","test","testing")) + ,status().isOk()); + } + @Test + void registerEmptyBodyShouldReturn400() throws Exception { + testRegister("{}",status().isBadRequest()); + } + @Test + void registerEmptyEmailShouldReturn400() throws Exception { + testRegister(asJsonString( new RegisterDto("","test","testing")), + status().isBadRequest()); + } + + @Test + void registerInvalidEmailShouldReturn400() throws Exception { + testRegister(asJsonString( new RegisterDto("iAmAnInvalidEmail","test","testing")), + status().isBadRequest()); + } + + @Test + void registerEmptyUsernameShouldReturn400() throws Exception { + testRegister(asJsonString( new RegisterDto("test@email.com","","testing")), + status().isBadRequest()); + } + + @Test + void registerEmptyPasswordShouldReturn400() throws Exception { + testRegister(asJsonString( new RegisterDto("test@email.com","test","")), + status().isBadRequest()); + } + + @Test + void loginUserShouldReturn200() throws Exception { + when(authService.login(any())).thenReturn(ResponseEntity.ok().build()); + testLogin(asJsonString( new LoginDto("test@email.com","password")) + ,status().isOk()); + } + + @Test + void loginEmptyBodyShouldReturn400() throws Exception { + testLogin("{}",status().isBadRequest()); + } + @Test + void loginEmptyEmailShouldReturn400() throws Exception { + testLogin(asJsonString( new LoginDto("","password")), + status().isBadRequest()); + } + + @Test + void loginInvalidEmailShouldReturn400() throws Exception { + testLogin(asJsonString( new LoginDto("iAmAnInvalidEmail","password")), + status().isBadRequest()); + } + + @Test + void refreshTokenShouldReturn200() throws Exception { + when(authService.refreshToken(any())).thenReturn(ResponseEntity.ok().build()); + testRefreshToken(asJsonString( new RefreshTokenDto("58ca95e9-c4ef-45fd-93cf-55c040aaff9c")) + ,status().isOk()); + } + + @Test + void refreshTokenEmptyBodyShouldReturn400() throws Exception { + when(authService.refreshToken(any())).thenReturn(ResponseEntity.ok().build()); + testRefreshToken("{}",status().isBadRequest()); + } + + @Test + void refreshTokenEmptyTokenShouldReturn400() throws Exception { + when(authService.refreshToken(any())).thenReturn(ResponseEntity.ok().build()); + testRefreshToken(asJsonString( new RefreshTokenDto("")), status().isBadRequest()); + } + + private void testRegister(String content, ResultMatcher code) throws Exception { + mockMvc.perform(post("/auth/register") + .content(content) + .contentType("application/json") + .with(csrf())) + .andExpect(code); + } + + private void testLogin(String content, ResultMatcher code) throws Exception { + mockMvc.perform(post("/auth/login") + .content(content) + .contentType("application/json") + .with(csrf())) + .andExpect(code); + } + + private void testRefreshToken(String content, ResultMatcher code) throws Exception { + mockMvc.perform(post("/auth/refresh-token") + .content(content) + .contentType("application/json") + .with(csrf())) + .andExpect(code); + } +} diff --git a/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java b/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java new file mode 100644 index 00000000..37af25a6 --- /dev/null +++ b/api/src/test/java/lab/en2b/quizapi/auth/AuthServiceTest.java @@ -0,0 +1,111 @@ +package lab.en2b.quizapi.auth; + +import ch.qos.logback.core.util.TimeUtil; +import lab.en2b.quizapi.auth.config.UserDetailsImpl; +import lab.en2b.quizapi.auth.dtos.*; +import lab.en2b.quizapi.auth.jwt.JwtUtils; +import lab.en2b.quizapi.commons.user.User; +import lab.en2b.quizapi.commons.user.UserRepository; +import lab.en2b.quizapi.commons.user.UserService; +import lab.en2b.quizapi.commons.user.role.Role; +import lab.en2b.quizapi.commons.user.role.RoleRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import javax.swing.text.html.Option; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith({MockitoExtension.class, SpringExtension.class}) +public class AuthServiceTest { + @InjectMocks + AuthService authService; + @Mock + UserService userService; + @Mock + UserRepository userRepository; + @Mock + RoleRepository roleRepository; + @Mock + AuthenticationManager authenticationManager; + @Mock + JwtUtils jwtUtils; + User defaultUser; + @BeforeEach + void setUp() { + this.userService = new UserService(userRepository,roleRepository); + this.authService = new AuthService(authenticationManager,userService,jwtUtils); + this.defaultUser = User.builder() + .id(1L) + .email("test@email.com") + .username("test") + .roles(Set.of(new Role("user"))) + .password("password") + .refreshToken("token") + .refreshExpiration(Instant.ofEpochSecond(TimeUtil.computeStartOfNextSecond(System.currentTimeMillis()+ 1000))) + .build(); + } + @Test + void testLogin(){ + Authentication authentication = mock(Authentication.class); + + when(authenticationManager.authenticate(any())).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(UserDetailsImpl.build(defaultUser)); + when(jwtUtils.generateJwtTokenUserPassword(authentication)).thenReturn("jwtToken"); + when(userRepository.findById(any())).thenReturn(Optional.of(defaultUser)); + + ResponseEntity actual = authService.login(new LoginDto("test","password")); + + assertEquals(ResponseEntity.of(Optional.of( + JwtResponseDto.builder() + .userId(1L) + .username(defaultUser.getUsername()) + .email(defaultUser.getEmail()) + .refreshToken(defaultUser.getRefreshToken()) + .token("jwtToken") + .roles(List.of("user")) + .build())) + ,actual); + + } + @Test + void testRegister(){ + + when(userRepository.existsByEmail(any())).thenReturn(false); + when(userRepository.existsByUsername(any())).thenReturn(false); + when(userRepository.save(any())).thenAnswer(i -> i.getArguments()[0]); + when(roleRepository.findByName(any())).thenReturn(Optional.of(new Role("user"))); + + ResponseEntity actual = authService.register(new RegisterDto("test","username","password")); + + assertEquals(ResponseEntity.of(Optional.of("User registered successfully!")),actual); + + } + + @Test + void testRefreshToken(){ + + when(userRepository.findByRefreshToken(any())).thenReturn(Optional.of(defaultUser)); + when(jwtUtils.generateTokenFromEmail(any())).thenReturn("jwtToken"); + + ResponseEntity actual = authService.refreshToken(new RefreshTokenDto("token")); + + assertEquals(ResponseEntity.of(Optional.of(new RefreshTokenResponseDto("jwtToken","token"))),actual); + + } +}