package org.zalando.problem.spring.web.advice; import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.context.request.NativeWebRequest; import org.zalando.problem.Problem; import org.zalando.problem.ThrowableProblem; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import java.net.URI; import java.util.List; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.hobsoft.hamcrest.compose.ComposeMatchers.compose; import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.RESET_CONTENT; import static org.zalando.problem.spring.web.advice.MediaTypes.PROBLEM; import static org.zalando.problem.spring.web.advice.MediaTypes.WILDCARD_JSON_VALUE; public class AdviceTraitTest { private final AdviceTrait unit = new AdviceTrait() { }; @Test public void buildsOnProblem() { final ThrowableProblem problem = mock(ThrowableProblem.class); when(problem.getStatus()).thenReturn(Status.RESET_CONTENT); final ResponseEntity<Problem> result = unit.create(problem, request()); assertThat(result, hasFeature("Status", ResponseEntity::getStatusCode, is(RESET_CONTENT))); assertThat(result.getHeaders(), hasFeature("Content-Type", HttpHeaders::getContentType, is(PROBLEM))); assertThat(result.getBody(), hasFeature("Status", Problem::getStatus, is(Status.RESET_CONTENT))); } @Test public void buildsOnThrowable() { final ResponseEntity<Problem> result = unit.create(Status.RESET_CONTENT, new IllegalStateException("Message"), request()); assertThat(result, hasFeature("Status", ResponseEntity::getStatusCode, is(RESET_CONTENT))); assertThat(result.getHeaders(), hasFeature("Content-Type", HttpHeaders::getContentType, is(PROBLEM))); assertThat(result.getBody(), compose(hasFeature("Status", Problem::getStatus, is(Status.RESET_CONTENT))) .and(hasFeature("Detail", Problem::getDetail, is("Message")))); } @Test public void buildsOnThrowableWithType() { URI type = URI.create("https://google.com"); final ResponseEntity<Problem> result = unit.create(Status.RESET_CONTENT, new IllegalStateException("Message"), request(), type); assertThat(result, hasFeature("Status", ResponseEntity::getStatusCode, is(RESET_CONTENT))); assertThat(result.getHeaders(), hasFeature("Content-Type", HttpHeaders::getContentType, is(PROBLEM))); assertThat(result.getBody(), compose(hasFeature("Status", Problem::getStatus, is(Status.RESET_CONTENT))) .and(hasFeature("Detail", Problem::getDetail, is("Message"))).and(hasFeature("Type", Problem::getType, is(type)))); } @Test public void buildsOnMessage() { final ResponseEntity<Problem> result = unit.create(Status.RESET_CONTENT, new IllegalStateException("Message"), request()); assertThat(result, hasFeature("Status", ResponseEntity::getStatusCode, is(RESET_CONTENT))); assertThat(result.getHeaders(), hasFeature("Content-Type", HttpHeaders::getContentType, is(PROBLEM))); assertThat(result.getBody(), compose(hasFeature("Status", Problem::getStatus, is(Status.RESET_CONTENT))) .and(hasFeature("Detail", Problem::getDetail, is("Message")))); } @Test public void buildsIfIncludes() { final String message = "Message"; final ResponseEntity<Problem> result = unit.create(Status.RESET_CONTENT, new IllegalStateException(message), request(WILDCARD_JSON_VALUE)); assertThat(result, hasFeature("Status", ResponseEntity::getStatusCode, is(RESET_CONTENT))); assertThat(result.getHeaders(), hasFeature("Content-Type", HttpHeaders::getContentType, is(PROBLEM))); assertThat(result.getBody(), compose(hasFeature("Status", Problem::getStatus, is(Status.RESET_CONTENT))) .and(hasFeature("Detail", Problem::getDetail, is(message)))); } @Test public void buildsStacktrace() { final Throwable throwable; try { try { try { throw newNullPointer(); } catch (final NullPointerException e) { throw newIllegalArgument(e); } } catch (final IllegalArgumentException e) { throw newIllegalState(e); } } catch (final IllegalStateException e) { throwable = e; } final ResponseEntity<Problem> entity = new AdviceTrait() { @Override public boolean isCausalChainsEnabled() { return true; } }.create(Status.INTERNAL_SERVER_ERROR, throwable, request()); assertThat(entity.getBody(), is(instanceOf(ThrowableProblem.class))); final ThrowableProblem illegalState = (ThrowableProblem) entity.getBody(); assertThat(illegalState.getType(), hasToString("about:blank")); assertThat(illegalState.getTitle(), is("Internal Server Error")); assertThat(illegalState.getStatus(), is(Status.INTERNAL_SERVER_ERROR)); assertThat(illegalState.getDetail(), is("Illegal State")); assertThat(stacktraceAsString(illegalState).get(0), startsWith(method("newIllegalState"))); assertThat(stacktraceAsString(illegalState).get(1), startsWith(method("buildsStacktrace"))); assertThat(illegalState.getCause(), is(notNullValue())); final ThrowableProblem illegalArgument = illegalState.getCause(); assertThat(illegalArgument.getType(), hasToString("about:blank")); assertThat(illegalArgument.getTitle(), is("Internal Server Error")); assertThat(illegalArgument.getStatus(), is(Status.INTERNAL_SERVER_ERROR)); assertThat(illegalArgument.getDetail(), is("Illegal Argument")); assertThat(stacktraceAsString(illegalArgument).get(0), startsWith(method("newIllegalArgument"))); assertThat(stacktraceAsString(illegalArgument).get(1), startsWith(method("buildsStacktrace"))); assertThat(illegalArgument.getCause(), is(notNullValue())); final ThrowableProblem nullPointer = illegalArgument.getCause(); assertThat(nullPointer.getType(), hasToString("about:blank")); assertThat(nullPointer.getTitle(), is("Internal Server Error")); assertThat(nullPointer.getStatus(), is(Status.INTERNAL_SERVER_ERROR)); assertThat(nullPointer.getDetail(), is("Null Pointer")); assertThat(stacktraceAsString(nullPointer).get(0), startsWith(method("newNullPointer"))); assertThat(stacktraceAsString(nullPointer).get(1), startsWith(method("buildsStacktrace"))); assertThat(nullPointer.getCause(), is(nullValue())); } private String method(final String s) { return "org.zalando.problem.spring.web.advice.AdviceTraitTest." + s; } private List<String> stacktraceAsString(final Throwable throwable) { return Stream.of(throwable.getStackTrace()) .map(Object::toString) .collect(toList()); } private IllegalStateException newIllegalState(final IllegalArgumentException e) { throw new IllegalStateException("Illegal State", e); } private IllegalArgumentException newIllegalArgument(final NullPointerException e) { throw new IllegalArgumentException("Illegal Argument", e); } private NullPointerException newNullPointer() { throw new NullPointerException("Null Pointer"); } @Test public void mapsStatus() { final HttpStatus expected = HttpStatus.BAD_REQUEST; final Response.StatusType input = Status.BAD_REQUEST; final ResponseEntity<Problem> entity = unit.create(input, new IllegalStateException("Checkpoint"), request()); assertThat(entity.getStatusCode(), is(expected)); } @Test(expected = IllegalArgumentException.class) public void throwsOnUnknownStatus() { final Response.StatusType input = mock(Response.StatusType.class); when(input.getReasonPhrase()).thenReturn("L33t"); when(input.getStatusCode()).thenReturn(1337); unit.create(input, new IllegalStateException("L33t"), request()); } private NativeWebRequest request(final String acceptMediaType) { final NativeWebRequest request = mock(NativeWebRequest.class); when(request.getHeader("Accept")).thenReturn(acceptMediaType); return request; } private NativeWebRequest request() { return mock(NativeWebRequest.class); } }