Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update exception handling and error response style #54

Merged
merged 5 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
package com.linkurlshorter.urlshortener.exception;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

/**
* Class representing the error response object.
*
* @author Vlas Pototskyi
*/
@Data
@AllArgsConstructor
public class ErrorResponse {
private LocalDateTime localDateTime;
private int statusCode;
private String message;
private String exceptionMessage;
}
public record ErrorResponse(
LocalDateTime dateTime,
int statusCode,
String message,
String path
) {}
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package com.linkurlshorter.urlshortener.exception;

import com.linkurlshorter.urlshortener.auth.exception.EmailAlreadyTakenException;
import com.linkurlshorter.urlshortener.security.ForbiddenException;
import com.linkurlshorter.urlshortener.security.UnauthorizedException;
import com.linkurlshorter.urlshortener.user.NoSuchEmailFoundException;
import com.linkurlshorter.urlshortener.user.NoUserFoundByEmailException;
import com.linkurlshorter.urlshortener.user.NoUserFoundByIdException;
import com.linkurlshorter.urlshortener.user.NullEmailException;
import org.apache.coyote.BadRequestException;
import org.springframework.data.crossstore.ChangeSetPersister;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
Expand All @@ -31,13 +27,16 @@ public class GlobalExceptionHandler {
* Handles method argument validation errors and invalid request errors (400).
* Returns a response with status 400 and the corresponding error message.
*
* @param exception method argument validation error
* @param ex method argument validation error
* @return {@link ResponseEntity} object with the appropriate status and error message
*/
@ExceptionHandler({MethodArgumentNotValidException.class, BadRequestException.class})
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException exception) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation failed!",
Objects.requireNonNull(exception.getFieldError()).getDefaultMessage());
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(
HttpStatus.BAD_REQUEST,
Objects.requireNonNull(ex.getFieldError()).getDefaultMessage(),
request.getRequestURI());
return ResponseEntity.badRequest().body(errorResponse);
}

Expand All @@ -49,9 +48,10 @@ public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotVali
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(NullEmailException.class)
public ResponseEntity<Object> handleNullEmailException(NullEmailException ex) {
public ResponseEntity<Object> handleNullEmailException(
NullEmailException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.BAD_REQUEST,
"Email provided is null, so request can not be processed!", ex.getMessage());
ex.getMessage(), request.getRequestURI());
return ResponseEntity.badRequest().body(errorResponse);
}

Expand All @@ -62,9 +62,10 @@ public ResponseEntity<Object> handleNullEmailException(NullEmailException ex) {
* @return {@link ResponseEntity} containing the error response for authentication failure
*/
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex) {
public ResponseEntity<Object> handleAuthenticationException(
AuthenticationException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.UNAUTHORIZED,
"Authentication failed!", ex.getMessage());
ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}

Expand All @@ -75,106 +76,39 @@ public ResponseEntity<Object> handleAuthenticationException(AuthenticationExcept
* @return {@link ResponseEntity} containing the error response for email already taken
*/
@ExceptionHandler(EmailAlreadyTakenException.class)
public ResponseEntity<Object> handleEmailAlreadyTakenException(EmailAlreadyTakenException ex) {
public ResponseEntity<Object> handleEmailAlreadyTakenException(
EmailAlreadyTakenException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.BAD_REQUEST,
"Email already taken!", ex.getMessage());
ex.getMessage(), request.getRequestURI());
return ResponseEntity.badRequest().body(errorResponse);
}

/**
* Handles authentication failure (401) errors.
* Returns a response with a 401 status and the corresponding error message.
*
* @param ex failed authentication error
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Object> handleUnauthorizedException(UnauthorizedException ex) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.UNAUTHORIZED,
"Unauthorized!", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}

/**
* Handles bad credentials (401) errors.
* Returns a response with a 401 status and the corresponding error message.
*
* @param ex bad credentials error
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<Object> handleBadCredentialsException(BadCredentialsException ex) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.UNAUTHORIZED,
"Bad Credentials!", ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}

/**
* Handles denied access (403) errors.
* Returns a response with a 403 status and the corresponding error message.
*
* @param ex denied access error
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<Object> handleForbiddenException(ForbiddenException ex) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.FORBIDDEN,
"Forbidden!", ex.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorResponse);
}

/**
* Handles no resource (404) exceptions for different types of requests.
* Returns a response with a 404 status and the corresponding error message.
*
* @param ex missing resource exception
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler({NoSuchEmailFoundException.class, NoUserFoundByEmailException.class, NoUserFoundByIdException.class})
public ResponseEntity<Object> handleNotFoundExceptions(Exception ex) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.NOT_FOUND, "Email Not Found!",
ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

/**
* Handles resource unavailable (404) errors.
* Returns a response with a 404 status and the corresponding error message.
*
* @param ex resource error
* @return {@link ResponseEntity} object with the corresponding status and error message
*/
@ExceptionHandler(ChangeSetPersister.NotFoundException.class)
public ResponseEntity<Object> handleNotFoundException(ChangeSetPersister.NotFoundException ex) {
@ExceptionHandler({NoSuchEmailFoundException.class,
NoUserFoundByEmailException.class, NoUserFoundByIdException.class})
public ResponseEntity<Object> handleNotFoundExceptions(
RuntimeException ex, HttpServletRequest request) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.NOT_FOUND,
"Not Found!", ex.getMessage());
ex.getMessage(), request.getRequestURI());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
}

