feat(persons): add PersonSummaryDTO with document count to GET /api/persons
Native queries compute sender + receiver document count in one SQL call, eliminating N+1. GET /api/persons now returns PersonSummaryDTO list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -57,14 +59,27 @@ class PersonControllerTest {
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getPersons_delegatesQueryParam_toService() throws Exception {
|
||||
Person person = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
|
||||
when(personService.findAll("Hans")).thenReturn(List.of(person));
|
||||
PersonSummaryDTO dto = mockPersonSummary("Hans", "Müller");
|
||||
when(personService.findAll("Hans")).thenReturn(List.of(dto));
|
||||
|
||||
mockMvc.perform(get("/api/persons").param("q", "Hans"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].firstName").value("Hans"));
|
||||
}
|
||||
|
||||
private PersonSummaryDTO mockPersonSummary(String firstName, String lastName) {
|
||||
return new PersonSummaryDTO() {
|
||||
public java.util.UUID getId() { return UUID.randomUUID(); }
|
||||
public String getFirstName() { return firstName; }
|
||||
public String getLastName() { return lastName; }
|
||||
public String getAlias() { return null; }
|
||||
public Integer getBirthYear() { return null; }
|
||||
public Integer getDeathYear() { return null; }
|
||||
public String getNotes() { return null; }
|
||||
public long getDocumentCount() { return 0; }
|
||||
};
|
||||
}
|
||||
|
||||
// ─── GET /api/persons/{id} ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabas
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.List;
|
||||
@@ -288,6 +290,76 @@ class PersonRepositoryTest {
|
||||
assertThat(targetCount).isEqualTo(1); // no duplicate
|
||||
}
|
||||
|
||||
// ─── Phase 3.2: findAllWithDocumentCount ──────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findAllWithDocumentCount_includesDocumentCountAsSenderAndReceiver() {
|
||||
Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
|
||||
Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
|
||||
// Walter sends 2 docs to Anna (Anna receives 2)
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 1").originalFilename("b1.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(anna)).build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 2").originalFilename("b2.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(anna)).build());
|
||||
// Anna also sends 1 doc to Walter
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 3").originalFilename("b3.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(anna).receivers(Set.of(walter)).build());
|
||||
|
||||
List<PersonSummaryDTO> result = personRepository.findAllWithDocumentCount();
|
||||
|
||||
PersonSummaryDTO walterSummary = result.stream()
|
||||
.filter(p -> p.getId().equals(walter.getId())).findFirst().orElseThrow();
|
||||
PersonSummaryDTO annaSummary = result.stream()
|
||||
.filter(p -> p.getId().equals(anna.getId())).findFirst().orElseThrow();
|
||||
|
||||
assertThat(walterSummary.getDocumentCount()).isEqualTo(3); // sent 2, received 1
|
||||
assertThat(annaSummary.getDocumentCount()).isEqualTo(3); // sent 1, received 2
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllWithDocumentCount_returnsZero_whenPersonHasNoDocuments() {
|
||||
Person solo = personRepository.save(Person.builder().firstName("Solo").lastName("Mensch").build());
|
||||
|
||||
List<PersonSummaryDTO> result = personRepository.findAllWithDocumentCount();
|
||||
|
||||
PersonSummaryDTO soloSummary = result.stream()
|
||||
.filter(p -> p.getId().equals(solo.getId())).findFirst().orElseThrow();
|
||||
assertThat(soloSummary.getDocumentCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchWithDocumentCount_filtersAndIncludesCount() {
|
||||
Person hans = personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
|
||||
Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(hans).receivers(Set.of(anna)).build());
|
||||
|
||||
List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("Hans");
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getFirstName()).isEqualTo("Hans");
|
||||
assertThat(result.get(0).getDocumentCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchWithDocumentCount_isCaseInsensitive() {
|
||||
personRepository.save(Person.builder().firstName("Hans").lastName("Müller").build());
|
||||
|
||||
List<PersonSummaryDTO> result = personRepository.searchWithDocumentCount("hans");
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
|
||||
// ─── deleteReceiverReferences ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
@@ -52,32 +53,32 @@ class PersonServiceTest {
|
||||
|
||||
@Test
|
||||
void findAll_returnsAll_whenQueryIsNull() {
|
||||
List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build());
|
||||
when(personRepository.findAllByOrderByLastNameAscFirstNameAsc()).thenReturn(expected);
|
||||
List<PersonSummaryDTO> expected = List.of();
|
||||
when(personRepository.findAllWithDocumentCount()).thenReturn(expected);
|
||||
|
||||
assertThat(personService.findAll(null)).isEqualTo(expected);
|
||||
verify(personRepository).findAllByOrderByLastNameAscFirstNameAsc();
|
||||
verify(personRepository, never()).searchByName(any());
|
||||
verify(personRepository).findAllWithDocumentCount();
|
||||
verify(personRepository, never()).searchWithDocumentCount(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAll_returnsAll_whenQueryIsBlank() {
|
||||
List<Person> expected = List.of();
|
||||
when(personRepository.findAllByOrderByLastNameAscFirstNameAsc()).thenReturn(expected);
|
||||
List<PersonSummaryDTO> expected = List.of();
|
||||
when(personRepository.findAllWithDocumentCount()).thenReturn(expected);
|
||||
|
||||
assertThat(personService.findAll(" ")).isEqualTo(expected);
|
||||
verify(personRepository).findAllByOrderByLastNameAscFirstNameAsc();
|
||||
verify(personRepository, never()).searchByName(any());
|
||||
verify(personRepository).findAllWithDocumentCount();
|
||||
verify(personRepository, never()).searchWithDocumentCount(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAll_searchesByName_whenQueryIsNonBlank() {
|
||||
List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Müller").build());
|
||||
when(personRepository.searchByName("Anna")).thenReturn(expected);
|
||||
List<PersonSummaryDTO> expected = List.of();
|
||||
when(personRepository.searchWithDocumentCount("Anna")).thenReturn(expected);
|
||||
|
||||
assertThat(personService.findAll("Anna")).isEqualTo(expected);
|
||||
verify(personRepository).searchByName("Anna");
|
||||
verify(personRepository, never()).findAllByOrderByLastNameAscFirstNameAsc();
|
||||
verify(personRepository).searchWithDocumentCount("Anna");
|
||||
verify(personRepository, never()).findAllWithDocumentCount();
|
||||
}
|
||||
|
||||
// ─── createPerson ─────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user