Skip to content
This repository was archived by the owner on Jul 26, 2024. It is now read-only.

Introduce validation for customer state and birthdate #2

Merged
merged 1 commit into from
Jun 25, 2024
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.sample.schulung.accounts.boundary;

import com.fasterxml.jackson.annotation.JsonProperty;
import de.sample.schulung.accounts.shared.validation.Adult;
import de.sample.schulung.accounts.shared.validation.CustomerStateString;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -21,7 +23,9 @@ public class CustomerDto {
@Size(min = 3, max = 100)
private String name;
@JsonProperty("birthdate")
@Adult
private LocalDate dateOfBirth;
@CustomerStateString
private String state;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import de.sample.schulung.accounts.domain.CustomersService;
import de.sample.schulung.accounts.domain.NotFoundException;
import de.sample.schulung.accounts.shared.validation.CustomerStateString;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand All @@ -27,6 +28,7 @@ public class CustomersController {
produces = MediaType.APPLICATION_JSON_VALUE)
Stream<CustomerDto> getCustomers(
@RequestParam(value = "state", required = false)
@CustomerStateString
String stateFilter
) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,21 @@ public void handleNotFoundException() {
public void handleValidationException() {
}

/*
// Validierung im @Validated @Service
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) {
return ...
}

// Validierung im @Controller
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ProblemDetail handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
return ...
}

*/

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.sample.schulung.accounts.domain;

import de.sample.schulung.accounts.shared.validation.Adult;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -22,6 +23,7 @@ public enum CustomerState {
private UUID uuid;
@Size(min = 3, max = 100)
private String name;
@Adult
private LocalDate dateOfBirth;
private CustomerState state;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.sample.schulung.accounts.shared.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

// Custom Validator
// see: https://dzone.com/articles/create-your-own-constraint-with-bean-validation-20
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = AdultValidator.class)
public @interface Adult {

String message() default "Must be an adult.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.sample.schulung.accounts.shared.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.time.LocalDate;

public class AdultValidator implements ConstraintValidator<Adult, LocalDate> {

@Override
public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
if(null == value) {
return true;
}
var currentDate = LocalDate.now();
var currentDateBefore18Years = currentDate.minusYears(18);
return value.isBefore(currentDateBefore18Years) || value.isEqual(currentDateBefore18Years);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.sample.schulung.accounts.shared.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import jakarta.validation.constraints.Pattern;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

// Composite Constraint
// see: https://www.baeldung.com/java-bean-validation-constraint-composition
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { })
@Pattern(regexp = "active|locked|disabled")
@ReportAsSingleViolation // use custom message()
public @interface CustomerStateString {

String message() default "Not a valid state string.";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,46 @@ void shouldReturn400OnCreateInvalidCustomer() throws Exception {
verifyNoInteractions(service);
}

@Test
void shouldReturn400OnCreateCustomerThatIsNoAdult() throws Exception {
mvc.perform(
post("/api/v1/customers")
.contentType(MediaType.APPLICATION_JSON)
// TODO dynamisch ermitteln
.content("""
{
"name": "Tom Mayer",
"birthdate": "2020-07-30",
"state": "active"
}
""")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isBadRequest());

// Mockito.verify(service, Mockito.never()).createCustomer(ArgumentMatchers.any());
verifyNoInteractions(service);
}

@Test
void shouldReturn400OnCreateCustomerWithInvalidState() throws Exception {
mvc.perform(
post("/api/v1/customers")
.contentType(MediaType.APPLICATION_JSON)
// TODO dynamisch ermitteln
.content("""
{
"name": "Tom Mayer",
"birthdate": "1985-07-30",
"state": "gelbekatze"
}
""")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isBadRequest());

// Mockito.verify(service, Mockito.never()).createCustomer(ArgumentMatchers.any());
verifyNoInteractions(service);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,17 @@ void shouldNotCreateInvalidCustomer() {

}

@Test
void shouldNotCreateCustomerThatIsNoAdult() {
var customer = new Customer();
customer.setName("Tom Mayer");
customer.setState(Customer.CustomerState.ACTIVE);
customer.setDateOfBirth(LocalDate.now().minusYears(17));

assertThatThrownBy(() -> service.createCustomer(customer))
.isNotNull();

}


}
14 changes: 13 additions & 1 deletion sample-requests/POST-customers.http
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ client.test('customer exists', () => {
client.assert(response.status === 200, 'status code is 200');
});

%}
%}

###

POST {{endpoint}}/customers
Content-Type: application/json
Accept: application/json

{
"name": "T",
"birthdate": "1985-07-30",
"state": "active"
}
Loading