From 8a1f1a11d37eae3197b3e28a12d1f2d47eee93ca Mon Sep 17 00:00:00 2001 From: AhmedBabaAlweimine Date: Thu, 9 Apr 2026 14:20:49 -0400 Subject: [PATCH 1/2] Fix #2337: Future Visit Booking Validation Signed-off-by: AhmedBabaAlweimine --- .../samples/petclinic/owner/Visit.java | 6 +++-- .../templates/fragments/inputField.html | 4 +-- .../pets/createOrUpdateVisitForm.html | 4 +-- .../petclinic/model/ValidatorTests.java | 19 ++++++++++++++ .../petclinic/owner/VisitControllerTests.java | 26 ++++++++++++++++++- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Visit.java b/src/main/java/org/springframework/samples/petclinic/owner/Visit.java index 085cd2849b8..0bee7a87e53 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Visit.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Visit.java @@ -17,6 +17,7 @@ import java.time.LocalDate; +import jakarta.validation.constraints.Future; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.samples.petclinic.model.BaseEntity; @@ -37,16 +38,17 @@ public class Visit extends BaseEntity { @Column(name = "visit_date") @DateTimeFormat(pattern = "yyyy-MM-dd") + @Future(message = "Visit date must be in the future") private LocalDate date; @NotBlank private String description; /** - * Creates a new instance of Visit for the current date + * Creates a new instance of Visit for at least tommorow */ public Visit() { - this.date = LocalDate.now(); + this.date = LocalDate.now().plusDays(1); } public LocalDate getDate() { diff --git a/src/main/resources/templates/fragments/inputField.html b/src/main/resources/templates/fragments/inputField.html index da1de477236..2ebe1e15465 100644 --- a/src/main/resources/templates/fragments/inputField.html +++ b/src/main/resources/templates/fragments/inputField.html @@ -11,7 +11,7 @@
- +
@@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/src/main/resources/templates/pets/createOrUpdateVisitForm.html b/src/main/resources/templates/pets/createOrUpdateVisitForm.html index 4f03e12d554..74dd676578f 100644 --- a/src/main/resources/templates/pets/createOrUpdateVisitForm.html +++ b/src/main/resources/templates/pets/createOrUpdateVisitForm.html @@ -29,7 +29,7 @@

- +
@@ -56,4 +56,4 @@

- \ No newline at end of file + diff --git a/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java b/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java index 559311ff842..01f00957171 100644 --- a/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java +++ b/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java @@ -18,11 +18,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.time.LocalDate; import java.util.Locale; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.samples.petclinic.owner.Visit; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import jakarta.validation.ConstraintViolation; @@ -57,4 +59,21 @@ void shouldNotValidateWhenFirstNameEmpty() { assertThat(violation.getMessage()).isEqualTo("must not be blank"); } + @Test + void shouldNotValidateWhenVisitDateIsNotInFuture() { + + LocaleContextHolder.setLocale(Locale.ENGLISH); + Visit visit = new Visit(); + visit.setDate(LocalDate.now()); + visit.setDescription("any description"); + + Validator validator = createValidator(); + Set> constraintViolations = validator.validate(visit); + + assertThat(constraintViolations).hasSize(1); + ConstraintViolation violation = constraintViolations.iterator().next(); + assertThat(violation.getPropertyPath()).hasToString("date"); + assertThat(violation.getMessage()).isEqualTo("Visit date must be in the future"); + } + } diff --git a/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java index bd51302ab5a..c446cfb5dcc 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/VisitControllerTests.java @@ -16,6 +16,7 @@ package org.springframework.samples.petclinic.owner; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -31,7 +32,9 @@ import org.springframework.test.context.aot.DisabledInAotMode; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.validation.BindingResult; +import java.time.LocalDate; import java.util.Optional; /** @@ -76,7 +79,8 @@ void processNewVisitFormSuccess() throws Exception { mockMvc .perform(post("/owners/{ownerId}/pets/{petId}/visits/new", TEST_OWNER_ID, TEST_PET_ID) .param("name", "George") - .param("description", "Visit Description")) + .param("description", "Visit Description") + .param("date", String.valueOf(LocalDate.now().plusDays(1)))) .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/owners/{ownerId}")); } @@ -91,4 +95,24 @@ void processNewVisitFormHasErrors() throws Exception { .andExpect(view().name("pets/createOrUpdateVisitForm")); } + @Test + void processNewVisitFormHasErrorsWhenDateIsNotInFuture() throws Exception { + mockMvc + .perform(post("/owners/{ownerId}/pets/{petId}/visits/new", TEST_OWNER_ID, TEST_PET_ID) + .param("name", "George") + .param("description", "Visit Description") // Removed 'name' for clarity + .param("date", String.valueOf(LocalDate.now()))) + .andExpect(status().isOk()) + .andExpect(model().attributeHasFieldErrors("visit", "date")) + .andExpect(model().attributeHasFieldErrorCode("visit", "date", "Future")) + .andExpect(view().name("pets/createOrUpdateVisitForm")) + .andExpect(result -> { + BindingResult bindingResult = (BindingResult) result.getModelAndView() + .getModel() + .get("org.springframework.validation.BindingResult.visit"); + String message = bindingResult.getFieldError("date").getDefaultMessage(); + assertEquals("Visit date must be in the future", message); + }); + } + } From e4ee41f703d1623ebcad8481164f58032302b730 Mon Sep 17 00:00:00 2001 From: AhmedBabaAlweimine Date: Thu, 9 Apr 2026 14:57:04 -0400 Subject: [PATCH 2/2] Fix #2337: Future Visit Booking Validation Signed-off-by: AhmedBabaAlweimine --- .../samples/petclinic/owner/VisitController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java index cc3e3ce1a09..1a15b812579 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java @@ -15,6 +15,7 @@ */ package org.springframework.samples.petclinic.owner; +import java.time.LocalDate; import java.util.Map; import java.util.Optional; @@ -91,6 +92,11 @@ public String initNewVisitForm() { @PostMapping("/owners/{ownerId}/pets/{petId}/visits/new") public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit, BindingResult result, RedirectAttributes redirectAttributes) { + + // Manual Backend Check (as requested by the specification) + if (visit.getDate() != null && !visit.getDate().isAfter(LocalDate.now())) { + result.rejectValue("date", "invalid", "Visit date must be in the future"); + } if (result.hasErrors()) { return "pets/createOrUpdateVisitForm"; }