*/
package org.sonar.server.exceptions;
+import java.util.Objects;
import javax.annotation.Nullable;
import static java.lang.String.format;
}
/**
- * @throws NotFoundException if the value if null
+ * @throws NotFoundException if the value is null
* @return the value
*/
public static <T> T checkFound(@Nullable T value, String message, Object... messageArguments) {
- if (value == null) {
+ return Objects.requireNonNullElseGet(value, () -> {
throw new NotFoundException(format(message, messageArguments));
- }
-
- return value;
+ });
}
/**
* @return the value
*/
public static <T> T checkFoundWithOptional(java.util.Optional<T> value, String message, Object... messageArguments) {
- if (!value.isPresent()) {
- throw new NotFoundException(format(message, messageArguments));
- }
-
- return value.get();
+ return value.orElseThrow(() -> new NotFoundException(format(message, messageArguments)));
}
+
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.v2.common;
+
+enum ErrorMessages {
+ ACCESS_DENIED("Access denied."),
+ AUTHENTICATION_FAILED("Authentication failed."),
+ BAD_REQUEST("Bad request."),
+ BIND_ERROR("Bind error."),
+ CONVERSION_FAILED("Failed to convert value."),
+ INTERNAL_SERVER_ERROR("An internal server error occurred."),
+ INVALID_INPUT_PROVIDED("Invalid input provided."),
+ INVALID_PARAMETER_TYPE("Invalid parameter type."),
+ INVALID_REQUEST_FORMAT("Invalid request format."),
+ INVALID_STATE("Invalid state."),
+ METHOD_NOT_SUPPORTED("Method not supported."),
+ RESOURCE_NOT_FOUND("Resource not found."),
+ REQUEST_TIMEOUT("The request timed out."),
+ SIZE_EXCEEDED("The uploaded file exceeds the maximum allowed size."),
+ UNEXPECTED_ERROR("An unexpected error occurred. Please contact support if the problem persists."),
+ UNACCEPTABLE_MEDIA_TYPE("The requested media type is not acceptable."),
+ UNSUPPORTED_MEDIA_TYPE("Unsupported media type."),
+ VALIDATION_ERROR("Validation error. Please check your input."),
+ ;
+
+ private final String message;
+
+ ErrorMessages(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
*/
package org.sonar.server.v2.common;
+import java.io.FileNotFoundException;
+import java.nio.file.AccessDeniedException;
+import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.exceptions.ForbiddenException;
-import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.authentication.event.AuthenticationException;
import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.v2.api.model.RestError;
+import org.springframework.core.convert.ConversionFailedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
+import org.springframework.web.HttpMediaTypeNotAcceptableException;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
@RestControllerAdvice
private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class);
- @ExceptionHandler({IllegalStateException.class})
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- protected ResponseEntity<RestError> handleIllegalStateException(IllegalStateException illegalStateException) {
- return new ResponseEntity<>(new RestError(illegalStateException.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
+ // region client
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ protected ResponseEntity<RestError> handleHttpMessageNotReadableException(HttpMessageNotReadableException ex) {
+ LOGGER.error(ErrorMessages.INVALID_REQUEST_FORMAT.getMessage(), ex);
+ return buildResponse(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_REQUEST_FORMAT);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ protected ResponseEntity<RestError> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
+ LOGGER.error(ErrorMessages.VALIDATION_ERROR.getMessage(), ex);
+ String validationErrors = ex.getFieldErrors().stream()
+ .map(RestResponseEntityExceptionHandler::handleFieldError)
+ .collect(Collectors.joining());
+ return buildResponse(HttpStatus.BAD_REQUEST, validationErrors);
+ }
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ protected ResponseEntity<RestError> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
+ LOGGER.error(ErrorMessages.INVALID_PARAMETER_TYPE.getMessage(), ex);
+ return buildResponse(HttpStatus.BAD_REQUEST, ErrorMessages.INVALID_PARAMETER_TYPE);
+ }
+
+ @ExceptionHandler(ConversionFailedException.class)
+ protected ResponseEntity<RestError> handleConversionFailedException(ConversionFailedException ex) {
+ LOGGER.error(ErrorMessages.CONVERSION_FAILED.getMessage(), ex);
+ return buildResponse(HttpStatus.BAD_REQUEST, ErrorMessages.CONVERSION_FAILED);
+ }
+
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ protected ResponseEntity<RestError> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
+ LOGGER.error(ErrorMessages.METHOD_NOT_SUPPORTED.getMessage(), ex);
+ return buildResponse(HttpStatus.METHOD_NOT_ALLOWED, ErrorMessages.METHOD_NOT_SUPPORTED);
+ }
+
+ @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
+ protected ResponseEntity<RestError> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException ex) {
+ LOGGER.error(ErrorMessages.UNSUPPORTED_MEDIA_TYPE.getMessage(), ex);
+ return buildResponse(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ErrorMessages.UNSUPPORTED_MEDIA_TYPE);
+ }
+
+ @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
+ protected ResponseEntity<RestError> handleHttpMediaTypeNotAcceptableException(HttpMediaTypeNotAcceptableException ex) {
+ LOGGER.error(ErrorMessages.UNACCEPTABLE_MEDIA_TYPE.getMessage(), ex);
+ return buildResponse(HttpStatus.NOT_ACCEPTABLE, ErrorMessages.UNACCEPTABLE_MEDIA_TYPE);
}
- @ExceptionHandler({IllegalArgumentException.class})
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- protected ResponseEntity<RestError> handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) {
- return new ResponseEntity<>(new RestError(illegalArgumentException.getMessage()), HttpStatus.BAD_REQUEST);
+ @ExceptionHandler(IllegalArgumentException.class)
+ protected ResponseEntity<RestError> handleIllegalArgumentException(IllegalArgumentException ex) {
+ LOGGER.error(ErrorMessages.INVALID_INPUT_PROVIDED.getMessage(), ex);
+ return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
}
@ExceptionHandler(BindException.class)
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- protected ResponseEntity<RestError> handleBindException(BindException bindException) {
- String validationErrors = bindException.getFieldErrors().stream()
+ protected ResponseEntity<RestError> handleBindException(BindException ex) {
+ LOGGER.error(ErrorMessages.BIND_ERROR.getMessage(), ex);
+ String validationErrors = ex.getFieldErrors().stream()
.map(RestResponseEntityExceptionHandler::handleFieldError)
.collect(Collectors.joining());
- return new ResponseEntity<>(new RestError(validationErrors), HttpStatus.BAD_REQUEST);
+ return buildResponse(HttpStatus.BAD_REQUEST, validationErrors);
}
- private static String handleFieldError(FieldError fieldError) {
- String fieldName = fieldError.getField();
- String rejectedValueAsString = Optional.ofNullable(fieldError.getRejectedValue()).map(Object::toString).orElse("{}");
- String defaultMessage = fieldError.getDefaultMessage();
- return String.format("Value %s for field %s was rejected. Error: %s.", rejectedValueAsString, fieldName, defaultMessage);
+ @ExceptionHandler({
+ NoSuchElementException.class,
+ ServletRequestBindingException.class,
+ })
+ protected ResponseEntity<RestError> handleBadRequests(Exception ex) {
+ LOGGER.error(ErrorMessages.BAD_REQUEST.getMessage(), ex);
+ return buildResponse(HttpStatus.BAD_REQUEST, ErrorMessages.BAD_REQUEST);
}
- @ExceptionHandler({ServerException.class, ForbiddenException.class, UnauthorizedException.class, BadRequestException.class})
- protected ResponseEntity<RestError> handleServerException(ServerException serverException) {
- return new ResponseEntity<>(new RestError(serverException.getMessage()),
- Optional.ofNullable(HttpStatus.resolve(serverException.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR));
+ // endregion client
+
+ // region security
+
+ @ExceptionHandler(AccessDeniedException.class)
+ protected ResponseEntity<RestError> handleAccessDeniedException(AccessDeniedException ex) {
+ LOGGER.error(ErrorMessages.ACCESS_DENIED.getMessage(), ex);
+ return buildResponse(HttpStatus.FORBIDDEN, ErrorMessages.ACCESS_DENIED);
}
- @ExceptionHandler({NotFoundException.class})
- @ResponseStatus(HttpStatus.NOT_FOUND)
- protected ResponseEntity<RestError> handleNotFoundException(NotFoundException notFoundException) {
- return new ResponseEntity<>(new RestError(notFoundException.getMessage()), HttpStatus.NOT_FOUND);
+ // endregion security
+
+ // region server
+
+ @ExceptionHandler(IllegalStateException.class)
+ protected ResponseEntity<RestError> handleIllegalStateException(IllegalStateException ex) {
+ LOGGER.error(ErrorMessages.INVALID_STATE.getMessage(), ex);
+ return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
}
@ExceptionHandler({
- HttpMessageNotReadableException.class,
- MethodArgumentTypeMismatchException.class,
- HttpRequestMethodNotSupportedException.class,
- ServletRequestBindingException.class,
- NoHandlerFoundException.class
+ FileNotFoundException.class,
+ NoHandlerFoundException.class,
})
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- protected ResponseEntity<RestError> handleClientSideException(Exception exception) {
- LOGGER.error("Error processing request", exception);
- return new ResponseEntity<>(new RestError("An error occurred while processing the request."), HttpStatus.BAD_REQUEST);
+ protected ResponseEntity<RestError> handleResourceNotFoundException(Exception ex) {
+ LOGGER.error(ErrorMessages.RESOURCE_NOT_FOUND.getMessage(), ex);
+ return buildResponse(HttpStatus.NOT_FOUND, ErrorMessages.RESOURCE_NOT_FOUND);
+ }
+
+ @ExceptionHandler(AsyncRequestTimeoutException.class)
+ protected ResponseEntity<RestError> handleAsyncRequestTimeoutException(AsyncRequestTimeoutException ex) {
+ LOGGER.error(ErrorMessages.REQUEST_TIMEOUT.getMessage(), ex);
+ return buildResponse(HttpStatus.SERVICE_UNAVAILABLE, ErrorMessages.REQUEST_TIMEOUT);
+ }
+
+ @ExceptionHandler(MaxUploadSizeExceededException.class)
+ protected ResponseEntity<RestError> handleMaxUploadSizeExceededException(MaxUploadSizeExceededException ex) {
+ LOGGER.error(ErrorMessages.SIZE_EXCEEDED.getMessage(), ex);
+ return buildResponse(HttpStatus.PAYLOAD_TOO_LARGE, ErrorMessages.SIZE_EXCEEDED);
+ }
+
+ @ExceptionHandler(AuthenticationException.class)
+ protected ResponseEntity<RestError> handleAuthenticationException(AuthenticationException ex) {
+ LOGGER.error(ErrorMessages.AUTHENTICATION_FAILED.getMessage(), ex);
+ return buildResponse(HttpStatus.UNAUTHORIZED, ErrorMessages.AUTHENTICATION_FAILED);
+ }
+
+ @ExceptionHandler(ServerException.class)
+ protected ResponseEntity<RestError> handleServerException(ServerException ex) {
+ final HttpStatus httpStatus = Optional.ofNullable(HttpStatus.resolve(ex.httpCode())).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
+ final String errorMessage = Optional.ofNullable(ex.getMessage()).orElse(ErrorMessages.INTERNAL_SERVER_ERROR.getMessage());
+ return buildResponse(httpStatus, errorMessage);
}
+ // endregion server
+
@ExceptionHandler({Exception.class})
- @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
- protected ResponseEntity<RestError> handleUnhandledException(Exception exception) {
- LOGGER.error("Unhandled exception", exception);
- return new ResponseEntity<>(new RestError("An unexpected error occurred. Please contact support if the problem persists."), HttpStatus.INTERNAL_SERVER_ERROR);
+ protected ResponseEntity<RestError> handleUnhandledException(Exception ex) {
+ LOGGER.error(ErrorMessages.UNEXPECTED_ERROR.getMessage(), ex);
+ return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ErrorMessages.UNEXPECTED_ERROR);
+ }
+
+ private static ResponseEntity<RestError> buildResponse(HttpStatus httpStatus, ErrorMessages errorMessages) {
+ return buildResponse(httpStatus, errorMessages.getMessage());
+ }
+
+ private static ResponseEntity<RestError> buildResponse(HttpStatus httpStatus, String errorMessage) {
+ return ResponseEntity.status(httpStatus).body(new RestError(errorMessage));
+ }
+
+ private static String handleFieldError(FieldError fieldError) {
+ String fieldName = fieldError.getField();
+ String rejectedValueAsString = Optional.ofNullable(fieldError.getRejectedValue()).map(Object::toString).orElse("{}");
+ String defaultMessage = fieldError.getDefaultMessage();
+ return String.format("Value %s for field %s was rejected. Error: %s.", rejectedValueAsString, fieldName, defaultMessage);
}
}
*/
package org.sonar.server.v2.common;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.nio.file.AccessDeniedException;
+import java.util.List;
+import java.util.NoSuchElementException;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.event.Level;
+import org.sonar.api.testfixtures.log.LogTesterJUnit5;
+import org.sonar.server.authentication.event.AuthenticationEvent;
+import org.sonar.server.authentication.event.AuthenticationException;
+import org.sonar.server.exceptions.BadConfigurationException;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.exceptions.TemplateMatchingKeyException;
+import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.v2.api.model.RestError;
-import org.springframework.http.HttpInputMessage;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.convert.ConversionFailedException;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.mock.http.MockHttpInputMessage;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpMediaTypeNotAcceptableException;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.ServletRequestBindingException;
+import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.NoHandlerFoundException;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
class RestResponseEntityExceptionHandlerTest {
private final RestResponseEntityExceptionHandler underTest = new RestResponseEntityExceptionHandler();
+ @RegisterExtension
+ private final LogTesterJUnit5 logs = new LogTesterJUnit5();
+
+ // region client
+
+ @Test
+ void handleHttpMessageNotReadableException_shouldReturnBadRequest() {
+ var ex = new HttpMessageNotReadableException("Invalid format", new MockHttpInputMessage(InputStream.nullInputStream()));
+ ResponseEntity<RestError> response = underTest.handleHttpMessageNotReadableException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.INVALID_REQUEST_FORMAT.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.INVALID_REQUEST_FORMAT.getMessage());
+ }
+
+ @Test
+ void handleMethodArgumentNotValidException_shouldReturnBadRequest() {
+ var fieldError = mock(FieldError.class);
+ var ex = mock(MethodArgumentNotValidException.class);
+
+ when(fieldError.getDefaultMessage()).thenReturn("<defaultMessage>");
+ when(fieldError.getField()).thenReturn("<field>");
+ when(ex.getFieldErrors()).thenReturn(List.of(fieldError));
+
+ ResponseEntity<RestError> response = underTest.handleMethodArgumentNotValidException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo("Value {} for field <field> was rejected. Error: <defaultMessage>." /* ErrorMessages.VALIDATION_ERROR.getMessage() */);
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.VALIDATION_ERROR.getMessage());
+ }
+
+ @Test
+ void handleMethodArgumentTypeMismatchException_shouldReturnBadRequest() {
+ MethodParameter methodParameter = mock(MethodParameter.class);
+ Throwable throwable = mock(Throwable.class);
+
+ var ex = new MethodArgumentTypeMismatchException(null, null, "name", methodParameter, throwable);
+
+ ResponseEntity<RestError> response = underTest.handleMethodArgumentTypeMismatchException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.INVALID_PARAMETER_TYPE.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.INVALID_PARAMETER_TYPE.getMessage());
+ }
+
+ @Test
+ void handleConversionFailedException_shouldReturnBadRequest() {
+ var ex = new ConversionFailedException(null, TypeDescriptor.valueOf(Byte.class), null, mock(Throwable.class));
+
+ ResponseEntity<RestError> response = underTest.handleConversionFailedException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.CONVERSION_FAILED.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.CONVERSION_FAILED.getMessage());
+ }
+
+ @Test
+ void handleHttpRequestMethodNotSupportedException_shouldReturnMethodNotAllowed() {
+ var ex = new HttpRequestMethodNotSupportedException("GET");
+
+ ResponseEntity<RestError> response = underTest.handleHttpRequestMethodNotSupportedException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.METHOD_NOT_ALLOWED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.METHOD_NOT_SUPPORTED.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.METHOD_NOT_SUPPORTED.getMessage());
+ }
+
+ @Test
+ void handleHttpMediaTypeNotSupportedException_shouldReturnUnsupportedMediaType() {
+ var ex = new HttpMediaTypeNotSupportedException("Unsupported media type");
+
+ ResponseEntity<RestError> response = underTest.handleHttpMediaTypeNotSupportedException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.UNSUPPORTED_MEDIA_TYPE.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.UNSUPPORTED_MEDIA_TYPE.getMessage());
+ }
+
+ @Test
+ void handleHttpMediaTypeNotAcceptableException_shouldReturnNotAcceptable() {
+ var ex = new HttpMediaTypeNotAcceptableException("Not acceptable");
+
+ ResponseEntity<RestError> response = underTest.handleHttpMediaTypeNotAcceptableException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.UNACCEPTABLE_MEDIA_TYPE.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.UNACCEPTABLE_MEDIA_TYPE.getMessage());
+ }
+
+ @Test
+ void handleIllegalArgumentException_shouldReturnBadRequest() {
+ var ex = new IllegalArgumentException("Invalid argument");
+
+ ResponseEntity<RestError> response = underTest.handleIllegalArgumentException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ex.getMessage() /* ErrorMessages.INVALID_INPUT_PROVIDED.getMessage() */);
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.INVALID_INPUT_PROVIDED.getMessage());
+ }
+
+ @Test
+ void handleBindException_shouldReturnBadRequest() {
+ var ex = new BindException(new Object(), "target");
+ ex.addError(new FieldError("target", "field", "Field error"));
+
+ ResponseEntity<RestError> response = underTest.handleBindException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).contains("Value {} for field field was rejected. Error: Field error.");
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.BIND_ERROR.getMessage());
+ }
+
+ @ParameterizedTest
+ @MethodSource("badRequestsProvider")
+ void handleBadRequests_shouldReturnBadRequest(Exception ex) {
+ ResponseEntity<RestError> response = underTest.handleBadRequests(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.BAD_REQUEST.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.BAD_REQUEST.getMessage());
+ }
+
+ static Stream<Arguments> badRequestsProvider() {
+ return Stream.of(
+ Arguments.of(new NoSuchElementException("Element not found")),
+ Arguments.of(new ServletRequestBindingException("Binding error")));
+ }
+
+ // endregion client
+
+ // region security
+
+ @Test
+ void handleAccessDeniedException_shouldReturnForbidden() {
+ var ex = new AccessDeniedException("Access denied");
+ ResponseEntity<RestError> response = underTest.handleAccessDeniedException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.ACCESS_DENIED.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.ACCESS_DENIED.getMessage());
+ }
+
+ @Test
+ void handleAuthenticationException_shouldReturnUnauthorized() {
+ var ex = AuthenticationException.newBuilder()
+ .setSource(AuthenticationEvent.Source.sso())
+ .setLogin("mockLogin")
+ .setPublicMessage("Authentication failed")
+ .build();
+ ResponseEntity<RestError> response = underTest.handleAuthenticationException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.AUTHENTICATION_FAILED.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.AUTHENTICATION_FAILED.getMessage());
+ }
+
+ // endregion security
+
+ // region server
+
+ @Test
+ void handleIllegalStateException_shouldReturnInternalServerError() {
+ var ex = new IllegalStateException();
+ ResponseEntity<RestError> response = underTest.handleIllegalStateException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ex.getMessage() /* ErrorMessages.INVALID_STATE.getMessage() */);
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.INVALID_STATE.getMessage());
+ }
+
@ParameterizedTest
- @MethodSource("clientSideExceptionsProvider")
- void handleClientSideException_shouldUseGenericMessage(Exception exception) {
+ @MethodSource("resourceNotFoundExceptionProvider")
+ void handleResourceNotFoundException_shouldReturnNotFound(Exception ex) {
+ ResponseEntity<RestError> response = underTest.handleResourceNotFoundException(ex);
- ResponseEntity<RestError> responseEntity = underTest.handleClientSideException(exception);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.RESOURCE_NOT_FOUND.getMessage());
- assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
- assertThat(responseEntity.getBody().message()).isEqualTo("An error occurred while processing the request.");
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.RESOURCE_NOT_FOUND.getMessage());
}
- private static Stream<Arguments> clientSideExceptionsProvider() {
+ static Stream<Arguments> resourceNotFoundExceptionProvider() {
return Stream.of(
- Arguments.of(new HttpMessageNotReadableException("", (HttpInputMessage) null)),
- Arguments.of(new MethodArgumentTypeMismatchException(null, null, null, null, null)),
- Arguments.of(new HttpRequestMethodNotSupportedException("")),
- Arguments.of(new ServletRequestBindingException("")),
- Arguments.of(new NoHandlerFoundException("", null, null)));
+ Arguments.of(new FileNotFoundException("File not found")),
+ Arguments.of(new NoHandlerFoundException("GET", "URL", HttpHeaders.EMPTY)));
}
@Test
- void handleUnhandledException_shouldUseGenericMessage() {
+ void handleAsyncRequestTimeoutException_shouldReturnServiceUnavailable() {
+ var ex = new AsyncRequestTimeoutException();
+ ResponseEntity<RestError> response = underTest.handleAsyncRequestTimeoutException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.REQUEST_TIMEOUT.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.REQUEST_TIMEOUT.getMessage());
+ }
- Exception exception = new Exception();
+ @Test
+ void handleMaxUploadSizeExceededException_shouldReturnPayloadTooLarge() {
+ var ex = new MaxUploadSizeExceededException(0);
+ ResponseEntity<RestError> response = underTest.handleMaxUploadSizeExceededException(ex);
- ResponseEntity<RestError> responseEntity = underTest.handleUnhandledException(exception);
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.PAYLOAD_TOO_LARGE);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.SIZE_EXCEEDED.getMessage());
- assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
- assertThat(responseEntity.getBody().message()).isEqualTo("An unexpected error occurred. Please contact support if the problem persists.");
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.SIZE_EXCEEDED.getMessage());
}
+ @ParameterizedTest
+ @MethodSource("serverExceptionsProvider")
+ void handleServerException_shouldReturnCorrectHttpStatus(ServerException ex, HttpStatus expectedStatus) {
+ ResponseEntity<RestError> response = underTest.handleServerException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(expectedStatus);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ex.getMessage());
+ }
+
+ static Stream<Arguments> serverExceptionsProvider() {
+ return Stream.of(
+ Arguments.of(new BadConfigurationException("Scope", "Bad config"), HttpStatus.BAD_REQUEST),
+ Arguments.of(BadRequestException.create("Bad request"), HttpStatus.BAD_REQUEST),
+ Arguments.of(new ForbiddenException("Access forbidden"), HttpStatus.FORBIDDEN),
+ Arguments.of(new NotFoundException("Not found"), HttpStatus.NOT_FOUND),
+ Arguments.of(new TemplateMatchingKeyException("Template matching error"), HttpStatus.BAD_REQUEST),
+ Arguments.of(new UnauthorizedException("Unauthorized access"), HttpStatus.UNAUTHORIZED));
+ }
+
+ @Test
+ void handleUnhandledException_shouldReturnInternalServerError() {
+ var ex = new Exception("Some unexpected error");
+ ResponseEntity<RestError> response = underTest.handleUnhandledException(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().message()).isEqualTo(ErrorMessages.UNEXPECTED_ERROR.getMessage());
+
+ // Verify logging
+ assertThat(logs.logs(Level.ERROR)).contains(ErrorMessages.UNEXPECTED_ERROR.getMessage());
+ }
+
+ // endregion server
+
}
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.exceptions.ServerException;
+import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.permission.PermissionService;
import org.sonar.server.permission.PermissionServiceImpl;
import org.sonar.server.ws.TestRequest;
.isInstanceOf(NotFoundException.class)
.hasMessage("Entity not found");
}
+
+ @Test
+ public void adding_permission_to_user_fails_if_not_authenticated_and_existing_login() {
+ userSession.anonymous();
+
+ TestRequest request = newRequest()
+ .setParam(PARAM_USER_LOGIN, user.getLogin())
+ .setParam(PARAM_PERMISSION, GlobalPermission.ADMINISTER.getKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(UnauthorizedException.class)
+ .hasMessage("Authentication is required");
+ }
+
+ @Test
+ public void adding_permission_to_user_fails_if_not_authenticated_and_non_existing_login() {
+ userSession.anonymous();
+
+ TestRequest request = newRequest()
+ .setParam(PARAM_USER_LOGIN, "non-existing-login")
+ .setParam(PARAM_PERMISSION, GlobalPermission.ADMINISTER.getKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(UnauthorizedException.class)
+ .hasMessage("Authentication is required");
+ }
}