From 0757c02db0b3731755dc194c2d5b3bf93a19d674 Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Mon, 22 Jan 2024 16:10:26 +0100 Subject: [PATCH] Added custom exception handlers --- .../exception/CustomExceptionsHandler.java | 8 +++ .../bits/exception/GenericErrorException.java | 21 +++++++ .../exception/UnauthorizedUserException.java | 2 +- .../ceticamarco/bits/post/PostController.java | 63 +++++++++---------- .../ceticamarco/bits/user/UserController.java | 35 +++++------ .../ceticamarco/bits/user/UserService.java | 1 - src/main/resources/application.properties | 14 ++--- .../ceticamarco/bits/PostControllerTests.java | 18 ++++-- .../ceticamarco/bits/UserControllerTest.java | 18 ++++++ 9 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 src/main/java/com/ceticamarco/bits/exception/GenericErrorException.java diff --git a/src/main/java/com/ceticamarco/bits/exception/CustomExceptionsHandler.java b/src/main/java/com/ceticamarco/bits/exception/CustomExceptionsHandler.java index 2d3b537..49070d7 100644 --- a/src/main/java/com/ceticamarco/bits/exception/CustomExceptionsHandler.java +++ b/src/main/java/com/ceticamarco/bits/exception/CustomExceptionsHandler.java @@ -14,6 +14,14 @@ import java.util.HashMap; @ControllerAdvice public class CustomExceptionsHandler { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(GenericErrorException.class) + public ResponseEntity genericErrorException(GenericErrorException ex) { + var error = new JsonEmitter<>(ex.getMessage()).emitJsonKey(ex.getKey()); + + return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); + } + @ResponseStatus(HttpStatus.BAD_REQUEST) @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { diff --git a/src/main/java/com/ceticamarco/bits/exception/GenericErrorException.java b/src/main/java/com/ceticamarco/bits/exception/GenericErrorException.java new file mode 100644 index 0000000..1b9e130 --- /dev/null +++ b/src/main/java/com/ceticamarco/bits/exception/GenericErrorException.java @@ -0,0 +1,21 @@ +package com.ceticamarco.bits.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Bad Request") +public class GenericErrorException extends RuntimeException { + private final String key; + + public GenericErrorException(String message, String key) { + super(message); + this.key = key; + } + + public GenericErrorException() { + super("Bad Request"); + this.key = "error"; + } + + public String getKey() { return key; } +} diff --git a/src/main/java/com/ceticamarco/bits/exception/UnauthorizedUserException.java b/src/main/java/com/ceticamarco/bits/exception/UnauthorizedUserException.java index 20466e8..99ccc5d 100644 --- a/src/main/java/com/ceticamarco/bits/exception/UnauthorizedUserException.java +++ b/src/main/java/com/ceticamarco/bits/exception/UnauthorizedUserException.java @@ -3,7 +3,7 @@ package com.ceticamarco.bits.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -@ResponseStatus(value= HttpStatus.UNAUTHORIZED, reason="Unauthorized user") +@ResponseStatus(value = HttpStatus.UNAUTHORIZED, reason = "Unauthorized User") public class UnauthorizedUserException extends RuntimeException { public UnauthorizedUserException(String message) { super(message); diff --git a/src/main/java/com/ceticamarco/bits/post/PostController.java b/src/main/java/com/ceticamarco/bits/post/PostController.java index fe90ba1..29c7d89 100644 --- a/src/main/java/com/ceticamarco/bits/post/PostController.java +++ b/src/main/java/com/ceticamarco/bits/post/PostController.java @@ -1,5 +1,6 @@ package com.ceticamarco.bits.post; +import com.ceticamarco.bits.exception.GenericErrorException; import com.ceticamarco.bits.exception.UnauthorizedUserException; import com.ceticamarco.bits.json.JsonEmitter; import com.ceticamarco.bits.user.User; @@ -28,14 +29,14 @@ public class PostController { public ResponseEntity> getPosts(@RequestBody User user) { // Check if email and password are specified if(user.getPassword() == null || user.getEmail() == null) { - throw new UnauthorizedUserException("Specify both email and password"); + throw new GenericErrorException("Specify both email and password", "error"); } // Get post list var res = postService.getPosts(user); // Check if user is authorized - if(res.isLeft()) { // TODO: implement proper generic exception handler + if(res.isLeft()) { throw new UnauthorizedUserException(res.getLeft().getMessage()); } @@ -50,15 +51,13 @@ public class PostController { */ @GetMapping("/api/posts/{postId}") public ResponseEntity getPostById(@PathVariable("postId") String postId) { - var res = postService.getPostById(postId) - .map(post -> new JsonEmitter<>(post).emitJsonKey()) - .swap() - .map(error -> new JsonEmitter<>(error.getMessage()).emitJsonKey("error")) - .swap(); + var res = postService.getPostById(postId); + if(res.isLeft()) { + throw new GenericErrorException(res.getLeft().getMessage(), "error"); + } - return res.isRight() - ? new ResponseEntity<>(res.get(), HttpStatus.OK) - : new ResponseEntity<>(res.getLeft(), HttpStatus.BAD_REQUEST); + var jsonOutput = new JsonEmitter<>(res.get()).emitJsonKey(); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } /** @@ -72,14 +71,14 @@ public class PostController { public ResponseEntity> getPostByTitle(@RequestBody Post req) { // Check if email and password are specified if(req.getUser() == null || req.getUser().getPassword() == null || req.getUser().getEmail() == null) { - throw new UnauthorizedUserException("Specify both email and password"); + throw new GenericErrorException("Specify both email and password", "error"); } // Get post by title var res = postService.getPostByTitle(req); // Check if user is authorized - if(res.isLeft()) { // TODO: implement proper generic exception handler + if(res.isLeft()) { throw new UnauthorizedUserException(res.getLeft().getMessage()); } @@ -94,15 +93,13 @@ public class PostController { */ @PostMapping("/api/posts/new") public ResponseEntity submitPost(@Valid @RequestBody Post post) { - var res = postService.addNewPost(post) - .map(postId -> new JsonEmitter<>(postId).emitJsonKey("post_id")) - .swap() - .map(error -> new JsonEmitter<>(error.getMessage()).emitJsonKey("error")) - .swap(); + var res =postService.addNewPost(post); + if(res.isLeft()) { + throw new GenericErrorException(res.getLeft().getMessage(), "error"); + } - return res.isRight() - ? new ResponseEntity<>(res.get(), HttpStatus.OK) - : new ResponseEntity<>(res.getLeft(), HttpStatus.BAD_REQUEST); + var jsonOutput = new JsonEmitter<>(res.get()).emitJsonKey("post_id"); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } /** @@ -114,15 +111,14 @@ public class PostController { */ @PutMapping("/api/posts/{postId}") public ResponseEntity updatePost(@Valid @RequestBody Post post, @PathVariable("postId") String postId) { + // Update post var res = postService.updatePost(post, postId); + if(res.isPresent()) { + throw new GenericErrorException(res.get().getMessage(), "error"); + } - return res.map(error -> { - var jsonOutput = new JsonEmitter<>(res.get().getMessage()).emitJsonKey("error"); - return new ResponseEntity<>(jsonOutput, HttpStatus.BAD_REQUEST); - }).orElseGet(() -> { - var jsonOutput = new JsonEmitter("OK").emitJsonKey("status"); - return new ResponseEntity<>(jsonOutput, HttpStatus.OK); - }); + var jsonOutput = new JsonEmitter<>("OK").emitJsonKey("status"); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } /** @@ -142,12 +138,11 @@ public class PostController { // Delete the post var res = postService.deletePost(user, postId); - return res.map(error -> { - var jsonOutput = new JsonEmitter<>(error.getMessage()).emitJsonKey("error"); - return new ResponseEntity<>(jsonOutput, HttpStatus.BAD_REQUEST); - }).orElseGet(() -> { - var jsonOutput = new JsonEmitter<>("OK").emitJsonKey("status"); - return new ResponseEntity<>(jsonOutput, HttpStatus.OK); - }); + if(res.isPresent()) { + throw new GenericErrorException(res.get().getMessage(), "error"); + } + + var jsonOutput = new JsonEmitter<>("OK").emitJsonKey("status"); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } } diff --git a/src/main/java/com/ceticamarco/bits/user/UserController.java b/src/main/java/com/ceticamarco/bits/user/UserController.java index 1a8a0a1..8645ac2 100644 --- a/src/main/java/com/ceticamarco/bits/user/UserController.java +++ b/src/main/java/com/ceticamarco/bits/user/UserController.java @@ -1,5 +1,6 @@ package com.ceticamarco.bits.user; +import com.ceticamarco.bits.exception.GenericErrorException; import com.ceticamarco.bits.exception.UnauthorizedUserException; import com.ceticamarco.bits.json.JsonEmitter; import jakarta.validation.Valid; @@ -29,14 +30,14 @@ public class UserController { public ResponseEntity> getUsers(@RequestBody User user) { // Check if email and password are specified if(user.getPassword() == null || user.getEmail() == null) { - throw new UnauthorizedUserException("Specify both email and password"); + throw new GenericErrorException("Specify both email and password", "error"); } // Get post list var res = userService.getUsers(user); // Check if user is authorized - if(res.isLeft()) { // TODO: implement proper generic exception handler + if(res.isLeft()) { throw new UnauthorizedUserException(res.getLeft().getMessage()); } @@ -51,15 +52,13 @@ public class UserController { */ @PostMapping("/api/users/new") public ResponseEntity submitUser(@Valid @RequestBody User user) { - var res = userService.addNewUser(user) - .map(userId -> new JsonEmitter<>(userId).emitJsonKey("user_id")) - .swap() - .map(error -> new JsonEmitter<>(error.getMessage()).emitJsonKey("error")) - .swap(); + var res = userService.addNewUser(user); + if(res.isLeft()) { + throw new GenericErrorException(res.getLeft().getMessage(), "error"); + } - return res.isRight() - ? new ResponseEntity<>(res.get(), HttpStatus.OK) - : new ResponseEntity<>(res.getLeft(), HttpStatus.BAD_REQUEST); + var jsonOutput = new JsonEmitter<>(res.get()).emitJsonKey("user_id"); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } /** @@ -72,17 +71,15 @@ public class UserController { public ResponseEntity deleteUser(@RequestBody User user) { // Check if email and password are specified if(user.getPassword() == null || user.getEmail() == null) { - var res = new JsonEmitter<>("Specify both email and password").emitJsonKey("error"); - return new ResponseEntity<>(res, HttpStatus.BAD_REQUEST); + throw new GenericErrorException("Specify both email and password", "error"); } // Delete the user var res = userService.deleteUser(user); - return res.map(error -> { - var jsonOutput = new JsonEmitter<>(error.getMessage()).emitJsonKey("error"); - return new ResponseEntity<>(jsonOutput, HttpStatus.BAD_REQUEST); - }).orElseGet(() -> { - var jsonOutput = new JsonEmitter<>("OK").emitJsonKey("status"); - return new ResponseEntity<>(jsonOutput, HttpStatus.OK); - }); + if(res.isPresent()) { + throw new GenericErrorException(res.get().getMessage(), "error"); + } + + var jsonOutput = new JsonEmitter<>("OK").emitJsonKey("status"); + return new ResponseEntity<>(jsonOutput, HttpStatus.OK); } } diff --git a/src/main/java/com/ceticamarco/bits/user/UserService.java b/src/main/java/com/ceticamarco/bits/user/UserService.java index ac5f6f5..a7e8268 100644 --- a/src/main/java/com/ceticamarco/bits/user/UserService.java +++ b/src/main/java/com/ceticamarco/bits/user/UserService.java @@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a53b7c..f56453d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,14 @@ -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true # Adjust these values in production -#server.port=3000 -#spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/bit -#spring.datasource.username=postgres -#spring.datasource.password=toor -#spring.security.user.name=admin -#spring.security.user.password=admin +server.port=3000 +spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/bit +spring.datasource.username=postgres +spring.datasource.password=toor +spring.security.user.name=admin +spring.security.user.password=admin # For unit tests only #spring.datasource.url=jdbc:h2:mem:testdb diff --git a/src/test/java/com/ceticamarco/bits/PostControllerTests.java b/src/test/java/com/ceticamarco/bits/PostControllerTests.java index c7060fa..b375ac3 100644 --- a/src/test/java/com/ceticamarco/bits/PostControllerTests.java +++ b/src/test/java/com/ceticamarco/bits/PostControllerTests.java @@ -41,14 +41,18 @@ public class PostControllerTests { post.setTitle("test"); post.setContent("This is a test"); - when(postService.getPosts()).thenReturn(List.of(post)); + var user = new User(); + user.setEmail("john@example.com"); + user.setPassword("qwerty"); + + when(postService.getPosts(any(User.class))).thenReturn(Either.right(List.of(post))); mockMvc.perform(MockMvcRequestBuilders.get("/api/posts") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(post))) + .content(objectMapper.writeValueAsString(user))) .andExpect(MockMvcResultMatchers.status().isOk()); - Mockito.verify(postService, Mockito.times(1)).getPosts(); + Mockito.verify(postService, Mockito.times(1)).getPosts(any(User.class)); } @Test @@ -71,18 +75,22 @@ public class PostControllerTests { @Test public void getPostByTitle() throws Exception { var post = new Post(); + var user = new User(); + user.setEmail("john@example.com"); + user.setPassword("qwerty"); post.setId("abc123"); post.setTitle("test"); post.setContent("This is a test"); + post.setUser(user); - when(postService.getPostByTitle(anyString())).thenReturn(List.of(post)); + when(postService.getPostByTitle(any(Post.class))).thenReturn(Either.right(List.of(post))); mockMvc.perform(MockMvcRequestBuilders.get("/api/posts/bytitle") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(post))) .andExpect(MockMvcResultMatchers.status().isOk()); - Mockito.verify(postService, Mockito.times(1)).getPostByTitle(anyString()); + Mockito.verify(postService, Mockito.times(1)).getPostByTitle(any(Post.class)); } @Test diff --git a/src/test/java/com/ceticamarco/bits/UserControllerTest.java b/src/test/java/com/ceticamarco/bits/UserControllerTest.java index f8e091c..a2d7565 100644 --- a/src/test/java/com/ceticamarco/bits/UserControllerTest.java +++ b/src/test/java/com/ceticamarco/bits/UserControllerTest.java @@ -15,6 +15,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import java.util.List; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; @@ -31,6 +32,23 @@ class UserControllerTest { @MockBean private UserService userService; + @Test + public void getUsers() throws Exception { + var user = new User(); + user.setUsername("john"); + user.setEmail("john@example.com"); + user.setPassword("qwerty"); + + when(userService.getUsers(any(User.class))).thenReturn(Either.right(List.of(user))); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(user))) + .andExpect(MockMvcResultMatchers.status().isOk()); + + Mockito.verify(userService, Mockito.times(1)).getUsers(any(User.class)); + } + @Test public void addNewUser() throws Exception { var user = new User();