Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/main/java/com/retailsvc/http/BadRequestException.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,28 @@ public final class BadRequestException extends RuntimeException {
private final String keyword;

public BadRequestException(String detail) {
this(DEFAULT_STATUS, detail, null, null);
this(DEFAULT_STATUS, detail, null, null, null);
}

public BadRequestException(String detail, Throwable cause) {
this(DEFAULT_STATUS, detail, null, null, cause);
}

public BadRequestException(int status, String detail) {
this(status, detail, null, null);
this(status, detail, null, null, null);
}

public BadRequestException(int status, String detail, Throwable cause) {
this(status, detail, null, null, cause);
}

public BadRequestException(int status, String detail, String pointer, String keyword) {
super(Objects.requireNonNull(detail, "detail must not be null"));
this(status, detail, pointer, keyword, null);
}

public BadRequestException(
int status, String detail, String pointer, String keyword, Throwable cause) {
super(Objects.requireNonNull(detail, "detail must not be null"), cause);
if (status < 400 || status > 499) {
throw new IllegalArgumentException("status must be 4xx, got " + status);
}
Expand Down
21 changes: 15 additions & 6 deletions src/main/java/com/retailsvc/http/Handlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,21 @@ public static ExceptionHandler defaultExceptionHandler() {
HTTP_BAD_REQUEST,
ProblemDetailRenderer.renderJson(ProblemDetail.forValidation(ve.error())),
"application/problem+json");
case BadRequestException bre ->
Response.bytes(
bre.status(),
ProblemDetailRenderer.renderJson(ProblemDetail.forBadRequest(bre)),
"application/problem+json");
case NotFoundException _ -> Response.notFound();
case BadRequestException bre -> {
if (bre.getCause() != null && LOG.isDebugEnabled()) {
LOG.debug("BadRequestException cause", bre.getCause());
}
yield Response.bytes(
bre.status(),
ProblemDetailRenderer.renderJson(ProblemDetail.forBadRequest(bre)),
"application/problem+json");
}
case NotFoundException nfe -> {
if (nfe.getCause() != null && LOG.isDebugEnabled()) {
LOG.debug("NotFoundException cause", nfe.getCause());
}
yield Response.notFound();
}
case MethodNotAllowedException mna ->
Response.status(HTTP_BAD_METHOD)
.withHeader(
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/retailsvc/http/NotFoundException.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ public final class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}

public NotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
17 changes: 17 additions & 0 deletions src/test/java/com/retailsvc/http/BadRequestExceptionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ void rejectsNon4xxStatus() {
.hasMessageContaining("4xx");
}

@Test
void preservesCause() {
Throwable cause = new IllegalStateException("root");

BadRequestException defaultStatus = new BadRequestException("bad", cause);
BadRequestException withStatus = new BadRequestException(422, "bad", cause);
BadRequestException full = new BadRequestException(422, "bad", "/x", "unique", cause);

assertThat(defaultStatus).hasCause(cause);
assertThat(defaultStatus.status()).isEqualTo(400);
assertThat(withStatus).hasCause(cause);
assertThat(withStatus.status()).isEqualTo(422);
assertThat(full).hasCause(cause);
assertThat(full.pointer()).contains("/x");
assertThat(full.keyword()).contains("unique");
}

@Test
void rejectsNullDetail() {
assertThatThrownBy(() -> new BadRequestException(null))
Expand Down
71 changes: 71 additions & 0 deletions src/test/java/com/retailsvc/http/HandlersDefaultExceptionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,44 @@

import static org.assertj.core.api.Assertions.assertThat;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.retailsvc.http.spec.HttpMethod;
import com.retailsvc.http.validate.ValidationError;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

class HandlersDefaultExceptionTest {

private static final TypeMapper JSON = new GsonTypeMapper();

private Logger handlersLogger;
private Level originalLevel;
private ListAppender<ILoggingEvent> appender;

@BeforeEach
void attachAppender() {
handlersLogger = (Logger) LoggerFactory.getLogger(Handlers.class);
originalLevel = handlersLogger.getLevel();
handlersLogger.setLevel(Level.DEBUG);
appender = new ListAppender<>();
appender.start();
handlersLogger.addAppender(appender);
}

@AfterEach
void detachAppender() {
handlersLogger.detachAppender(appender);
handlersLogger.setLevel(originalLevel);
}

@Test
void validationExceptionRendersProblemJson() {
Response resp =
Expand Down Expand Up @@ -70,6 +97,50 @@ void methodNotAllowedReturns405WithAllowHeader() {
assertThat(resp.headers().get("Allow")).contains("GET").contains("POST");
}

@Test
void badRequestCauseLoggedAtDebug() {
Throwable cause = new IllegalStateException("root");

Handlers.defaultExceptionHandler().handle(new BadRequestException("bad", cause));

assertThat(appender.list)
.anySatisfy(
event -> {
assertThat(event.getLevel()).isEqualTo(Level.DEBUG);
assertThat(event.getThrowableProxy().getClassName())
.isEqualTo(IllegalStateException.class.getName());
});
}

@Test
void badRequestWithoutCauseDoesNotLog() {
Handlers.defaultExceptionHandler().handle(new BadRequestException("bad"));

assertThat(appender.list).isEmpty();
}

@Test
void notFoundCauseLoggedAtDebug() {
Throwable cause = new IllegalStateException("root");

Handlers.defaultExceptionHandler().handle(new NotFoundException("missing", cause));

assertThat(appender.list)
.anySatisfy(
event -> {
assertThat(event.getLevel()).isEqualTo(Level.DEBUG);
assertThat(event.getThrowableProxy().getClassName())
.isEqualTo(IllegalStateException.class.getName());
});
}

@Test
void notFoundWithoutCauseDoesNotLog() {
Handlers.defaultExceptionHandler().handle(new NotFoundException("missing"));

assertThat(appender.list).isEmpty();
}

@Test
void unknownExceptionReturns500() {
Response resp = Handlers.defaultExceptionHandler().handle(new RuntimeException("kaboom"));
Expand Down
26 changes: 26 additions & 0 deletions src/test/java/com/retailsvc/http/NotFoundExceptionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.retailsvc.http;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

class NotFoundExceptionTest {

@Test
void carriesMessage() {
NotFoundException e = new NotFoundException("missing");

assertThat(e.getMessage()).isEqualTo("missing");
assertThat(e.getCause()).isNull();
}

@Test
void preservesCause() {
Throwable cause = new IllegalStateException("root");

NotFoundException e = new NotFoundException("missing", cause);

assertThat(e.getMessage()).isEqualTo("missing");
assertThat(e).hasCause(cause);
}
}
Loading