Improve error messages for invalid media types (#1607)

Show separate error messages for invalid partial media types and application/json if it unsupported for the url.
This commit is contained in:
Sebastian Sdorra
2021-03-25 13:09:40 +01:00
committed by GitHub
parent d94ebb2e3e
commit 4cbe6b9873
4 changed files with 340 additions and 1 deletions

View File

@@ -0,0 +1,221 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2;
import org.jboss.resteasy.mock.MockDispatcherFactory;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.jboss.resteasy.spi.Dispatcher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.api.WebApplicationExceptionMapper;
import sonia.scm.api.v2.resources.ErrorDto;
import sonia.scm.web.RestDispatcher;
import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MediaType;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class InvalidAcceptHeaderFilterTest {
@Mock
private ContainerRequestContext requestContext;
@Mock
private ContainerResponseContext responseContext;
@Captor
private ArgumentCaptor<Object> entityCaptor;
private final InvalidAcceptHeaderFilter filter = new InvalidAcceptHeaderFilter();
@Test
void shouldIgnoreWithOtherStatusCode() {
when(responseContext.getStatus()).thenReturn(HttpServletResponse.SC_NOT_FOUND);
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Test
void shouldIgnoreWithoutErrorDto() {
when(responseContext.getStatus()).thenReturn(HttpServletResponse.SC_NOT_ACCEPTABLE);
when(responseContext.getEntity()).thenReturn("Hello");
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Test
void shouldIgnoreWithoutResteasyError() {
when(responseContext.getStatus()).thenReturn(HttpServletResponse.SC_NOT_ACCEPTABLE);
ErrorDto dto = new ErrorDto();
dto.setMessage("Other");
when(responseContext.getEntity()).thenReturn(dto);
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Nested
class NoMatchForAcceptHeader {
@BeforeEach
void setStatusCode() {
when(responseContext.getStatus()).thenReturn(HttpServletResponse.SC_NOT_ACCEPTABLE);
ErrorDto error = new ErrorDto();
error.setMessage("RESTEASY003635: No match for accept header");
when(responseContext.getEntity()).thenReturn(error);
}
@Test
void shouldNotModifyResponse() {
mediaTypes();
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Test
void shouldNotModifyResponseWithFullQualifiedAccept() {
mediaTypes(VndMediaType.ANNOTATE);
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Test
void shouldNotModifyResponseWithMultipleFullQualifiedAccept() {
mediaTypes(VndMediaType.ANNOTATE, VndMediaType.SOURCE, VndMediaType.BRANCH);
filter.filter(requestContext, responseContext);
verify(responseContext, never()).setEntity(any());
}
@Test
void shouldReturnErrorForPartialWildcard() {
mediaTypes("application/*+json");
filter.filter(requestContext, responseContext);
assertError(InvalidAcceptHeaderFilter.CODE_PARTIAL_WILDCARD);
}
@Test
void shouldReturnApplicationJsonError() {
mediaTypes("application/json");
filter.filter(requestContext, responseContext);
assertError(InvalidAcceptHeaderFilter.CODE_APPLICATION_JSON);
}
}
@Nested
class Integration {
private Dispatcher dispatcher;
@BeforeEach
void prepare() {
dispatcher = MockDispatcherFactory.createDispatcher();
dispatcher.getProviderFactory().registerProvider(InvalidAcceptHeaderFilter.class);
dispatcher.getProviderFactory().registerProvider(WebApplicationExceptionMapper.class);
dispatcher.getRegistry().addSingletonResource(new SampleResource());
}
@Test
void shouldReturnApplicationJsonError() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/").accept("application/json");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getContentAsString()).contains(InvalidAcceptHeaderFilter.CODE_APPLICATION_JSON);
}
@Test
void shouldReturnErrorForPartialWildcard() throws URISyntaxException, UnsupportedEncodingException {
MockHttpRequest request = MockHttpRequest.get("/").accept("application/*+json");
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertThat(response.getContentAsString()).contains(InvalidAcceptHeaderFilter.CODE_PARTIAL_WILDCARD);
}
}
@Path("/")
class SampleResource {
@GET
@Produces("text/plain")
public String hello() {
return "hello";
}
}
private void assertError(String code) {
verify(responseContext).setEntity(entityCaptor.capture());
Object value = entityCaptor.getValue();
assertThat(value).isInstanceOfSatisfying(ErrorDto.class, error -> {
assertThat(error.getErrorCode()).isEqualTo(code);
});
}
private void mediaTypes(String... types) {
List<MediaType> mediaTypes = Arrays.stream(types)
.map(MediaType::valueOf).collect(Collectors.toList());
when(requestContext.getAcceptableMediaTypes()).thenReturn(mediaTypes);
}
}