package com.ankurm.problems; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.assertThat; /** * The contract of the recommended setup (profiles advice + errors): every failure, including the * ones produced outside Spring MVC, is application/problem+json with the right status. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles({"advice", "errors"}) class AdviceContractTest { private static final String PROBLEM = "application/problem+json"; @LocalServerPort int port; @Test void domainExceptionIs404WithStableType() throws Exception { Http.Response r = Http.get(port, "/orders/999"); assertThat(r.status()).isEqualTo(404); assertThat(r.contentType()).isEqualTo(PROBLEM); assertThat(r.body()).contains("\"type\":\"https://ankurm.com/problems/order-not-found\"") .contains("\"orderId\":999") .contains("\"instance\":\"/orders/999\""); } @Test void validationListsEveryViolationWithAPointer() throws Exception { Http.Response r = Http.send(port, "POST", "/orders", "{\"sku\":\"\",\"quantity\":0,\"customerEmail\":\"nope\"}"); assertThat(r.status()).isEqualTo(400); assertThat(r.contentType()).isEqualTo(PROBLEM); // Order of the three is not stable between runs - assert membership, not position. assertThat(r.body()).contains("\"pointer\":\"#/sku\"", "\"pointer\":\"#/quantity\"", "\"pointer\":\"#/customerEmail\""); } @Test void unexpectedExceptionDoesNotLeakItsMessage() throws Exception { Http.Response r = Http.get(port, "/orders/boom"); assertThat(r.status()).isEqualTo(500); assertThat(r.contentType()).isEqualTo(PROBLEM); assertThat(r.body()).contains("\"errorId\":").doesNotContain("jdbc:").doesNotContain("orders_rw"); } @Test void filterExceptionReachesTheErrorControllerNotTheAdvice() throws Exception { Http.Response r = Http.get(port, "/orders/1", "X-Tenant", "BAD!"); assertThat(r.status()).isEqualTo(500); assertThat(r.contentType()).isEqualTo(PROBLEM); assertThat(r.body()).doesNotContain("X-Tenant").doesNotContain("errorId"); } @Test void securityResponsesAreProblemsToo() throws Exception { Http.Response anonymous = Http.get(port, "/admin/orders"); assertThat(anonymous.status()).isEqualTo(401); assertThat(anonymous.contentType()).isEqualTo(PROBLEM); assertThat(anonymous.headers().firstValue("WWW-Authenticate")).hasValue("Basic realm=\"orders\""); Http.Response user = Http.get(port, "/admin/orders", "Authorization", Http.basic("user", "user")); assertThat(user.status()).isEqualTo(403); assertThat(user.contentType()).isEqualTo(PROBLEM); } @Test void anAcceptOfApplicationXmlStillGetsJson() throws Exception { // application/xml is not compatible with application/problem+xml, so the error falls back // to problem+json - while a successful response to the same Accept header is XML. assertThat(Http.get(port, "/orders/1", "Accept", "application/xml").contentType()) .startsWith("application/xml"); assertThat(Http.get(port, "/orders/999", "Accept", "application/xml").contentType()) .isEqualTo(PROBLEM); assertThat(Http.get(port, "/orders/999", "Accept", "application/problem+xml").contentType()) .isEqualTo("application/problem+xml"); } @Test void anUnsatisfiableAcceptDoesNotTurnTheErrorInto406() throws Exception { Http.Response r = Http.get(port, "/orders/999", "Accept", "image/png"); assertThat(r.status()).isEqualTo(404); assertThat(r.contentType()).isEqualTo(PROBLEM); } @Test void messageSourceOverridesTitleAndDetailPerLocale() throws Exception { String body = "{\"sku\":\"SKU-2\",\"quantity\":3,\"customerEmail\":\"a@b.co\"}"; assertThat(Http.send(port, "POST", "/orders", body).body()) .contains("\"title\":\"Out of stock\"") .contains("\"detail\":\"Only 0 unit(s) of SKU-2 are available.\""); assertThat(Http.send(port, "POST", "/orders", body, "Accept-Language", "de").body()) .contains("\"title\":\"Nicht vorrätig\""); } }