diff --git a/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipService.java b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipService.java index 14f46018..da4766d5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipService.java @@ -86,10 +86,9 @@ public class RelationshipService { Person person = personService.getById(personId); Person relatedPerson = personService.getById(dto.relatedPersonId()); - RelationType type = parseType(dto.relationType()); validateYears(dto.fromYear(), dto.toYear()); - if (type == RelationType.PARENT_OF + if (dto.relationType() == RelationType.PARENT_OF && relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType( relatedPerson.getId(), personId, RelationType.PARENT_OF)) { throw DomainException.conflict( @@ -100,7 +99,7 @@ public class RelationshipService { PersonRelationship rel = PersonRelationship.builder() .person(person) .relatedPerson(relatedPerson) - .relationType(type) + .relationType(dto.relationType()) .fromYear(dto.fromYear()) .toYear(dto.toYear()) .notes(blankToNull(dto.notes())) @@ -113,7 +112,7 @@ public class RelationshipService { } catch (DataIntegrityViolationException e) { throw DomainException.conflict( ErrorCode.DUPLICATE_RELATIONSHIP, - "Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + type + ")"); + "Relationship already exists for (" + personId + ", " + relatedPerson.getId() + ", " + dto.relationType() + ")"); } } @@ -141,18 +140,6 @@ public class RelationshipService { return (s == null || s.isBlank()) ? null : s.trim(); } - private static RelationType parseType(String typeName) { - if (typeName == null) { - throw DomainException.badRequest(ErrorCode.VALIDATION_ERROR, "relationType is required"); - } - try { - return RelationType.valueOf(typeName); - } catch (IllegalArgumentException e) { - throw DomainException.badRequest( - ErrorCode.VALIDATION_ERROR, "Invalid relationType: " + typeName); - } - } - private static void validateYears(Integer fromYear, Integer toYear) { if (fromYear != null && toYear != null && toYear < fromYear) { throw DomainException.badRequest( diff --git a/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/CreateRelationshipRequest.java b/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/CreateRelationshipRequest.java index 3bc7d712..40fa5d45 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/CreateRelationshipRequest.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/relationship/dto/CreateRelationshipRequest.java @@ -1,19 +1,14 @@ package org.raddatz.familienarchiv.relationship.dto; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import org.raddatz.familienarchiv.relationship.RelationType; import java.util.UUID; -/** - * POST body for {@code /api/persons/{id}/relationships}. {@code relationType} - * is a string here; the controller validates it against the {@code RelationType} - * enum at the boundary. - */ public record CreateRelationshipRequest( @NotNull UUID relatedPersonId, - @NotBlank String relationType, + @NotNull RelationType relationType, Integer fromYear, Integer toYear, @Size(max = 2000) String notes diff --git a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java index b1b35f5e..9f77ebd8 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java @@ -122,6 +122,15 @@ class RelationshipControllerTest { .andExpect(jsonPath("$[0].hops").value(2)); } + @Test + @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) + void addRelationship_returns400_when_relationType_is_unknown_value() throws Exception { + mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"NOT_A_REAL_TYPE\"}")) + .andExpect(status().isBadRequest()); + } + @Test @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"}) void addRelationship_returns201_with_RelationshipDTO_for_WRITE_ALL_user() throws Exception { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java index 003922ab..4cfd0960 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceIntegrationTest.java @@ -65,7 +65,7 @@ class RelationshipServiceIntegrationTest { @Test void addRelationship_stores_and_is_readable() { - var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, null); + var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, 1900, null, null); RelationshipDTO created = relationshipService.addRelationship(alice.getId(), dto); @@ -80,7 +80,7 @@ class RelationshipServiceIntegrationTest { @Test void addRelationship_throws_409_when_duplicate() { - var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null); + var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null); relationshipService.addRelationship(alice.getId(), dto); assertThatThrownBy(() -> relationshipService.addRelationship(alice.getId(), dto)) @@ -93,9 +93,9 @@ class RelationshipServiceIntegrationTest { void addRelationship_throws_409_when_circular_parent() { // alice PARENT_OF bob; now try bob PARENT_OF alice → must be rejected. relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null)); + new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null)); - var reverse = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null); + var reverse = new CreateRelationshipRequest(alice.getId(), RelationType.PARENT_OF, null, null, null); assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse)) .isInstanceOf(DomainException.class) .extracting("code") @@ -105,7 +105,7 @@ class RelationshipServiceIntegrationTest { @Test void deleteRelationship_throws_403_when_rel_belongs_to_different_person() { RelationshipDTO created = relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null)); + new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null)); // Charlie is unrelated to this row. assertThatThrownBy(() -> relationshipService.deleteRelationship(charlie.getId(), created.id())) @@ -122,9 +122,9 @@ class RelationshipServiceIntegrationTest { // V55 enforces symmetric uniqueness for SPOUSE_OF. Inserting (alice, bob, SPOUSE_OF) // and then (bob, alice, SPOUSE_OF) must be rejected, just like reverse SIBLING_OF. relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null)); + new CreateRelationshipRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null)); - var reverse = new CreateRelationshipRequest(alice.getId(), "SPOUSE_OF", null, null, null); + var reverse = new CreateRelationshipRequest(alice.getId(), RelationType.SPOUSE_OF, null, null, null); assertThatThrownBy(() -> relationshipService.addRelationship(bob.getId(), reverse)) .isInstanceOf(DomainException.class) .extracting("code") @@ -135,7 +135,7 @@ class RelationshipServiceIntegrationTest { void deleteRelationship_succeeds_for_symmetric_type_from_either_side() { // alice SPOUSE_OF bob. Bob deletes from his side. RelationshipDTO created = relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(bob.getId(), "SPOUSE_OF", null, null, null)); + new CreateRelationshipRequest(bob.getId(), RelationType.SPOUSE_OF, null, null, null)); relationshipService.deleteRelationship(bob.getId(), created.id()); @@ -147,7 +147,7 @@ class RelationshipServiceIntegrationTest { // charlie starts with familyMember = false. Add a PARENT_OF edge alice→charlie // so the edge exists, then flip charlie's flag and verify he appears in nodes. relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(charlie.getId(), "PARENT_OF", null, null, null)); + new CreateRelationshipRequest(charlie.getId(), RelationType.PARENT_OF, null, null, null)); NetworkDTO before = relationshipService.getFamilyNetwork(); assertThat(before.nodes()).extracting("id").doesNotContain(charlie.getId()); @@ -163,7 +163,7 @@ class RelationshipServiceIntegrationTest { @Test void delete_person_cascades_to_relationships() { RelationshipDTO created = relationshipService.addRelationship(alice.getId(), - new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null)); + new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null)); UUID relId = created.id(); assertThat(relationshipRepository.findById(relId)).isPresent(); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceTest.java index 0844a5a5..6e378459 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipServiceTest.java @@ -80,7 +80,7 @@ class RelationshipServiceTest { when(relationshipRepository.existsByPersonIdAndRelatedPersonIdAndRelationType( alice.getId(), bob.getId(), RelationType.PARENT_OF)).thenReturn(true); - var dto = new CreateRelationshipRequest(alice.getId(), "PARENT_OF", null, null, null); + var dto = new CreateRelationshipRequest(alice.getId(), RelationType.PARENT_OF, null, null, null); assertThatThrownBy(() -> service.addRelationship(bob.getId(), dto)) .isInstanceOf(DomainException.class) .extracting("code") @@ -96,7 +96,7 @@ class RelationshipServiceTest { bob.getId(), alice.getId(), RelationType.PARENT_OF)).thenReturn(false); when(relationshipRepository.saveAndFlush(any())).thenThrow(new DataIntegrityViolationException("unique_rel")); - var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", null, null, null); + var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, null, null, null); assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto)) .isInstanceOf(DomainException.class) .extracting("code") @@ -105,7 +105,7 @@ class RelationshipServiceTest { @Test void addRelationship_throws_BAD_REQUEST_when_self_relationship() { - var dto = new CreateRelationshipRequest(alice.getId(), "FRIEND", null, null, null); + var dto = new CreateRelationshipRequest(alice.getId(), RelationType.FRIEND, null, null, null); assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto)) .isInstanceOf(DomainException.class) .extracting("code") @@ -117,7 +117,7 @@ class RelationshipServiceTest { void addRelationship_throws_BAD_REQUEST_when_to_year_before_from_year() { when(personService.getById(alice.getId())).thenReturn(alice); when(personService.getById(bob.getId())).thenReturn(bob); - var dto = new CreateRelationshipRequest(bob.getId(), "FRIEND", 1950, 1940, null); + var dto = new CreateRelationshipRequest(bob.getId(), RelationType.FRIEND, 1950, 1940, null); assertThatThrownBy(() -> service.addRelationship(alice.getId(), dto)) .isInstanceOf(DomainException.class) .extracting("code") @@ -138,7 +138,7 @@ class RelationshipServiceTest { return r; }); - var dto = new CreateRelationshipRequest(bob.getId(), "PARENT_OF", 1900, null, "first born"); + var dto = new CreateRelationshipRequest(bob.getId(), RelationType.PARENT_OF, 1900, null, "first born"); var result = service.addRelationship(alice.getId(), dto); assertThat(result.personId()).isEqualTo(alice.getId());