20 changed files with 400 additions and 42 deletions
@ -0,0 +1,349 @@
@@ -0,0 +1,349 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ProblemDetail; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.validation.BindException; |
||||
import org.springframework.validation.BindingResult; |
||||
import org.springframework.validation.FieldError; |
||||
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||
import org.springframework.web.bind.MissingMatrixVariableException; |
||||
import org.springframework.web.bind.MissingPathVariableException; |
||||
import org.springframework.web.bind.MissingRequestCookieException; |
||||
import org.springframework.web.bind.MissingRequestHeaderException; |
||||
import org.springframework.web.bind.MissingServletRequestParameterException; |
||||
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; |
||||
import org.springframework.web.bind.support.WebExchangeBindException; |
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException; |
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException; |
||||
import org.springframework.web.server.MethodNotAllowedException; |
||||
import org.springframework.web.server.NotAcceptableStatusException; |
||||
import org.springframework.web.server.UnsupportedMediaTypeStatusException; |
||||
import org.springframework.web.testfixture.method.ResolvableMethod; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
|
||||
/** |
||||
* Unit tests that verify the HTTP response details exposed by exceptions in the |
||||
* {@link ErrorResponse} hierarchy. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class ErrorResponseExceptionTests { |
||||
|
||||
private final MethodParameter methodParameter = |
||||
new MethodParameter(ResolvableMethod.on(getClass()).resolveMethod("handle"), 0); |
||||
|
||||
|
||||
@Test |
||||
void httpMediaTypeNotSupportedException() { |
||||
|
||||
List<MediaType> mediaTypes = |
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); |
||||
|
||||
ErrorResponse ex = new HttpMediaTypeNotSupportedException( |
||||
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH, "Custom message"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); |
||||
assertDetail(ex, "Content-Type 'application/xml' is not supported."); |
||||
|
||||
HttpHeaders headers = ex.getHeaders(); |
||||
assertThat(headers.getAccept()).isEqualTo(mediaTypes); |
||||
assertThat(headers.getAcceptPatch()).isEqualTo(mediaTypes); |
||||
} |
||||
|
||||
@Test |
||||
void httpMediaTypeNotSupportedExceptionWithParseError() { |
||||
|
||||
ErrorResponse ex = new HttpMediaTypeNotSupportedException( |
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); |
||||
assertDetail(ex, "Could not parse Content-Type."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void httpMediaTypeNotAcceptableException() { |
||||
|
||||
List<MediaType> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); |
||||
ErrorResponse ex = new HttpMediaTypeNotAcceptableException(mediaTypes); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); |
||||
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); |
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1); |
||||
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); |
||||
} |
||||
|
||||
@Test |
||||
void httpMediaTypeNotAcceptableExceptionWithParseError() { |
||||
|
||||
ErrorResponse ex = new HttpMediaTypeNotAcceptableException( |
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); |
||||
assertDetail(ex, "Could not parse Accept header."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void asyncRequestTimeoutException() { |
||||
|
||||
ErrorResponse ex = new AsyncRequestTimeoutException(); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.SERVICE_UNAVAILABLE); |
||||
assertDetail(ex, null); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void httpRequestMethodNotSupportedException() { |
||||
|
||||
String[] supportedMethods = new String[] { "GET", "POST" }; |
||||
ErrorResponse ex = new HttpRequestMethodNotSupportedException("PUT", supportedMethods, "Custom message"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); |
||||
assertDetail(ex, "Method 'PUT' is not supported."); |
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1); |
||||
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); |
||||
} |
||||
|
||||
@Test |
||||
void missingRequestHeaderException() { |
||||
|
||||
ErrorResponse ex = new MissingRequestHeaderException("Authorization", this.methodParameter); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Required header 'Authorization' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void missingServletRequestParameterException() { |
||||
|
||||
ErrorResponse ex = new MissingServletRequestParameterException("query", "String"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Required parameter 'query' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void missingMatrixVariableException() { |
||||
|
||||
ErrorResponse ex = new MissingMatrixVariableException("region", this.methodParameter); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Required path parameter 'region' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void missingPathVariableException() { |
||||
|
||||
ErrorResponse ex = new MissingPathVariableException("id", this.methodParameter); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR); |
||||
assertDetail(ex, "Required path variable 'id' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void missingRequestCookieException() { |
||||
|
||||
ErrorResponse ex = new MissingRequestCookieException("oreo", this.methodParameter); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Required cookie 'oreo' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void unsatisfiedServletRequestParameterException() { |
||||
|
||||
ErrorResponse ex = new UnsatisfiedServletRequestParameterException( |
||||
new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"})); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Invalid request parameters."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void missingServletRequestPartException() { |
||||
|
||||
ErrorResponse ex = new MissingServletRequestPartException("file"); |
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Required part 'file' is not present."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void methodArgumentNotValidException() { |
||||
|
||||
BindingResult bindingResult = new BindException(new Object(), "object"); |
||||
bindingResult.addError(new FieldError("object", "field", "message")); |
||||
|
||||
ErrorResponse ex = new MethodArgumentNotValidException(this.methodParameter, bindingResult); |
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Invalid request content."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void unsupportedMediaTypeStatusException() { |
||||
|
||||
List<MediaType> mediaTypes = |
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); |
||||
|
||||
ErrorResponse ex = new UnsupportedMediaTypeStatusException( |
||||
MediaType.APPLICATION_XML, mediaTypes, HttpMethod.PATCH); |
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); |
||||
assertDetail(ex, "Content-Type 'application/xml' is not supported."); |
||||
|
||||
HttpHeaders headers = ex.getHeaders(); |
||||
assertThat(headers.getAccept()).isEqualTo(mediaTypes); |
||||
assertThat(headers.getAcceptPatch()).isEqualTo(mediaTypes); |
||||
} |
||||
|
||||
@Test |
||||
void unsupportedMediaTypeStatusExceptionWithParseError() { |
||||
|
||||
ErrorResponse ex = new UnsupportedMediaTypeStatusException( |
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); |
||||
|
||||
assertStatus(ex, HttpStatus.UNSUPPORTED_MEDIA_TYPE); |
||||
assertDetail(ex, "Could not parse Content-Type."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void notAcceptableStatusException() { |
||||
|
||||
List<MediaType> mediaTypes = |
||||
Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); |
||||
|
||||
ErrorResponse ex = new NotAcceptableStatusException(mediaTypes); |
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); |
||||
assertDetail(ex, "Acceptable representations: 'application/json, application/cbor'."); |
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1); |
||||
assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); |
||||
} |
||||
|
||||
@Test |
||||
void notAcceptableStatusExceptionWithParseError() { |
||||
|
||||
ErrorResponse ex = new NotAcceptableStatusException( |
||||
"Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.NOT_ACCEPTABLE); |
||||
assertDetail(ex, "Could not parse Accept header."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void webExchangeBindException() { |
||||
|
||||
BindingResult bindingResult = new BindException(new Object(), "object"); |
||||
bindingResult.addError(new FieldError("object", "field", "message")); |
||||
|
||||
ErrorResponse ex = new WebExchangeBindException(this.methodParameter, bindingResult); |
||||
|
||||
assertStatus(ex, HttpStatus.BAD_REQUEST); |
||||
assertDetail(ex, "Invalid request content."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void methodNotAllowedException() { |
||||
|
||||
List<HttpMethod> supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST); |
||||
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); |
||||
assertDetail(ex, "Supported methods: 'GET', 'POST'"); |
||||
|
||||
assertThat(ex.getHeaders()).hasSize(1); |
||||
assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); |
||||
} |
||||
|
||||
@Test |
||||
void methodNotAllowedExceptionWithoutSupportedMethods() { |
||||
|
||||
ErrorResponse ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList()); |
||||
|
||||
|
||||
assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); |
||||
assertDetail(ex, "Request method 'PUT' is not supported."); |
||||
assertThat(ex.getHeaders()).isEmpty(); |
||||
} |
||||
|
||||
private void assertStatus(ErrorResponse ex, HttpStatus status) { |
||||
ProblemDetail body = ex.getBody(); |
||||
assertThat(ex.getStatus()).isEqualTo(status); |
||||
assertThat(body.getStatus()).isEqualTo(status.value()); |
||||
assertThat(body.getTitle()).isEqualTo(status.getReasonPhrase()); |
||||
} |
||||
|
||||
private void assertDetail(ErrorResponse ex, @Nullable String detail) { |
||||
if (detail != null) { |
||||
assertThat(ex.getBody().getDetail()).isEqualTo(detail); |
||||
} |
||||
else { |
||||
assertThat(ex.getBody().getDetail()).isNull(); |
||||
} |
||||
} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
private void handle(String arg) {} |
||||
|
||||
} |
||||
Loading…
Reference in new issue