diff --git a/.gitignore b/.gitignore
index ac695fa..f8c0405 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
### IntelliJ IDEA ###
.idea/
+
+### Logs ###
+backend/**/logs/*
+!backend/**/logs/.gitkeep
+.idea
\ No newline at end of file
diff --git a/backend/.idea/workspace.xml b/backend/.idea/workspace.xml
deleted file mode 100644
index 8bb6e82..0000000
--- a/backend/.idea/workspace.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1783800962578
-
-
- 1783800962578
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/appointment-service/pom.xml b/backend/appointment-service/pom.xml
index d3667a2..0c573cb 100644
--- a/backend/appointment-service/pom.xml
+++ b/backend/appointment-service/pom.xml
@@ -105,6 +105,12 @@
io.confluent
kafka-avro-serializer
7.5.1
+
+
+ io.swagger.core.v3
+ swagger-annotations
+
+
com.google.guava
diff --git a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentController.java b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentController.java
index 039381b..d0f3b2b 100644
--- a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentController.java
+++ b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentController.java
@@ -10,6 +10,7 @@
import com.appointment.usecases.ports.in.FindAppointmentByIdUseCase;
import com.appointment.usecases.ports.in.ListAppointmentsUseCase;
import com.appointment.usecases.ports.in.UpdateAppointmentStatusUseCase;
+import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -59,6 +60,7 @@ public AppointmentController(
@PostMapping("/appointment")
public ResponseEntity>> create(
+ @Parameter(example = "11111111-1111-1111-1111-111111111111")
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody AppointmentRequest request
) {
@@ -72,7 +74,7 @@ public ResponseEntity>>>
@RequestParam(required = false) Status status,
@PageableDefault(size = 10, sort = "scheduledAt") Pageable pageable
) {
- Page page = listAppointmentsUseCase.execute(status, pageable);
+ Page page = listAppointmentsUseCase.execute(status, AppointmentSortSanitizer.sanitize(pageable));
PagedModel> pagedModel =
pagedResourcesAssembler.toModel(page, appointmentModelAssembler::toModel);
@@ -91,7 +93,7 @@ public ResponseEntity>> updateStatu
@PathVariable UUID id,
@Valid @RequestBody UpdateAppointmentStatusRequest request
) {
- Appointment updated = updateAppointmentStatusUseCase.execute(id, request.status(), request.observation());
+ Appointment updated = updateAppointmentStatusUseCase.execute(id, request.toStatus(), request.observation());
return ResponseEntity.ok(ApiResponse.of(appointmentModelAssembler.toModel(updated), "Appointment status updated successfully"));
}
diff --git a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentSortSanitizer.java b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentSortSanitizer.java
new file mode 100644
index 0000000..2f2e478
--- /dev/null
+++ b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/AppointmentSortSanitizer.java
@@ -0,0 +1,29 @@
+package com.appointment.adapters.in.controller;
+
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import java.util.Set;
+
+final class AppointmentSortSanitizer {
+
+ private static final Set SORTABLE_PROPERTIES =
+ Set.of("scheduledAt", "status", "patientName", "createdAt", "updatedAt");
+
+ private static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.ASC, "scheduledAt");
+
+ private AppointmentSortSanitizer() {
+ }
+
+ static Pageable sanitize(Pageable pageable) {
+ Sort validOrders = Sort.by(pageable.getSort().stream()
+ .filter(order -> SORTABLE_PROPERTIES.contains(order.getProperty()))
+ .toList());
+
+ Sort sort = validOrders.isSorted() ? validOrders : DEFAULT_SORT;
+ return pageable.getSort().equals(sort)
+ ? pageable
+ : PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
+ }
+}
diff --git a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/AppointmentRequest.java b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/AppointmentRequest.java
index 9c00b11..1de407f 100644
--- a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/AppointmentRequest.java
+++ b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/AppointmentRequest.java
@@ -2,6 +2,7 @@
import com.appointment.entities.Appointment;
import com.appointment.enums.Status;
+import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Future;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -13,13 +14,16 @@
public record AppointmentRequest(
@NotNull
@CPF(message = "${response.error-messages.invalid-cpf}")
+ @Schema(example = "529.982.247-25")
String patientCpf,
@NotBlank
+ @Schema(example = "John Doe")
String patientName,
@NotNull
@Future(message = "${response.error-messages.appointment-with-invalid-date}")
+ @Schema(example = "2030-01-01T10:00:00")
LocalDateTime scheduledAt
) {
public Appointment toEntity(String idempotencyKey) {
diff --git a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/UpdateAppointmentStatusRequest.java b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/UpdateAppointmentStatusRequest.java
index 6b4b100..a0c8ed5 100644
--- a/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/UpdateAppointmentStatusRequest.java
+++ b/backend/appointment-service/src/main/java/com/appointment/adapters/in/controller/dtos/UpdateAppointmentStatusRequest.java
@@ -1,10 +1,16 @@
package com.appointment.adapters.in.controller.dtos;
import com.appointment.enums.Status;
+import com.appointment.frameworks.validation.ValidEnum;
import jakarta.validation.constraints.NotNull;
public record UpdateAppointmentStatusRequest(
- @NotNull Status status,
+ @NotNull
+ @ValidEnum(enumClass = Status.class)
+ String status,
String observation
) {
+ public Status toStatus() {
+ return Status.valueOf(status);
+ }
}
diff --git a/backend/appointment-service/src/main/java/com/appointment/frameworks/config/ValidationMessageConfig.java b/backend/appointment-service/src/main/java/com/appointment/frameworks/config/ValidationMessageConfig.java
new file mode 100644
index 0000000..e950576
--- /dev/null
+++ b/backend/appointment-service/src/main/java/com/appointment/frameworks/config/ValidationMessageConfig.java
@@ -0,0 +1,59 @@
+package com.appointment.frameworks.config;
+
+import jakarta.validation.MessageInterpolator;
+import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Configuration
+public class ValidationMessageConfig {
+
+ @Bean
+ public LocalValidatorFactoryBean localValidatorFactoryBean(Environment environment) {
+ LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
+ factoryBean.setMessageInterpolator(
+ new PropertyPlaceholderMessageInterpolator(new ParameterMessageInterpolator(), environment)
+ );
+ return factoryBean;
+ }
+
+ private static class PropertyPlaceholderMessageInterpolator implements MessageInterpolator {
+
+ private static final Pattern PLACEHOLDER = Pattern.compile("\\$\\{([^}]+)}");
+
+ private final MessageInterpolator delegate;
+ private final Environment environment;
+
+ PropertyPlaceholderMessageInterpolator(MessageInterpolator delegate, Environment environment) {
+ this.delegate = delegate;
+ this.environment = environment;
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context) {
+ return delegate.interpolate(resolve(messageTemplate), context);
+ }
+
+ @Override
+ public String interpolate(String messageTemplate, Context context, Locale locale) {
+ return delegate.interpolate(resolve(messageTemplate), context, locale);
+ }
+
+ private String resolve(String messageTemplate) {
+ Matcher matcher = PLACEHOLDER.matcher(messageTemplate);
+ StringBuilder result = new StringBuilder();
+ while (matcher.find()) {
+ String value = environment.getProperty(matcher.group(1));
+ matcher.appendReplacement(result, Matcher.quoteReplacement(value != null ? value : matcher.group(0)));
+ }
+ matcher.appendTail(result);
+ return result.toString();
+ }
+ }
+}
diff --git a/backend/appointment-service/src/main/java/com/appointment/frameworks/exceptions/GlobalExceptionHandler.java b/backend/appointment-service/src/main/java/com/appointment/frameworks/exceptions/GlobalExceptionHandler.java
index 8ed2188..cda7b43 100644
--- a/backend/appointment-service/src/main/java/com/appointment/frameworks/exceptions/GlobalExceptionHandler.java
+++ b/backend/appointment-service/src/main/java/com/appointment/frameworks/exceptions/GlobalExceptionHandler.java
@@ -8,11 +8,13 @@
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.LinkedHashMap;
@@ -70,6 +72,18 @@ public ResponseEntity handleNoResourceFound(NoResourceFoundException ex) {
return ResponseEntity.notFound().build();
}
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ResponseEntity> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
+ ApiResponse body = ApiResponse.of(null, "Invalid value for parameter: " + ex.getName());
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
+ }
+
+ @ExceptionHandler(HttpMessageNotReadableException.class)
+ public ResponseEntity> handleHttpMessageNotReadable(HttpMessageNotReadableException ex) {
+ ApiResponse body = ApiResponse.of(null, "Malformed or invalid request body.");
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
+ }
+
@ExceptionHandler(DataAccessException.class)
public ResponseEntity> handleDataAccessException(DataAccessException ex) {
log.error("Database error", ex);
diff --git a/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/EnumValidator.java b/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/EnumValidator.java
new file mode 100644
index 0000000..f08cb0b
--- /dev/null
+++ b/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/EnumValidator.java
@@ -0,0 +1,39 @@
+package com.appointment.frameworks.validation;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+public class EnumValidator implements ConstraintValidator {
+
+ private Class extends Enum>> enumClass;
+
+ @Override
+ public void initialize(ValidEnum annotation) {
+ this.enumClass = annotation.enumClass();
+ }
+
+ @Override
+ public boolean isValid(String value, ConstraintValidatorContext context) {
+ if (value == null) {
+ return true;
+ }
+
+ boolean valid = Arrays.stream(enumClass.getEnumConstants())
+ .anyMatch(candidate -> candidate.name().equals(value));
+
+ if (!valid) {
+ String allowedValues = Arrays.stream(enumClass.getEnumConstants())
+ .map(Enum::name)
+ .collect(Collectors.joining(", "));
+
+ context.disableDefaultConstraintViolation();
+ context.buildConstraintViolationWithTemplate("must be one of: " + allowedValues)
+ .addConstraintViolation();
+ }
+
+ return valid;
+ }
+}
diff --git a/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/ValidEnum.java b/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/ValidEnum.java
new file mode 100644
index 0000000..da119e4
--- /dev/null
+++ b/backend/appointment-service/src/main/java/com/appointment/frameworks/validation/ValidEnum.java
@@ -0,0 +1,23 @@
+package com.appointment.frameworks.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT})
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = EnumValidator.class)
+public @interface ValidEnum {
+
+ Class extends Enum>> enumClass();
+
+ String message() default "must be one of the accepted values";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+}
diff --git a/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentControllerIntegrationTest.java b/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentControllerIntegrationTest.java
index faeeef0..144de9a 100644
--- a/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentControllerIntegrationTest.java
+++ b/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentControllerIntegrationTest.java
@@ -249,7 +249,7 @@ void shouldUpdateStatusFromPendingToConfirmed() throws Exception {
mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED, null))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED.name(), null))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("CONFIRMED"))
.andExpect(jsonPath("$.data.links[?(@.rel=='cancel')]").exists())
@@ -262,7 +262,7 @@ void shouldUpdateStatusToCanceledWithObservation() throws Exception {
mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CANCELED, "patient requested cancellation"))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CANCELED.name(), "patient requested cancellation"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("CANCELED"))
.andExpect(jsonPath("$.data.observation").value("patient requested cancellation"))
@@ -276,7 +276,7 @@ void shouldReturnConflictWhenCancelingWithoutObservation() throws Exception {
mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CANCELED, null))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CANCELED.name(), null))))
.andExpect(status().isConflict());
}
@@ -286,15 +286,26 @@ void shouldReturnConflictWhenUpdatingAlreadyCanceledAppointment() throws Excepti
mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED, null))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED.name(), null))))
.andExpect(status().isConflict());
}
+ @Test
+ void shouldReturnBadRequestForStatusOutsideEnum() throws Exception {
+ UUID id = persist(Status.PENDING, 1).id();
+
+ mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
+ .contentType("application/json")
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest("FOOBAR", null))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.data.status").value("must be one of: PENDING, CONFIRMED, CANCELED"));
+ }
+
@Test
void shouldReturnNotFoundWhenUpdatingStatusOfUnknownId() throws Exception {
mockMvc.perform(patch("/api/v1/appointments/{id}/status", UUID.randomUUID())
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED, null))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.CONFIRMED.name(), null))))
.andExpect(status().isNotFound());
}
@@ -304,7 +315,7 @@ void shouldRepublishToQueueWhenUpdatingStatusBackToPending() throws Exception {
mockMvc.perform(patch("/api/v1/appointments/{id}/status", id)
.contentType("application/json")
- .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.PENDING, null))))
+ .content(objectMapper.writeValueAsString(new UpdateAppointmentStatusRequest(Status.PENDING.name(), null))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.status").value("PENDING"));
diff --git a/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentSortSanitizerTest.java b/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentSortSanitizerTest.java
new file mode 100644
index 0000000..2525bc8
--- /dev/null
+++ b/backend/appointment-service/src/test/java/com/appointment/adapters/in/controller/AppointmentSortSanitizerTest.java
@@ -0,0 +1,49 @@
+package com.appointment.adapters.in.controller;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AppointmentSortSanitizerTest {
+
+ @Test
+ void shouldFallBackToDefaultSortWhenPropertyIsNotSortable() {
+ Pageable pageable = PageRequest.of(0, 10, Sort.by("string"));
+
+ Pageable sanitized = AppointmentSortSanitizer.sanitize(pageable);
+
+ assertThat(sanitized.getSort()).isEqualTo(Sort.by(Sort.Direction.ASC, "scheduledAt"));
+ }
+
+ @Test
+ void shouldKeepValidSortProperty() {
+ Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "patientName"));
+
+ Pageable sanitized = AppointmentSortSanitizer.sanitize(pageable);
+
+ assertThat(sanitized.getSort()).isEqualTo(Sort.by(Sort.Direction.DESC, "patientName"));
+ }
+
+ @Test
+ void shouldDropOnlyInvalidPropertiesAndKeepValidOnesWhenMixed() {
+ Sort mixedSort = Sort.by(Sort.Order.asc("status")).and(Sort.by(Sort.Order.desc("string")));
+ Pageable pageable = PageRequest.of(0, 10, mixedSort);
+
+ Pageable sanitized = AppointmentSortSanitizer.sanitize(pageable);
+
+ assertThat(sanitized.getSort()).isEqualTo(Sort.by(Sort.Direction.ASC, "status"));
+ }
+
+ @Test
+ void shouldPreservePageNumberAndSize() {
+ Pageable pageable = PageRequest.of(2, 5, Sort.by("string"));
+
+ Pageable sanitized = AppointmentSortSanitizer.sanitize(pageable);
+
+ assertThat(sanitized.getPageNumber()).isEqualTo(2);
+ assertThat(sanitized.getPageSize()).isEqualTo(5);
+ }
+}
diff --git a/backend/appointment-service/src/test/java/com/appointment/frameworks/exceptions/GlobalExceptionHandlerTest.java b/backend/appointment-service/src/test/java/com/appointment/frameworks/exceptions/GlobalExceptionHandlerTest.java
index a81102a..f276579 100644
--- a/backend/appointment-service/src/test/java/com/appointment/frameworks/exceptions/GlobalExceptionHandlerTest.java
+++ b/backend/appointment-service/src/test/java/com/appointment/frameworks/exceptions/GlobalExceptionHandlerTest.java
@@ -5,14 +5,19 @@
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Path;
import org.junit.jupiter.api.Test;
+import org.springframework.core.MethodParameter;
import org.springframework.dao.QueryTimeoutException;
+import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.Map;
import java.util.Set;
+import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -60,6 +65,34 @@ void shouldReturnBadRequestForConstraintViolation() {
assertThat(response.getBody().data()).containsEntry("scheduledAt", "must be in the future");
}
+ @Test
+ void shouldReturnBadRequestForMethodArgumentTypeMismatch() {
+ MethodParameter parameter = mock(MethodParameter.class);
+ MethodArgumentTypeMismatchException ex =
+ new MethodArgumentTypeMismatchException("string", UUID.class, "id", parameter, null);
+
+ ResponseEntity> response = handler.handleMethodArgumentTypeMismatch(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().data()).isNull();
+ assertThat(response.getBody().message()).isEqualTo("Invalid value for parameter: id");
+ }
+
+ @Test
+ void shouldReturnBadRequestForHttpMessageNotReadable() {
+ HttpInputMessage inputMessage = mock(HttpInputMessage.class);
+ HttpMessageNotReadableException ex =
+ new HttpMessageNotReadableException("Cannot deserialize value of type `Status`", inputMessage);
+
+ ResponseEntity> response = handler.handleHttpMessageNotReadable(ex);
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
+ assertThat(response.getBody()).isNotNull();
+ assertThat(response.getBody().data()).isNull();
+ assertThat(response.getBody().message()).isEqualTo("Malformed or invalid request body.");
+ }
+
@Test
void shouldReturnNotFoundForUnmappedStaticResource() {
ResponseEntity response = handler.handleNoResourceFound(new NoResourceFoundException(HttpMethod.GET, "/unknown", "/unknown"));