diff --git a/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java b/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java index 904da0f..8c1629e 100644 --- a/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java +++ b/src/main/java/com/fredmaina/chatapp/Auth/controllers/AuthController.java @@ -6,6 +6,10 @@ import com.fredmaina.chatapp.Auth.Repositories.UserRepository; import com.fredmaina.chatapp.Auth.services.AuthService; import com.fredmaina.chatapp.Auth.services.JWTService; +import com.fredmaina.chatapp.core.error.BadRequestException; +import com.fredmaina.chatapp.core.error.DataAlreadyExistsException; +import com.fredmaina.chatapp.core.error.ResourceNotFoundException; +import com.fredmaina.chatapp.core.error.UnauthorizedException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -34,7 +38,7 @@ public ResponseEntity login(@RequestBody LoginRequest loginRequest if(authResponse.isSuccess()){ return ResponseEntity.ok(authResponse); } - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authResponse); + throw new UnauthorizedException("Invalid username or password"); } @@ -45,9 +49,9 @@ public ResponseEntity register(@RequestBody SignUpRequest signUpRe return ResponseEntity.status(HttpStatus.CREATED).body(authResponse); } if ("Username already exists (case-insensitive)".equals(authResponse.getMessage()) || "Email already exists".equals(authResponse.getMessage())) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(authResponse); + throw new DataAlreadyExistsException("Username or email already exists"); } - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(authResponse); // General bad request for other issues + throw new BadRequestException("Bad request"); // General bad request for other issues } @PostMapping("/oauth/google") public ResponseEntity googleOAuth(@RequestBody GoogleOAuthRequest request) { @@ -58,7 +62,7 @@ public ResponseEntity googleOAuth(@RequestBody GoogleOAuthRequest request) { return ResponseEntity.ok(response); } else { log.warn("Google OAuth failed: {}", response.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + throw new UnauthorizedException("Error while contacting Google OAuth"); } } @@ -69,8 +73,7 @@ public ResponseEntity me(@RequestHeader("Authorization") String au User user = userRepository.findByEmail(email).orElse(null); if (user == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(AuthResponse.builder().success(false).message("User not found for provided token.").build()); + throw new ResourceNotFoundException("User not found for provided token."); } return ResponseEntity.ok( @@ -86,8 +89,7 @@ public ResponseEntity setUsername(@RequestBody Map String username = map.get("username"); if (email == null || email.isBlank() || username == null || username.isBlank()) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(AuthResponse.builder().success(false).message("Email and username are required.").build()); + throw new BadRequestException("Email and username are required."); } AuthResponse authResponse = authService.setUsername(email,username); @@ -96,9 +98,9 @@ public ResponseEntity setUsername(@RequestBody Map } // Distinguish between user not found and username taken if ("Username already taken (case-insensitive)".equals(authResponse.getMessage())) { - return ResponseEntity.status(HttpStatus.CONFLICT).body(authResponse); + throw new DataAlreadyExistsException(authResponse.getMessage()); } - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(authResponse); // Assuming "Invalid email" means user not found + throw new ResourceNotFoundException("User not found"); // Assuming "Invalid email" means user not found } @@ -114,17 +116,11 @@ public ResponseEntity> checkUsername(@PathVariable String us )); } else { log.error("username {} not found for some weird reason", username); - return ResponseEntity.ok(Map.of( - "success", true, - "exists", false - )); + throw new ResourceNotFoundException("username not found"); } } catch (Exception e) { log.error("Error checking username: {}", username, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of( - "success", false, - "message", "Error checking username" - )); + throw new RuntimeException("Error checking username"); } } } \ No newline at end of file diff --git a/src/main/java/com/fredmaina/chatapp/core/Controllers/ChatController.java b/src/main/java/com/fredmaina/chatapp/core/Controllers/ChatController.java index 894075b..3fde3ad 100644 --- a/src/main/java/com/fredmaina/chatapp/core/Controllers/ChatController.java +++ b/src/main/java/com/fredmaina/chatapp/core/Controllers/ChatController.java @@ -8,6 +8,8 @@ import com.fredmaina.chatapp.core.DTOs.ChatSessionDto; import com.fredmaina.chatapp.core.Repositories.ChatMessageRepository; import com.fredmaina.chatapp.core.Services.ChatService; +import com.fredmaina.chatapp.core.error.ResourceNotFoundException; +import com.fredmaina.chatapp.core.error.UnauthorizedException; import com.fredmaina.chatapp.core.models.ChatMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +40,7 @@ public class ChatController { @GetMapping("/chats") public ResponseEntity getUserChats(@RequestHeader(value = "Authorization", required = false) String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("success",false,"message","Missing or invalid Authorization header")); + throw new UnauthorizedException("Missing or invalid Authorization header"); } String token = authHeader.replace("Bearer ", ""); @@ -46,13 +48,13 @@ public ResponseEntity getUserChats(@RequestHeader(value = "Authorization", re try { email = jwtService.getUsernameFromToken(token); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("success",false,"message","Invalid token")); + throw new UnauthorizedException("Invalid token"); } Optional userOptional = userRepository.findByEmail(email); if (userOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("success",false,"message","User not found")); + throw new ResourceNotFoundException("User not found"); } User user = userOptional.get(); @@ -77,7 +79,7 @@ public ResponseEntity deleteChatSession( @PathVariable String anonSessionId) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("success", false, "message", "Missing or invalid Authorization header")); + throw new UnauthorizedException("Missing or invalid Authorization header"); } String token = authHeader.replace("Bearer ", ""); @@ -85,12 +87,12 @@ public ResponseEntity deleteChatSession( try { email = jwtService.getUsernameFromToken(token); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of("success", false, "message", "Invalid token")); + throw new UnauthorizedException("Invalid token"); } Optional userOptional = userRepository.findByEmail(email); if (userOptional.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("success", false, "message", "User not found")); + throw new ResourceNotFoundException("User not found"); } User user = userOptional.get(); @@ -98,8 +100,9 @@ public ResponseEntity deleteChatSession( chatService.deleteChatSession(user.getId(), anonSessionId); return ResponseEntity.ok(Map.of("success", true, "message", "Chat session deleted successfully.")); } catch (RuntimeException e) { - // Log the exception e - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("success", false, "message", "Failed to delete chat session: " + e.getMessage())); + final String errMsg = String.format("Error deleting chat session %s",e.getMessage()); + log.error(errMsg); + throw new RuntimeException(errMsg); } } } \ No newline at end of file diff --git a/src/main/java/com/fredmaina/chatapp/core/error/BadRequestException.java b/src/main/java/com/fredmaina/chatapp/core/error/BadRequestException.java new file mode 100644 index 0000000..efa0194 --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/BadRequestException.java @@ -0,0 +1,7 @@ +package com.fredmaina.chatapp.core.error; + +public class BadRequestException extends RuntimeException{ + public BadRequestException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandler.java b/src/main/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandler.java new file mode 100644 index 0000000..618874f --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandler.java @@ -0,0 +1,48 @@ +package com.fredmaina.chatapp.core.error; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.time.Instant; + +@ControllerAdvice +public class ChatAppExceptionHandler { + + @ExceptionHandler(BadRequestException.class) + public ResponseEntity badRequestHandler(final BadRequestException ex) { + return buildResponseEntity(ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(UnauthorizedException.class) + public ResponseEntity unauthorizedHandler(final UnauthorizedException ex) { + return buildResponseEntity(ex.getMessage(), HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity resourceNotFoundHandler(final ResourceNotFoundException ex) { + return buildResponseEntity(ex.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(DataAlreadyExistsException.class) + public ResponseEntity duplicateDataRequestHandler(final DataAlreadyExistsException ex) { + return buildResponseEntity(ex.getMessage(), HttpStatus.CONFLICT); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGlobalException(final Exception ex) { + return buildResponseEntity(ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + private ResponseEntity buildResponseEntity(final String errorMessage, final HttpStatus httpStatus){ + ErrorResponse errorDetails = ErrorResponse.builder() + .timestamp(Instant.now()) + .status(httpStatus.value()) + .error(httpStatus.getReasonPhrase()) + .message(errorMessage) + .build(); + return new ResponseEntity<>(errorDetails, httpStatus); + + } +} diff --git a/src/main/java/com/fredmaina/chatapp/core/error/DataAlreadyExistsException.java b/src/main/java/com/fredmaina/chatapp/core/error/DataAlreadyExistsException.java new file mode 100644 index 0000000..75e89b6 --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/DataAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.fredmaina.chatapp.core.error; + +public class DataAlreadyExistsException extends RuntimeException{ + public DataAlreadyExistsException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/fredmaina/chatapp/core/error/ErrorResponse.java b/src/main/java/com/fredmaina/chatapp/core/error/ErrorResponse.java new file mode 100644 index 0000000..a715945 --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/ErrorResponse.java @@ -0,0 +1,19 @@ +package com.fredmaina.chatapp.core.error; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ErrorResponse { + private Instant timestamp; + private Integer status; + private String error; + private String message; +} diff --git a/src/main/java/com/fredmaina/chatapp/core/error/ResourceNotFoundException.java b/src/main/java/com/fredmaina/chatapp/core/error/ResourceNotFoundException.java new file mode 100644 index 0000000..37c9550 --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.fredmaina.chatapp.core.error; + +public class ResourceNotFoundException extends RuntimeException{ + public ResourceNotFoundException(final String message) { + super(message); + } +} diff --git a/src/main/java/com/fredmaina/chatapp/core/error/UnauthorizedException.java b/src/main/java/com/fredmaina/chatapp/core/error/UnauthorizedException.java new file mode 100644 index 0000000..1fe8e8d --- /dev/null +++ b/src/main/java/com/fredmaina/chatapp/core/error/UnauthorizedException.java @@ -0,0 +1,7 @@ +package com.fredmaina.chatapp.core.error; + +public class UnauthorizedException extends RuntimeException{ + public UnauthorizedException(final String message) { + super(message); + } +} diff --git a/src/test/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandlerTest.java b/src/test/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandlerTest.java new file mode 100644 index 0000000..b44cd13 --- /dev/null +++ b/src/test/java/com/fredmaina/chatapp/core/error/ChatAppExceptionHandlerTest.java @@ -0,0 +1,71 @@ +package com.fredmaina.chatapp.core.error; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.*; + +class ChatAppExceptionHandlerTest { + private final ChatAppExceptionHandler exceptionHandler = new ChatAppExceptionHandler(); + + @Test + void badRequestHandler() { + // Given + String errorMessage = "Invalid request!"; + BadRequestException ex = new BadRequestException(errorMessage); + // When + ResponseEntity response = exceptionHandler.badRequestHandler(ex); + // Then + assertResult(response, HttpStatus.BAD_REQUEST); + } + + @Test + void unauthorizedHandler() { + // Given + String errorMessage = "Your are not authorized!"; + UnauthorizedException ex = new UnauthorizedException(errorMessage); + // When + ResponseEntity response = exceptionHandler.unauthorizedHandler(ex); + // Then + assertResult(response, HttpStatus.UNAUTHORIZED); + } + + @Test + void resourceNotFoundHandler() { + // Given + String errorMessage = "User not found"; + ResourceNotFoundException ex = new ResourceNotFoundException(errorMessage); + // When + ResponseEntity response = exceptionHandler.resourceNotFoundHandler(ex); + // Then + assertResult(response, HttpStatus.NOT_FOUND); + } + + @Test + void duplicateDataRequestHandler() { + // Given + String errorMessage = "User already exists"; + DataAlreadyExistsException ex = new DataAlreadyExistsException(errorMessage); + // When + ResponseEntity response = exceptionHandler.duplicateDataRequestHandler(ex); + // Then + assertResult(response, HttpStatus.CONFLICT); + } + + @Test + void handleGlobalException() { + // Given + String errorMessage = "User already exists"; + Exception ex = new RuntimeException(errorMessage); + // When + ResponseEntity response = exceptionHandler.handleGlobalException(ex); + // Then + assertResult(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private void assertResult(ResponseEntity response, HttpStatus status){ + assertNotNull(response); + assertEquals(status, response.getStatusCode()); + } +} \ No newline at end of file diff --git a/src/test/java/com/fredmaina/chatapp/core/error/ErrorResponseTest.java b/src/test/java/com/fredmaina/chatapp/core/error/ErrorResponseTest.java new file mode 100644 index 0000000..e33170b --- /dev/null +++ b/src/test/java/com/fredmaina/chatapp/core/error/ErrorResponseTest.java @@ -0,0 +1,31 @@ +package com.fredmaina.chatapp.core.error; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.*; + +class ErrorResponseTest { + + private static final String ERRMSG = "Not found"; + private static final int STATUS = 404; + + @Test + void builder() { + final ErrorResponse result = buildErrorResponse(); + Assertions.assertAll(()->assertEquals(STATUS, result.getStatus()), + ()->assertEquals(ERRMSG, result.getMessage()), + ()->assertEquals(ERRMSG, result.getError())); + } + + final ErrorResponse buildErrorResponse(){ + return ErrorResponse.builder() + .timestamp(Instant.now()) + .status(STATUS) + .error(ERRMSG) + .message(ERRMSG) + .build(); + } +} \ No newline at end of file