diff --git a/pom.xml b/pom.xml index 0186e46..982dae7 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,17 @@ org.springframework.boot spring-boot-starter-security + + com.h2database + h2 + test + + + it.ozimov + embedded-redis + 0.7.3 + test + com.mysql mysql-connector-j diff --git a/src/main/java/dev/felipemlozx/api_auth/controller/AuthController.java b/src/main/java/dev/felipemlozx/api_auth/controller/AuthController.java index 01ed1e7..a49cb9b 100644 --- a/src/main/java/dev/felipemlozx/api_auth/controller/AuthController.java +++ b/src/main/java/dev/felipemlozx/api_auth/controller/AuthController.java @@ -3,6 +3,10 @@ import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; import dev.felipemlozx.api_auth.controller.dto.LoginDTO; import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO; +import dev.felipemlozx.api_auth.core.AuthError; +import dev.felipemlozx.api_auth.core.LoginFailure; +import dev.felipemlozx.api_auth.core.LoginResult; +import dev.felipemlozx.api_auth.core.LoginSuccess; import dev.felipemlozx.api_auth.services.AuthService; import dev.felipemlozx.api_auth.utils.ApiResponse; import jakarta.mail.MessagingException; @@ -24,32 +28,48 @@ public AuthController(AuthService authService) { @PostMapping("/register") public ResponseEntity>> register(@RequestBody CreateUserDto body) throws MessagingException { - var fails = authService.register(body); - if (fails.isEmpty()){ + List response = authService.register(body); + if (response.isEmpty()){ return ResponseEntity .status(HttpStatus.CREATED) .body(ApiResponse - .success("User registered.", List.of("Verification email sent."))); + .success("User created. Verify your email.", null)); } - return ResponseEntity.badRequest().body(ApiResponse.error(fails)); + return ResponseEntity.badRequest() + .body(ApiResponse.error("Validation errors", response)); } @PostMapping("/login") public ResponseEntity> login(@RequestBody LoginDTO body){ - ResponseLoginDTO response = authService.login(body); - if(response != null ){ - ApiResponse apiResponse = ApiResponse.success("User logged in successfully", response); - return ResponseEntity.ok().body(apiResponse); - } else { - ApiResponse apiResponse = ApiResponse.error("User or password is Incorrect", null); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiResponse); + LoginResult result = authService.login(body); + + if(result instanceof LoginSuccess(var accessToken, var refreshToken)) { + ResponseLoginDTO response = new ResponseLoginDTO(accessToken, refreshToken); + return ResponseEntity.ok().body(ApiResponse.success(response)); + } + + LoginFailure loginFailure = (LoginFailure) result; + + if(loginFailure.error().equals(AuthError.EMAIL_NOT_VERIFIED)){ + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error("Email not verified", null)); } + if(loginFailure.error().equals(AuthError.INVALID_CREDENTIALS)){ + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error("User or password is incorrect", null)); + } + + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error("User not register.", null)); } - @GetMapping("/verify-email/{id}") - public ResponseEntity> verifyEmail(@PathVariable(name = "id") String token){ + @GetMapping("/verify-email/{token}") + public ResponseEntity> verifyEmail(@PathVariable String token){ boolean isValid = authService.verifyEmailToken(token); - if(!isValid) return ResponseEntity.badRequest().body(ApiResponse.error("Link invalid.")); - return ResponseEntity.ok().build(); + if (isValid) { + return ResponseEntity.ok(ApiResponse.success("Email verified", null)); + } + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error("Invalid or expired token", null)); } } diff --git a/src/main/java/dev/felipemlozx/api_auth/controller/dto/UserResponse.java b/src/main/java/dev/felipemlozx/api_auth/controller/dto/UserResponse.java deleted file mode 100644 index ce7061e..0000000 --- a/src/main/java/dev/felipemlozx/api_auth/controller/dto/UserResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.felipemlozx.api_auth.controller.dto; - -import dev.felipemlozx.api_auth.entity.User; - -public record UserResponse(Long id, - String name, - String email) { - - public static UserResponse fromUser(User user){ - return new UserResponse( - user.getId(), - user.getName(), - user.getEmail() - ); - } -} diff --git a/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckFailure.java b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckFailure.java new file mode 100644 index 0000000..16590e0 --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckFailure.java @@ -0,0 +1,3 @@ +package dev.felipemlozx.api_auth.core; + +public record AuthCheckFailure(AuthError error) implements AuthCheckResult { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckResult.java b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckResult.java new file mode 100644 index 0000000..822381a --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckResult.java @@ -0,0 +1,3 @@ +package dev.felipemlozx.api_auth.core; + +public sealed interface AuthCheckResult permits AuthCheckSuccess, AuthCheckFailure { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckSuccess.java b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckSuccess.java new file mode 100644 index 0000000..83974b0 --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/AuthCheckSuccess.java @@ -0,0 +1,5 @@ +package dev.felipemlozx.api_auth.core; + +import dev.felipemlozx.api_auth.entity.User; + +public record AuthCheckSuccess(User user) implements AuthCheckResult { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/AuthError.java b/src/main/java/dev/felipemlozx/api_auth/core/AuthError.java new file mode 100644 index 0000000..63fccb8 --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/AuthError.java @@ -0,0 +1,8 @@ +package dev.felipemlozx.api_auth.core; + +public enum AuthError { + USER_NOT_FOUND, + EMAIL_NOT_VERIFIED, + INVALID_CREDENTIALS, + USER_NOT_REGISTER +} \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/LoginFailure.java b/src/main/java/dev/felipemlozx/api_auth/core/LoginFailure.java new file mode 100644 index 0000000..6e05d73 --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/LoginFailure.java @@ -0,0 +1,3 @@ +package dev.felipemlozx.api_auth.core; + +public record LoginFailure(AuthError error) implements LoginResult { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/LoginResult.java b/src/main/java/dev/felipemlozx/api_auth/core/LoginResult.java new file mode 100644 index 0000000..052e720 --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/LoginResult.java @@ -0,0 +1,3 @@ +package dev.felipemlozx.api_auth.core; + +public sealed interface LoginResult permits LoginSuccess, LoginFailure { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/core/LoginSuccess.java b/src/main/java/dev/felipemlozx/api_auth/core/LoginSuccess.java new file mode 100644 index 0000000..c2f99cf --- /dev/null +++ b/src/main/java/dev/felipemlozx/api_auth/core/LoginSuccess.java @@ -0,0 +1,3 @@ +package dev.felipemlozx.api_auth.core; + +public record LoginSuccess(String accessToken, String refreshToken) implements LoginResult { } \ No newline at end of file diff --git a/src/main/java/dev/felipemlozx/api_auth/repository/UserRepository.java b/src/main/java/dev/felipemlozx/api_auth/repository/UserRepository.java index cbae92e..22f3ba0 100644 --- a/src/main/java/dev/felipemlozx/api_auth/repository/UserRepository.java +++ b/src/main/java/dev/felipemlozx/api_auth/repository/UserRepository.java @@ -11,4 +11,5 @@ public interface UserRepository extends JpaRepository { List findByVerifiedIsFalse(); + boolean existsByEmail(String email); } diff --git a/src/main/java/dev/felipemlozx/api_auth/services/AuthService.java b/src/main/java/dev/felipemlozx/api_auth/services/AuthService.java index d49c053..761712c 100644 --- a/src/main/java/dev/felipemlozx/api_auth/services/AuthService.java +++ b/src/main/java/dev/felipemlozx/api_auth/services/AuthService.java @@ -2,7 +2,12 @@ import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; import dev.felipemlozx.api_auth.controller.dto.LoginDTO; -import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO; +import dev.felipemlozx.api_auth.core.AuthCheckFailure; +import dev.felipemlozx.api_auth.core.AuthCheckResult; +import dev.felipemlozx.api_auth.core.AuthCheckSuccess; +import dev.felipemlozx.api_auth.core.LoginFailure; +import dev.felipemlozx.api_auth.core.LoginResult; +import dev.felipemlozx.api_auth.core.LoginSuccess; import dev.felipemlozx.api_auth.infra.security.TokenService; import jakarta.mail.MessagingException; import org.springframework.beans.factory.annotation.Value; @@ -30,7 +35,9 @@ public List register(CreateUserDto body) throws MessagingException { List result = userService.register(body); if(result.isEmpty()){ String token = userService.createEmailVerificationToken(body.email()); - emailService.sendEmail(body.email(), body.name(), generateLinkToVerifyEmail(token)); + if(token != null ) { + emailService.sendEmail(body.email(), body.name(), generateLinkToVerifyEmail(token)); + } } return result; } @@ -39,14 +46,19 @@ protected String generateLinkToVerifyEmail(String token){ return this.apiUrl + "/verify-email/" + token; } - public ResponseLoginDTO login(LoginDTO request) { - boolean authorization = userService.login(request); - if(!authorization) return null; + public LoginResult login(LoginDTO request) { + AuthCheckResult checkResult = userService.login(request); + if(checkResult instanceof AuthCheckFailure(var error)) { + return new LoginFailure(error); + } + + var success = (AuthCheckSuccess) checkResult; + var user = success.user(); - String accessToken = tokenService.generateToken(request.email()); - String refreshToken = tokenService.generateToken(request.email()); + String accessToken = tokenService.generateToken(user.getEmail()); + String refreshToken = tokenService.generateToken(user.getEmail()); - return new ResponseLoginDTO(accessToken, refreshToken); + return new LoginSuccess(accessToken, refreshToken); } public boolean verifyEmailToken(String token) { diff --git a/src/main/java/dev/felipemlozx/api_auth/services/UserService.java b/src/main/java/dev/felipemlozx/api_auth/services/UserService.java index 63c233a..ed482fc 100644 --- a/src/main/java/dev/felipemlozx/api_auth/services/UserService.java +++ b/src/main/java/dev/felipemlozx/api_auth/services/UserService.java @@ -2,6 +2,10 @@ import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; import dev.felipemlozx.api_auth.controller.dto.LoginDTO; +import dev.felipemlozx.api_auth.core.AuthCheckFailure; +import dev.felipemlozx.api_auth.core.AuthCheckResult; +import dev.felipemlozx.api_auth.core.AuthCheckSuccess; +import dev.felipemlozx.api_auth.core.AuthError; import dev.felipemlozx.api_auth.entity.User; import dev.felipemlozx.api_auth.repository.UserRepository; import dev.felipemlozx.api_auth.utils.CheckUtils; @@ -14,6 +18,7 @@ import java.time.Instant; import java.util.List; +import java.util.Optional; import java.util.UUID; @Service @@ -29,28 +34,13 @@ public UserService(UserRepository userRepository, PasswordEncoder encoder, Cach this.cacheManager = cacheManager; } - public void saveToken(String token, String email) { - Cache cache = cacheManager.getCache("EmailVerificationTokens"); - if (cache != null) { - cache.put(token, email); - } - } - - public String recuperarToken(String token) { - Cache cache = cacheManager.getCache("EmailVerificationTokens"); - if (cache != null) { - Cache.ValueWrapper wrapper = cache.get(token); - if (wrapper != null) { - return (String) wrapper.get(); - } - } - return null; - } - @Transactional - public List register(CreateUserDto userDto){ + public List register(CreateUserDto userDto) { List errors = CheckUtils.validatePasswordAndEmail(userDto.password(), userDto.email()); + boolean userExist = userRepository.existsByEmail(userDto.email()); + if(userExist) errors.add("Email already exists"); + if (errors.isEmpty()) { User user = new User(); user.setName(userDto.name()); @@ -61,34 +51,45 @@ public List register(CreateUserDto userDto){ return errors; } - public Boolean login(LoginDTO userLogin) { - User user = userRepository.findByEmail(userLogin.email()) - .orElseThrow(() -> new RuntimeException("User not found.")); + public AuthCheckResult login(LoginDTO userLogin) { + Optional maybeUser = userRepository.findByEmail(userLogin.email()); + + if(maybeUser.isEmpty()) return new AuthCheckFailure(AuthError.USER_NOT_REGISTER); + User user = maybeUser.get(); + + if(!user.isVerified()) return new AuthCheckFailure(AuthError.EMAIL_NOT_VERIFIED); - if(!user.isVerified()){ - throw new RuntimeException("Email not verify"); + boolean matches = passwordEncoder.matches(userLogin.password(), user.getPassword()); + + if (!matches) { + return new AuthCheckFailure(AuthError.INVALID_CREDENTIALS); } - return passwordEncoder.matches(userLogin.password(), user.getPassword()); + return new AuthCheckSuccess(user); } public String createEmailVerificationToken(String email) { - userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("User not found.")); + Optional maybeUser = userRepository.findByEmail(email); + if(maybeUser.isEmpty()) return null; + UUID token = UUID.randomUUID(); saveToken(token.toString(), email); return token.toString(); } public Boolean verifyEmailToken(String token) { - String email = recuperarToken(token); - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("Link invalid.")); + String email = recoverToken(token); + if(email == null) return false; + + Optional maybeUser = userRepository.findByEmail(email); + if(maybeUser.isEmpty()) return false; + User user = maybeUser.get(); boolean isValid = Instant.now().isBefore(user.getTimeVerify()); if(!isValid){ return false; } + user.setVerified(true); userRepository.save(user); return true; @@ -98,11 +99,10 @@ public Boolean verifyEmailToken(String token) { @Scheduled(fixedRate = 1800000) public void deleteUserNotVerify() { List userList = userRepository.findByVerifiedIsFalse(); + Instant now = Instant.now(); for(User user : userList){ - boolean isValid = Instant.now().isBefore(user.getTimeVerify()); - if(!isValid){ - userRepository.delete(user); - } + boolean isValid = now.isBefore(user.getTimeVerify()); + if(!isValid) userRepository.delete(user); } } @@ -110,4 +110,16 @@ public User findById(Long id){ return userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found.")); } + + public void saveToken(String token, String email) { + Cache cache = cacheManager.getCache("EmailVerificationTokens"); + if (cache != null) cache.put(token, email); + } + + public String recoverToken(String token) { + Cache cache = cacheManager.getCache("EmailVerificationTokens"); + if (cache == null) return null; + Cache.ValueWrapper wrapper = cache.get(token); + return wrapper != null ? (String) wrapper.get() : null; + } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index db0da58..dfd9db4 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -44,4 +44,4 @@ api: secret: key: secrete -API_URL: http://localhost:8080/api/v1 +API_URL: http://localhost:4200/veryfi-email/ diff --git a/src/test/java/dev/felipemlozx/api_auth/ApiAuthApplicationTests.java b/src/test/java/dev/felipemlozx/api_auth/ApiAuthApplicationTests.java index 553fbdf..df8da66 100644 --- a/src/test/java/dev/felipemlozx/api_auth/ApiAuthApplicationTests.java +++ b/src/test/java/dev/felipemlozx/api_auth/ApiAuthApplicationTests.java @@ -1,13 +1,38 @@ package dev.felipemlozx.api_auth; +import dev.felipemlozx.api_auth.config.EmbeddedRedisConfig; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; 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.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + + +import java.io.IOException; @SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) class ApiAuthApplicationTests { - // @Test - // void contextLoads() { - // } + @Autowired + private MockMvc mockMvc; + + @BeforeAll + static void up() throws IOException { + EmbeddedRedisConfig.startRedis(); + } + + @AfterAll + static void down() { + EmbeddedRedisConfig.stopRedis(); + } + + @Test + void contextLoads() { + } } diff --git a/src/test/java/dev/felipemlozx/api_auth/config/EmbeddedRedisConfig.java b/src/test/java/dev/felipemlozx/api_auth/config/EmbeddedRedisConfig.java new file mode 100644 index 0000000..4afeb44 --- /dev/null +++ b/src/test/java/dev/felipemlozx/api_auth/config/EmbeddedRedisConfig.java @@ -0,0 +1,30 @@ +package dev.felipemlozx.api_auth.config; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.test.util.TestSocketUtils; +import redis.embedded.RedisServer; + +import java.io.IOException; + +@TestConfiguration +public class EmbeddedRedisConfig { + private static RedisServer redisServer; + + @PostConstruct + public static void startRedis() throws IOException { + int port = 6379; + redisServer = new RedisServer(port); + redisServer.start(); + System.out.printf("Redis server up! port: " + port); + } + + @PreDestroy + public static void stopRedis(){ + if(redisServer != null){ + redisServer.stop(); + System.out.println("Redis server stoped!"); + } + } +} diff --git a/src/test/java/dev/felipemlozx/api_auth/controller/AuthControllerTest.java b/src/test/java/dev/felipemlozx/api_auth/controller/AuthControllerTest.java index 0498ef0..539d3f6 100644 --- a/src/test/java/dev/felipemlozx/api_auth/controller/AuthControllerTest.java +++ b/src/test/java/dev/felipemlozx/api_auth/controller/AuthControllerTest.java @@ -1,6 +1,12 @@ package dev.felipemlozx.api_auth.controller; import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; +import dev.felipemlozx.api_auth.controller.dto.LoginDTO; +import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO; +import dev.felipemlozx.api_auth.core.AuthError; +import dev.felipemlozx.api_auth.core.LoginFailure; +import dev.felipemlozx.api_auth.core.LoginResult; +import dev.felipemlozx.api_auth.core.LoginSuccess; import dev.felipemlozx.api_auth.services.AuthService; import dev.felipemlozx.api_auth.utils.ApiResponse; import jakarta.mail.MessagingException; @@ -18,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; @@ -45,8 +52,7 @@ void shouldRegisterUserAndReturnSuccessResponse() throws MessagingException { assertEquals(HttpStatus.CREATED, result.getStatusCode()); assertTrue(result.getBody().isSuccess()); - assertEquals("User registered.", result.getBody().getMessage()); - assertEquals("Verification email sent.", result.getBody().getData().get(0)); + assertEquals("User created. Verify your email.", result.getBody().getMessage()); } @Test @@ -62,4 +68,81 @@ void shouldReturnBadRequestWhenRegisterFails() throws MessagingException { assertEquals(fails, result.getBody().getData()); } + @Test + void shouldReturnSussedWhenLoginIsAccepted() { + LoginDTO request = new LoginDTO("test@gmail.com", "Test#1"); + LoginResult responseAuth = new LoginSuccess("accessToken", "refreshToken"); + ResponseLoginDTO responseLoginDTO = new ResponseLoginDTO("accessToken", "refreshToken"); + when(authService.login(request)).thenReturn(responseAuth); + + ResponseEntity> response = authController.login(request); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(responseLoginDTO, response.getBody().getData()); + assertTrue(response.getBody().isSuccess()); + assertEquals("Success", response.getBody().getMessage()); + } + + @Test + void shouldReturnFailsEmailNotVerifyWhenLoginIsAccepted() { + LoginDTO request = new LoginDTO("test@gmail.com", "Test#1"); + LoginResult responseAuth = new LoginFailure(AuthError.EMAIL_NOT_VERIFIED); + when(authService.login(request)).thenReturn(responseAuth); + + ResponseEntity> response = authController.login(request); + + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + assertEquals("Email not verified", response.getBody().getMessage()); + assertNull(response.getBody().getData()); + } + + @Test + void shouldReturnFailsInvalidCredentialsWhenLoginIsAccepted() { + LoginDTO request = new LoginDTO("test@gmail.com", "Test#1"); + LoginResult responseAuth = new LoginFailure(AuthError.INVALID_CREDENTIALS); + when(authService.login(request)).thenReturn(responseAuth); + + ResponseEntity> response = authController.login(request); + + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + assertEquals("User or password is incorrect", response.getBody().getMessage()); + assertNull(response.getBody().getData()); + } + + @Test + void shouldReturnFailsUserNotRegisterWhenLoginIsAccepted() { + LoginDTO request = new LoginDTO("test@gmail.com", "Test#1"); + LoginResult responseAuth = new LoginFailure(AuthError.USER_NOT_REGISTER); + when(authService.login(request)).thenReturn(responseAuth); + + ResponseEntity> response = authController.login(request); + + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + assertEquals("User not register.", response.getBody().getMessage()); + assertNull(response.getBody().getData()); + } + + @Test + void shouldReturnSussedWhenEmailIsVerified() { + String token = "fake-token"; + boolean responseAuth = true; + + when(authService.verifyEmailToken(token)).thenReturn(responseAuth); + + ResponseEntity> response = authController.verifyEmail(token); + assertEquals("Email verified", response.getBody().getMessage()); + assertNull(response.getBody().getData()); + } + + @Test + void shouldReturnFailsWhenTokenIsExpired() { + String token = "fake-token"; + boolean responseAuth = false; + + when(authService.verifyEmailToken(token)).thenReturn(responseAuth); + + ResponseEntity> response = authController.verifyEmail(token); + assertEquals("Invalid or expired token", response.getBody().getMessage()); + assertNull(response.getBody().getData()); + } } diff --git a/src/test/java/dev/felipemlozx/api_auth/integration/AuthControllerIntegrationTest.java b/src/test/java/dev/felipemlozx/api_auth/integration/AuthControllerIntegrationTest.java new file mode 100644 index 0000000..9522ef8 --- /dev/null +++ b/src/test/java/dev/felipemlozx/api_auth/integration/AuthControllerIntegrationTest.java @@ -0,0 +1,240 @@ +package dev.felipemlozx.api_auth.integration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.felipemlozx.api_auth.config.EmbeddedRedisConfig; +import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; +import dev.felipemlozx.api_auth.controller.dto.LoginDTO; +import dev.felipemlozx.api_auth.entity.User; +import dev.felipemlozx.api_auth.repository.UserRepository; +import dev.felipemlozx.api_auth.services.EmailService; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +class AuthControllerIntegrationTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + UserRepository userRepository; + + @Autowired + BCryptPasswordEncoder passwordEncoder; + + @Autowired + CacheManager cacheManager; + + @MockBean + EmailService emailService; + + @BeforeAll + static void startRedis() throws Exception { + EmbeddedRedisConfig.startRedis(); + } + @BeforeEach + void cleanDatabase() { + userRepository.deleteAll(); + } + @AfterAll + static void stopRedis() { + EmbeddedRedisConfig.stopRedis(); + } + + @Test + @DisplayName("Register user successfully") + void shouldRegisterUserAndReturnSuccessResponse() throws Exception { + CreateUserDto dto = new CreateUserDto("test", "test@gmail.com", "Password!1"); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("User created. Verify your email.")) + .andReturn(); + + + User user = userRepository.findByEmail(dto.email()).orElseThrow(); + assert user.getEmail().equals(dto.email()); + assert !user.isVerified(); + assert user.getName().equals(dto.name()); + assert passwordEncoder.matches(dto.password(), user.getPassword()); + verify(emailService, times(1)).sendEmail(any(), any(), any()); + } + + @Test + @DisplayName("Register fails with existing email") + void shouldReturnBadRequestWhenRegisterFails() throws Exception { + + User existingUser = new User(); + existingUser.setName("test"); + existingUser.setEmail("test@gmail.com"); + existingUser.setPassword(passwordEncoder.encode("Password!1")); + existingUser.setVerified(false); + userRepository.save(existingUser); + + CreateUserDto dto = new CreateUserDto(existingUser.getName(), existingUser.getEmail(), existingUser.getPassword()); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/register") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(dto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data[0]").value("Email already exists")); + } + + @Test + @DisplayName("Login success returns tokens") + void shouldReturnSuccessWhenLoginIsAccepted() throws Exception { + + String rawPassword = "Test#1"; + User user = new User(); + user.setName("test"); + user.setEmail("test@gmail.com"); + user.setPassword(passwordEncoder.encode(rawPassword)); + user.setVerified(true); + userRepository.save(user); + + LoginDTO loginDto = new LoginDTO("test@gmail.com", rawPassword); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/login") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Success")) + .andExpect(jsonPath("$.data.accessToken").exists()) + .andExpect(jsonPath("$.data.refreshToken").exists()); + } + + @Test + @DisplayName("Login fails if email not verified") + void shouldReturnFailsEmailNotVerifyWhenLogin() throws Exception { + String rawPassword = "Test#1"; + User user = new User(); + user.setName("test"); + user.setEmail("test@gmail.com"); + user.setPassword(passwordEncoder.encode(rawPassword)); + user.setVerified(false); + userRepository.save(user); + + LoginDTO loginDto = new LoginDTO("test@gmail.com", rawPassword); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/login") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("Email not verified")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("Login fails with invalid credentials") + void shouldReturnFailsInvalidCredentialsWhenLogin() throws Exception { + String rawPassword = "Test#1"; + User user = new User(); + user.setName("test"); + user.setEmail("test@gmail.com"); + user.setPassword(passwordEncoder.encode(rawPassword)); + user.setVerified(true); + userRepository.save(user); + + LoginDTO loginDto = new LoginDTO("test@gmail.com", "wrongPassword"); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/login") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("User or password is incorrect")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("Login fails when user not registered") + void shouldReturnFailsUserNotRegisterWhenLogin() throws Exception { + LoginDTO loginDto = new LoginDTO("nonexistent@gmail.com", "Test#1"); + + mockMvc.perform(MockMvcRequestBuilders.post("/auth/login") + .characterEncoding("UTF-8") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("User not register.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("Verify email token success") + void shouldReturnSuccessWhenEmailIsVerified() throws Exception { + String validToken = "valid-token-sample"; + String email = "test@gmail.com"; + userRepository.save(new User("test", email, "Password1@", false)); + setCache(email, validToken); + + mockMvc.perform(MockMvcRequestBuilders.get("/auth/verify-email/" + validToken)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Email verified")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("Verify email token fails when token invalid or expired") + void shouldReturnFailsWhenTokenIsExpired() throws Exception { + String invalidToken = "fake-token"; + + mockMvc.perform(MockMvcRequestBuilders.get("/auth/verify-email/" + invalidToken)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("Invalid or expired token")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + + private void setCache(String email, String token) { + Cache cache = cacheManager.getCache("EmailVerificationTokens"); + if (cache != null) cache.put(token, email); + } +} \ No newline at end of file diff --git a/src/test/java/dev/felipemlozx/api_auth/services/AuthServiceTest.java b/src/test/java/dev/felipemlozx/api_auth/services/AuthServiceTest.java index 22f9849..70ad559 100644 --- a/src/test/java/dev/felipemlozx/api_auth/services/AuthServiceTest.java +++ b/src/test/java/dev/felipemlozx/api_auth/services/AuthServiceTest.java @@ -2,7 +2,13 @@ import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; import dev.felipemlozx.api_auth.controller.dto.LoginDTO; -import dev.felipemlozx.api_auth.controller.dto.ResponseLoginDTO; +import dev.felipemlozx.api_auth.core.AuthCheckFailure; +import dev.felipemlozx.api_auth.core.AuthCheckSuccess; +import dev.felipemlozx.api_auth.core.AuthError; +import dev.felipemlozx.api_auth.core.LoginFailure; +import dev.felipemlozx.api_auth.core.LoginResult; +import dev.felipemlozx.api_auth.core.LoginSuccess; +import dev.felipemlozx.api_auth.entity.User; import dev.felipemlozx.api_auth.infra.security.TokenService; import jakarta.mail.MessagingException; import org.junit.jupiter.api.BeforeEach; @@ -10,24 +16,23 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; - +import org.mockito.Spy; import java.util.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class AuthServiceTest { @InjectMocks + @Spy private AuthService authService; @Mock @@ -71,23 +76,25 @@ void shouldGenerateVerificationLinkWithCorrectToken() { void shouldReturnToken_whenLoginIsSuccessful() { LoginDTO loginDTO = new LoginDTO("test@gmail.com", "Password123!"); String token = UUID.randomUUID().toString(); - ResponseLoginDTO mock = new ResponseLoginDTO(token, token); - - when(userService.login(loginDTO)).thenReturn(true); + LoginSuccess mock = new LoginSuccess(token, token); + User user = new User("test", "test@gmail.com", "Password123!", true); + when(userService.login(loginDTO)).thenReturn(new AuthCheckSuccess(user)); when(tokenService.generateToken(loginDTO.email())).thenReturn(token); - ResponseLoginDTO result = authService.login(loginDTO); + LoginResult result = authService.login(loginDTO); verify(userService).login(loginDTO); assertEquals(mock, result); } @Test - void shouldThrowException_whenLoginCredentialsAreInvalid() { + void shouldReturnNull_whenLoginCredentialsAreInvalid() { LoginDTO loginDTO = new LoginDTO("test@gmail.com", "Password123!"); + AuthCheckFailure error = new AuthCheckFailure(AuthError.INVALID_CREDENTIALS); + when(userService.login(loginDTO)).thenReturn(error); - when(userService.login(loginDTO)).thenReturn(false); + LoginFailure response = (LoginFailure) authService.login(loginDTO); - assertNull(authService.login(loginDTO)); + assertTrue(response.error().equals(error.error())); verify(userService).login(loginDTO); } @@ -115,8 +122,22 @@ void shouldNotSendVerificationEmail_whenRegisterReturnsNonEmptyList() throws Mes List result = authService.register(createUserDto); verify(userService).register(createUserDto); - verify(userService, org.mockito.Mockito.never()).createEmailVerificationToken(anyString()); - verify(emailService, org.mockito.Mockito.never()).sendEmail(anyString(), anyString(), anyString()); + verify(userService, never()).createEmailVerificationToken(anyString()); + verify(emailService, never()).sendEmail(anyString(), anyString(), anyString()); assertEquals(errors, result); } + + @Test + void shouldNotSedEmailWhenTokenIsInValid() throws MessagingException { + CreateUserDto createUserDto = new CreateUserDto("name", "test@gmail.com", "Password123!"); + + when(userService.register(createUserDto)).thenReturn(List.of()); + when(userService.createEmailVerificationToken(createUserDto.email())).thenReturn(null); + + List result = authService.register(createUserDto); + + verify(userService).register(createUserDto); + verify(userService).createEmailVerificationToken(createUserDto.email()); + verify(emailService, never()).sendEmail(anyString(), anyString(), anyString()); + } } diff --git a/src/test/java/dev/felipemlozx/api_auth/services/UserServiceTest.java b/src/test/java/dev/felipemlozx/api_auth/services/UserServiceTest.java index e8d58aa..ae39ea7 100644 --- a/src/test/java/dev/felipemlozx/api_auth/services/UserServiceTest.java +++ b/src/test/java/dev/felipemlozx/api_auth/services/UserServiceTest.java @@ -2,6 +2,10 @@ import dev.felipemlozx.api_auth.controller.dto.CreateUserDto; import dev.felipemlozx.api_auth.controller.dto.LoginDTO; +import dev.felipemlozx.api_auth.core.AuthCheckFailure; +import dev.felipemlozx.api_auth.core.AuthCheckResult; +import dev.felipemlozx.api_auth.core.AuthCheckSuccess; +import dev.felipemlozx.api_auth.core.AuthError; import dev.felipemlozx.api_auth.entity.User; import dev.felipemlozx.api_auth.repository.UserRepository; import dev.felipemlozx.api_auth.utils.CheckUtils; @@ -24,13 +28,13 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -99,44 +103,44 @@ void shouldReturnErrorsWhenValidationFails() { } @Test - void shouldThrowsErrorWhenEmailIsNotVerify() { + void shouldThrowsErrorWhenUserIsNotFound() { LoginDTO loginDTO = new LoginDTO("test@test.com", "Password!32"); - User user = new User("test", "test@test.com","Password!32", false); - when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.of(user)); - - RuntimeException ex = assertThrows(RuntimeException.class, - () -> userService.login(loginDTO)); - assertEquals("Email not verify", ex.getMessage()); + when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.empty()); + AuthCheckResult result = userService.login(loginDTO); + assertInstanceOf(AuthCheckFailure.class, result); + assertEquals(AuthError.USER_NOT_REGISTER, ((AuthCheckFailure) result).error()); } @Test - void shouldThrowsErrorWhenUserIsNotFound() { + void shouldThrowsErrorWhenEmailIsNotVerify() { LoginDTO loginDTO = new LoginDTO("test@test.com", "Password!32"); - when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.empty()); - - RuntimeException ex = assertThrows(RuntimeException.class, - () -> userService.login(loginDTO)); - assertEquals("User not found.", ex.getMessage()); + User user = new User("test", "test@test.com","Password!32", false); + when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.of(user)); + AuthCheckResult result = userService.login(loginDTO); + assertInstanceOf(AuthCheckFailure.class, result); + assertEquals(AuthError.EMAIL_NOT_VERIFIED, ((AuthCheckFailure) result).error()); } @Test - void shouldReturnTrueWhenPasswordIsEquals() { + void shouldReturnSuccessWhenPasswordIsEquals() { LoginDTO loginDTO = new LoginDTO("test@test.com", "Password!32"); User user = new User("test", "test@test.com","Password!32", true); when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.of(user)); when(passwordEncoder.matches(loginDTO.password(), user.getPassword())).thenReturn(true); - boolean result = userService.login(loginDTO); - assertTrue(result); + AuthCheckResult result = userService.login(loginDTO); + assertInstanceOf(AuthCheckSuccess.class, result); + assertEquals(user, ((AuthCheckSuccess) result).user()); } @Test - void shouldReturnFalseWhenPasswordIsEquals() { + void shouldReturnFailureWhenPasswordIsIncorrect() { LoginDTO loginDTO = new LoginDTO("test@test.com", "Password!32"); User user = new User("test", "test@test.com","Password32", true); when(userRepository.findByEmail(loginDTO.email())).thenReturn(Optional.of(user)); when(passwordEncoder.matches(loginDTO.password(), user.getPassword())).thenReturn(false); - boolean result = userService.login(loginDTO); - assertFalse(result); + AuthCheckResult result = userService.login(loginDTO); + assertInstanceOf(AuthCheckFailure.class, result); + assertEquals(AuthError.INVALID_CREDENTIALS, ((AuthCheckFailure) result).error()); } @Test @@ -214,16 +218,24 @@ void shouldReturnFalseAndNotVerifyUser_whenEmailTokenIsExpired() { } @Test - void shouldThrowRuntimeException_whenEmailTokenUserNotFound() { + void shouldReturnNull_whenCreatingEmailVerificationTokenForNonexistentUser() { + String email = "teste@gmail.com"; + when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); + String result = userService.createEmailVerificationToken(email); + verify(userRepository).findByEmail(email); + assertNull(result); + } + + @Test + void shouldReturnFalse_whenEmailTokenUserNotFound() { String token = "fake-token123"; String email = "teste@gmail.com"; when(cacheManager.getCache("EmailVerificationTokens")).thenReturn(cache); when(cache.get(token)).thenReturn(valueWrapper); when(valueWrapper.get()).thenReturn(email); when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); - - RuntimeException ex = assertThrows(RuntimeException.class, () -> userService.verifyEmailToken(token)); - assertEquals("Link invalid.", ex.getMessage()); + Boolean result = userService.verifyEmailToken(token); + assertFalse(result); } @Test @@ -238,15 +250,6 @@ void shouldCreateEmailVerificationToken_whenUserExists() { assertNotNull(result); } - @Test - void shouldThrowRuntimeException_whenCreatingEmailVerificationTokenForNonexistentUser() { - String email = "teste@gmail.com"; - when(userRepository.findByEmail(email)).thenReturn(Optional.empty()); - - RuntimeException result = assertThrows(RuntimeException.class, () -> userService.createEmailVerificationToken(email)); - verify(userRepository).findByEmail(email); - assertEquals("User not found.", result.getMessage()); - } @Test void shouldSaveTokenInCache_whenCacheIsAvailable() { @@ -294,7 +297,7 @@ void shouldReturnEmail_whenTokenExistsInCache() { when(cache.get(token)).thenReturn(valueWrapper); when(valueWrapper.get()).thenReturn(email); - String result = userService.recuperarToken(token); + String result = userService.recoverToken(token); assertEquals(email, result); } @@ -302,9 +305,7 @@ void shouldReturnEmail_whenTokenExistsInCache() { @Test void shouldReturnNull_whenCacheIsNullOnTokenRetrieval() { when(cacheManager.getCache("EmailVerificationTokens")).thenReturn(null); - - String result = userService.recuperarToken("any-token"); - + String result = userService.recoverToken("any-token"); assertNull(result); } @@ -313,10 +314,19 @@ void shouldReturnNull_whenTokenNotFoundInCache() { when(cacheManager.getCache("EmailVerificationTokens")).thenReturn(cache); when(cache.get("token-inexistente")).thenReturn(null); - String result = userService.recuperarToken("token-inexistente"); - + String result = userService.recoverToken("token-inexistente"); assertNull(result); } + + @Test + void shouldReturnFalse_whenRecoverTokenReturnsNull() { + String token = "invalid-token"; + when(cacheManager.getCache("EmailVerificationTokens")).thenReturn(cache); + when(cache.get(token)).thenReturn(null); + Boolean result = userService.verifyEmailToken(token); + assertFalse(result); + } + private void mockUserTime(User user) { user.setTimeVerify( Instant diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml new file mode 100644 index 0000000..6195cf7 --- /dev/null +++ b/src/test/resources/application-test.yaml @@ -0,0 +1,15 @@ +spring: + datasource: + url: jdbc:h2:mem:db;DB_CLOSE_DELAY=-1 + username: sa + password: + driver-class-name: org.h2.Driver + + jpa: + show-sql: false + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + format-sql: false