From 22752ac1ae6791f2b5c3a29b41f412d5203ca805 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 10:19:09 +0200 Subject: [PATCH] fix(stammbaum): structured error codes in RelationshipController MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getRelationshipBetween now throws DomainException with RELATIONSHIP_NOT_FOUND instead of ResponseStatusException, so the frontend receives a typed error code. Removed redundant validateRelationType() guard — RelationshipService.parseType() already handles this with the same DomainException/VALIDATION_ERROR path. Co-Authored-By: Claude Sonnet 4.6 --- .../relationship/RelationshipController.java | 19 ++----- .../RelationshipControllerTest.java | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java index 1a94cc29..210ad41e 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/relationship/RelationshipController.java @@ -9,12 +9,13 @@ import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipDTO; import org.raddatz.familienarchiv.relationship.dto.InferredRelationshipWithPersonDTO; import org.raddatz.familienarchiv.relationship.dto.NetworkDTO; import org.raddatz.familienarchiv.relationship.dto.RelationshipDTO; +import org.raddatz.familienarchiv.exception.DomainException; +import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.security.Permission; import org.raddatz.familienarchiv.security.RequirePermission; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.server.ResponseStatusException; import java.util.List; import java.util.UUID; @@ -51,8 +52,8 @@ public class RelationshipController { @GetMapping("/api/persons/{aId}/relationship-to/{bId}") public InferredRelationshipDTO getRelationshipBetween(@PathVariable UUID aId, @PathVariable UUID bId) { return relationshipService.getRelationshipBetween(aId, bId) - .orElseThrow(() -> new ResponseStatusException( - HttpStatus.NOT_FOUND, "No relationship path between " + aId + " and " + bId)); + .orElseThrow(() -> DomainException.notFound( + ErrorCode.RELATIONSHIP_NOT_FOUND, "No relationship path between " + aId + " and " + bId)); } @PostMapping("/api/persons/{id}/relationships") @@ -60,7 +61,6 @@ public class RelationshipController { public ResponseEntity addRelationship( @PathVariable UUID id, @Valid @RequestBody CreateRelationshipRequest dto) { - validateRelationType(dto.relationType()); return ResponseEntity.status(HttpStatus.CREATED) .body(relationshipService.addRelationship(id, dto)); } @@ -78,15 +78,4 @@ public class RelationshipController { return relationshipService.setFamilyMember(id, dto.familyMember()); } - private static void validateRelationType(String typeName) { - if (typeName == null || typeName.isBlank()) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "relationType is required"); - } - try { - RelationType.valueOf(typeName); - } catch (IllegalArgumentException e) { - throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "Unknown relationType: " + typeName); - } - } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java new file mode 100644 index 00000000..0036c415 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/relationship/RelationshipControllerTest.java @@ -0,0 +1,57 @@ +package org.raddatz.familienarchiv.relationship; + +import org.junit.jupiter.api.Test; +import org.raddatz.familienarchiv.config.SecurityConfig; +import org.raddatz.familienarchiv.exception.ErrorCode; +import org.raddatz.familienarchiv.security.PermissionAspect; +import org.raddatz.familienarchiv.service.CustomUserDetailsService; +import org.raddatz.familienarchiv.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest; +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.Optional; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(RelationshipController.class) +@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class}) +class RelationshipControllerTest { + + @Autowired MockMvc mockMvc; + + @MockitoBean RelationshipService relationshipService; + @MockitoBean UserService userService; + @MockitoBean CustomUserDetailsService customUserDetailsService; + + private static final UUID PERSON_ID = UUID.randomUUID(); + private static final UUID OTHER_ID = UUID.randomUUID(); + + @Test + @WithMockUser(username = "testuser", authorities = {"READ_ALL"}) + void getRelationshipBetween_returns404_with_RELATIONSHIP_NOT_FOUND_code_when_no_path() throws Exception { + when(relationshipService.getRelationshipBetween(any(), any())).thenReturn(Optional.empty()); + + mockMvc.perform(get("/api/persons/{aId}/relationship-to/{bId}", PERSON_ID, OTHER_ID)) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value(ErrorCode.RELATIONSHIP_NOT_FOUND.name())); + } + + @Test + @WithMockUser(username = "testuser", authorities = {"READ_ALL"}) + void addRelationship_returns403_for_user_with_READ_ALL_only() throws Exception { + mockMvc.perform(post("/api/persons/{id}/relationships", PERSON_ID) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"relatedPersonId\":\"" + OTHER_ID + "\",\"relationType\":\"PARENT_OF\"}")) + .andExpect(status().isForbidden()); + } +}