diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpdateDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpdateDTO.java index 2cce1ea0..a462943c 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpdateDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpdateDTO.java @@ -1,5 +1,7 @@ package org.raddatz.familienarchiv.person; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; @@ -21,4 +23,9 @@ public class PersonUpdateDTO { private String notes; private Integer birthYear; private Integer deathYear; + // Mirror of the persons.generation CHECK constraint (V70). + // null clears the field; 0..10 is the allowlist of valid indices. + @Min(0) + @Max(10) + private Integer generation; } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpsertCommand.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpsertCommand.java index 63864ab6..a1a5c71e 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpsertCommand.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonUpsertCommand.java @@ -18,6 +18,7 @@ public record PersonUpsertCommand( String notes, Integer birthYear, Integer deathYear, + Integer generation, boolean familyMember, PersonType personType, boolean provisional diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java index d43e9a9a..9bbd75be 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java @@ -718,4 +718,69 @@ class PersonControllerTest { .content("{\"lastName\":\"de Gruyter\"}")) .andExpect(status().isBadRequest()); } + + // ─── generation field validation (#689) ──────────────────────────────────── + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void updatePerson_returns400_whenGenerationAboveRange() throws Exception { + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," + + "\"personType\":\"PERSON\",\"generation\":11}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.VALIDATION_ERROR.name())); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void updatePerson_returns400_whenGenerationBelowRange() throws Exception { + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," + + "\"personType\":\"PERSON\",\"generation\":-1}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(ErrorCode.VALIDATION_ERROR.name())); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void updatePerson_returns200_whenGenerationNull() throws Exception { + Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build(); + when(personService.updatePerson(any(), any())).thenReturn(saved); + + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," + + "\"personType\":\"PERSON\",\"generation\":null}")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void updatePerson_returns200_whenGenerationInRange() throws Exception { + Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").generation(3).build(); + when(personService.updatePerson(any(), any())).thenReturn(saved); + + mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID()).with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," + + "\"personType\":\"PERSON\",\"generation\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.generation").value(3)); + } + + @Test + @WithMockUser(authorities = "WRITE_ALL") + void createPerson_returns200_whenGenerationInRange() throws Exception { + Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").generation(3).build(); + when(personService.createPerson(any(org.raddatz.familienarchiv.person.PersonUpdateDTO.class))).thenReturn(saved); + + mockMvc.perform(post("/api/persons").with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," + + "\"personType\":\"PERSON\",\"generation\":3}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.generation").value(3)); + } }