feat(person): generation on PersonUpsertCommand + PersonUpdateDTO (#689)
Adds the optional generation field to both DTOs: - PersonUpsertCommand gains Integer generation in the canonical-import builder chain; service wiring lands in the next commit. - PersonUpdateDTO gains @Min(0)@Max(10) Integer generation, the form-path surface. The constraints mirror the V70 CHECK so validation fails fast at the controller before reaching the DB. PersonControllerTest pins the validation behaviour: -1 → 400, 11 → 400, null → 200, 3 → 200 for both PUT (update) and POST (create) paths. The GlobalExceptionHandler maps MethodArgumentNotValidException to VALIDATION_ERROR so the frontend's extractErrorCode keeps working. Refs #689 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public record PersonUpsertCommand(
|
||||
String notes,
|
||||
Integer birthYear,
|
||||
Integer deathYear,
|
||||
Integer generation,
|
||||
boolean familyMember,
|
||||
PersonType personType,
|
||||
boolean provisional
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user