/**
* Handles general exceptions (500).
* Returns a response with status 500 and the corresponding error message.
*
* @param ex general exception
* @return {@link ResponseEntity} object with the appropriate status and error message
*/
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleInternalServerError(Exception ex) {
ErrorResponse errorResponse = buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR,
"Internal Server Error!", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}

/**
* Creates an error response object.
*
* @param status status of the error
* @param message error message
* @param exceptionMessage exception message
* @param status status of the error
* @param message error message
* @param requestURI request URL
* @return an {@link ErrorResponse} object with the appropriate data
*/
private ErrorResponse buildErrorResponse(HttpStatus status, String message, String exceptionMessage) {
return new ErrorResponse(LocalDateTime.now(), status.value(), message, exceptionMessage);
private ErrorResponse buildErrorResponse(HttpStatus status, String message, String requestURI) {
return new ErrorResponse(LocalDateTime.now(), status.value(), message, requestURI);
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.linkurlshorter.urlshortener.security;
package com.linkurlshorter.urlshortener.link;

public class ForbiddenException extends RuntimeException {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.linkurlshorter.urlshortener.link;

import com.linkurlshorter.urlshortener.security.ForbiddenException;
import com.linkurlshorter.urlshortener.user.User;
import com.linkurlshorter.urlshortener.user.UserService;
import jakarta.persistence.EntityManager;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;
import java.util.Comparator;
Expand Down Expand Up @@ -135,6 +138,7 @@ public ResponseEntity<LinkModifyingResponse> refreshLink(@RequestParam UUID id)
throw new ForbiddenException(OPERATION_FORBIDDEN_MSG);
}
}

/**
* Retrieves information about a link using its short link.
*
Expand All @@ -153,6 +157,7 @@ public ResponseEntity<LinkInfoResponse> getInfoByShortLink(@RequestParam String
throw new ForbiddenException(OPERATION_FORBIDDEN_MSG);
}
}

/**
* Retrieves information about all links associated with the authenticated user.
*
Expand All @@ -170,6 +175,7 @@ public ResponseEntity<LinkInfoResponse> getAllLinksForUser() {
.toList();
return ResponseEntity.ok(new LinkInfoResponse(linksDto, "ok"));
}

/**
* Retrieves usage statistics for all links associated with the authenticated user.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.linkurlshorter.urlshortener.auth.dto.AuthRequest;
import com.linkurlshorter.urlshortener.user.ChangeUserEmailRequest;
import com.linkurlshorter.urlshortener.user.ChangeUserPasswordRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -74,8 +72,7 @@ void loginFailedWhenUserDoesNotExistTest() throws Exception {
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(MockMvcResultMatchers.status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(401))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Authentication failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage").value("No user by provided email found"));
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("No user by provided email found"));
}

/**
Expand All @@ -91,8 +88,7 @@ void loginFailedWhenPasswordDoesNotMatchTest() throws Exception {
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(MockMvcResultMatchers.status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(401))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Bad Credentials!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage").value("Bad credentials"));
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Bad credentials"));
}

/**
Expand All @@ -110,8 +106,7 @@ void loginFailedWhenInvalidPasswordGivenTest(String password) throws Exception {
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage").value("Password " +
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Password " +
"must be at least 8 characters long and contain at least one digit, one uppercase letter, " +
"and one lowercase letter. No spaces are allowed."));
}
Expand All @@ -136,9 +131,7 @@ void loginFailedWhenInvalidEmailGivenTest(String email) throws Exception {
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage")
.value("Email address entered incorrectly!"));
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Email address entered incorrectly!"));
}

/**
Expand All @@ -156,8 +149,7 @@ void registerFailedWhenInvalidPasswordGivenTest(String password) throws Exceptio
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage").value("Password " +
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Password " +
"must be at least 8 characters long and contain at least one digit, one uppercase letter, " +
"and one lowercase letter. No spaces are allowed."));
}
Expand All @@ -182,8 +174,6 @@ void registerFailedWhenInvalidEmailGivenTest(String email) throws Exception {
.content(objectMapper.writeValueAsString(authRequest)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage")
.value("Email address entered incorrectly!"));
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Email address entered incorrectly!"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;


import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand Down Expand Up @@ -106,8 +104,7 @@ void changePasswordFailedWhenInvalidPasswordGivenTest(String password) throws Ex
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage").value("Password " +
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Password " +
"must be at least 8 characters long and contain at least one digit, one uppercase letter, " +
"and one lowercase letter. No spaces are allowed."));
}
Expand Down Expand Up @@ -158,8 +155,7 @@ void changeEmailFailedWhenInvalidEmailGivenTest(String email) throws Exception {
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().is4xxClientError())
.andExpect(MockMvcResultMatchers.jsonPath("$.statusCode").value(400))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Validation failed!"))
.andExpect(MockMvcResultMatchers.jsonPath("$.exceptionMessage")
.andExpect(MockMvcResultMatchers.jsonPath("$.message")
.value("Email address entered incorrectly!"));
}
}
Loading