test(person): tighten generation null-clear coverage (#689)

Sara's QA concerns:

1. PersonControllerTest.updatePerson_returns200_whenGenerationNull was
   asymmetric — only checked status 200, no body assertion. Now also
   asserts `$.generation` is null in the JSON response, mirroring the
   in-range test's body check.

2. New full-stack PUT→DB→GET round-trip in PersonServiceIntegrationTest
   (updatePerson_clearGenerationToNull_readsBackNullFromDb) seeds a
   person with generation=3, calls updatePerson with generation=null,
   flushes the persistence context, and asserts the column reads back
   null from the DB. Without this we only had the mocked WebMvcTest
   boundary; nothing proved JPA actually wrote SQL NULL.

3. Sibling test (updatePerson_setGenerationToZero_readsBackZeroFromDb)
   pins the G 0 end-to-end so a primitive zero can't silently coerce
   to null anywhere along controller → service → JPA.

Refs #689

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-28 16:19:13 +02:00
parent 516a0a3814
commit 61ca5a6e40
2 changed files with 59 additions and 1 deletions

View File

@@ -746,6 +746,10 @@ class PersonControllerTest {
@Test @Test
@WithMockUser(authorities = "WRITE_ALL") @WithMockUser(authorities = "WRITE_ALL")
void updatePerson_returns200_whenGenerationNull() throws Exception { void updatePerson_returns200_whenGenerationNull() throws Exception {
// Symmetric body assertion: the response must echo generation as null (not
// absent), so the frontend re-hydrates the "(none)" option after a clear.
// Without this, the in-range test below would be the only end-to-end proof
// that the field flows through the controller.
Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build(); Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
when(personService.updatePerson(any(), any())).thenReturn(saved); when(personService.updatePerson(any(), any())).thenReturn(saved);
@@ -753,7 +757,8 @@ class PersonControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"," .content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\","
+ "\"personType\":\"PERSON\",\"generation\":null}")) + "\"personType\":\"PERSON\",\"generation\":null}"))
.andExpect(status().isOk()); .andExpect(status().isOk())
.andExpect(jsonPath("$.generation").value(org.hamcrest.Matchers.nullValue()));
} }
@Test @Test

View File

@@ -124,6 +124,59 @@ class PersonServiceIntegrationTest {
assertThat(personRepository.findById(target.getId())).isEmpty(); assertThat(personRepository.findById(target.getId())).isEmpty();
} }
// ─── generation full-stack round-trip (#689) ──────────────────────────────
@Test
void updatePerson_clearGenerationToNull_readsBackNullFromDb() {
// Sara's QA concern: pin the full PUT→DB→GET round-trip for the
// null-clear path. Without this we only have the WebMvcTest mocked
// boundary; nothing proved the JPA flush actually wrote SQL NULL.
Person seeded = personRepository.save(Person.builder()
.firstName("Hans").lastName("Raddatz")
.personType(PersonType.PERSON).generation(3).build());
entityManager.flush();
entityManager.clear();
PersonUpdateDTO dto = new PersonUpdateDTO();
dto.setPersonType(PersonType.PERSON);
dto.setFirstName("Hans");
dto.setLastName("Raddatz");
dto.setGeneration(null);
personService.updatePerson(seeded.getId(), dto);
entityManager.flush();
entityManager.clear();
Person reloaded = personRepository.findById(seeded.getId()).orElseThrow();
assertThat(reloaded.getGeneration()).isNull();
}
@Test
void updatePerson_setGenerationToZero_readsBackZeroFromDb() {
// Pin the G 0 case end-to-end. The form-action spec covers that 0
// doesn't get spread-dropped at the SvelteKit boundary; this test
// covers that the controller + service + JPA chain preserves the
// primitive zero (not coerced to null somewhere along the way).
Person seeded = personRepository.save(Person.builder()
.firstName("Walter").lastName("Raddatz")
.personType(PersonType.PERSON).build());
entityManager.flush();
entityManager.clear();
PersonUpdateDTO dto = new PersonUpdateDTO();
dto.setPersonType(PersonType.PERSON);
dto.setFirstName("Walter");
dto.setLastName("Raddatz");
dto.setGeneration(0);
personService.updatePerson(seeded.getId(), dto);
entityManager.flush();
entityManager.clear();
Person reloaded = personRepository.findById(seeded.getId()).orElseThrow();
assertThat(reloaded.getGeneration()).isEqualTo(0);
}
@Test @Test
void deletePerson_detachesSentAndReceivedReferences_beforeDelete_noOrphan() { void deletePerson_detachesSentAndReceivedReferences_beforeDelete_noOrphan() {
// A person referenced as BOTH a document sender and a document receiver must delete // A person referenced as BOTH a document sender and a document receiver must delete