test(#148): add PersonController, DocumentSpecifications, and PersonRepository tests
- PersonControllerTest: expand from 2 to 26 tests — covers all endpoints (GET persons/id/correspondents/documents, POST create/merge, PUT update) and all validation branches (missing/blank firstName, lastName, targetPersonId → 400). Reveals and fixes a real bug: ResponseStatusException thrown by controllers was caught by the catch-all ExceptionHandler(Exception) in GlobalExceptionHandler, returning 500 instead of the intended status. Fix: add explicit ExceptionHandler(ResponseStatusException) handler. - DocumentSpecificationsTest: 18 @DataJpaTest tests covering every branch in DocumentSpecifications (hasText null/blank/match/case, hasSender null/match, hasReceiver null/match, isBetween both-null/both-set/start-only/end-only, hasTags null/empty/match/AND-logic/case/whitespace-skip). This is the primary driver of the 0% repository branch coverage reported in #148. - PersonRepositoryTest: 10 new tests for previously untested native queries — findCorrespondents (order by doc count), findCorrespondentsWithFilter (case-insensitive), reassignSender, insertMissingReceiverReference (no-duplicate guard), deleteReceiverReferences. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -30,6 +31,12 @@ public class GlobalExceptionHandler {
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse(ErrorCode.VALIDATION_ERROR, message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
|
||||
return ResponseEntity.status(ex.getStatusCode())
|
||||
.body(new ErrorResponse(ErrorCode.VALIDATION_ERROR, ex.getReason()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
|
||||
log.error("Unhandled exception", ex);
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.raddatz.familienarchiv.security.PermissionAspect;
|
||||
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
|
||||
import org.raddatz.familienarchiv.service.DocumentService;
|
||||
@@ -11,15 +12,20 @@ import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.raddatz.familienarchiv.config.SecurityConfig;
|
||||
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(PersonController.class)
|
||||
@@ -32,6 +38,101 @@ class PersonControllerTest {
|
||||
@MockitoBean DocumentService documentService;
|
||||
@MockitoBean CustomUserDetailsService customUserDetailsService;
|
||||
|
||||
// ─── GET /api/persons ─────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getPersons_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/persons"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getPersons_returns200_withEmptyList() throws Exception {
|
||||
when(personService.findAll(null)).thenReturn(Collections.emptyList());
|
||||
mockMvc.perform(get("/api/persons"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
mockMvc.perform(get("/api/persons").param("q", "Hans"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].firstName").value("Hans"));
|
||||
}
|
||||
|
||||
// ─── GET /api/persons/{id} ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getPerson_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/persons/{id}", UUID.randomUUID()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getPerson_returns200_whenFound() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person person = Person.builder().id(id).firstName("Anna").lastName("Schmidt").build();
|
||||
when(personService.getById(id)).thenReturn(person);
|
||||
|
||||
mockMvc.perform(get("/api/persons/{id}", id))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("Anna"));
|
||||
}
|
||||
|
||||
// ─── GET /api/persons/{id}/correspondents ─────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getCorrespondents_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/persons/{id}/correspondents", UUID.randomUUID()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCorrespondents_returns200_withoutFilter() throws Exception {
|
||||
UUID personId = UUID.randomUUID();
|
||||
when(personService.findCorrespondents(personId, null)).thenReturn(Collections.emptyList());
|
||||
|
||||
mockMvc.perform(get("/api/persons/{id}/correspondents", personId))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getCorrespondents_returns200_withFilter() throws Exception {
|
||||
UUID personId = UUID.randomUUID();
|
||||
Person correspondent = Person.builder().id(UUID.randomUUID()).firstName("Walter").lastName("Gruyter").build();
|
||||
when(personService.findCorrespondents(personId, "Walter")).thenReturn(List.of(correspondent));
|
||||
|
||||
mockMvc.perform(get("/api/persons/{id}/correspondents", personId).param("q", "Walter"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].firstName").value("Walter"));
|
||||
}
|
||||
|
||||
// ─── GET /api/persons/{id}/documents ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getPersonDocuments_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(get("/api/persons/{id}/documents", UUID.randomUUID()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void getPersonDocuments_returns200_whenAuthenticated() throws Exception {
|
||||
UUID personId = UUID.randomUUID();
|
||||
when(documentService.getDocumentsBySender(personId)).thenReturn(Collections.emptyList());
|
||||
|
||||
mockMvc.perform(get("/api/persons/{id}/documents", personId))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
// ─── GET /api/persons/{id}/received-documents ─────────────────────────────
|
||||
|
||||
@Test
|
||||
@@ -49,4 +150,145 @@ class PersonControllerTest {
|
||||
mockMvc.perform(get("/api/persons/{id}/received-documents", personId))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
// ─── POST /api/persons ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void createPerson_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void createPerson_returns400_whenFirstNameIsMissing() throws Exception {
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void createPerson_returns400_whenFirstNameIsBlank() throws Exception {
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\" \",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void createPerson_returns400_whenLastNameIsMissing() throws Exception {
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void createPerson_returns400_whenLastNameIsBlank() throws Exception {
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\" \"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void createPerson_returns200_whenValid() throws Exception {
|
||||
Person saved = Person.builder().id(UUID.randomUUID()).firstName("Hans").lastName("Müller").build();
|
||||
when(personService.createPerson(eq("Hans"), eq("Müller"), any())).thenReturn(saved);
|
||||
|
||||
mockMvc.perform(post("/api/persons")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.firstName").value("Hans"));
|
||||
}
|
||||
|
||||
// ─── PUT /api/persons/{id} ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void updatePerson_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void updatePerson_returns400_whenFirstNameIsBlank() throws Exception {
|
||||
mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"\",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void updatePerson_returns400_whenLastNameIsNull() throws Exception {
|
||||
mockMvc.perform(put("/api/persons/{id}", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void updatePerson_returns200_whenValid() throws Exception {
|
||||
UUID id = UUID.randomUUID();
|
||||
Person updated = Person.builder().id(id).firstName("Hans").lastName("Müller").build();
|
||||
when(personService.updatePerson(eq(id), any())).thenReturn(updated);
|
||||
|
||||
mockMvc.perform(put("/api/persons/{id}", id)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"firstName\":\"Hans\",\"lastName\":\"Müller\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.lastName").value("Müller"));
|
||||
}
|
||||
|
||||
// ─── POST /api/persons/{id}/merge ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void mergePerson_returns401_whenUnauthenticated() throws Exception {
|
||||
mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetPersonId\":\"" + UUID.randomUUID() + "\"}"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void mergePerson_returns400_whenTargetPersonIdIsMissing() throws Exception {
|
||||
mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void mergePerson_returns400_whenTargetPersonIdIsBlank() throws Exception {
|
||||
mockMvc.perform(post("/api/persons/{id}/merge", UUID.randomUUID())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetPersonId\":\" \"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
void mergePerson_returns204_whenValid() throws Exception {
|
||||
UUID sourceId = UUID.randomUUID();
|
||||
UUID targetId = UUID.randomUUID();
|
||||
|
||||
mockMvc.perform(post("/api/persons/{id}/merge", sourceId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetPersonId\":\"" + targetId + "\"}"))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package org.raddatz.familienarchiv.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.raddatz.familienarchiv.model.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.raddatz.familienarchiv.repository.DocumentSpecifications.*;
|
||||
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class DocumentSpecificationsTest {
|
||||
|
||||
@Autowired DocumentRepository documentRepository;
|
||||
@Autowired PersonRepository personRepository;
|
||||
@Autowired TagRepository tagRepository;
|
||||
|
||||
private Person sender;
|
||||
private Person receiver;
|
||||
private Document briefEarly;
|
||||
private Document briefLate;
|
||||
private Document photoDoc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
documentRepository.deleteAll();
|
||||
personRepository.deleteAll();
|
||||
tagRepository.deleteAll();
|
||||
|
||||
sender = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
|
||||
receiver = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
|
||||
Tag tagFamilie = tagRepository.save(Tag.builder().name("Familie").build());
|
||||
Tag tagUrlaub = tagRepository.save(Tag.builder().name("Urlaub").build());
|
||||
|
||||
briefEarly = documentRepository.save(Document.builder()
|
||||
.title("Alter Brief")
|
||||
.originalFilename("brief_early.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.documentDate(LocalDate.of(1940, 5, 1))
|
||||
.transcription("Liebe Anna, ich schreibe dir aus dem Krieg")
|
||||
.location("Berlin")
|
||||
.sender(sender)
|
||||
.receivers(Set.of(receiver))
|
||||
.tags(Set.of(tagFamilie))
|
||||
.build());
|
||||
|
||||
briefLate = documentRepository.save(Document.builder()
|
||||
.title("Neuerer Brief")
|
||||
.originalFilename("brief_late.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.documentDate(LocalDate.of(1960, 8, 15))
|
||||
.sender(sender)
|
||||
.tags(Set.of(tagUrlaub))
|
||||
.build());
|
||||
|
||||
photoDoc = documentRepository.save(Document.builder()
|
||||
.title("Familienfoto")
|
||||
.originalFilename("familienfoto.jpg")
|
||||
.status(DocumentStatus.PLACEHOLDER)
|
||||
.build());
|
||||
}
|
||||
|
||||
// ─── hasText ──────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void hasText_returnsAllDocuments_whenTextIsNull() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText(null)));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_returnsAllDocuments_whenTextIsBlank() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText(" ")));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_filtersOnTitle() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("familienfoto")));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Familienfoto");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_filtersOnOriginalFilename() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("brief_late")));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Neuerer Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_filtersOnTranscription() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("schreibe dir")));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_filtersOnLocation() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("berlin")));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_isCaseInsensitive() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("BRIEF")));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactlyInAnyOrder("Alter Brief", "Neuerer Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasText_returnsEmpty_whenNoMatch() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasText("xyznotexist")));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// ─── hasSender ────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void hasSender_returnsAllDocuments_whenPersonIdIsNull() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasSender(null)));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasSender_filtersDocumentsBySender() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasSender(sender.getId())));
|
||||
assertThat(result).extracting(Document::getTitle)
|
||||
.containsExactlyInAnyOrder("Alter Brief", "Neuerer Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasSender_returnsEmpty_whenSenderHasNoDocuments() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasSender(receiver.getId())));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// ─── hasReceiver ──────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void hasReceiver_returnsAllDocuments_whenPersonIdIsNull() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(null)));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasReceiver_filtersDocumentsByReceiver() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(receiver.getId())));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasReceiver_returnsEmpty_whenReceiverHasNoDocuments() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasReceiver(sender.getId())));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// ─── isBetween ────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void isBetween_returnsAllDocuments_whenBothDatesAreNull() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(isBetween(null, null)));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isBetween_filtersByBothDates() {
|
||||
List<Document> result = documentRepository.findAll(
|
||||
Specification.where(isBetween(LocalDate.of(1939, 1, 1), LocalDate.of(1945, 12, 31))));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isBetween_filtersByStartDateOnly() {
|
||||
List<Document> result = documentRepository.findAll(
|
||||
Specification.where(isBetween(LocalDate.of(1950, 1, 1), null)));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Neuerer Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isBetween_filtersByEndDateOnly() {
|
||||
List<Document> result = documentRepository.findAll(
|
||||
Specification.where(isBetween(null, LocalDate.of(1945, 12, 31))));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void isBetween_returnsEmpty_whenNoDatesInRange() {
|
||||
List<Document> result = documentRepository.findAll(
|
||||
Specification.where(isBetween(LocalDate.of(1970, 1, 1), LocalDate.of(1980, 12, 31))));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
// ─── hasTags ──────────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void hasTags_returnsAllDocuments_whenTagListIsNull() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(null)));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_returnsAllDocuments_whenTagListIsEmpty() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of())));
|
||||
assertThat(result).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_filtersDocumentsByTag() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of("Familie"))));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_isCaseInsensitive() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of("familie"))));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_requiresAllTagsToBePresent_andLogic() {
|
||||
// briefEarly has "Familie" but not "Urlaub" — should be excluded
|
||||
List<Document> result = documentRepository.findAll(
|
||||
Specification.where(hasTags(List.of("Familie", "Urlaub"))));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_skipsEmptyTagNames() {
|
||||
// An empty string in the tag list should be ignored
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of(" ", "Familie"))));
|
||||
assertThat(result).extracting(Document::getTitle).containsExactly("Alter Brief");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasTags_returnsEmpty_whenTagDoesNotExist() {
|
||||
List<Document> result = documentRepository.findAll(Specification.where(hasTags(List.of("Unbekannt"))));
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.raddatz.familienarchiv.repository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.DocumentStatus;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
@@ -11,6 +13,7 @@ import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -22,6 +25,9 @@ class PersonRepositoryTest {
|
||||
@Autowired
|
||||
private PersonRepository personRepository;
|
||||
|
||||
@Autowired
|
||||
private DocumentRepository documentRepository;
|
||||
|
||||
// ─── save and findById ────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
@@ -133,4 +139,163 @@ class PersonRepositoryTest {
|
||||
assertThat(found).isPresent();
|
||||
assertThat(found.get().getFirstName()).isEqualTo("Maria");
|
||||
}
|
||||
|
||||
// ─── findCorrespondents ───────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findCorrespondents_returnsPersonsWhoSharedDocumentsWith() {
|
||||
Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
|
||||
Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
Person clara = personRepository.save(Person.builder().firstName("Clara").lastName("Cram").build());
|
||||
|
||||
// Walter sends to Anna (1 document)
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 1").originalFilename("brief1.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(anna)).build());
|
||||
|
||||
// Walter sends to Clara (2 documents — Clara should rank higher)
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 2").originalFilename("brief2.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(clara)).build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief 3").originalFilename("brief3.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(clara)).build());
|
||||
|
||||
List<Person> correspondents = personRepository.findCorrespondents(walter.getId());
|
||||
|
||||
assertThat(correspondents).extracting(Person::getFirstName)
|
||||
.containsExactly("Clara", "Anna"); // Clara ranks first (2 documents)
|
||||
}
|
||||
|
||||
@Test
|
||||
void findCorrespondents_returnsEmpty_whenPersonHasNoDocuments() {
|
||||
Person solo = personRepository.save(Person.builder().firstName("Solo").lastName("Mensch").build());
|
||||
|
||||
List<Person> correspondents = personRepository.findCorrespondents(solo.getId());
|
||||
|
||||
assertThat(correspondents).isEmpty();
|
||||
}
|
||||
|
||||
// ─── findCorrespondentsWithFilter ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findCorrespondentsWithFilter_returnsOnlyMatchingCorrespondents() {
|
||||
Person walter = personRepository.save(Person.builder().firstName("Walter").lastName("Müller").build());
|
||||
Person anna = personRepository.save(Person.builder().firstName("Anna").lastName("Schmidt").build());
|
||||
Person bernd = personRepository.save(Person.builder().firstName("Bernd").lastName("Braun").build());
|
||||
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief an Anna").originalFilename("anna.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(anna)).build());
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief an Bernd").originalFilename("bernd.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(walter).receivers(Set.of(bernd)).build());
|
||||
|
||||
List<Person> filtered = personRepository.findCorrespondentsWithFilter(walter.getId(), "Anna");
|
||||
|
||||
assertThat(filtered).extracting(Person::getFirstName).containsExactly("Anna");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findCorrespondentsWithFilter_isCaseInsensitive() {
|
||||
Person walter = personRepository.save(Person.builder().firstName("Walter").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(walter).receivers(Set.of(anna)).build());
|
||||
|
||||
List<Person> filtered = personRepository.findCorrespondentsWithFilter(walter.getId(), "schmidt");
|
||||
|
||||
assertThat(filtered).hasSize(1);
|
||||
assertThat(filtered.get(0).getLastName()).isEqualTo("Schmidt");
|
||||
}
|
||||
|
||||
// ─── reassignSender ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void reassignSender_updatesDocumentsSenderFromSourceToTarget() {
|
||||
Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
|
||||
Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());
|
||||
|
||||
documentRepository.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(source).build());
|
||||
|
||||
personRepository.reassignSender(source.getId(), target.getId());
|
||||
|
||||
List<Document> docs = documentRepository.findBySenderId(target.getId());
|
||||
assertThat(docs).hasSize(1);
|
||||
assertThat(documentRepository.findBySenderId(source.getId())).isEmpty();
|
||||
}
|
||||
|
||||
// ─── insertMissingReceiverReference ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void insertMissingReceiverReference_addsTargetWhereSourceWasReceiver() {
|
||||
Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
|
||||
Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());
|
||||
Person sender = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());
|
||||
|
||||
Document doc = documentRepository.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(sender).receivers(Set.of(source)).build());
|
||||
|
||||
personRepository.insertMissingReceiverReference(source.getId(), target.getId());
|
||||
|
||||
Document reloaded = documentRepository.findById(doc.getId()).orElseThrow();
|
||||
assertThat(reloaded.getReceivers())
|
||||
.extracting(Person::getId)
|
||||
.contains(target.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertMissingReceiverReference_doesNotCreateDuplicate_whenTargetAlreadyReceiver() {
|
||||
Person source = personRepository.save(Person.builder().firstName("Alt").lastName("Person").build());
|
||||
Person target = personRepository.save(Person.builder().firstName("Neu").lastName("Person").build());
|
||||
Person sender = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());
|
||||
|
||||
// target is already a receiver together with source
|
||||
Document doc = documentRepository.save(Document.builder()
|
||||
.title("Brief").originalFilename("brief.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(sender).receivers(Set.of(source, target)).build());
|
||||
|
||||
personRepository.insertMissingReceiverReference(source.getId(), target.getId());
|
||||
|
||||
Document reloaded = documentRepository.findById(doc.getId()).orElseThrow();
|
||||
long targetCount = reloaded.getReceivers().stream()
|
||||
.filter(p -> p.getId().equals(target.getId())).count();
|
||||
assertThat(targetCount).isEqualTo(1); // no duplicate
|
||||
}
|
||||
|
||||
// ─── deleteReceiverReferences ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void deleteReceiverReferences_removesPersonFromAllDocumentReceivers() {
|
||||
Person toDelete = personRepository.save(Person.builder().firstName("Weg").lastName("Person").build());
|
||||
Person sender = personRepository.save(Person.builder().firstName("Send").lastName("Er").build());
|
||||
|
||||
Document doc1 = documentRepository.save(Document.builder()
|
||||
.title("Brief 1").originalFilename("b1.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(sender).receivers(Set.of(toDelete)).build());
|
||||
Document doc2 = documentRepository.save(Document.builder()
|
||||
.title("Brief 2").originalFilename("b2.pdf")
|
||||
.status(DocumentStatus.UPLOADED)
|
||||
.sender(sender).receivers(Set.of(toDelete)).build());
|
||||
|
||||
personRepository.deleteReceiverReferences(toDelete.getId());
|
||||
|
||||
assertThat(documentRepository.findById(doc1.getId()).orElseThrow().getReceivers()).isEmpty();
|
||||
assertThat(documentRepository.findById(doc2.getId()).orElseThrow().getReceivers()).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user