fix(stammbaum): structured error codes in RelationshipController
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<RelationshipDTO> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user