diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 1fb5c28..351b279 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -68,7 +68,7 @@ runtime true - + org.springframework.boot spring-boot-starter-test @@ -177,6 +177,18 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + diff --git a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java index 22f5952..2a822a1 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java @@ -23,209 +23,183 @@ * Handles login, registration, and health check requests */ @RestController -// Class-level request mapping removed — gateway strips prefixes before forwarding +// Class-level request mapping removed — gateway strips prefixes before +// forwarding // @RequestMapping("/api/v1/auth") // CORS handled at the API Gateway; remove @CrossOrigin to avoid conflicts // @CrossOrigin(origins = "*", maxAge = 3600) @Tag(name = "Authentication", description = "Authentication and user management endpoints") public class AuthController { - + @Autowired private AuthService authService; - + // --- NEW DEPENDENCY --- // We need UserService to call the createEmployee method @Autowired private UserService userService; - + /** * User login endpoint + * * @param loginRequest Login credentials * @return JWT token and user details */ - @Operation( - summary = "User Login", - description = "Authenticate user with username/email and password. Returns JWT token on success. Rate limited to prevent brute force attacks." - ) + @Operation(summary = "User Login", description = "Authenticate user with username/email and password. Returns JWT token on success. Rate limited to prevent brute force attacks.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Login successful, JWT token returned"), - @ApiResponse(responseCode = "401", description = "Invalid credentials or account locked"), - @ApiResponse(responseCode = "400", description = "Invalid request format") + @ApiResponse(responseCode = "200", description = "Login successful, JWT token returned"), + @ApiResponse(responseCode = "401", description = "Invalid credentials or account locked"), + @ApiResponse(responseCode = "400", description = "Invalid request format") }) @PostMapping("/login") - public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest, HttpServletRequest request) { + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest, + HttpServletRequest request) { LoginResponse loginResponse = authService.authenticateUser(loginRequest, request); return ResponseEntity.ok(loginResponse); } - + /** * User registration endpoint + * * @param registerRequest Registration details * @return Success message */ - @Operation( - summary = "Register New User", - description = "Register a new customer account. Email verification is required before login." - ) + @Operation(summary = "Register New User", description = "Register a new customer account. Email verification is required before login.") @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Registration successful, verification email sent"), - @ApiResponse(responseCode = "400", description = "Invalid request or username/email already exists") + @ApiResponse(responseCode = "201", description = "Registration successful, verification email sent"), + @ApiResponse(responseCode = "400", description = "Invalid request or username/email already exists") }) @PostMapping("/register") public ResponseEntity registerUser(@Valid @RequestBody RegisterRequest registerRequest) { String message = authService.registerUser(registerRequest); return ResponseEntity.status(HttpStatus.CREATED).body(ApiSuccess.of(message)); } - + /** * Verify email with token */ - @Operation( - summary = "Verify Email", - description = "Verify user email address with token sent via email. Returns JWT tokens on success." - ) + @Operation(summary = "Verify Email", description = "Verify user email address with token sent via email. Returns JWT tokens on success.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Email verified successfully, user logged in"), - @ApiResponse(responseCode = "400", description = "Invalid, expired, or already used token") + @ApiResponse(responseCode = "200", description = "Email verified successfully, user logged in"), + @ApiResponse(responseCode = "400", description = "Invalid, expired, or already used token") }) @PostMapping("/verify-email") - public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request, HttpServletRequest httpRequest) { + public ResponseEntity verifyEmail(@Valid @RequestBody VerifyEmailRequest request, + HttpServletRequest httpRequest) { LoginResponse response = authService.verifyEmail(request.getToken(), httpRequest); return ResponseEntity.ok(response); } - + /** * Resend verification email */ - @Operation( - summary = "Resend Verification Email", - description = "Resend verification email to the specified address" - ) + @Operation(summary = "Resend Verification Email", description = "Resend verification email to the specified address") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Verification email sent successfully"), - @ApiResponse(responseCode = "400", description = "Email not found or already verified") + @ApiResponse(responseCode = "200", description = "Verification email sent successfully"), + @ApiResponse(responseCode = "400", description = "Email not found or already verified") }) @PostMapping("/resend-verification") public ResponseEntity resendVerification(@Valid @RequestBody ResendVerificationRequest request) { String message = authService.resendVerificationEmail(request.getEmail()); return ResponseEntity.ok(ApiSuccess.of(message)); } - + /** * Refresh JWT token */ - @Operation( - summary = "Refresh Access Token", - description = "Get a new access token using a valid refresh token" - ) + @Operation(summary = "Refresh Access Token", description = "Get a new access token using a valid refresh token") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "New access token generated"), - @ApiResponse(responseCode = "401", description = "Invalid, expired, or revoked refresh token") + @ApiResponse(responseCode = "200", description = "New access token generated"), + @ApiResponse(responseCode = "401", description = "Invalid, expired, or revoked refresh token") }) @PostMapping("/refresh") public ResponseEntity refreshToken(@Valid @RequestBody RefreshTokenRequest request) { LoginResponse response = authService.refreshToken(request.getRefreshToken()); return ResponseEntity.ok(response); } - + /** * Logout endpoint */ - @Operation( - summary = "Logout User", - description = "Logout user and revoke refresh token", - security = @SecurityRequirement(name = "bearerAuth") - ) + @Operation(summary = "Logout User", description = "Logout user and revoke refresh token", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Logged out successfully"), - @ApiResponse(responseCode = "400", description = "Invalid refresh token") + @ApiResponse(responseCode = "200", description = "Logged out successfully"), + @ApiResponse(responseCode = "400", description = "Invalid refresh token") }) @PostMapping("/logout") public ResponseEntity logout(@Valid @RequestBody LogoutRequest request) { authService.logout(request.getRefreshToken()); return ResponseEntity.ok(ApiSuccess.of("Logged out successfully")); } - + /** * Forgot password - request reset */ - @Operation( - summary = "Forgot Password", - description = "Request password reset email" - ) + @Operation(summary = "Forgot Password", description = "Request password reset email") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset email sent"), - @ApiResponse(responseCode = "404", description = "Email not found") + @ApiResponse(responseCode = "200", description = "Password reset email sent"), + @ApiResponse(responseCode = "404", description = "Email not found") }) @PostMapping("/forgot-password") public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { String message = authService.forgotPassword(request.getEmail()); return ResponseEntity.ok(ApiSuccess.of(message)); } - + /** * Reset password with token */ - @Operation( - summary = "Reset Password", - description = "Reset password using token from email" - ) + @Operation(summary = "Reset Password", description = "Reset password using token from email") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password reset successfully"), - @ApiResponse(responseCode = "400", description = "Invalid, expired, or already used token") + @ApiResponse(responseCode = "200", description = "Password reset successfully"), + @ApiResponse(responseCode = "400", description = "Invalid, expired, or already used token") }) @PostMapping("/reset-password") public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordWithTokenRequest request) { String message = authService.resetPassword(request.getToken(), request.getNewPassword()); return ResponseEntity.ok(ApiSuccess.of(message)); } - + /** * Change password (authenticated users) * Note: This endpoint moved to UserController as /users/me/change-password * Keeping for backwards compatibility */ - @Operation( - summary = "Change Password", - description = "Change password for authenticated user. Use current password for verification.", - security = @SecurityRequirement(name = "bearerAuth") - ) + @Operation(summary = "Change Password", description = "Change password for authenticated user. Use current password for verification.", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Password changed successfully"), - @ApiResponse(responseCode = "400", description = "Invalid current password"), - @ApiResponse(responseCode = "401", description = "Authentication required") + @ApiResponse(responseCode = "200", description = "Password changed successfully"), + @ApiResponse(responseCode = "400", description = "Invalid current password"), + @ApiResponse(responseCode = "401", description = "Authentication required") }) @PutMapping("/change-password") @PreAuthorize("hasRole('CUSTOMER') or hasRole('EMPLOYEE') or hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity changePassword(@Valid @RequestBody ChangePasswordRequest changeRequest) { try { - Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); + Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext() + .getAuthentication(); String username = authentication.getName(); - - userService.changeUserPassword(username, changeRequest.getCurrentPassword(), changeRequest.getNewPassword()); + + userService.changeUserPassword(username, changeRequest.getCurrentPassword(), + changeRequest.getNewPassword()); return ResponseEntity.ok(ApiSuccess.of("Password changed successfully")); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(ApiSuccess.of("Error: " + e.getMessage())); } } - // --- NEW ENDPOINT FOR CREATING EMPLOYEES --- /** * ADMIN-ONLY endpoint for creating a new employee account. + * * @param createEmployeeRequest DTO with username, email, and password. * @return A success or error message. */ - @Operation( - summary = "Create Employee Account", - description = "Create a new employee account. Requires ADMIN role.", - security = @SecurityRequirement(name = "bearerAuth") - ) + @Operation(summary = "Create Employee Account", description = "Create a new employee account. Requires ADMIN role.", security = @SecurityRequirement(name = "bearerAuth")) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "Employee account created successfully"), - @ApiResponse(responseCode = "400", description = "Invalid request or username already exists"), - @ApiResponse(responseCode = "401", description = "Authentication required"), - @ApiResponse(responseCode = "403", description = "Admin role required") + @ApiResponse(responseCode = "201", description = "Employee account created successfully"), + @ApiResponse(responseCode = "400", description = "Invalid request or username already exists"), + @ApiResponse(responseCode = "401", description = "Authentication required"), + @ApiResponse(responseCode = "403", description = "Admin role required") }) @PostMapping("/users/employee") @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") @@ -233,10 +207,10 @@ public ResponseEntity createEmployee(@Valid @RequestBody CreateEmployeeReques try { // Now we are calling the method that was previously unused userService.createEmployee( - createEmployeeRequest.getUsername(), - createEmployeeRequest.getEmail(), - createEmployeeRequest.getPassword() - ); + createEmployeeRequest.getUsername(), + createEmployeeRequest.getEmail(), + createEmployeeRequest.getPassword(), + createEmployeeRequest.getFullName()); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiSuccess.of("Employee account created successfully!")); } catch (RuntimeException e) { @@ -248,6 +222,7 @@ public ResponseEntity createEmployee(@Valid @RequestBody CreateEmployeeReques // --- NEW ENDPOINT FOR CREATING ADMINS (SUPER_ADMIN ONLY) --- /** * SUPER_ADMIN-ONLY endpoint for creating a new admin account. + * * @param createAdminRequest DTO with username, email, and password. * @return A success or error message. */ @@ -256,28 +231,30 @@ public ResponseEntity createEmployee(@Valid @RequestBody CreateEmployeeReques public ResponseEntity createAdmin(@Valid @RequestBody CreateAdminRequest createAdminRequest) { try { userService.createAdmin( - createAdminRequest.getUsername(), - createAdminRequest.getEmail(), - createAdminRequest.getPassword() - ); + createAdminRequest.getUsername(), + createAdminRequest.getEmail(), + createAdminRequest.getPassword(), + createAdminRequest.getFullName()); return ResponseEntity.status(HttpStatus.CREATED) .body(ApiSuccess.of("Admin account created successfully!")); } catch (RuntimeException e) { return ResponseEntity.badRequest().body(ApiSuccess.of("Error: " + e.getMessage())); } } - + /** * Health check endpoint + * * @return Service status */ @GetMapping("/health") public ResponseEntity health() { return ResponseEntity.ok(ApiSuccess.of("Authentication Service is running!")); } - + /** * Test endpoint for authenticated users + * * @return Test message */ @GetMapping("/test") diff --git a/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java index 9a1e837..711bf17 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java @@ -29,7 +29,6 @@ @RequestMapping("/users") // CORS handled by API Gateway; remove @CrossOrigin to avoid conflicts // @CrossOrigin(origins = "*", maxAge = 3600) -@PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") @Tag(name = "User Management", description = "User management endpoints (Admin/Super Admin only)") @SecurityRequirement(name = "bearerAuth") public class UserController { @@ -44,6 +43,7 @@ public class UserController { * Get a list of all users in the system. */ @GetMapping + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity> getAllUsers(@RequestParam(required = false) String role) { List users = userService.findAllUsers().stream() .map(this::convertToDto) @@ -56,6 +56,7 @@ public ResponseEntity> getAllUsers(@RequestParam(required = false) * Get detailed information for a single user by their username. */ @GetMapping("/{username}") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity getUserByUsername(@PathVariable String username) { return userService.findByUsername(username) .map(user -> ResponseEntity.ok(convertToDto(user))) @@ -66,6 +67,7 @@ public ResponseEntity getUserByUsername(@PathVariable String username) * Disable a user's account. */ @PostMapping("/{username}/disable") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity disableUser(@PathVariable String username) { try { userService.disableUser(username); @@ -79,6 +81,7 @@ public ResponseEntity disableUser(@PathVariable String username) { * Enable a user's account. */ @PostMapping("/{username}/enable") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity enableUser(@PathVariable String username) { try { userService.enableUser(username); @@ -102,6 +105,7 @@ public ResponseEntity unlockUser(@PathVariable String username) { * Delete a user from the system permanently. */ @DeleteMapping("/{username}") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity deleteUser(@PathVariable String username) { try { userService.deleteUser(username); @@ -116,6 +120,7 @@ public ResponseEntity deleteUser(@PathVariable String username) { * PUT /api/v1/users/{username} */ @PutMapping("/{username}") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity updateUser(@PathVariable String username, @Valid @RequestBody UpdateUserRequest updateRequest) { try { @@ -136,6 +141,7 @@ public ResponseEntity updateUser(@PathVariable String username, * POST /api/v1/users/{username}/reset-password */ @PostMapping("/{username}/reset-password") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity resetUserPassword(@PathVariable String username, @Valid @RequestBody ResetPasswordRequest resetRequest) { try { diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateAdminRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateAdminRequest.java index 1dc5e16..c4c0e78 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateAdminRequest.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateAdminRequest.java @@ -14,11 +14,13 @@ @AllArgsConstructor @Builder public class CreateAdminRequest { - + private String username; private String email; private String password; - + + private String fullName; + // Optional: Additional admin-specific fields private String firstName; private String lastName; diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateEmployeeRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateEmployeeRequest.java index 20cb3f1..19e41e7 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateEmployeeRequest.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/request/CreateEmployeeRequest.java @@ -14,11 +14,13 @@ @AllArgsConstructor @Builder public class CreateEmployeeRequest { - + private String username; private String email; private String password; - + + private String fullName; + // Optional: Additional employee-specific fields private String firstName; private String lastName; diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java index 4a83235..1b26160 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java @@ -33,19 +33,19 @@ @Service @Transactional public class AuthService { - + @Autowired private AuthenticationManager authenticationManager; - + @Autowired private UserRepository userRepository; - + @Autowired private RoleRepository roleRepository; - + @Autowired private PasswordEncoder passwordEncoder; - + @Autowired private JwtUtil jwtUtil; @@ -64,13 +64,13 @@ public class AuthService { @Autowired private EmailService emailService; - @Value("${security.login.max-failed-attempts:3}") - private int maxFailedAttempts; + @Value("${security.login.max-failed-attempts:3}") + private int maxFailedAttempts; + + // duration in minutes + @Value("${security.login.lock-duration-minutes:15}") + private long lockDurationMinutes; - // duration in minutes - @Value("${security.login.lock-duration-minutes:15}") - private long lockDurationMinutes; - public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequest request) { String uname = loginRequest.getUsername(); @@ -79,32 +79,34 @@ public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequ if (userOpt.isEmpty()) { userOpt = userRepository.findByEmail(uname); } - + if (userOpt.isPresent()) { User user = userOpt.get(); - + // Check if account is disabled (deactivated by admin) if (!user.getEnabled()) { if (!user.getEmailVerified()) { throw new org.springframework.security.authentication.DisabledException( - "Please verify your email address before logging in. Check your inbox for the verification link."); + "Please verify your email address before logging in. Check your inbox for the verification link."); } else { throw new org.springframework.security.authentication.DisabledException( - "Your account has been deactivated. Please contact the administrator for assistance."); + "Your account has been deactivated. Please contact the administrator for assistance."); } } } - // load or create lock record - com.techtorque.auth_service.entity.LoginLock lock = loginLockRepository.findByUsername(uname) - .orElseGet(() -> com.techtorque.auth_service.entity.LoginLock.builder().username(uname).failedAttempts(0).build()); + // load or create lock record + com.techtorque.auth_service.entity.LoginLock lock = loginLockRepository.findByUsername(uname) + .orElseGet(() -> com.techtorque.auth_service.entity.LoginLock.builder().username(uname) + .failedAttempts(0).build()); if (lock.getLockUntil() != null && lock.getLockUntil().isAfter(LocalDateTime.now())) { long minutesLeft = ChronoUnit.MINUTES.between(LocalDateTime.now(), lock.getLockUntil()); - // record login log using audit service - String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For")) : null; - String ua = request != null ? request.getHeader("User-Agent") : null; - loginAuditService.recordLogin(uname, false, ip, ua); + // record login log using audit service + String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() + : request.getHeader("X-Forwarded-For")) : null; + String ua = request != null ? request.getHeader("User-Agent") : null; + loginAuditService.recordLogin(uname, false, ip, ua); throw new org.springframework.security.authentication.BadCredentialsException( "Account is temporarily locked. Try again in " + minutesLeft + " minutes."); } @@ -113,9 +115,7 @@ public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequ Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), - loginRequest.getPassword() - ) - ); + loginRequest.getPassword())); // Successful authentication -> reset failed attempts on lock record lock.setFailedAttempts(0); @@ -139,9 +139,10 @@ public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequ .collect(Collectors.toSet()); recordLogin(uname, true, request); - + // Create refresh token - String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For")) : null; + String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() + : request.getHeader("X-Forwarded-For")) : null; String ua = request != null ? request.getHeader("User-Agent") : null; String refreshToken = tokenService.createRefreshToken(foundUser, ip, ua); @@ -154,14 +155,17 @@ public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequ .build(); } catch (BadCredentialsException ex) { - // increment failed attempts and possibly lock the user using separate transaction + // increment failed attempts and possibly lock the user using separate + // transaction loginAuditService.incrementFailedAttempt(uname, lockDurationMinutes, maxFailedAttempts); - String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For")) : null; + String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() + : request.getHeader("X-Forwarded-For")) : null; String ua = request != null ? request.getHeader("User-Agent") : null; loginAuditService.recordLogin(uname, false, ip, ua); - throw new org.springframework.security.authentication.BadCredentialsException("Invalid username or password"); + throw new org.springframework.security.authentication.BadCredentialsException( + "Invalid username or password"); } } @@ -170,7 +174,8 @@ private void recordLogin(String username, boolean success, HttpServletRequest re String ua = null; if (request != null) { ip = request.getHeader("X-Forwarded-For"); - if (ip == null) ip = request.getRemoteAddr(); + if (ip == null) + ip = request.getRemoteAddr(); ua = request.getHeader("User-Agent"); } com.techtorque.auth_service.entity.LoginLog log = com.techtorque.auth_service.entity.LoginLog.builder() @@ -182,12 +187,12 @@ private void recordLogin(String username, boolean success, HttpServletRequest re .build(); loginLogRepository.save(log); } - + public String registerUser(RegisterRequest registerRequest) { if (userRepository.existsByEmail(registerRequest.getEmail())) { throw new RuntimeException("Error: Email is already in use!"); } - + User user = User.builder() .username(registerRequest.getEmail()) // Use email as username for simplicity .email(registerRequest.getEmail()) @@ -200,10 +205,10 @@ public String registerUser(RegisterRequest registerRequest) { .emailVerificationDeadline(LocalDateTime.now().plus(7, ChronoUnit.DAYS)) // 1 week deadline .roles(new HashSet<>()) .build(); - + Set strRoles = registerRequest.getRoles(); Set roles = new HashSet<>(); - + if (strRoles == null || strRoles.isEmpty()) { Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); @@ -228,57 +233,58 @@ public String registerUser(RegisterRequest registerRequest) { } }); } - + user.setRoles(roles); User savedUser = userRepository.save(user); - + // Create verification token and send email String token = tokenService.createVerificationToken(savedUser); - emailService.sendVerificationEmail(savedUser.getEmail(), savedUser.getUsername(), token); - + emailService.sendVerificationEmail(savedUser.getEmail(), savedUser.getFullName(), token); + return "User registered successfully! Please check your email to verify your account."; } - + /** * Verify email with token */ public LoginResponse verifyEmail(String token, HttpServletRequest request) { - com.techtorque.auth_service.entity.VerificationToken verificationToken = - tokenService.validateToken(token, com.techtorque.auth_service.entity.VerificationToken.TokenType.EMAIL_VERIFICATION); - + com.techtorque.auth_service.entity.VerificationToken verificationToken = tokenService.validateToken(token, + com.techtorque.auth_service.entity.VerificationToken.TokenType.EMAIL_VERIFICATION); + User user = verificationToken.getUser(); user.setEnabled(true); user.setEmailVerified(true); // Mark email as verified User updatedUser = userRepository.save(user); tokenService.markTokenAsUsed(verificationToken); - + // Send welcome email emailService.sendWelcomeEmail(updatedUser.getEmail(), updatedUser.getUsername()); // Auto-login after verification - Set roleNames = updatedUser.getRoles() != null ? - updatedUser.getRoles().stream() + Set roleNames = updatedUser.getRoles() != null ? updatedUser.getRoles().stream() .map(role -> role.getName().name()) - .collect(Collectors.toSet()) : - Set.of("CUSTOMER"); + .collect(Collectors.toSet()) : Set.of("CUSTOMER"); List roles = new java.util.ArrayList<>(roleNames); Set authorities = new java.util.HashSet<>(); if (updatedUser.getRoles() != null) { updatedUser.getRoles().stream() - .flatMap(role -> role.getPermissions() != null ? role.getPermissions().stream() : java.util.stream.Stream.empty()) - .forEach(permission -> authorities.add(new org.springframework.security.core.authority.SimpleGrantedAuthority(permission.getName()))); + .flatMap(role -> role.getPermissions() != null ? role.getPermissions().stream() + : java.util.stream.Stream.empty()) + .forEach(permission -> authorities + .add(new org.springframework.security.core.authority.SimpleGrantedAuthority( + permission.getName()))); } String jwt = jwtUtil.generateJwtToken(new org.springframework.security.core.userdetails.User( updatedUser.getUsername(), updatedUser.getPassword(), - authorities - ), roles); - - String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() : request.getHeader("X-Forwarded-For")) : null; + authorities), roles); + + String ip = request != null ? (request.getHeader("X-Forwarded-For") == null ? request.getRemoteAddr() + : request.getHeader("X-Forwarded-For")) : null; String ua = request != null ? request.getHeader("User-Agent") : null; String refreshToken = tokenService.createRefreshToken(updatedUser, ip, ua); @@ -290,30 +296,31 @@ public LoginResponse verifyEmail(String token, HttpServletRequest request) { .roles(roleNames) .build(); } - + /** * Resend verification email */ public String resendVerificationEmail(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("User not found with email: " + email)); - + if (user.getEmailVerified()) { throw new RuntimeException("Email is already verified"); } - + String token = tokenService.createVerificationToken(user); emailService.sendVerificationEmail(user.getEmail(), user.getUsername(), token); - + return "Verification email sent successfully!"; } - + /** * Refresh JWT token */ public LoginResponse refreshToken(String refreshTokenString) { - com.techtorque.auth_service.entity.RefreshToken refreshToken = tokenService.validateRefreshToken(refreshTokenString); - + com.techtorque.auth_service.entity.RefreshToken refreshToken = tokenService + .validateRefreshToken(refreshTokenString); + User user = refreshToken.getUser(); List roles = user.getRoles().stream() @@ -325,14 +332,15 @@ public LoginResponse refreshToken(String refreshTokenString) { user.getPassword(), user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) - .map(permission -> new org.springframework.security.core.authority.SimpleGrantedAuthority(permission.getName())) - .collect(Collectors.toSet()) - ), roles); - + .map(permission -> new org.springframework.security.core.authority.SimpleGrantedAuthority( + permission.getName())) + .collect(Collectors.toSet())), + roles); + Set roleNames = user.getRoles().stream() .map(role -> role.getName().name()) .collect(Collectors.toSet()); - + return LoginResponse.builder() .token(jwt) .refreshToken(refreshTokenString) // Return same refresh token @@ -341,43 +349,43 @@ public LoginResponse refreshToken(String refreshTokenString) { .roles(roleNames) .build(); } - + /** * Logout - revoke refresh token */ public void logout(String refreshToken) { tokenService.revokeRefreshToken(refreshToken); } - + /** * Request password reset */ public String forgotPassword(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new RuntimeException("User not found with email: " + email)); - + String token = tokenService.createPasswordResetToken(user); emailService.sendPasswordResetEmail(user.getEmail(), user.getUsername(), token); - + return "Password reset email sent successfully!"; } - + /** * Reset password with token */ public String resetPassword(String token, String newPassword) { - com.techtorque.auth_service.entity.VerificationToken resetToken = - tokenService.validateToken(token, com.techtorque.auth_service.entity.VerificationToken.TokenType.PASSWORD_RESET); - + com.techtorque.auth_service.entity.VerificationToken resetToken = tokenService.validateToken(token, + com.techtorque.auth_service.entity.VerificationToken.TokenType.PASSWORD_RESET); + User user = resetToken.getUser(); user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); - + tokenService.markTokenAsUsed(resetToken); - + // Revoke all existing refresh tokens for security tokenService.revokeAllUserTokens(user); - + return "Password reset successfully!"; } } diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java index 39f8135..3b50bc3 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java @@ -37,17 +37,21 @@ @Transactional public class UserService implements UserDetailsService { - private final UserRepository userRepository; - private final RoleRepository roleRepository; - private final PasswordEncoder passwordEncoder; - private final LoginLockRepository loginLockRepository; - private final RefreshTokenRepository refreshTokenRepository; - private final VerificationTokenRepository verificationTokenRepository; - private final LoginLogRepository loginLogRepository; - - public UserService(UserRepository userRepository, RoleRepository roleRepository, @Lazy PasswordEncoder passwordEncoder, - LoginLockRepository loginLockRepository, RefreshTokenRepository refreshTokenRepository, - VerificationTokenRepository verificationTokenRepository, LoginLogRepository loginLogRepository) { + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + private final LoginLockRepository loginLockRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final VerificationTokenRepository verificationTokenRepository; + private final LoginLogRepository loginLogRepository; + private final EmailService emailService; + private final TokenService tokenService; + + public UserService(UserRepository userRepository, RoleRepository roleRepository, + @Lazy PasswordEncoder passwordEncoder, + LoginLockRepository loginLockRepository, RefreshTokenRepository refreshTokenRepository, + VerificationTokenRepository verificationTokenRepository, LoginLogRepository loginLogRepository, + EmailService emailService, TokenService tokenService) { this.userRepository = userRepository; this.roleRepository = roleRepository; this.passwordEncoder = passwordEncoder; @@ -55,11 +59,14 @@ public UserService(UserRepository userRepository, RoleRepository roleRepository, this.refreshTokenRepository = refreshTokenRepository; this.verificationTokenRepository = verificationTokenRepository; this.loginLogRepository = loginLogRepository; + this.emailService = emailService; + this.tokenService = tokenService; } /** * Load user by username for Spring Security authentication * This method is called during login to authenticate the user + * * @param username The username to authenticate * @return UserDetails object with user info and authorities * @throws UsernameNotFoundException if user not found @@ -87,32 +94,34 @@ public UserDetails loadUserByUsername(String identifier) throws UsernameNotFound } /** - * Convert user roles and permissions to Spring Security GrantedAuthority objects + * Convert user roles and permissions to Spring Security GrantedAuthority + * objects * This enables role-based and permission-based security checks + * * @param user The user whose authorities to build * @return Collection of granted authorities */ private Collection getAuthorities(User user) { Set authorities = new HashSet<>(); - + // Add role-based authorities (prefixed with ROLE_) user.getRoles().forEach(role -> { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName().name())); - + // Add permission-based authorities (used for @PreAuthorize checks) - role.getPermissions().forEach(permission -> - authorities.add(new SimpleGrantedAuthority(permission.getName())) - ); + role.getPermissions() + .forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getName()))); }); - + return authorities; } /** * Register a new customer (public registration) * Only allows CUSTOMER role creation through public endpoint + * * @param username Unique username - * @param email Unique email + * @param email Unique email * @param password Plain text password (will be encoded) * @return The created customer user * @throws RuntimeException if username or email already exists @@ -122,15 +131,15 @@ public User registerCustomer(String username, String email, String password) { if (userRepository.findByUsername(username).isPresent()) { throw new IllegalArgumentException("Username already exists: " + username); } - + // Validate email doesn't exist if (userRepository.findByEmail(email).isPresent()) { throw new IllegalArgumentException("Email already exists: " + email); } // Get CUSTOMER role from database - Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) - .orElseThrow(() -> new EntityNotFoundException("Customer role not found")); + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new EntityNotFoundException("Customer role not found")); // Create user with CUSTOMER role only User user = User.builder() @@ -147,77 +156,96 @@ public User registerCustomer(String username, String email, String password) { /** * Create an employee account (admin only) * Only admins can call this method + * * @param username Unique username - * @param email Unique email + * @param email Unique email * @param password Plain text password (will be encoded) + * @param fullName Full name of the employee * @return The created employee user * @throws RuntimeException if username or email already exists */ - public User createEmployee(String username, String email, String password) { + public User createEmployee(String username, String email, String password, String fullName) { // Validate username doesn't exist if (userRepository.findByUsername(username).isPresent()) { throw new IllegalArgumentException("Username already exists: " + username); } - + // Validate email doesn't exist if (userRepository.findByEmail(email).isPresent()) { throw new IllegalArgumentException("Email already exists: " + email); } // Get EMPLOYEE role from database - Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) - .orElseThrow(() -> new EntityNotFoundException("Employee role not found")); + Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) + .orElseThrow(() -> new EntityNotFoundException("Employee role not found")); // Create user with EMPLOYEE role User user = User.builder() .username(username) .email(email) .password(passwordEncoder.encode(password)) + .fullName(fullName) .enabled(true) .roles(Set.of(employeeRole)) // Only EMPLOYEE role .build(); - return userRepository.save(user); + User savedUser = userRepository.save(user); + + // Create verification token and send email + String token = tokenService.createVerificationToken(savedUser); + emailService.sendVerificationEmail(savedUser.getEmail(), savedUser.getFullName(), token); + + return savedUser; } /** * Create an admin account (admin only) * Only existing admins can call this method + * * @param username Unique username - * @param email Unique email + * @param email Unique email * @param password Plain text password (will be encoded) + * @param fullName Full name of the admin * @return The created admin user * @throws RuntimeException if username or email already exists */ - public User createAdmin(String username, String email, String password) { + public User createAdmin(String username, String email, String password, String fullName) { // Validate username doesn't exist if (userRepository.findByUsername(username).isPresent()) { throw new IllegalArgumentException("Username already exists: " + username); } - + // Validate email doesn't exist if (userRepository.findByEmail(email).isPresent()) { throw new IllegalArgumentException("Email already exists: " + email); } // Get ADMIN role from database - Role adminRole = roleRepository.findByName(RoleName.ADMIN) - .orElseThrow(() -> new EntityNotFoundException("Admin role not found")); + Role adminRole = roleRepository.findByName(RoleName.ADMIN) + .orElseThrow(() -> new EntityNotFoundException("Admin role not found")); // Create user with ADMIN role User user = User.builder() .username(username) .email(email) .password(passwordEncoder.encode(password)) + .fullName(fullName) .enabled(true) .roles(Set.of(adminRole)) // Only ADMIN role .build(); - return userRepository.save(user); + User savedUser = userRepository.save(user); + + // Create verification token and send email + String token = tokenService.createVerificationToken(savedUser); + emailService.sendVerificationEmail(savedUser.getEmail(), savedUser.getFullName(), token); + + return savedUser; } /** * Find user by username + * * @param username Username to search for * @return Optional containing user if found */ @@ -227,6 +255,7 @@ public Optional findByUsername(String username) { /** * Find user by email + * * @param email Email to search for * @return Optional containing user if found */ @@ -236,6 +265,7 @@ public Optional findByEmail(String email) { /** * Get all users in the system (admin only) + * * @return List of all users */ public List findAllUsers() { @@ -244,13 +274,14 @@ public List findAllUsers() { /** * Get all permissions for a user (from all their roles) + * * @param username Username to get permissions for * @return Set of permission names */ public Set getUserPermissions(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + return user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(permission -> permission.getName()) @@ -259,13 +290,14 @@ public Set getUserPermissions(String username) { /** * Get all roles for a user + * * @param username Username to get roles for * @return Set of role names */ public Set getUserRoles(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + return user.getRoles().stream() .map(role -> role.getName().name()) .collect(Collectors.toSet()); @@ -273,6 +305,7 @@ public Set getUserRoles(String username) { /** * Enable a user account (admin only) + * * @param username Username to enable */ public void enableUser(String username) { @@ -284,6 +317,7 @@ public void enableUser(String username) { /** * Disable a user account (admin only) + * * @param username Username to disable */ public void disableUser(String username) { @@ -295,6 +329,7 @@ public void disableUser(String username) { /** * Delete a user from the system (admin only) + * * @param username Username to delete * @throws RuntimeException if user not found */ @@ -302,7 +337,8 @@ public void deleteUser(String username) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - // Clear all related records before deleting the user to avoid foreign key constraint issues + // Clear all related records before deleting the user to avoid foreign key + // constraint issues // Clear roles user.getRoles().clear(); @@ -329,7 +365,8 @@ public void deleteUser(String username) { * Resets failed attempts and lock timestamp if an entry exists. */ public void clearLoginLock(String username) { - java.util.Optional lockOpt = loginLockRepository.findByUsername(username); + java.util.Optional lockOpt = loginLockRepository + .findByUsername(username); if (lockOpt.isPresent()) { com.techtorque.auth_service.entity.LoginLock lock = lockOpt.get(); lock.setFailedAttempts(0); @@ -340,6 +377,7 @@ public void clearLoginLock(String username) { /** * Check if a user has a specific role + * * @param username Username to check * @param roleName Role to check for * @return true if user has the role @@ -347,21 +385,22 @@ public void clearLoginLock(String username) { public boolean hasRole(String username, RoleName roleName) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + return user.getRoles().stream() .anyMatch(role -> role.getName().equals(roleName)); } /** * Check if a user has a specific permission - * @param username Username to check + * + * @param username Username to check * @param permissionName Permission to check for * @return true if user has the permission through any of their roles */ public boolean hasPermission(String username, String permissionName) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + return user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .anyMatch(permission -> permission.getName().equals(permissionName)); @@ -369,17 +408,18 @@ public boolean hasPermission(String username, String permissionName) { /** * Update user details (admin only) - * @param username Username of the user to update + * + * @param username Username of the user to update * @param newUsername New username (optional) - * @param newEmail New email (optional) - * @param enabled New enabled status (optional) + * @param newEmail New email (optional) + * @param enabled New enabled status (optional) * @return Updated user * @throws RuntimeException if user not found or new values already exist */ public User updateUserDetails(String username, String newUsername, String newEmail, Boolean enabled) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + // Check if new username is provided and different if (newUsername != null && !newUsername.equals(user.getUsername())) { if (userRepository.existsByUsername(newUsername)) { @@ -387,7 +427,7 @@ public User updateUserDetails(String username, String newUsername, String newEma } user.setUsername(newUsername); } - + // Check if new email is provided and different if (newEmail != null && !newEmail.equals(user.getEmail())) { if (userRepository.existsByEmail(newEmail)) { @@ -395,51 +435,54 @@ public User updateUserDetails(String username, String newUsername, String newEma } user.setEmail(newEmail); } - + // Update enabled status if provided if (enabled != null) { user.setEnabled(enabled); } - + return userRepository.save(user); } /** * Reset a user's password (admin only) - * @param username Username whose password to reset + * + * @param username Username whose password to reset * @param newPassword New password (plain text) * @throws RuntimeException if user not found */ public void resetUserPassword(String username, String newPassword) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } /** * Change user's own password (requires current password verification) - * @param username Username of the user changing password + * + * @param username Username of the user changing password * @param currentPassword Current password for verification - * @param newPassword New password (plain text) + * @param newPassword New password (plain text) * @throws RuntimeException if user not found or current password is incorrect */ public void changeUserPassword(String username, String currentPassword, String newPassword) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + // Verify current password if (!passwordEncoder.matches(currentPassword, user.getPassword())) { throw new IllegalStateException("Current password is incorrect"); } - + user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } /** * Assign a role to a user (admin only) + * * @param username Username to assign role to * @param roleName Role name to assign * @throws RuntimeException if user or role not found, or role already assigned @@ -447,14 +490,14 @@ public void changeUserPassword(String username, String currentPassword, String n public void assignRoleToUser(String username, String roleName) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + RoleName roleNameEnum; try { roleNameEnum = RoleName.valueOf(roleName.toUpperCase()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid role name: " + roleName); } - + // Rule: Only a SUPER_ADMIN can assign the ADMIN role. if (roleNameEnum == RoleName.ADMIN) { Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); @@ -468,20 +511,21 @@ public void assignRoleToUser(String username, String roleName) { throw new AccessDeniedException("Permission denied: Only a SUPER_ADMIN can assign the ADMIN role."); } } - - Role role = roleRepository.findByName(roleNameEnum) - .orElseThrow(() -> new EntityNotFoundException("Role not found: " + roleName)); - + + Role role = roleRepository.findByName(roleNameEnum) + .orElseThrow(() -> new EntityNotFoundException("Role not found: " + roleName)); + if (user.getRoles().contains(role)) { throw new IllegalStateException("User already has role: " + roleName); } - + user.getRoles().add(role); userRepository.save(user); } /** * Revoke a role from a user (admin only) + * * @param username Username to revoke role from * @param roleName Role name to revoke * @throws RuntimeException if user or role not found, or role not assigned @@ -489,14 +533,14 @@ public void assignRoleToUser(String username, String roleName) { public void revokeRoleFromUser(String username, String roleName) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + RoleName roleNameEnum; try { roleNameEnum = RoleName.valueOf(roleName.toUpperCase()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid role name: " + roleName); } - + // Rule: A user cannot revoke their own SUPER_ADMIN role. if (roleNameEnum == RoleName.SUPER_ADMIN) { Authentication currentUser = SecurityContextHolder.getContext().getAuthentication(); @@ -506,28 +550,29 @@ public void revokeRoleFromUser(String username, String roleName) { String currentUsername = currentUser.getName(); if (currentUsername.equals(username)) { - throw new AccessDeniedException("Action denied: A SUPER_ADMIN cannot revoke their own SUPER_ADMIN role."); + throw new AccessDeniedException( + "Action denied: A SUPER_ADMIN cannot revoke their own SUPER_ADMIN role."); } } - Role role = roleRepository.findByName(roleNameEnum) - .orElseThrow(() -> new EntityNotFoundException("Role not found: " + roleName)); - - if (!user.getRoles().contains(role)) { - throw new IllegalStateException("User does not have role: " + roleName); - } - + Role role = roleRepository.findByName(roleNameEnum) + .orElseThrow(() -> new EntityNotFoundException("Role not found: " + roleName)); + + if (!user.getRoles().contains(role)) { + throw new IllegalStateException("User does not have role: " + roleName); + } + user.getRoles().remove(role); userRepository.save(user); } - + /** * Update user profile */ public User updateProfile(String username, String fullName, String phone, String address) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + if (fullName != null) { user.setFullName(fullName); } @@ -537,17 +582,17 @@ public User updateProfile(String username, String fullName, String phone, String if (address != null) { user.setAddress(address); } - + return userRepository.save(user); } - + /** * Update profile photo URL */ public User updateProfilePhoto(String username, String photoUrl) { User user = userRepository.findByUsername(username) .orElseThrow(() -> new EntityNotFoundException("User not found: " + username)); - + user.setProfilePhotoUrl(photoUrl); return userRepository.save(user); } diff --git a/auth-service/src/test/java/com/techtorque/auth_service/controller/UserControllerTest.java b/auth-service/src/test/java/com/techtorque/auth_service/controller/UserControllerTest.java new file mode 100644 index 0000000..dd67821 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/controller/UserControllerTest.java @@ -0,0 +1,46 @@ +package com.techtorque.auth_service.controller; + +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.service.UserService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService userService; + + @MockBean + private com.techtorque.auth_service.service.PreferencesService preferencesService; + + @Test + @WithMockUser(username = "testuser", roles = {"CUSTOMER"}) + void getCurrentUserProfile_whenAuthenticatedCustomer_shouldReturnProfile() throws Exception { + User user = new User(); + user.setUsername("testuser"); + user.setFullName("Test User"); + + when(userService.findByUsername(anyString())).thenReturn(Optional.of(user)); + + mockMvc.perform(get("/users/me")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value("testuser")) + .andExpect(jsonPath("$.fullName").value("Test User")); + } +} diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLockRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLockRepositoryTest.java new file mode 100644 index 0000000..f9a919d --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLockRepositoryTest.java @@ -0,0 +1,286 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.LoginLock; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for LoginLockRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class LoginLockRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private LoginLockRepository loginLockRepository; + + private LoginLock testLoginLock; + + @BeforeEach + void setUp() { + testLoginLock = LoginLock.builder() + .username("testuser") + .failedAttempts(3) + .lockUntil(LocalDateTime.now().plusMinutes(15)) + .build(); + } + + @Test + void save_WhenValidLoginLock_ShouldPersistSuccessfully() { + // When + LoginLock savedLock = loginLockRepository.save(testLoginLock); + + // Then + assertThat(savedLock.getId()).isNotNull(); + assertThat(savedLock.getUsername()).isEqualTo("testuser"); + assertThat(savedLock.getFailedAttempts()).isEqualTo(3); + assertThat(savedLock.getLockUntil()).isNotNull(); + } + + @Test + void save_WithDefaultFailedAttempts_ShouldUseZero() { + // Given + LoginLock lockWithDefaults = LoginLock.builder() + .username("newuser") + .build(); + + // When + LoginLock savedLock = loginLockRepository.save(lockWithDefaults); + + // Then + assertThat(savedLock.getId()).isNotNull(); + assertThat(savedLock.getUsername()).isEqualTo("newuser"); + assertThat(savedLock.getFailedAttempts()).isEqualTo(0); + assertThat(savedLock.getLockUntil()).isNull(); + } + + @Test + void findByUsername_WhenLockExists_ShouldReturnLock() { + // Given + entityManager.persistAndFlush(testLoginLock); + + // When + Optional result = loginLockRepository.findByUsername("testuser"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + assertThat(result.get().getFailedAttempts()).isEqualTo(3); + } + + @Test + void findByUsername_WhenLockDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = loginLockRepository.findByUsername("nonexistent"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByUsername_CaseSensitive_ShouldReturnEmpty() { + // Given + entityManager.persistAndFlush(testLoginLock); + + // When + Optional result = loginLockRepository.findByUsername("TESTUSER"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @Transactional + void deleteByUsername_WhenLockExists_ShouldDeleteLock() { + // Given + entityManager.persistAndFlush(testLoginLock); + + LoginLock anotherLock = LoginLock.builder() + .username("anotheruser") + .failedAttempts(1) + .build(); + entityManager.persistAndFlush(anotherLock); + + // When + loginLockRepository.deleteByUsername("testuser"); + entityManager.flush(); + + // Then + Optional deletedLock = loginLockRepository.findByUsername("testuser"); + Optional remainingLock = loginLockRepository.findByUsername("anotheruser"); + + assertThat(deletedLock).isEmpty(); + assertThat(remainingLock).isPresent(); + } + + @Test + void update_WhenLockExists_ShouldUpdateSuccessfully() { + // Given + LoginLock savedLock = entityManager.persistAndFlush(testLoginLock); + entityManager.detach(savedLock); + + // When + savedLock.setFailedAttempts(5); + savedLock.setLockUntil(LocalDateTime.now().plusMinutes(30)); + LoginLock updatedLock = loginLockRepository.save(savedLock); + + // Then + assertThat(updatedLock.getFailedAttempts()).isEqualTo(5); + assertThat(updatedLock.getLockUntil()).isNotNull(); + assertThat(updatedLock.getUsername()).isEqualTo("testuser"); // Should remain unchanged + } + + @Test + void findById_WhenLockExists_ShouldReturnLock() { + // Given + LoginLock savedLock = entityManager.persistAndFlush(testLoginLock); + + // When + Optional result = loginLockRepository.findById(savedLock.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedLock.getId()); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + } + + @Test + void findById_WhenLockDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = loginLockRepository.findById(999L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllLocks() { + // Given + LoginLock lock1 = LoginLock.builder() + .username("user1") + .failedAttempts(2) + .lockUntil(LocalDateTime.now().plusMinutes(10)) + .build(); + + LoginLock lock2 = LoginLock.builder() + .username("user2") + .failedAttempts(1) + .build(); + + entityManager.persistAndFlush(lock1); + entityManager.persistAndFlush(lock2); + + // When + List allLocks = loginLockRepository.findAll(); + + // Then + assertThat(allLocks).hasSize(2); + assertThat(allLocks).extracting("username") + .containsExactlyInAnyOrder("user1", "user2"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testLoginLock); + + LoginLock secondLock = LoginLock.builder() + .username("user2") + .failedAttempts(1) + .build(); + entityManager.persistAndFlush(secondLock); + + // When + long count = loginLockRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void save_WithoutLockUntil_ShouldPersistSuccessfully() { + // Given + LoginLock lockWithoutLockUntil = LoginLock.builder() + .username("testuser2") + .failedAttempts(1) + .lockUntil(null) + .build(); + + // When + LoginLock savedLock = loginLockRepository.save(lockWithoutLockUntil); + + // Then + assertThat(savedLock.getId()).isNotNull(); + assertThat(savedLock.getUsername()).isEqualTo("testuser2"); + assertThat(savedLock.getFailedAttempts()).isEqualTo(1); + assertThat(savedLock.getLockUntil()).isNull(); + } + + @Test + void save_WithZeroFailedAttempts_ShouldPersistSuccessfully() { + // Given + LoginLock lockWithZeroAttempts = LoginLock.builder() + .username("testuser3") + .failedAttempts(0) + .build(); + + // When + LoginLock savedLock = loginLockRepository.save(lockWithZeroAttempts); + + // Then + assertThat(savedLock.getId()).isNotNull(); + assertThat(savedLock.getUsername()).isEqualTo("testuser3"); + assertThat(savedLock.getFailedAttempts()).isEqualTo(0); + } + + @Test + void existsById_WhenLockExists_ShouldReturnTrue() { + // Given + LoginLock savedLock = entityManager.persistAndFlush(testLoginLock); + + // When + boolean exists = loginLockRepository.existsById(savedLock.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenLockDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = loginLockRepository.existsById(999L); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void deleteById_WhenLockExists_ShouldRemoveLock() { + // Given + LoginLock savedLock = entityManager.persistAndFlush(testLoginLock); + Long lockId = savedLock.getId(); + + // When + loginLockRepository.deleteById(lockId); + entityManager.flush(); + + // Then + Optional result = loginLockRepository.findById(lockId); + assertThat(result).isEmpty(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLogRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLogRepositoryTest.java new file mode 100644 index 0000000..0dbe12f --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/LoginLogRepositoryTest.java @@ -0,0 +1,240 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.LoginLog; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for LoginLogRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class LoginLogRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private LoginLogRepository loginLogRepository; + + private LoginLog testLoginLog; + + @BeforeEach + void setUp() { + testLoginLog = LoginLog.builder() + .username("testuser") + .success(true) + .ipAddress("192.168.1.1") + .userAgent("Mozilla/5.0 Test Browser") + .createdAt(LocalDateTime.now()) + .build(); + } + + @Test + void save_WhenValidLoginLog_ShouldPersistSuccessfully() { + // When + LoginLog savedLog = loginLogRepository.save(testLoginLog); + + // Then + assertThat(savedLog.getId()).isNotNull(); + assertThat(savedLog.getUsername()).isEqualTo("testuser"); + assertThat(savedLog.getSuccess()).isTrue(); + assertThat(savedLog.getIpAddress()).isEqualTo("192.168.1.1"); + assertThat(savedLog.getUserAgent()).isEqualTo("Mozilla/5.0 Test Browser"); + assertThat(savedLog.getCreatedAt()).isNotNull(); + } + + @Test + void save_WithFailedLogin_ShouldPersistSuccessfully() { + // Given + LoginLog failedLog = LoginLog.builder() + .username("testuser") + .success(false) + .ipAddress("192.168.1.2") + .userAgent("Chrome Test") + .createdAt(LocalDateTime.now()) + .build(); + + // When + LoginLog savedLog = loginLogRepository.save(failedLog); + + // Then + assertThat(savedLog.getId()).isNotNull(); + assertThat(savedLog.getUsername()).isEqualTo("testuser"); + assertThat(savedLog.getSuccess()).isFalse(); + assertThat(savedLog.getIpAddress()).isEqualTo("192.168.1.2"); + assertThat(savedLog.getUserAgent()).isEqualTo("Chrome Test"); + } + + @Test + @Transactional + void deleteByUsername_WhenUserHasLogs_ShouldDeleteAllUserLogs() { + // Given + LoginLog log1 = LoginLog.builder() + .username("testuser") + .success(true) + .ipAddress("192.168.1.1") + .userAgent("Browser 1") + .createdAt(LocalDateTime.now()) + .build(); + + LoginLog log2 = LoginLog.builder() + .username("testuser") + .success(false) + .ipAddress("192.168.1.2") + .userAgent("Browser 2") + .createdAt(LocalDateTime.now()) + .build(); + + LoginLog anotherUserLog = LoginLog.builder() + .username("anotheruser") + .success(true) + .ipAddress("192.168.1.3") + .userAgent("Browser 3") + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(log1); + entityManager.persistAndFlush(log2); + entityManager.persistAndFlush(anotherUserLog); + + // When + loginLogRepository.deleteByUsername("testuser"); + entityManager.flush(); + + // Then + List remainingLogs = loginLogRepository.findAll(); + assertThat(remainingLogs).hasSize(1); + assertThat(remainingLogs.get(0).getUsername()).isEqualTo("anotheruser"); + } + + @Test + void findById_WhenLogExists_ShouldReturnLog() { + // Given + LoginLog savedLog = entityManager.persistAndFlush(testLoginLog); + + // When + Optional result = loginLogRepository.findById(savedLog.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedLog.getId()); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + } + + @Test + void findById_WhenLogDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = loginLogRepository.findById(999L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllLogs() { + // Given + LoginLog log1 = LoginLog.builder() + .username("user1") + .success(true) + .ipAddress("192.168.1.1") + .userAgent("Browser 1") + .createdAt(LocalDateTime.now()) + .build(); + + LoginLog log2 = LoginLog.builder() + .username("user2") + .success(false) + .ipAddress("192.168.1.2") + .userAgent("Browser 2") + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(log1); + entityManager.persistAndFlush(log2); + + // When + List allLogs = loginLogRepository.findAll(); + + // Then + assertThat(allLogs).hasSize(2); + assertThat(allLogs).extracting("username") + .containsExactlyInAnyOrder("user1", "user2"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testLoginLog); + + LoginLog secondLog = LoginLog.builder() + .username("user2") + .success(false) + .ipAddress("192.168.1.2") + .userAgent("Browser 2") + .createdAt(LocalDateTime.now()) + .build(); + entityManager.persistAndFlush(secondLog); + + // When + long count = loginLogRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void save_WithNullOptionalFields_ShouldPersistSuccessfully() { + // Given + LoginLog logWithNulls = LoginLog.builder() + .username("testuser") + .success(true) + .ipAddress(null) + .userAgent(null) + .createdAt(LocalDateTime.now()) + .build(); + + // When + LoginLog savedLog = loginLogRepository.save(logWithNulls); + + // Then + assertThat(savedLog.getId()).isNotNull(); + assertThat(savedLog.getUsername()).isEqualTo("testuser"); + assertThat(savedLog.getSuccess()).isTrue(); + assertThat(savedLog.getIpAddress()).isNull(); + assertThat(savedLog.getUserAgent()).isNull(); + } + + @Test + void existsById_WhenLogExists_ShouldReturnTrue() { + // Given + LoginLog savedLog = entityManager.persistAndFlush(testLoginLog); + + // When + boolean exists = loginLogRepository.existsById(savedLog.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenLogDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = loginLogRepository.existsById(999L); + + // Then + assertThat(exists).isFalse(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/PermissionRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/PermissionRepositoryTest.java new file mode 100644 index 0000000..c4905f3 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/PermissionRepositoryTest.java @@ -0,0 +1,316 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Permission; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for PermissionRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class PermissionRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private PermissionRepository permissionRepository; + + private Permission testPermission; + + @BeforeEach + void setUp() { + testPermission = Permission.builder() + .name("CREATE_USER") + .description("Permission to create new users") + .build(); + } + + @Test + void save_WhenValidPermission_ShouldPersistSuccessfully() { + // When + Permission savedPermission = permissionRepository.save(testPermission); + + // Then + assertThat(savedPermission.getId()).isNotNull(); + assertThat(savedPermission.getName()).isEqualTo("CREATE_USER"); + assertThat(savedPermission.getDescription()).isEqualTo("Permission to create new users"); + } + + @Test + void findByName_WhenPermissionExists_ShouldReturnPermission() { + // Given + entityManager.persistAndFlush(testPermission); + + // When + Optional result = permissionRepository.findByName("CREATE_USER"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getName()).isEqualTo("CREATE_USER"); + assertThat(result.get().getDescription()).isEqualTo("Permission to create new users"); + } + + @Test + void findByName_WhenPermissionDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = permissionRepository.findByName("NON_EXISTENT"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByName_CaseSensitive_ShouldReturnEmpty() { + // Given + entityManager.persistAndFlush(testPermission); + + // When + Optional result = permissionRepository.findByName("create_user"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByNameIn_WhenPermissionsExist_ShouldReturnMatchingPermissions() { + // Given + Permission permission1 = Permission.builder() + .name("READ_USER") + .description("Permission to read user data") + .build(); + + Permission permission2 = Permission.builder() + .name("UPDATE_USER") + .description("Permission to update user data") + .build(); + + Permission permission3 = Permission.builder() + .name("DELETE_USER") + .description("Permission to delete users") + .build(); + + entityManager.persistAndFlush(testPermission); + entityManager.persistAndFlush(permission1); + entityManager.persistAndFlush(permission2); + entityManager.persistAndFlush(permission3); + + // When + Set names = Set.of("CREATE_USER", "READ_USER", "NON_EXISTENT"); + Set result = permissionRepository.findByNameIn(names); + + // Then + assertThat(result).hasSize(2); + assertThat(result).extracting("name") + .containsExactlyInAnyOrder("CREATE_USER", "READ_USER"); + } + + @Test + void findByNameIn_WhenEmptySet_ShouldReturnEmptySet() { + // Given + entityManager.persistAndFlush(testPermission); + + // When + Set result = permissionRepository.findByNameIn(Set.of()); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByNameIn_WhenNoMatches_ShouldReturnEmptySet() { + // Given + entityManager.persistAndFlush(testPermission); + + // When + Set names = Set.of("NON_EXISTENT1", "NON_EXISTENT2"); + Set result = permissionRepository.findByNameIn(names); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void existsByName_WhenPermissionExists_ShouldReturnTrue() { + // Given + entityManager.persistAndFlush(testPermission); + + // When + boolean exists = permissionRepository.existsByName("CREATE_USER"); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsByName_WhenPermissionDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = permissionRepository.existsByName("NON_EXISTENT"); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void findById_WhenPermissionExists_ShouldReturnPermission() { + // Given + Permission savedPermission = entityManager.persistAndFlush(testPermission); + + // When + Optional result = permissionRepository.findById(savedPermission.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedPermission.getId()); + assertThat(result.get().getName()).isEqualTo("CREATE_USER"); + } + + @Test + void findById_WhenPermissionDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = permissionRepository.findById(999L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllPermissions() { + // Given + Permission permission1 = Permission.builder() + .name("READ_REPORTS") + .description("Permission to read reports") + .build(); + + Permission permission2 = Permission.builder() + .name("WRITE_REPORTS") + .description("Permission to write reports") + .build(); + + entityManager.persistAndFlush(permission1); + entityManager.persistAndFlush(permission2); + + // When + List allPermissions = permissionRepository.findAll(); + + // Then + assertThat(allPermissions).hasSize(2); + assertThat(allPermissions).extracting("name") + .containsExactlyInAnyOrder("READ_REPORTS", "WRITE_REPORTS"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testPermission); + + Permission secondPermission = Permission.builder() + .name("DELETE_USER") + .description("Permission to delete users") + .build(); + entityManager.persistAndFlush(secondPermission); + + // When + long count = permissionRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void update_WhenPermissionExists_ShouldUpdateSuccessfully() { + // Given + Permission savedPermission = entityManager.persistAndFlush(testPermission); + entityManager.detach(savedPermission); + + // When + savedPermission.setDescription("Updated description for creating users"); + Permission updatedPermission = permissionRepository.save(savedPermission); + + // Then + assertThat(updatedPermission.getDescription()).isEqualTo("Updated description for creating users"); + assertThat(updatedPermission.getName()).isEqualTo("CREATE_USER"); // Should remain unchanged + } + + @Test + void deleteById_WhenPermissionExists_ShouldRemovePermission() { + // Given + Permission savedPermission = entityManager.persistAndFlush(testPermission); + Long permissionId = savedPermission.getId(); + + // When + permissionRepository.deleteById(permissionId); + entityManager.flush(); + + // Then + Optional result = permissionRepository.findById(permissionId); + assertThat(result).isEmpty(); + } + + @Test + void save_WithNullDescription_ShouldPersistSuccessfully() { + // Given + Permission permissionWithoutDescription = Permission.builder() + .name("SIMPLE_PERMISSION") + .description(null) + .build(); + + // When + Permission savedPermission = permissionRepository.save(permissionWithoutDescription); + + // Then + assertThat(savedPermission.getId()).isNotNull(); + assertThat(savedPermission.getName()).isEqualTo("SIMPLE_PERMISSION"); + assertThat(savedPermission.getDescription()).isNull(); + } + + @Test + void save_WithEmptyDescription_ShouldPersistSuccessfully() { + // Given + Permission permissionWithEmptyDescription = Permission.builder() + .name("ANOTHER_PERMISSION") + .description("") + .build(); + + // When + Permission savedPermission = permissionRepository.save(permissionWithEmptyDescription); + + // Then + assertThat(savedPermission.getId()).isNotNull(); + assertThat(savedPermission.getName()).isEqualTo("ANOTHER_PERMISSION"); + assertThat(savedPermission.getDescription()).isEmpty(); + } + + @Test + void existsById_WhenPermissionExists_ShouldReturnTrue() { + // Given + Permission savedPermission = entityManager.persistAndFlush(testPermission); + + // When + boolean exists = permissionRepository.existsById(savedPermission.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenPermissionDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = permissionRepository.existsById(999L); + + // Then + assertThat(exists).isFalse(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/RefreshTokenRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/RefreshTokenRepositoryTest.java new file mode 100644 index 0000000..8990ed3 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/RefreshTokenRepositoryTest.java @@ -0,0 +1,357 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.RefreshToken; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for RefreshTokenRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class RefreshTokenRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private RefreshTokenRepository refreshTokenRepository; + + private User testUser; + private RefreshToken testToken; + + @BeforeEach + void setUp() { + // Create test role + Role testRole = Role.builder() + .name(RoleName.CUSTOMER) + .description("Test customer role") + .build(); + testRole = entityManager.persistAndFlush(testRole); + + // Create test user + testUser = User.builder() + .username("testuser") + .password("encodedPassword") + .email("test@example.com") + .fullName("Test User") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + testUser.addRole(testRole); + testUser = entityManager.persistAndFlush(testUser); + + // Create test refresh token + testToken = RefreshToken.builder() + .token("test-refresh-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .ipAddress("192.168.1.1") + .userAgent("Test User Agent") + .build(); + } + + @Test + void save_WhenValidToken_ShouldPersistSuccessfully() { + // When + RefreshToken savedToken = refreshTokenRepository.save(testToken); + + // Then + assertThat(savedToken.getId()).isNotNull(); + assertThat(savedToken.getToken()).isEqualTo("test-refresh-token"); + assertThat(savedToken.getUser().getId()).isEqualTo(testUser.getId()); + assertThat(savedToken.getExpiryDate()).isNotNull(); + assertThat(savedToken.getCreatedAt()).isNotNull(); + assertThat(savedToken.getIpAddress()).isEqualTo("192.168.1.1"); + assertThat(savedToken.getUserAgent()).isEqualTo("Test User Agent"); + assertThat(savedToken.getRevokedAt()).isNull(); + } + + @Test + void findByToken_WhenTokenExists_ShouldReturnToken() { + // Given + entityManager.persistAndFlush(testToken); + + // When + Optional result = refreshTokenRepository.findByToken("test-refresh-token"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getToken()).isEqualTo("test-refresh-token"); + assertThat(result.get().getUser().getId()).isEqualTo(testUser.getId()); + assertThat(result.get().getIpAddress()).isEqualTo("192.168.1.1"); + } + + @Test + void findByToken_WhenTokenDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = refreshTokenRepository.findByToken("non-existent-token"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @Transactional + void deleteByUser_WhenUserHasTokens_ShouldDeleteAllUserTokens() { + // Given + RefreshToken token1 = RefreshToken.builder() + .token("token-1") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + + RefreshToken token2 = RefreshToken.builder() + .token("token-2") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(token1); + entityManager.persistAndFlush(token2); + + // Create another user with a token to ensure we only delete current user's + // tokens + User anotherUser = User.builder() + .username("anotheruser") + .password("password") + .email("another@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + anotherUser = entityManager.persistAndFlush(anotherUser); + + RefreshToken anotherUserToken = RefreshToken.builder() + .token("another-token") + .user(anotherUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + entityManager.persistAndFlush(anotherUserToken); + + // When + refreshTokenRepository.deleteByUser(testUser); + entityManager.flush(); + + // Then + assertThat(refreshTokenRepository.findByToken("token-1")).isEmpty(); + assertThat(refreshTokenRepository.findByToken("token-2")).isEmpty(); + assertThat(refreshTokenRepository.findByToken("another-token")).isPresent(); + } + + @Test + @Transactional + void deleteExpiredTokens_WhenExpiredTokensExist_ShouldDeleteOnlyExpiredTokens() { + // Given + RefreshToken expiredToken1 = RefreshToken.builder() + .token("expired-token-1") + .user(testUser) + .expiryDate(LocalDateTime.now().minusDays(1)) // Expired + .createdAt(LocalDateTime.now().minusDays(2)) + .build(); + + RefreshToken expiredToken2 = RefreshToken.builder() + .token("expired-token-2") + .user(testUser) + .expiryDate(LocalDateTime.now().minusHours(1)) // Expired + .createdAt(LocalDateTime.now().minusDays(1)) + .build(); + + RefreshToken validToken = RefreshToken.builder() + .token("valid-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) // Not expired + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(expiredToken1); + entityManager.persistAndFlush(expiredToken2); + entityManager.persistAndFlush(validToken); + + // When + refreshTokenRepository.deleteExpiredTokens(LocalDateTime.now()); + entityManager.flush(); + + // Then + assertThat(refreshTokenRepository.findByToken("expired-token-1")).isEmpty(); + assertThat(refreshTokenRepository.findByToken("expired-token-2")).isEmpty(); + assertThat(refreshTokenRepository.findByToken("valid-token")).isPresent(); + } + + @Test + void isExpired_WhenTokenIsExpired_ShouldReturnTrue() { + // Given + RefreshToken expiredToken = RefreshToken.builder() + .token("expired-token") + .user(testUser) + .expiryDate(LocalDateTime.now().minusDays(1)) + .createdAt(LocalDateTime.now().minusDays(2)) + .build(); + + // When + boolean isExpired = expiredToken.isExpired(); + + // Then + assertThat(isExpired).isTrue(); + } + + @Test + void isExpired_WhenTokenIsNotExpired_ShouldReturnFalse() { + // Given + RefreshToken validToken = RefreshToken.builder() + .token("valid-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + + // When + boolean isExpired = validToken.isExpired(); + + // Then + assertThat(isExpired).isFalse(); + } + + @Test + void isRevoked_WhenTokenIsRevoked_ShouldReturnTrue() { + // Given + RefreshToken revokedToken = RefreshToken.builder() + .token("revoked-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .revokedAt(LocalDateTime.now()) + .build(); + + // When + boolean isRevoked = revokedToken.isRevoked(); + + // Then + assertThat(isRevoked).isTrue(); + } + + @Test + void isRevoked_WhenTokenIsNotRevoked_ShouldReturnFalse() { + // Given + RefreshToken activeToken = RefreshToken.builder() + .token("active-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .revokedAt(null) + .build(); + + // When + boolean isRevoked = activeToken.isRevoked(); + + // Then + assertThat(isRevoked).isFalse(); + } + + @Test + void findById_WhenTokenExists_ShouldReturnToken() { + // Given + RefreshToken savedToken = entityManager.persistAndFlush(testToken); + + // When + Optional result = refreshTokenRepository.findById(savedToken.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedToken.getId()); + assertThat(result.get().getToken()).isEqualTo("test-refresh-token"); + } + + @Test + void findById_WhenTokenDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = refreshTokenRepository.findById("non-existent-id"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void update_WhenTokenIsRevoked_ShouldUpdateRevokedAt() { + // Given + RefreshToken savedToken = entityManager.persistAndFlush(testToken); + entityManager.detach(savedToken); + + // When + LocalDateTime revokedTime = LocalDateTime.now(); + savedToken.setRevokedAt(revokedTime); + RefreshToken updatedToken = refreshTokenRepository.save(savedToken); + + // Then + assertThat(updatedToken.getRevokedAt()).isEqualTo(revokedTime); + assertThat(updatedToken.isRevoked()).isTrue(); + } + + @Test + void findAll_ShouldReturnAllTokens() { + // Given + RefreshToken token1 = RefreshToken.builder() + .token("token-1") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + + RefreshToken token2 = RefreshToken.builder() + .token("token-2") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(token1); + entityManager.persistAndFlush(token2); + + // When + var allTokens = refreshTokenRepository.findAll(); + + // Then + assertThat(allTokens).hasSize(2); + assertThat(allTokens).extracting("token") + .containsExactlyInAnyOrder("token-1", "token-2"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testToken); + + RefreshToken secondToken = RefreshToken.builder() + .token("second-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + entityManager.persistAndFlush(secondToken); + + // When + long count = refreshTokenRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/RoleRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/RoleRepositoryTest.java new file mode 100644 index 0000000..4d35dbd --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/RoleRepositoryTest.java @@ -0,0 +1,268 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for RoleRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class RoleRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private RoleRepository roleRepository; + + private Role testRole; + + @BeforeEach + void setUp() { + testRole = Role.builder() + .name(RoleName.ADMIN) + .description("Administrator role with full access") + .build(); + } + + @Test + void findByName_WhenRoleExists_ShouldReturnRole() { + // Given + entityManager.persistAndFlush(testRole); + + // When + Optional result = roleRepository.findByName(RoleName.ADMIN); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getName()).isEqualTo(RoleName.ADMIN); + assertThat(result.get().getDescription()).isEqualTo("Administrator role with full access"); + } + + @Test + void findByName_WhenRoleDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = roleRepository.findByName(RoleName.SUPER_ADMIN); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void existsByName_WhenRoleExists_ShouldReturnTrue() { + // Given + entityManager.persistAndFlush(testRole); + + // When + boolean exists = roleRepository.existsByName(RoleName.ADMIN); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsByName_WhenRoleDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = roleRepository.existsByName(RoleName.CUSTOMER); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void save_WhenValidRole_ShouldPersistSuccessfully() { + // Given + Role newRole = Role.builder() + .name(RoleName.EMPLOYEE) + .description("Employee role with limited access") + .build(); + + // When + Role savedRole = roleRepository.save(newRole); + + // Then + assertThat(savedRole.getId()).isNotNull(); + assertThat(savedRole.getName()).isEqualTo(RoleName.EMPLOYEE); + assertThat(savedRole.getDescription()).isEqualTo("Employee role with limited access"); + } + + @Test + void save_AllRoleNames_ShouldPersistAllEnumValues() { + // Given & When & Then + for (RoleName roleName : RoleName.values()) { + Role role = Role.builder() + .name(roleName) + .description(roleName.name() + " role") + .build(); + + Role savedRole = roleRepository.save(role); + + assertThat(savedRole.getId()).isNotNull(); + assertThat(savedRole.getName()).isEqualTo(roleName); + assertThat(savedRole.getDescription()).isEqualTo(roleName.name() + " role"); + } + } + + @Test + void findById_WhenRoleExists_ShouldReturnRole() { + // Given + Role savedRole = entityManager.persistAndFlush(testRole); + + // When + Optional result = roleRepository.findById(savedRole.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedRole.getId()); + assertThat(result.get().getName()).isEqualTo(RoleName.ADMIN); + } + + @Test + void findById_WhenRoleDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = roleRepository.findById(999L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void deleteById_WhenRoleExists_ShouldRemoveRole() { + // Given + Role savedRole = entityManager.persistAndFlush(testRole); + Long roleId = savedRole.getId(); + + // When + roleRepository.deleteById(roleId); + entityManager.flush(); + + // Then + Optional result = roleRepository.findById(roleId); + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllRoles() { + // Given + Role adminRole = Role.builder() + .name(RoleName.ADMIN) + .description("Admin role") + .build(); + + Role customerRole = Role.builder() + .name(RoleName.CUSTOMER) + .description("Customer role") + .build(); + + entityManager.persistAndFlush(adminRole); + entityManager.persistAndFlush(customerRole); + + // When + var allRoles = roleRepository.findAll(); + + // Then + assertThat(allRoles).hasSize(2); + assertThat(allRoles).extracting("name") + .containsExactlyInAnyOrder(RoleName.ADMIN, RoleName.CUSTOMER); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testRole); + + Role secondRole = Role.builder() + .name(RoleName.EMPLOYEE) + .description("Employee role") + .build(); + entityManager.persistAndFlush(secondRole); + + // When + long count = roleRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void update_WhenRoleExists_ShouldUpdateSuccessfully() { + // Given + Role savedRole = entityManager.persistAndFlush(testRole); + entityManager.detach(savedRole); + + // When + savedRole.setDescription("Updated admin role description"); + Role updatedRole = roleRepository.save(savedRole); + + // Then + assertThat(updatedRole.getDescription()).isEqualTo("Updated admin role description"); + assertThat(updatedRole.getName()).isEqualTo(RoleName.ADMIN); // Should remain unchanged + } + + @Test + void existsById_WhenRoleExists_ShouldReturnTrue() { + // Given + Role savedRole = entityManager.persistAndFlush(testRole); + + // When + boolean exists = roleRepository.existsById(savedRole.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenRoleDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = roleRepository.existsById(999L); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void save_WithNullDescription_ShouldPersistSuccessfully() { + // Given + Role roleWithoutDescription = Role.builder() + .name(RoleName.CUSTOMER) + .description(null) + .build(); + + // When + Role savedRole = roleRepository.save(roleWithoutDescription); + + // Then + assertThat(savedRole.getId()).isNotNull(); + assertThat(savedRole.getName()).isEqualTo(RoleName.CUSTOMER); + assertThat(savedRole.getDescription()).isNull(); + } + + @Test + void save_WithEmptyDescription_ShouldPersistSuccessfully() { + // Given + Role roleWithEmptyDescription = Role.builder() + .name(RoleName.CUSTOMER) + .description("") + .build(); + + // When + Role savedRole = roleRepository.save(roleWithEmptyDescription); + + // Then + assertThat(savedRole.getId()).isNotNull(); + assertThat(savedRole.getName()).isEqualTo(RoleName.CUSTOMER); + assertThat(savedRole.getDescription()).isEmpty(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/UserPreferencesRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/UserPreferencesRepositoryTest.java new file mode 100644 index 0000000..3feb79a --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/UserPreferencesRepositoryTest.java @@ -0,0 +1,395 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.entity.UserPreferences; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for UserPreferencesRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class UserPreferencesRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private UserPreferencesRepository userPreferencesRepository; + + private User testUser; + private UserPreferences testPreferences; + + @BeforeEach + void setUp() { + // Create test role + Role testRole = Role.builder() + .name(RoleName.CUSTOMER) + .description("Test customer role") + .build(); + testRole = entityManager.persistAndFlush(testRole); + + // Create test user + testUser = User.builder() + .username("testuser") + .password("encodedPassword") + .email("test@example.com") + .fullName("Test User") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + testUser.addRole(testRole); + testUser = entityManager.persistAndFlush(testUser); + + // Create test user preferences + testPreferences = UserPreferences.builder() + .user(testUser) + .emailNotifications(true) + .smsNotifications(false) + .pushNotifications(true) + .language("en") + .appointmentReminders(true) + .serviceUpdates(true) + .marketingEmails(false) + .build(); + } + + @Test + void save_WhenValidPreferences_ShouldPersistSuccessfully() { + // When + UserPreferences savedPreferences = userPreferencesRepository.save(testPreferences); + + // Then + assertThat(savedPreferences.getId()).isNotNull(); + assertThat(savedPreferences.getUser().getId()).isEqualTo(testUser.getId()); + assertThat(savedPreferences.getEmailNotifications()).isTrue(); + assertThat(savedPreferences.getSmsNotifications()).isFalse(); + assertThat(savedPreferences.getPushNotifications()).isTrue(); + assertThat(savedPreferences.getLanguage()).isEqualTo("en"); + assertThat(savedPreferences.getAppointmentReminders()).isTrue(); + assertThat(savedPreferences.getServiceUpdates()).isTrue(); + assertThat(savedPreferences.getMarketingEmails()).isFalse(); + } + + @Test + void save_WithDefaults_ShouldUseDefaultValues() { + // Given + UserPreferences preferencesWithDefaults = UserPreferences.builder() + .user(testUser) + .build(); + + // When + UserPreferences savedPreferences = userPreferencesRepository.save(preferencesWithDefaults); + + // Then + assertThat(savedPreferences.getId()).isNotNull(); + assertThat(savedPreferences.getEmailNotifications()).isTrue(); // Default + assertThat(savedPreferences.getSmsNotifications()).isFalse(); // Default + assertThat(savedPreferences.getPushNotifications()).isTrue(); // Default + assertThat(savedPreferences.getLanguage()).isEqualTo("en"); // Default + assertThat(savedPreferences.getAppointmentReminders()).isTrue(); // Default + assertThat(savedPreferences.getServiceUpdates()).isTrue(); // Default + assertThat(savedPreferences.getMarketingEmails()).isFalse(); // Default + } + + @Test + void findByUser_WhenPreferencesExist_ShouldReturnPreferences() { + // Given + entityManager.persistAndFlush(testPreferences); + + // When + Optional result = userPreferencesRepository.findByUser(testUser); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getUser().getId()).isEqualTo(testUser.getId()); + assertThat(result.get().getEmailNotifications()).isTrue(); + assertThat(result.get().getLanguage()).isEqualTo("en"); + } + + @Test + void findByUser_WhenPreferencesDoNotExist_ShouldReturnEmpty() { + // Given + User anotherUser = User.builder() + .username("anotheruser") + .password("password") + .email("another@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + anotherUser = entityManager.persistAndFlush(anotherUser); + + // When + Optional result = userPreferencesRepository.findByUser(anotherUser); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @Transactional + void deleteByUser_WhenPreferencesExist_ShouldDeleteUserPreferences() { + // Given + entityManager.persistAndFlush(testPreferences); + + // Create another user with preferences to ensure we only delete current user's + // preferences + User anotherUser = User.builder() + .username("anotheruser") + .password("password") + .email("another@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + anotherUser = entityManager.persistAndFlush(anotherUser); + + UserPreferences anotherPreferences = UserPreferences.builder() + .user(anotherUser) + .emailNotifications(false) + .language("fr") + .build(); + entityManager.persistAndFlush(anotherPreferences); + + // When + userPreferencesRepository.deleteByUser(testUser); + entityManager.flush(); + + // Then + Optional deletedPreferences = userPreferencesRepository.findByUser(testUser); + Optional remainingPreferences = userPreferencesRepository.findByUser(anotherUser); + + assertThat(deletedPreferences).isEmpty(); + assertThat(remainingPreferences).isPresent(); + } + + @Test + void update_WhenPreferencesExist_ShouldUpdateSuccessfully() { + // Given + UserPreferences savedPreferences = entityManager.persistAndFlush(testPreferences); + entityManager.detach(savedPreferences); + + // When + savedPreferences.setEmailNotifications(false); + savedPreferences.setSmsNotifications(true); + savedPreferences.setLanguage("es"); + savedPreferences.setMarketingEmails(true); + UserPreferences updatedPreferences = userPreferencesRepository.save(savedPreferences); + + // Then + assertThat(updatedPreferences.getEmailNotifications()).isFalse(); + assertThat(updatedPreferences.getSmsNotifications()).isTrue(); + assertThat(updatedPreferences.getLanguage()).isEqualTo("es"); + assertThat(updatedPreferences.getMarketingEmails()).isTrue(); + assertThat(updatedPreferences.getUser().getId()).isEqualTo(testUser.getId()); // Should remain unchanged + } + + @Test + void findById_WhenPreferencesExist_ShouldReturnPreferences() { + // Given + UserPreferences savedPreferences = entityManager.persistAndFlush(testPreferences); + + // When + Optional result = userPreferencesRepository.findById(savedPreferences.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedPreferences.getId()); + assertThat(result.get().getUser().getId()).isEqualTo(testUser.getId()); + } + + @Test + void findById_WhenPreferencesDoNotExist_ShouldReturnEmpty() { + // When + Optional result = userPreferencesRepository.findById("non-existent-id"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllPreferences() { + // Given + User user1 = User.builder() + .username("user1") + .password("password") + .email("user1@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + user1 = entityManager.persistAndFlush(user1); + + User user2 = User.builder() + .username("user2") + .password("password") + .email("user2@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + user2 = entityManager.persistAndFlush(user2); + + UserPreferences prefs1 = UserPreferences.builder() + .user(user1) + .language("en") + .emailNotifications(true) + .build(); + + UserPreferences prefs2 = UserPreferences.builder() + .user(user2) + .language("fr") + .emailNotifications(false) + .build(); + + entityManager.persistAndFlush(prefs1); + entityManager.persistAndFlush(prefs2); + + // When + List allPreferences = userPreferencesRepository.findAll(); + + // Then + assertThat(allPreferences).hasSize(2); + assertThat(allPreferences).extracting("language") + .containsExactlyInAnyOrder("en", "fr"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testPreferences); + + User anotherUser = User.builder() + .username("user2") + .password("password") + .email("user2@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + anotherUser = entityManager.persistAndFlush(anotherUser); + + UserPreferences secondPreferences = UserPreferences.builder() + .user(anotherUser) + .language("fr") + .build(); + entityManager.persistAndFlush(secondPreferences); + + // When + long count = userPreferencesRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void save_WithDifferentLanguages_ShouldPersistSuccessfully() { + // Given + String[] languages = { "en", "es", "fr", "de", "it", "pt", "ja", "zh", "ko", "ar" }; + + for (int i = 0; i < languages.length; i++) { + User user = User.builder() + .username("user" + i) + .password("password") + .email("user" + i + "@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + user = entityManager.persistAndFlush(user); + + UserPreferences preferences = UserPreferences.builder() + .user(user) + .language(languages[i]) + .build(); + + // When + UserPreferences savedPreferences = userPreferencesRepository.save(preferences); + + // Then + assertThat(savedPreferences.getId()).isNotNull(); + assertThat(savedPreferences.getLanguage()).isEqualTo(languages[i]); + assertThat(savedPreferences.getUser().getId()).isEqualTo(user.getId()); + } + } + + @Test + void save_WithAllNotificationsDisabled_ShouldPersistSuccessfully() { + // Given + UserPreferences allDisabledPreferences = UserPreferences.builder() + .user(testUser) + .emailNotifications(false) + .smsNotifications(false) + .pushNotifications(false) + .appointmentReminders(false) + .serviceUpdates(false) + .marketingEmails(false) + .language("en") + .build(); + + // When + UserPreferences savedPreferences = userPreferencesRepository.save(allDisabledPreferences); + + // Then + assertThat(savedPreferences.getId()).isNotNull(); + assertThat(savedPreferences.getEmailNotifications()).isFalse(); + assertThat(savedPreferences.getSmsNotifications()).isFalse(); + assertThat(savedPreferences.getPushNotifications()).isFalse(); + assertThat(savedPreferences.getAppointmentReminders()).isFalse(); + assertThat(savedPreferences.getServiceUpdates()).isFalse(); + assertThat(savedPreferences.getMarketingEmails()).isFalse(); + } + + @Test + void existsById_WhenPreferencesExist_ShouldReturnTrue() { + // Given + UserPreferences savedPreferences = entityManager.persistAndFlush(testPreferences); + + // When + boolean exists = userPreferencesRepository.existsById(savedPreferences.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenPreferencesDoNotExist_ShouldReturnFalse() { + // When + boolean exists = userPreferencesRepository.existsById("non-existent-id"); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void deleteById_WhenPreferencesExist_ShouldRemovePreferences() { + // Given + UserPreferences savedPreferences = entityManager.persistAndFlush(testPreferences); + String preferencesId = savedPreferences.getId(); + + // When + userPreferencesRepository.deleteById(preferencesId); + entityManager.flush(); + + // Then + Optional result = userPreferencesRepository.findById(preferencesId); + assertThat(result).isEmpty(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/UserRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/UserRepositoryTest.java new file mode 100644 index 0000000..208abbf --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/UserRepositoryTest.java @@ -0,0 +1,382 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for UserRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class UserRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + private User testUser; + private Role testRole; + + @BeforeEach + void setUp() { + // Create test role + testRole = Role.builder() + .name(RoleName.CUSTOMER) + .description("Test customer role") + .build(); + testRole = entityManager.persistAndFlush(testRole); + + // Create test user + testUser = User.builder() + .username("testuser") + .password("encodedPassword") + .email("test@example.com") + .fullName("Test User") + .phone("1234567890") + .address("123 Test Street") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + testUser.addRole(testRole); + } + + @Test + void findByUsername_WhenUserExists_ShouldReturnUser() { + // Given + entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findByUsername("testuser"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + assertThat(result.get().getEmail()).isEqualTo("test@example.com"); + assertThat(result.get().getFullName()).isEqualTo("Test User"); + } + + @Test + void findByUsername_WhenUserDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = userRepository.findByUsername("nonexistent"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByUsername_CaseSensitive_ShouldReturnEmpty() { + // Given + entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findByUsername("TESTUSER"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByEmail_WhenEmailExists_ShouldReturnUser() { + // Given + entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findByEmail("test@example.com"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getEmail()).isEqualTo("test@example.com"); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + } + + @Test + void findByEmail_WhenEmailDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = userRepository.findByEmail("nonexistent@example.com"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByEmail_CaseSensitive_ShouldReturnEmpty() { + // Given + entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findByEmail("TEST@EXAMPLE.COM"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void existsByUsername_WhenUsernameExists_ShouldReturnTrue() { + // Given + entityManager.persistAndFlush(testUser); + + // When + boolean exists = userRepository.existsByUsername("testuser"); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsByUsername_WhenUsernameDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = userRepository.existsByUsername("nonexistent"); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void existsByEmail_WhenEmailExists_ShouldReturnTrue() { + // Given + entityManager.persistAndFlush(testUser); + + // When + boolean exists = userRepository.existsByEmail("test@example.com"); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsByEmail_WhenEmailDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = userRepository.existsByEmail("nonexistent@example.com"); + + // Then + assertThat(exists).isFalse(); + } + + @Test + void findByUsernameWithRoles_WhenUserExists_ShouldReturnUserWithRoles() { + // Given + entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findByUsernameWithRoles("testuser"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + assertThat(result.get().getRoles()).isNotEmpty(); + assertThat(result.get().getRoles()).hasSize(1); + assertThat(result.get().getRoles().iterator().next().getName()).isEqualTo(RoleName.CUSTOMER); + } + + @Test + void findByUsernameWithRoles_WhenUserDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = userRepository.findByUsernameWithRoles("nonexistent"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void save_WhenValidUser_ShouldPersistSuccessfully() { + // Given + User newUser = User.builder() + .username("newuser") + .password("password") + .email("newuser@example.com") + .fullName("New User") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + + // When + User savedUser = userRepository.save(newUser); + + // Then + assertThat(savedUser.getId()).isNotNull(); + assertThat(savedUser.getUsername()).isEqualTo("newuser"); + assertThat(savedUser.getEmail()).isEqualTo("newuser@example.com"); + assertThat(savedUser.getEnabled()).isTrue(); + assertThat(savedUser.getEmailVerified()).isFalse(); + } + + @Test + void save_WithProfilePhoto_ShouldPersistPhotoData() { + // Given + byte[] photoData = "fake-image-data".getBytes(); + User userWithPhoto = User.builder() + .username("photouser") + .password("password") + .email("photo@example.com") + .profilePhoto(photoData) + .profilePhotoMimeType("image/jpeg") + .profilePhotoUpdatedAt(LocalDateTime.now()) + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + + // When + User savedUser = userRepository.save(userWithPhoto); + + // Then + assertThat(savedUser.getId()).isNotNull(); + assertThat(savedUser.getProfilePhoto()).isEqualTo(photoData); + assertThat(savedUser.getProfilePhotoMimeType()).isEqualTo("image/jpeg"); + assertThat(savedUser.getProfilePhotoUpdatedAt()).isNotNull(); + } + + @Test + void findById_WhenUserExists_ShouldReturnUser() { + // Given + User savedUser = entityManager.persistAndFlush(testUser); + + // When + Optional result = userRepository.findById(savedUser.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedUser.getId()); + assertThat(result.get().getUsername()).isEqualTo("testuser"); + } + + @Test + void findById_WhenUserDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = userRepository.findById(999L); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void deleteById_WhenUserExists_ShouldRemoveUser() { + // Given + User savedUser = entityManager.persistAndFlush(testUser); + Long userId = savedUser.getId(); + + // When + userRepository.deleteById(userId); + entityManager.flush(); + + // Then + Optional result = userRepository.findById(userId); + assertThat(result).isEmpty(); + } + + @Test + void findAll_ShouldReturnAllUsers() { + // Given + User user1 = User.builder() + .username("user1") + .password("password1") + .email("user1@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + + User user2 = User.builder() + .username("user2") + .password("password2") + .email("user2@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + + entityManager.persistAndFlush(user1); + entityManager.persistAndFlush(user2); + + // When + var allUsers = userRepository.findAll(); + + // Then + assertThat(allUsers).hasSize(2); + assertThat(allUsers).extracting("username") + .containsExactlyInAnyOrder("user1", "user2"); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testUser); + + User secondUser = User.builder() + .username("user2") + .password("password2") + .email("user2@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + entityManager.persistAndFlush(secondUser); + + // When + long count = userRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } + + @Test + void update_WhenUserExists_ShouldUpdateSuccessfully() { + // Given + User savedUser = entityManager.persistAndFlush(testUser); + entityManager.detach(savedUser); + + // When + savedUser.setFullName("Updated Name"); + savedUser.setPhone("9876543210"); + savedUser.setEmailVerified(true); + User updatedUser = userRepository.save(savedUser); + + // Then + assertThat(updatedUser.getFullName()).isEqualTo("Updated Name"); + assertThat(updatedUser.getPhone()).isEqualTo("9876543210"); + assertThat(updatedUser.getEmailVerified()).isTrue(); + assertThat(updatedUser.getUsername()).isEqualTo("testuser"); // Should remain unchanged + } + + @Test + void existsById_WhenUserExists_ShouldReturnTrue() { + // Given + User savedUser = entityManager.persistAndFlush(testUser); + + // When + boolean exists = userRepository.existsById(savedUser.getId()); + + // Then + assertThat(exists).isTrue(); + } + + @Test + void existsById_WhenUserDoesNotExist_ShouldReturnFalse() { + // When + boolean exists = userRepository.existsById(999L); + + // Then + assertThat(exists).isFalse(); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/repository/VerificationTokenRepositoryTest.java b/auth-service/src/test/java/com/techtorque/auth_service/repository/VerificationTokenRepositoryTest.java new file mode 100644 index 0000000..d4c6043 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/repository/VerificationTokenRepositoryTest.java @@ -0,0 +1,388 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.entity.VerificationToken; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Comprehensive test class for VerificationTokenRepository + * Tests all repository methods, edge cases, and database constraints + */ +@DataJpaTest +@ActiveProfiles("test") +class VerificationTokenRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private VerificationTokenRepository verificationTokenRepository; + + private User testUser; + private VerificationToken testToken; + + @BeforeEach + void setUp() { + // Create test role + Role testRole = Role.builder() + .name(RoleName.CUSTOMER) + .description("Test customer role") + .build(); + testRole = entityManager.persistAndFlush(testRole); + + // Create test user + testUser = User.builder() + .username("testuser") + .password("encodedPassword") + .email("test@example.com") + .fullName("Test User") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + testUser.addRole(testRole); + testUser = entityManager.persistAndFlush(testUser); + + // Create test verification token + testToken = VerificationToken.builder() + .token("test-verification-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + } + + @Test + void save_WhenValidToken_ShouldPersistSuccessfully() { + // When + VerificationToken savedToken = verificationTokenRepository.save(testToken); + + // Then + assertThat(savedToken.getId()).isNotNull(); + assertThat(savedToken.getToken()).isEqualTo("test-verification-token"); + assertThat(savedToken.getUser().getId()).isEqualTo(testUser.getId()); + assertThat(savedToken.getExpiryDate()).isNotNull(); + assertThat(savedToken.getCreatedAt()).isNotNull(); + assertThat(savedToken.getTokenType()).isEqualTo(VerificationToken.TokenType.EMAIL_VERIFICATION); + assertThat(savedToken.getUsedAt()).isNull(); + } + + @Test + void findByToken_WhenTokenExists_ShouldReturnToken() { + // Given + entityManager.persistAndFlush(testToken); + + // When + Optional result = verificationTokenRepository.findByToken("test-verification-token"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getToken()).isEqualTo("test-verification-token"); + assertThat(result.get().getUser().getId()).isEqualTo(testUser.getId()); + assertThat(result.get().getTokenType()).isEqualTo(VerificationToken.TokenType.EMAIL_VERIFICATION); + } + + @Test + void findByToken_WhenTokenDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = verificationTokenRepository.findByToken("non-existent-token"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByUserAndTokenType_WhenTokenExists_ShouldReturnToken() { + // Given + entityManager.persistAndFlush(testToken); + + // When + Optional result = verificationTokenRepository + .findByUserAndTokenType(testUser, VerificationToken.TokenType.EMAIL_VERIFICATION); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getUser().getId()).isEqualTo(testUser.getId()); + assertThat(result.get().getTokenType()).isEqualTo(VerificationToken.TokenType.EMAIL_VERIFICATION); + } + + @Test + void findByUserAndTokenType_WhenTokenDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = verificationTokenRepository + .findByUserAndTokenType(testUser, VerificationToken.TokenType.PASSWORD_RESET); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void findByUserAndTokenType_WhenDifferentTokenType_ShouldReturnEmpty() { + // Given + entityManager.persistAndFlush(testToken); + + // When + Optional result = verificationTokenRepository + .findByUserAndTokenType(testUser, VerificationToken.TokenType.PASSWORD_RESET); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @Transactional + void deleteByUser_WhenUserHasTokens_ShouldDeleteAllUserTokens() { + // Given + VerificationToken emailToken = VerificationToken.builder() + .token("email-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + VerificationToken passwordToken = VerificationToken.builder() + .token("password-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusHours(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + + entityManager.persistAndFlush(emailToken); + entityManager.persistAndFlush(passwordToken); + + // Create another user with a token to ensure we only delete current user's + // tokens + User anotherUser = User.builder() + .username("anotheruser") + .password("password") + .email("another@example.com") + .enabled(true) + .emailVerified(false) + .createdAt(LocalDateTime.now()) + .build(); + anotherUser = entityManager.persistAndFlush(anotherUser); + + VerificationToken anotherUserToken = VerificationToken.builder() + .token("another-token") + .user(anotherUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + entityManager.persistAndFlush(anotherUserToken); + + // When + verificationTokenRepository.deleteByUser(testUser); + entityManager.flush(); + + // Then + assertThat(verificationTokenRepository.findByToken("email-token")).isEmpty(); + assertThat(verificationTokenRepository.findByToken("password-token")).isEmpty(); + assertThat(verificationTokenRepository.findByToken("another-token")).isPresent(); + } + + @Test + void isExpired_WhenTokenIsExpired_ShouldReturnTrue() { + // Given + VerificationToken expiredToken = VerificationToken.builder() + .token("expired-token") + .user(testUser) + .expiryDate(LocalDateTime.now().minusDays(1)) + .createdAt(LocalDateTime.now().minusDays(2)) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + // When + boolean isExpired = expiredToken.isExpired(); + + // Then + assertThat(isExpired).isTrue(); + } + + @Test + void isExpired_WhenTokenIsNotExpired_ShouldReturnFalse() { + // Given + VerificationToken validToken = VerificationToken.builder() + .token("valid-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + // When + boolean isExpired = validToken.isExpired(); + + // Then + assertThat(isExpired).isFalse(); + } + + @Test + void isUsed_WhenTokenIsUsed_ShouldReturnTrue() { + // Given + VerificationToken usedToken = VerificationToken.builder() + .token("used-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .usedAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + // When + boolean isUsed = usedToken.isUsed(); + + // Then + assertThat(isUsed).isTrue(); + } + + @Test + void isUsed_WhenTokenIsNotUsed_ShouldReturnFalse() { + // Given + VerificationToken unusedToken = VerificationToken.builder() + .token("unused-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .usedAt(null) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + // When + boolean isUsed = unusedToken.isUsed(); + + // Then + assertThat(isUsed).isFalse(); + } + + @Test + void save_WithPasswordResetType_ShouldPersistSuccessfully() { + // Given + VerificationToken passwordResetToken = VerificationToken.builder() + .token("password-reset-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusHours(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + + // When + VerificationToken savedToken = verificationTokenRepository.save(passwordResetToken); + + // Then + assertThat(savedToken.getId()).isNotNull(); + assertThat(savedToken.getToken()).isEqualTo("password-reset-token"); + assertThat(savedToken.getTokenType()).isEqualTo(VerificationToken.TokenType.PASSWORD_RESET); + } + + @Test + void findById_WhenTokenExists_ShouldReturnToken() { + // Given + VerificationToken savedToken = entityManager.persistAndFlush(testToken); + + // When + Optional result = verificationTokenRepository.findById(savedToken.getId()); + + // Then + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(savedToken.getId()); + assertThat(result.get().getToken()).isEqualTo("test-verification-token"); + } + + @Test + void findById_WhenTokenDoesNotExist_ShouldReturnEmpty() { + // When + Optional result = verificationTokenRepository.findById("non-existent-id"); + + // Then + assertThat(result).isEmpty(); + } + + @Test + void update_WhenTokenIsUsed_ShouldUpdateUsedAt() { + // Given + VerificationToken savedToken = entityManager.persistAndFlush(testToken); + entityManager.detach(savedToken); + + // When + LocalDateTime usedTime = LocalDateTime.now(); + savedToken.setUsedAt(usedTime); + VerificationToken updatedToken = verificationTokenRepository.save(savedToken); + + // Then + assertThat(updatedToken.getUsedAt()).isEqualTo(usedTime); + assertThat(updatedToken.isUsed()).isTrue(); + } + + @Test + void findAll_ShouldReturnAllTokens() { + // Given + VerificationToken emailToken = VerificationToken.builder() + .token("email-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + VerificationToken passwordToken = VerificationToken.builder() + .token("password-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusHours(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + + entityManager.persistAndFlush(emailToken); + entityManager.persistAndFlush(passwordToken); + + // When + var allTokens = verificationTokenRepository.findAll(); + + // Then + assertThat(allTokens).hasSize(2); + assertThat(allTokens).extracting("token") + .containsExactlyInAnyOrder("email-token", "password-token"); + assertThat(allTokens).extracting("tokenType") + .containsExactlyInAnyOrder( + VerificationToken.TokenType.EMAIL_VERIFICATION, + VerificationToken.TokenType.PASSWORD_RESET); + } + + @Test + void count_ShouldReturnCorrectCount() { + // Given + entityManager.persistAndFlush(testToken); + + VerificationToken secondToken = VerificationToken.builder() + .token("second-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(1)) + .createdAt(LocalDateTime.now()) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + entityManager.persistAndFlush(secondToken); + + // When + long count = verificationTokenRepository.count(); + + // Then + assertThat(count).isEqualTo(2); + } +} \ No newline at end of file diff --git a/auth-service/src/test/java/com/techtorque/auth_service/service/AuthServiceTest.java b/auth-service/src/test/java/com/techtorque/auth_service/service/AuthServiceTest.java new file mode 100644 index 0000000..e3d2e02 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/service/AuthServiceTest.java @@ -0,0 +1,596 @@ +package com.techtorque.auth_service.service; + +import com.techtorque.auth_service.dto.request.LoginRequest; +import com.techtorque.auth_service.dto.request.RegisterRequest; +import com.techtorque.auth_service.dto.response.LoginResponse; +import com.techtorque.auth_service.entity.*; +import com.techtorque.auth_service.repository.*; +import com.techtorque.auth_service.util.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; +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.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.*; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Comprehensive test class for AuthService + * Tests authentication, registration, token management, and security features + */ +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @Mock + private AuthenticationManager authenticationManager; + + @Mock + private UserRepository userRepository; + + @Mock + private RoleRepository roleRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private JwtUtil jwtUtil; + + @Mock + private LoginLockRepository loginLockRepository; + + @Mock + private LoginLogRepository loginLogRepository; + + @Mock + private LoginAuditService loginAuditService; + + @Mock + private TokenService tokenService; + + @Mock + private EmailService emailService; + + @Mock + private HttpServletRequest request; + + @Mock + private Authentication authentication; + + @Mock + private UserDetails userDetails; + + @InjectMocks + private AuthService authService; + + private User testUser; + private Role customerRole; + private Role adminRole; + private LoginRequest loginRequest; + private RegisterRequest registerRequest; + private LoginLock loginLock; + private VerificationToken verificationToken; + private RefreshToken refreshToken; + + @BeforeEach + void setUp() { + // Set up test configuration + ReflectionTestUtils.setField(authService, "maxFailedAttempts", 3); + ReflectionTestUtils.setField(authService, "lockDurationMinutes", 15L); + + // Mock request IP address (lenient to avoid unnecessary stubbing warnings) + lenient().when(request.getRemoteAddr()).thenReturn("192.168.1.1"); + lenient().when(request.getHeader("X-Forwarded-For")).thenReturn(null); + + // Create test roles + customerRole = Role.builder() + .id(1L) + .name(RoleName.CUSTOMER) + .description("Customer role") + .permissions(Collections.emptySet()) + .build(); + + adminRole = Role.builder() + .id(2L) + .name(RoleName.ADMIN) + .description("Admin role") + .permissions(Collections.emptySet()) + .build(); + + // Create test user + testUser = User.builder() + .id(1L) + .username("testuser") + .email("test@example.com") + .password("encoded-password") + .fullName("Test User") + .phone("1234567890") + .address("Test Address") + .enabled(true) + .emailVerified(true) + .createdAt(LocalDateTime.now()) + .roles(Set.of(customerRole)) + .build(); + + // Create test requests + loginRequest = new LoginRequest(); + loginRequest.setUsername("testuser"); + loginRequest.setPassword("password"); + + registerRequest = new RegisterRequest(); + registerRequest.setEmail("new@example.com"); + registerRequest.setPassword("newpassword"); + registerRequest.setFullName("New User"); + registerRequest.setPhone("9876543210"); + registerRequest.setAddress("New Address"); + + // Create test login lock + loginLock = LoginLock.builder() + .id(1L) + .username("testuser") + .failedAttempts(0) + .lockUntil(null) + .build(); + + // Create test verification token + verificationToken = VerificationToken.builder() + .id("token-1") + .token("verification-token") + .user(testUser) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .expiryDate(LocalDateTime.now().plusHours(24)) + .createdAt(LocalDateTime.now()) + .build(); + + // Create test refresh token + refreshToken = RefreshToken.builder() + .id("refresh-1") + .token("refresh-token") + .user(testUser) + .expiryDate(LocalDateTime.now().plusDays(7)) + .createdAt(LocalDateTime.now()) + .build(); + } + + @Test + void authenticateUser_WhenValidCredentials_ShouldReturnLoginResponse() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.of(loginLock)); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorities()) + .thenReturn((Collection) java.util.Collections.singletonList( + new SimpleGrantedAuthority("ROLE_CUSTOMER"))); + when(jwtUtil.generateJwtToken(eq(userDetails), anyList())).thenReturn("jwt-token"); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(tokenService.createRefreshToken(any(User.class), anyString(), anyString())) + .thenReturn("refresh-token"); + when(request.getHeader("X-Forwarded-For")).thenReturn(null); + when(request.getRemoteAddr()).thenReturn("127.0.0.1"); + when(request.getHeader("User-Agent")).thenReturn("Test-Agent"); + + // When + LoginResponse response = authService.authenticateUser(loginRequest, request); + + // Then + assertThat(response).isNotNull(); + assertThat(response.getToken()).isEqualTo("jwt-token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh-token"); + assertThat(response.getUsername()).isEqualTo("testuser"); + assertThat(response.getEmail()).isEqualTo("test@example.com"); + assertThat(response.getRoles()).contains("CUSTOMER"); + + verify(loginLockRepository).save(any(LoginLock.class)); + verify(loginLogRepository).save(any(LoginLog.class)); + } + + @Test + void authenticateUser_WhenUserNotFound_ShouldCheckByEmail() { + // Given + when(userRepository.findByUsername("test@example.com")).thenReturn(Optional.empty()); + lenient().when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("test@example.com")).thenReturn(Optional.of(loginLock)); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn("test@example.com"); + when(userDetails.getAuthorities()).thenReturn((Collection) java.util.Collections + .singletonList(new SimpleGrantedAuthority("ROLE_CUSTOMER"))); + + loginRequest.setUsername("test@example.com"); + + // When/Then + when(jwtUtil.generateJwtToken(eq(userDetails), anyList())).thenReturn("jwt-token"); + when(userRepository.findByUsername("test@example.com")).thenReturn(Optional.of(testUser)); + when(tokenService.createRefreshToken(any(User.class), eq("192.168.1.1"), isNull())) + .thenReturn("refresh-token"); + + LoginResponse response = authService.authenticateUser(loginRequest, request); + assertThat(response).isNotNull(); + } + + @Test + void authenticateUser_WhenUserDisabledAndNotVerified_ShouldThrowDisabledException() { + // Given + testUser.setEnabled(false); + testUser.setEmailVerified(false); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When/Then + assertThatThrownBy(() -> authService.authenticateUser(loginRequest, request)) + .isInstanceOf(DisabledException.class) + .hasMessageContaining("Please verify your email address"); + } + + @Test + void authenticateUser_WhenUserDisabledAndVerified_ShouldThrowDisabledException() { + // Given + testUser.setEnabled(false); + testUser.setEmailVerified(true); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When/Then + assertThatThrownBy(() -> authService.authenticateUser(loginRequest, request)) + .isInstanceOf(DisabledException.class) + .hasMessageContaining("Your account has been deactivated"); + } + + @Test + void authenticateUser_WhenAccountLocked_ShouldThrowBadCredentialsException() { + // Given + loginLock.setLockUntil(LocalDateTime.now().plusMinutes(10)); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.of(loginLock)); + + // When/Then + assertThatThrownBy(() -> authService.authenticateUser(loginRequest, request)) + .isInstanceOf(BadCredentialsException.class) + .hasMessageContaining("Account is temporarily locked"); + + verify(loginAuditService).recordLogin(eq("testuser"), eq(false), eq("192.168.1.1"), isNull()); + } + + @Test + void authenticateUser_WhenNoLockRecord_ShouldCreateNewLock() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.empty()); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorities()) + .thenReturn((Collection) java.util.Collections + .singleton(new SimpleGrantedAuthority("ROLE_CUSTOMER"))); + when(jwtUtil.generateJwtToken(eq(userDetails), anyList())).thenReturn("jwt-token"); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(tokenService.createRefreshToken(any(User.class), eq("192.168.1.1"), isNull())) + .thenReturn("refresh-token"); + + // When + LoginResponse response = authService.authenticateUser(loginRequest, request); + + // Then + assertThat(response).isNotNull(); + verify(loginLockRepository) + .save(argThat(lock -> lock.getUsername().equals("testuser") + && lock.getFailedAttempts() == 0)); + } + + @Test + void authenticateUser_WhenBadCredentials_ShouldIncrementFailedAttempts() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.of(loginLock)); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenThrow(new BadCredentialsException("Invalid credentials")); + + // When/Then + assertThatThrownBy(() -> authService.authenticateUser(loginRequest, request)) + .isInstanceOf(BadCredentialsException.class) + .hasMessage("Invalid username or password"); + + verify(loginAuditService).incrementFailedAttempt("testuser", 15L, 3); + verify(loginAuditService).recordLogin(eq("testuser"), eq(false), eq("192.168.1.1"), isNull()); + } + + @Test + void registerUser_WhenValidRequest_ShouldCreateUserAndSendVerificationEmail() { + // Given + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.of(customerRole)); + when(passwordEncoder.encode("newpassword")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(tokenService.createVerificationToken(any(User.class))).thenReturn("verification-token"); + + // When + String result = authService.registerUser(registerRequest); + + // Then + assertThat(result).contains("User registered successfully"); + verify(userRepository).save(argThat(user -> user.getEmail().equals("new@example.com") && + user.getFullName().equals("New User") && + !user.getEnabled() && + !user.getEmailVerified())); + verify(emailService).sendVerificationEmail("test@example.com", "testuser", "verification-token"); + } + + @Test + void registerUser_WhenEmailExists_ShouldThrowRuntimeException() { + // Given + when(userRepository.existsByEmail("new@example.com")).thenReturn(true); + + // When/Then + assertThatThrownBy(() -> authService.registerUser(registerRequest)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Error: Email is already in use!"); + } + + @Test + void registerUser_WhenAdminRole_ShouldAssignAdminRole() { + // Given + registerRequest.setRoles(Set.of("admin")); + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Optional.of(adminRole)); + when(passwordEncoder.encode("newpassword")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(tokenService.createVerificationToken(any(User.class))).thenReturn("verification-token"); + + // When + authService.registerUser(registerRequest); + + // Then + verify(roleRepository).findByName(RoleName.ADMIN); + verify(userRepository).save(argThat(user -> user.getRoles().contains(adminRole))); + } + + @Test + void registerUser_WhenEmployeeRole_ShouldAssignEmployeeRole() { + // Given + Role employeeRole = Role.builder().id(3L).name(RoleName.EMPLOYEE).build(); + registerRequest.setRoles(Set.of("employee")); + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(roleRepository.findByName(RoleName.EMPLOYEE)).thenReturn(Optional.of(employeeRole)); + when(passwordEncoder.encode("newpassword")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(tokenService.createVerificationToken(any(User.class))).thenReturn("verification-token"); + + // When + authService.registerUser(registerRequest); + + // Then + verify(roleRepository).findByName(RoleName.EMPLOYEE); + verify(userRepository).save(argThat(user -> user.getRoles().contains(employeeRole))); + } + + @Test + void registerUser_WhenInvalidRole_ShouldAssignCustomerRole() { + // Given + registerRequest.setRoles(Set.of("invalid")); + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.of(customerRole)); + when(passwordEncoder.encode("newpassword")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(tokenService.createVerificationToken(any(User.class))).thenReturn("verification-token"); + + // When + authService.registerUser(registerRequest); + + // Then + verify(roleRepository).findByName(RoleName.CUSTOMER); + verify(userRepository).save(argThat(user -> user.getRoles().contains(customerRole))); + } + + @Test + void registerUser_WhenRoleNotFound_ShouldThrowRuntimeException() { + // Given + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.empty()); + when(passwordEncoder.encode("newpassword")).thenReturn("encoded-password"); + + // When/Then + assertThatThrownBy(() -> authService.registerUser(registerRequest)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Error: Customer Role not found."); + } + + @Test + void verifyEmail_WhenValidToken_ShouldEnableUserAndReturnLoginResponse() { + // Given + testUser.setEnabled(false); + testUser.setEmailVerified(false); + when(tokenService.validateToken("verification-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .thenReturn(verificationToken); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(jwtUtil.generateJwtToken(any(UserDetails.class), anyList())).thenReturn("jwt-token"); + when(tokenService.createRefreshToken(any(User.class), eq("192.168.1.1"), isNull())) + .thenReturn("refresh-token"); + + // When + LoginResponse response = authService.verifyEmail("verification-token", request); + + // Then + assertThat(response).isNotNull(); + assertThat(response.getToken()).isEqualTo("jwt-token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh-token"); + verify(userRepository).save(argThat(user -> user.getEnabled() && user.getEmailVerified())); + verify(tokenService).markTokenAsUsed(verificationToken); + verify(emailService).sendWelcomeEmail("test@example.com", "testuser"); + } + + @Test + void resendVerificationEmail_WhenUserExists_ShouldSendEmail() { + // Given + testUser.setEmailVerified(false); + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + when(tokenService.createVerificationToken(testUser)).thenReturn("new-verification-token"); + + // When + String result = authService.resendVerificationEmail("test@example.com"); + + // Then + assertThat(result).contains("Verification email sent successfully"); + verify(emailService).sendVerificationEmail("test@example.com", "testuser", "new-verification-token"); + } + + @Test + void resendVerificationEmail_WhenUserNotFound_ShouldThrowRuntimeException() { + // Given + when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> authService.resendVerificationEmail("nonexistent@example.com")) + .isInstanceOf(RuntimeException.class) + .hasMessage("User not found with email: nonexistent@example.com"); + } + + @Test + void resendVerificationEmail_WhenEmailAlreadyVerified_ShouldThrowRuntimeException() { + // Given + testUser.setEmailVerified(true); + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + + // When/Then + assertThatThrownBy(() -> authService.resendVerificationEmail("test@example.com")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Email is already verified"); + } + + @Test + void refreshToken_WhenValidToken_ShouldReturnNewLoginResponse() { + // Given + when(tokenService.validateRefreshToken("refresh-token")).thenReturn(refreshToken); + when(jwtUtil.generateJwtToken(any(UserDetails.class), anyList())).thenReturn("new-jwt-token"); + + // When + LoginResponse response = authService.refreshToken("refresh-token"); + + // Then + assertThat(response).isNotNull(); + assertThat(response.getToken()).isEqualTo("new-jwt-token"); + assertThat(response.getRefreshToken()).isEqualTo("refresh-token"); + assertThat(response.getUsername()).isEqualTo("testuser"); + assertThat(response.getEmail()).isEqualTo("test@example.com"); + } + + @Test + void logout_ShouldRevokeRefreshToken() { + // When + authService.logout("refresh-token"); + + // Then + verify(tokenService).revokeRefreshToken("refresh-token"); + } + + @Test + void forgotPassword_WhenUserExists_ShouldSendResetEmail() { + // Given + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + when(tokenService.createPasswordResetToken(testUser)).thenReturn("reset-token"); + + // When + String result = authService.forgotPassword("test@example.com"); + + // Then + assertThat(result).contains("Password reset email sent successfully"); + verify(emailService).sendPasswordResetEmail("test@example.com", "testuser", "reset-token"); + } + + @Test + void forgotPassword_WhenUserNotFound_ShouldThrowRuntimeException() { + // Given + when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> authService.forgotPassword("nonexistent@example.com")) + .isInstanceOf(RuntimeException.class) + .hasMessage("User not found with email: nonexistent@example.com"); + } + + @Test + void resetPassword_WhenValidToken_ShouldUpdatePasswordAndRevokeTokens() { + // Given + VerificationToken resetToken = VerificationToken.builder() + .token("reset-token") + .user(testUser) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + + when(tokenService.validateToken("reset-token", VerificationToken.TokenType.PASSWORD_RESET)) + .thenReturn(resetToken); + when(passwordEncoder.encode("newpassword")).thenReturn("new-encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + + // When + String result = authService.resetPassword("reset-token", "newpassword"); + + // Then + assertThat(result).contains("Password reset successfully"); + verify(userRepository).save(argThat(user -> user.getPassword().equals("new-encoded-password"))); + verify(tokenService).markTokenAsUsed(resetToken); + verify(tokenService).revokeAllUserTokens(testUser); + } + + @Test + void authenticateUser_WhenNullRequest_ShouldHandleGracefully() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.of(loginLock)); + when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class))) + .thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(userDetails); + when(userDetails.getUsername()).thenReturn("testuser"); + when(userDetails.getAuthorities()) + .thenReturn((Collection) java.util.Collections.singletonList( + new SimpleGrantedAuthority("ROLE_CUSTOMER"))); + when(jwtUtil.generateJwtToken(eq(userDetails), anyList())).thenReturn("jwt-token"); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(tokenService.createRefreshToken(any(User.class), isNull(), isNull())).thenReturn("refresh-token"); + + // When + LoginResponse response = authService.authenticateUser(loginRequest, null); + + // Then + assertThat(response).isNotNull(); + verify(tokenService).createRefreshToken(eq(testUser), isNull(), isNull()); + } + + @Test + void verifyEmail_WhenNullRequest_ShouldHandleGracefully() { + // Given + testUser.setEnabled(false); + testUser.setEmailVerified(false); + when(tokenService.validateToken("verification-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .thenReturn(verificationToken); + when(userRepository.save(any(User.class))).thenReturn(testUser); + when(jwtUtil.generateJwtToken(any(UserDetails.class), anyList())).thenReturn("jwt-token"); + when(tokenService.createRefreshToken(any(User.class), isNull(), isNull())).thenReturn("refresh-token"); + + // When + LoginResponse response = authService.verifyEmail("verification-token", null); + + // Then + assertThat(response).isNotNull(); + verify(tokenService).createRefreshToken(eq(testUser), isNull(), isNull()); + } +} diff --git a/auth-service/src/test/java/com/techtorque/auth_service/service/TokenServiceTest.java b/auth-service/src/test/java/com/techtorque/auth_service/service/TokenServiceTest.java new file mode 100644 index 0000000..49065c8 --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/service/TokenServiceTest.java @@ -0,0 +1,470 @@ +package com.techtorque.auth_service.service; + +import com.techtorque.auth_service.entity.RefreshToken; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.entity.VerificationToken; +import com.techtorque.auth_service.repository.RefreshTokenRepository; +import com.techtorque.auth_service.repository.VerificationTokenRepository; +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.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Comprehensive test class for TokenService + * Tests token creation, validation, expiry, and cleanup operations + */ +@ExtendWith(MockitoExtension.class) +class TokenServiceTest { + + @Mock + private VerificationTokenRepository verificationTokenRepository; + + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @InjectMocks + private TokenService tokenService; + + private User testUser; + private VerificationToken verificationToken; + private RefreshToken refreshToken; + + @BeforeEach + void setUp() { + // Set up test configuration + ReflectionTestUtils.setField(tokenService, "verificationExpiryHours", 24); + ReflectionTestUtils.setField(tokenService, "passwordResetExpiryHours", 1); + ReflectionTestUtils.setField(tokenService, "refreshTokenExpiryDays", 7); + + // Create test user + testUser = User.builder() + .id(1L) + .username("testuser") + .email("test@example.com") + .password("encoded-password") + .enabled(true) + .build(); + + // Create test verification token + verificationToken = VerificationToken.builder() + .id("token-1") + .token("verification-token") + .user(testUser) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .createdAt(LocalDateTime.now()) + .expiryDate(LocalDateTime.now().plusHours(24)) + .build(); + + // Create test refresh token + refreshToken = RefreshToken.builder() + .id("refresh-1") + .token("refresh-token") + .user(testUser) + .createdAt(LocalDateTime.now()) + .expiryDate(LocalDateTime.now().plusDays(7)) + .ipAddress("127.0.0.1") + .userAgent("Test-Agent") + .build(); + } + + @Test + void createVerificationToken_WhenValidUser_ShouldCreateAndReturnToken() { + // Given + when(verificationTokenRepository.findByUserAndTokenType(testUser, + VerificationToken.TokenType.EMAIL_VERIFICATION)) + .thenReturn(Optional.empty()); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createVerificationToken(testUser); + + // Then + assertThat(result).isNotNull(); + assertThat(result).hasSize(36); // UUID length + verify(verificationTokenRepository).save(argThat(token -> token.getUser().equals(testUser) && + token.getTokenType().equals(VerificationToken.TokenType.EMAIL_VERIFICATION) && + token.getExpiryDate().isAfter(LocalDateTime.now().plusHours(23)))); + } + + @Test + void createVerificationToken_WhenExistingTokenExists_ShouldDeleteExistingToken() { + // Given + VerificationToken existingToken = VerificationToken.builder() + .token("existing-token") + .user(testUser) + .tokenType(VerificationToken.TokenType.EMAIL_VERIFICATION) + .build(); + + when(verificationTokenRepository.findByUserAndTokenType(testUser, + VerificationToken.TokenType.EMAIL_VERIFICATION)) + .thenReturn(Optional.of(existingToken)); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createVerificationToken(testUser); + + // Then + assertThat(result).isNotNull(); + verify(verificationTokenRepository).delete(existingToken); + verify(verificationTokenRepository).save(any(VerificationToken.class)); + } + + @Test + void createPasswordResetToken_WhenValidUser_ShouldCreateAndReturnToken() { + // Given + when(verificationTokenRepository.findByUserAndTokenType(testUser, VerificationToken.TokenType.PASSWORD_RESET)) + .thenReturn(Optional.empty()); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createPasswordResetToken(testUser); + + // Then + assertThat(result).isNotNull(); + assertThat(result).hasSize(36); // UUID length + verify(verificationTokenRepository).save(argThat(token -> token.getUser().equals(testUser) && + token.getTokenType().equals(VerificationToken.TokenType.PASSWORD_RESET) && + token.getExpiryDate().isAfter(LocalDateTime.now().plusMinutes(30)))); + } + + @Test + void createPasswordResetToken_WhenExistingTokenExists_ShouldDeleteExistingToken() { + // Given + VerificationToken existingToken = VerificationToken.builder() + .token("existing-reset-token") + .user(testUser) + .tokenType(VerificationToken.TokenType.PASSWORD_RESET) + .build(); + + when(verificationTokenRepository.findByUserAndTokenType(testUser, VerificationToken.TokenType.PASSWORD_RESET)) + .thenReturn(Optional.of(existingToken)); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createPasswordResetToken(testUser); + + // Then + assertThat(result).isNotNull(); + verify(verificationTokenRepository).delete(existingToken); + verify(verificationTokenRepository).save(any(VerificationToken.class)); + } + + @Test + void validateToken_WhenValidToken_ShouldReturnToken() { + // Given + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When + VerificationToken result = tokenService.validateToken("verification-token", + VerificationToken.TokenType.EMAIL_VERIFICATION); + + // Then + assertThat(result).isEqualTo(verificationToken); + } + + @Test + void validateToken_WhenTokenNotFound_ShouldThrowRuntimeException() { + // Given + when(verificationTokenRepository.findByToken("invalid-token")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy( + () -> tokenService.validateToken("invalid-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Invalid token"); + } + + @Test + void validateToken_WhenWrongTokenType_ShouldThrowRuntimeException() { + // Given + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When/Then + assertThatThrownBy( + () -> tokenService.validateToken("verification-token", VerificationToken.TokenType.PASSWORD_RESET)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Invalid token type"); + } + + @Test + void validateToken_WhenTokenAlreadyUsed_ShouldThrowRuntimeException() { + // Given + verificationToken.setUsedAt(LocalDateTime.now()); + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When/Then + assertThatThrownBy( + () -> tokenService.validateToken("verification-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Token has already been used"); + } + + @Test + void validateToken_WhenTokenExpired_ShouldThrowRuntimeException() { + // Given + verificationToken.setExpiryDate(LocalDateTime.now().minusHours(1)); + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When/Then + assertThatThrownBy( + () -> tokenService.validateToken("verification-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Token has expired"); + } + + @Test + void markTokenAsUsed_ShouldSetUsedAtTimestamp() { + // Given + when(verificationTokenRepository.save(verificationToken)).thenReturn(verificationToken); + + // When + tokenService.markTokenAsUsed(verificationToken); + + // Then + assertThat(verificationToken.getUsedAt()).isNotNull(); + assertThat(verificationToken.getUsedAt()).isBefore(LocalDateTime.now().plusSeconds(1)); + verify(verificationTokenRepository).save(verificationToken); + } + + @Test + void createRefreshToken_WhenValidUser_ShouldCreateAndReturnToken() { + // Given + when(refreshTokenRepository.save(any(RefreshToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createRefreshToken(testUser, "192.168.1.1", "Mozilla/5.0"); + + // Then + assertThat(result).isNotNull(); + assertThat(result).hasSize(36); // UUID length + verify(refreshTokenRepository).save(argThat(token -> token.getUser().equals(testUser) && + token.getIpAddress().equals("192.168.1.1") && + token.getUserAgent().equals("Mozilla/5.0") && + token.getExpiryDate().isAfter(LocalDateTime.now().plusDays(6)))); + } + + @Test + void createRefreshToken_WhenNullIpAndUserAgent_ShouldCreateTokenWithNulls() { + // Given + when(refreshTokenRepository.save(any(RefreshToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createRefreshToken(testUser, null, null); + + // Then + assertThat(result).isNotNull(); + verify(refreshTokenRepository).save(argThat(token -> token.getUser().equals(testUser) && + token.getIpAddress() == null && + token.getUserAgent() == null)); + } + + @Test + void validateRefreshToken_WhenValidToken_ShouldReturnToken() { + // Given + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + + // When + RefreshToken result = tokenService.validateRefreshToken("refresh-token"); + + // Then + assertThat(result).isEqualTo(refreshToken); + } + + @Test + void validateRefreshToken_WhenTokenNotFound_ShouldThrowRuntimeException() { + // Given + when(refreshTokenRepository.findByToken("invalid-token")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> tokenService.validateRefreshToken("invalid-token")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Invalid refresh token"); + } + + @Test + void validateRefreshToken_WhenTokenRevoked_ShouldThrowRuntimeException() { + // Given + refreshToken.setRevokedAt(LocalDateTime.now()); + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + + // When/Then + assertThatThrownBy(() -> tokenService.validateRefreshToken("refresh-token")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Refresh token has been revoked"); + } + + @Test + void validateRefreshToken_WhenTokenExpired_ShouldThrowRuntimeException() { + // Given + refreshToken.setExpiryDate(LocalDateTime.now().minusDays(1)); + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + + // When/Then + assertThatThrownBy(() -> tokenService.validateRefreshToken("refresh-token")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Refresh token has expired"); + } + + @Test + void revokeRefreshToken_WhenValidToken_ShouldSetRevokedAt() { + // Given + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + when(refreshTokenRepository.save(refreshToken)).thenReturn(refreshToken); + + // When + tokenService.revokeRefreshToken("refresh-token"); + + // Then + assertThat(refreshToken.getRevokedAt()).isNotNull(); + assertThat(refreshToken.getRevokedAt()).isBefore(LocalDateTime.now().plusSeconds(1)); + verify(refreshTokenRepository).save(refreshToken); + } + + @Test + void revokeRefreshToken_WhenTokenNotFound_ShouldThrowRuntimeException() { + // Given + when(refreshTokenRepository.findByToken("invalid-token")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> tokenService.revokeRefreshToken("invalid-token")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Invalid refresh token"); + } + + @Test + void revokeAllUserTokens_ShouldDeleteAllUserTokens() { + // When + tokenService.revokeAllUserTokens(testUser); + + // Then + verify(refreshTokenRepository).deleteByUser(testUser); + } + + @Test + void cleanupExpiredTokens_ShouldDeleteExpiredTokens() { + // When + tokenService.cleanupExpiredTokens(); + + // Then + verify(refreshTokenRepository).deleteExpiredTokens(any(LocalDateTime.class)); + } + + @Test + void createVerificationToken_WithCustomExpiryHours_ShouldUseConfiguredExpiry() { + // Given + ReflectionTestUtils.setField(tokenService, "verificationExpiryHours", 48); + when(verificationTokenRepository.findByUserAndTokenType(testUser, + VerificationToken.TokenType.EMAIL_VERIFICATION)) + .thenReturn(Optional.empty()); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createVerificationToken(testUser); + + // Then + assertThat(result).isNotNull(); + verify(verificationTokenRepository) + .save(argThat(token -> token.getExpiryDate().isAfter(LocalDateTime.now().plusHours(47)))); + } + + @Test + void createPasswordResetToken_WithCustomExpiryHours_ShouldUseConfiguredExpiry() { + // Given + ReflectionTestUtils.setField(tokenService, "passwordResetExpiryHours", 2); + when(verificationTokenRepository.findByUserAndTokenType(testUser, VerificationToken.TokenType.PASSWORD_RESET)) + .thenReturn(Optional.empty()); + when(verificationTokenRepository.save(any(VerificationToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createPasswordResetToken(testUser); + + // Then + assertThat(result).isNotNull(); + verify(verificationTokenRepository).save( + argThat(token -> token.getExpiryDate().isAfter(LocalDateTime.now().plusHours(1).plusMinutes(30)))); + } + + @Test + void createRefreshToken_WithCustomExpiryDays_ShouldUseConfiguredExpiry() { + // Given + ReflectionTestUtils.setField(tokenService, "refreshTokenExpiryDays", 14); + when(refreshTokenRepository.save(any(RefreshToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + // When + String result = tokenService.createRefreshToken(testUser, "127.0.0.1", "Test-Agent"); + + // Then + assertThat(result).isNotNull(); + verify(refreshTokenRepository) + .save(argThat(token -> token.getExpiryDate().isAfter(LocalDateTime.now().plusDays(13)))); + } + + @Test + void validateToken_WhenTokenJustExpired_ShouldThrowRuntimeException() { + // Given + verificationToken.setExpiryDate(LocalDateTime.now().minusSeconds(1)); + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When/Then + assertThatThrownBy( + () -> tokenService.validateToken("verification-token", VerificationToken.TokenType.EMAIL_VERIFICATION)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Token has expired"); + } + + @Test + void validateRefreshToken_WhenTokenJustExpired_ShouldThrowRuntimeException() { + // Given + refreshToken.setExpiryDate(LocalDateTime.now().minusSeconds(1)); + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + + // When/Then + assertThatThrownBy(() -> tokenService.validateRefreshToken("refresh-token")) + .isInstanceOf(RuntimeException.class) + .hasMessage("Refresh token has expired"); + } + + @Test + void validateToken_WhenTokenJustBeforeExpiry_ShouldReturnToken() { + // Given + verificationToken.setExpiryDate(LocalDateTime.now().plusSeconds(1)); + when(verificationTokenRepository.findByToken("verification-token")).thenReturn(Optional.of(verificationToken)); + + // When + VerificationToken result = tokenService.validateToken("verification-token", + VerificationToken.TokenType.EMAIL_VERIFICATION); + + // Then + assertThat(result).isEqualTo(verificationToken); + } + + @Test + void validateRefreshToken_WhenTokenJustBeforeExpiry_ShouldReturnToken() { + // Given + refreshToken.setExpiryDate(LocalDateTime.now().plusSeconds(1)); + when(refreshTokenRepository.findByToken("refresh-token")).thenReturn(Optional.of(refreshToken)); + + // When + RefreshToken result = tokenService.validateRefreshToken("refresh-token"); + + // Then + assertThat(result).isEqualTo(refreshToken); + } +} diff --git a/auth-service/src/test/java/com/techtorque/auth_service/service/UserServiceTest.java b/auth-service/src/test/java/com/techtorque/auth_service/service/UserServiceTest.java new file mode 100644 index 0000000..b0048cf --- /dev/null +++ b/auth-service/src/test/java/com/techtorque/auth_service/service/UserServiceTest.java @@ -0,0 +1,704 @@ +package com.techtorque.auth_service.service; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.*; +import jakarta.persistence.EntityNotFoundException; +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.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.*; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Comprehensive test class for UserService + * Tests user management, security, and role/permission operations + */ +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private RoleRepository roleRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private LoginLockRepository loginLockRepository; + + @Mock + private RefreshTokenRepository refreshTokenRepository; + + @Mock + private VerificationTokenRepository verificationTokenRepository; + + @Mock + private LoginLogRepository loginLogRepository; + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + @InjectMocks + private UserService userService; + + private User testUser; + private Role customerRole; + private Role employeeRole; + private Role adminRole; + private Role superAdminRole; + + @BeforeEach + void setUp() { + // Create test roles + customerRole = Role.builder() + .id(1L) + .name(RoleName.CUSTOMER) + .description("Customer role") + .permissions(new HashSet<>()) + .build(); + + employeeRole = Role.builder() + .id(2L) + .name(RoleName.EMPLOYEE) + .description("Employee role") + .permissions(new HashSet<>()) + .build(); + + adminRole = Role.builder() + .id(3L) + .name(RoleName.ADMIN) + .description("Admin role") + .permissions(new HashSet<>()) + .build(); + + superAdminRole = Role.builder() + .id(4L) + .name(RoleName.SUPER_ADMIN) + .description("Super Admin role") + .permissions(new HashSet<>()) + .build(); + + // Create test user + testUser = User.builder() + .id(1L) + .username("testuser") + .email("test@example.com") + .password("encoded-password") + .fullName("Test User") + .phone("1234567890") + .address("Test Address") + .enabled(true) + .emailVerified(true) + .roles(new HashSet<>(Set.of(customerRole))) + .build(); + } + + @Test + void loadUserByUsername_WhenUserFoundByUsername_ShouldReturnUserDetails() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + UserDetails userDetails = userService.loadUserByUsername("testuser"); + + // Then + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo("testuser"); + assertThat(userDetails.getPassword()).isEqualTo("encoded-password"); + assertThat(userDetails.isEnabled()).isTrue(); + assertThat(userDetails.isAccountNonLocked()).isTrue(); + assertThat(userDetails.getAuthorities()).hasSize(1); + assertThat(userDetails.getAuthorities()).extracting(GrantedAuthority::getAuthority) + .contains("ROLE_CUSTOMER"); + } + + @Test + void loadUserByUsername_WhenUserFoundByEmail_ShouldReturnUserDetails() { + // Given + when(userRepository.findByUsername("test@example.com")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + + // When + UserDetails userDetails = userService.loadUserByUsername("test@example.com"); + + // Then + assertThat(userDetails).isNotNull(); + assertThat(userDetails.getUsername()).isEqualTo("testuser"); + verify(userRepository).findByUsername("test@example.com"); + verify(userRepository).findByEmail("test@example.com"); + } + + @Test + void loadUserByUsername_WhenUserNotFound_ShouldThrowUsernameNotFoundException() { + // Given + when(userRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("nonexistent")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> userService.loadUserByUsername("nonexistent")) + .isInstanceOf(UsernameNotFoundException.class) + .hasMessage("User not found: nonexistent"); + } + + @Test + void loadUserByUsername_WhenUserDisabled_ShouldReturnDisabledUserDetails() { + // Given + testUser.setEnabled(false); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + UserDetails userDetails = userService.loadUserByUsername("testuser"); + + // Then + assertThat(userDetails.isEnabled()).isFalse(); + assertThat(userDetails.isAccountNonLocked()).isFalse(); + } + + @Test + void registerCustomer_WhenValidInput_ShouldCreateCustomerUser() { + // Given + when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty()); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.of(customerRole)); + when(passwordEncoder.encode("password")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + + // When + User result = userService.registerCustomer("newuser", "new@example.com", "password"); + + // Then + assertThat(result).isNotNull(); + verify(userRepository).save(argThat(user -> user.getUsername().equals("newuser") && + user.getEmail().equals("new@example.com") && + user.getPassword().equals("encoded-password") && + user.getEnabled() && + user.getRoles().contains(customerRole))); + } + + @Test + void registerCustomer_WhenUsernameExists_ShouldThrowIllegalArgumentException() { + // Given + when(userRepository.findByUsername("existinguser")).thenReturn(Optional.of(testUser)); + + // When/Then + assertThatThrownBy(() -> userService.registerCustomer("existinguser", "new@example.com", "password")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Username already exists: existinguser"); + } + + @Test + void registerCustomer_WhenEmailExists_ShouldThrowIllegalArgumentException() { + // Given + when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(testUser)); + + // When/Then + assertThatThrownBy(() -> userService.registerCustomer("newuser", "existing@example.com", "password")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Email already exists: existing@example.com"); + } + + @Test + void registerCustomer_WhenCustomerRoleNotFound_ShouldThrowEntityNotFoundException() { + // Given + when(userRepository.findByUsername("newuser")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty()); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> userService.registerCustomer("newuser", "new@example.com", "password")) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("Customer role not found"); + } + + @Test + void createEmployee_WhenValidInput_ShouldCreateEmployeeUser() { + // Given + when(userRepository.findByUsername("employee")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("employee@example.com")).thenReturn(Optional.empty()); + when(roleRepository.findByName(RoleName.EMPLOYEE)).thenReturn(Optional.of(employeeRole)); + when(passwordEncoder.encode("password")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + + // When + User result = userService.createEmployee("employee", "employee@example.com", "password", null); + + // Then + assertThat(result).isNotNull(); + verify(userRepository).save(argThat(user -> user.getUsername().equals("employee") && + user.getEmail().equals("employee@example.com") && + user.getRoles().contains(employeeRole))); + } + + @Test + void createAdmin_WhenValidInput_ShouldCreateAdminUser() { + // Given + when(userRepository.findByUsername("admin")).thenReturn(Optional.empty()); + when(userRepository.findByEmail("admin@example.com")).thenReturn(Optional.empty()); + when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Optional.of(adminRole)); + when(passwordEncoder.encode("password")).thenReturn("encoded-password"); + when(userRepository.save(any(User.class))).thenReturn(testUser); + + // When + User result = userService.createAdmin("admin", "admin@example.com", "password", null); + + // Then + assertThat(result).isNotNull(); + verify(userRepository).save(argThat(user -> user.getUsername().equals("admin") && + user.getEmail().equals("admin@example.com") && + user.getRoles().contains(adminRole))); + } + + @Test + void findByUsername_WhenUserExists_ShouldReturnUser() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + Optional result = userService.findByUsername("testuser"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(testUser); + } + + @Test + void findByEmail_WhenUserExists_ShouldReturnUser() { + // Given + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + + // When + Optional result = userService.findByEmail("test@example.com"); + + // Then + assertThat(result).isPresent(); + assertThat(result.get()).isEqualTo(testUser); + } + + @Test + void findAllUsers_ShouldReturnAllUsers() { + // Given + List users = List.of(testUser); + when(userRepository.findAll()).thenReturn(users); + + // When + List result = userService.findAllUsers(); + + // Then + assertThat(result).isEqualTo(users); + } + + @Test + void getUserPermissions_WhenUserExists_ShouldReturnPermissions() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + Set permissions = userService.getUserPermissions("testuser"); + + // Then + assertThat(permissions).isNotNull(); + // Note: permissions are empty in our test setup, so this just verifies the + // method works + } + + @Test + void getUserPermissions_WhenUserNotFound_ShouldThrowEntityNotFoundException() { + // Given + when(userRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> userService.getUserPermissions("nonexistent")) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found: nonexistent"); + } + + @Test + void getUserRoles_WhenUserExists_ShouldReturnRoles() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + Set roles = userService.getUserRoles("testuser"); + + // Then + assertThat(roles).contains("CUSTOMER"); + } + + @Test + void enableUser_WhenUserExists_ShouldEnableUser() { + // Given + testUser.setEnabled(false); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.enableUser("testuser"); + + // Then + assertThat(testUser.getEnabled()).isTrue(); + verify(userRepository).save(testUser); + } + + @Test + void disableUser_WhenUserExists_ShouldDisableUser() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.disableUser("testuser"); + + // Then + assertThat(testUser.getEnabled()).isFalse(); + verify(userRepository).save(testUser); + } + + @Test + void deleteUser_WhenUserExists_ShouldDeleteUserAndRelatedData() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + userService.deleteUser("testuser"); + + // Then + verify(userRepository).save(testUser); // Clear roles + verify(refreshTokenRepository).deleteByUser(testUser); + verify(verificationTokenRepository).deleteByUser(testUser); + verify(loginLockRepository).deleteByUsername("testuser"); + verify(loginLogRepository).deleteByUsername("testuser"); + verify(userRepository).delete(testUser); + } + + @Test + void deleteUser_WhenUserNotFound_ShouldThrowEntityNotFoundException() { + // Given + when(userRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + // When/Then + assertThatThrownBy(() -> userService.deleteUser("nonexistent")) + .isInstanceOf(EntityNotFoundException.class) + .hasMessage("User not found: nonexistent"); + } + + @Test + void clearLoginLock_WhenLockExists_ShouldClearLock() { + // Given + com.techtorque.auth_service.entity.LoginLock loginLock = com.techtorque.auth_service.entity.LoginLock.builder() + .username("testuser") + .failedAttempts(3) + .lockUntil(java.time.LocalDateTime.now().plusMinutes(15)) + .build(); + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.of(loginLock)); + + // When + userService.clearLoginLock("testuser"); + + // Then + assertThat(loginLock.getFailedAttempts()).isEqualTo(0); + assertThat(loginLock.getLockUntil()).isNull(); + verify(loginLockRepository).save(loginLock); + } + + @Test + void clearLoginLock_WhenLockNotExists_ShouldNotThrowException() { + // Given + when(loginLockRepository.findByUsername("testuser")).thenReturn(Optional.empty()); + + // When/Then + assertThatCode(() -> userService.clearLoginLock("testuser")) + .doesNotThrowAnyException(); + } + + @Test + void hasRole_WhenUserHasRole_ShouldReturnTrue() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + boolean result = userService.hasRole("testuser", RoleName.CUSTOMER); + + // Then + assertThat(result).isTrue(); + } + + @Test + void hasRole_WhenUserDoesNotHaveRole_ShouldReturnFalse() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + boolean result = userService.hasRole("testuser", RoleName.ADMIN); + + // Then + assertThat(result).isFalse(); + } + + @Test + void hasPermission_WhenUserHasPermission_ShouldReturnTrue() { + // Given + com.techtorque.auth_service.entity.Permission permission = com.techtorque.auth_service.entity.Permission + .builder() + .name("READ_USERS") + .build(); + customerRole.getPermissions().add(permission); + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + boolean result = userService.hasPermission("testuser", "READ_USERS"); + + // Then + assertThat(result).isTrue(); + } + + @Test + void hasPermission_WhenUserDoesNotHavePermission_ShouldReturnFalse() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + + // When + boolean result = userService.hasPermission("testuser", "DELETE_USERS"); + + // Then + assertThat(result).isFalse(); + } + + @Test + void updateUserDetails_WhenValidInput_ShouldUpdateUser() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.existsByUsername("newusername")).thenReturn(false); + when(userRepository.existsByEmail("new@example.com")).thenReturn(false); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + User result = userService.updateUserDetails("testuser", "newusername", "new@example.com", false); + + // Then + assertThat(result.getUsername()).isEqualTo("newusername"); + assertThat(result.getEmail()).isEqualTo("new@example.com"); + assertThat(result.getEnabled()).isFalse(); + } + + @Test + void updateUserDetails_WhenUsernameExists_ShouldThrowIllegalArgumentException() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.existsByUsername("existingusername")).thenReturn(true); + + // When/Then + assertThatThrownBy(() -> userService.updateUserDetails("testuser", "existingusername", null, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Username already exists: existingusername"); + } + + @Test + void resetUserPassword_WhenUserExists_ShouldUpdatePassword() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(passwordEncoder.encode("newpassword")).thenReturn("new-encoded-password"); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.resetUserPassword("testuser", "newpassword"); + + // Then + assertThat(testUser.getPassword()).isEqualTo("new-encoded-password"); + verify(userRepository).save(testUser); + } + + @Test + void changeUserPassword_WhenCurrentPasswordCorrect_ShouldUpdatePassword() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(passwordEncoder.matches("currentpassword", "encoded-password")).thenReturn(true); + when(passwordEncoder.encode("newpassword")).thenReturn("new-encoded-password"); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.changeUserPassword("testuser", "currentpassword", "newpassword"); + + // Then + assertThat(testUser.getPassword()).isEqualTo("new-encoded-password"); + verify(userRepository).save(testUser); + } + + @Test + void changeUserPassword_WhenCurrentPasswordIncorrect_ShouldThrowIllegalStateException() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(passwordEncoder.matches("wrongpassword", "encoded-password")).thenReturn(false); + + // When/Then + assertThatThrownBy(() -> userService.changeUserPassword("testuser", "wrongpassword", "newpassword")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Current password is incorrect"); + } + + @Test + void assignRoleToUser_WhenValidRole_ShouldAssignRole() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(roleRepository.findByName(RoleName.EMPLOYEE)).thenReturn(Optional.of(employeeRole)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.assignRoleToUser("testuser", "EMPLOYEE"); + + // Then + assertThat(testUser.getRoles()).contains(employeeRole); + verify(userRepository).save(testUser); + } + + @Test + void assignRoleToUser_WhenAssigningAdminRoleWithoutSuperAdminAuth_ShouldThrowAccessDeniedException() { + // Given + lenient().when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + lenient().when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Optional.of(adminRole)); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getAuthorities()) + .thenReturn((Collection) java.util.Collections.singletonList(new SimpleGrantedAuthority("ROLE_ADMIN"))); + + // When/Then + assertThatThrownBy(() -> userService.assignRoleToUser("testuser", "ADMIN")) + .isInstanceOf(AccessDeniedException.class) + .hasMessage("Permission denied: Only a SUPER_ADMIN can assign the ADMIN role."); + + SecurityContextHolder.clearContext(); + } + + @Test + void assignRoleToUser_WhenAssigningAdminRoleWithSuperAdminAuth_ShouldSucceed() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Optional.of(adminRole)); + when(userRepository.save(testUser)).thenReturn(testUser); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getAuthorities()).thenReturn( + (Collection) java.util.Collections.singletonList(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))); + + // When + userService.assignRoleToUser("testuser", "ADMIN"); + + // Then + assertThat(testUser.getRoles()).contains(adminRole); + verify(userRepository).save(testUser); + SecurityContextHolder.clearContext(); + } + + @Test + void assignRoleToUser_WhenRoleAlreadyAssigned_ShouldThrowIllegalStateException() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.of(customerRole)); + + // When/Then + assertThatThrownBy(() -> userService.assignRoleToUser("testuser", "CUSTOMER")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("User already has role: CUSTOMER"); + } + + @Test + void revokeRoleFromUser_WhenValidRole_ShouldRevokeRole() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(roleRepository.findByName(RoleName.CUSTOMER)).thenReturn(Optional.of(customerRole)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + userService.revokeRoleFromUser("testuser", "CUSTOMER"); + + // Then + assertThat(testUser.getRoles()).doesNotContain(customerRole); + verify(userRepository).save(testUser); + } + + @Test + void revokeRoleFromUser_WhenRevokingOwnSuperAdminRole_ShouldThrowAccessDeniedException() { + // Given + testUser.getRoles().add(superAdminRole); + lenient().when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + lenient().when(roleRepository.findByName(RoleName.SUPER_ADMIN)).thenReturn(Optional.of(superAdminRole)); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getName()).thenReturn("testuser"); + + // When/Then + assertThatThrownBy(() -> userService.revokeRoleFromUser("testuser", "SUPER_ADMIN")) + .isInstanceOf(AccessDeniedException.class) + .hasMessage("Action denied: A SUPER_ADMIN cannot revoke their own SUPER_ADMIN role."); + + SecurityContextHolder.clearContext(); + } + + @Test + void revokeRoleFromUser_WhenRoleNotAssigned_ShouldThrowIllegalStateException() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(roleRepository.findByName(RoleName.ADMIN)).thenReturn(Optional.of(adminRole)); + + // When/Then + assertThatThrownBy(() -> userService.revokeRoleFromUser("testuser", "ADMIN")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("User does not have role: ADMIN"); + } + + @Test + void updateProfile_WhenValidInput_ShouldUpdateProfile() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + User result = userService.updateProfile("testuser", "New Full Name", "9876543210", "New Address"); + + // Then + assertThat(result.getFullName()).isEqualTo("New Full Name"); + assertThat(result.getPhone()).isEqualTo("9876543210"); + assertThat(result.getAddress()).isEqualTo("New Address"); + verify(userRepository).save(testUser); + } + + @Test + void updateProfilePhoto_WhenValidInput_ShouldUpdatePhotoUrl() { + // Given + when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(testUser)); + when(userRepository.save(testUser)).thenReturn(testUser); + + // When + User result = userService.updateProfilePhoto("testuser", "http://example.com/photo.jpg"); + + // Then + assertThat(result.getProfilePhotoUrl()).isEqualTo("http://example.com/photo.jpg"); + verify(userRepository).save(testUser); + } +}