fix(review): replace Set<Person> with Set<PersonView> in GeschichteView — prevents leaking admin fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-08 18:12:13 +02:00
parent 2f471155b8
commit 7ed0032661
4 changed files with 34 additions and 4 deletions

View File

@@ -81,10 +81,14 @@ public class GeschichteService {
if (displayName.isBlank()) displayName = "[Unbekannt]";
authorView = new GeschichteView.AuthorView(author.getId(), displayName);
}
Set<GeschichteView.PersonView> personViews = new HashSet<>();
for (Person p : g.getPersons()) {
personViews.add(new GeschichteView.PersonView(p.getId(), p.getFirstName(), p.getLastName()));
}
return new GeschichteView(
g.getId(), g.getTitle(), g.getBody(),
g.getStatus(), g.getType(),
authorView, g.getPersons(),
authorView, personViews,
items,
g.getPublishedAt(), g.getCreatedAt(), g.getUpdatedAt()
);

View File

@@ -2,7 +2,6 @@ package org.raddatz.familienarchiv.geschichte;
import io.swagger.v3.oas.annotations.media.Schema;
import org.raddatz.familienarchiv.geschichte.journeyitem.JourneyItemView;
import org.raddatz.familienarchiv.person.Person;
import java.time.LocalDateTime;
import java.util.List;
@@ -21,7 +20,7 @@ public record GeschichteView(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) GeschichteStatus status,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) GeschichteType type,
AuthorView author,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) Set<Person> persons,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) Set<PersonView> persons,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) List<JourneyItemView> items,
LocalDateTime publishedAt,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) LocalDateTime createdAt,
@@ -32,4 +31,11 @@ public record GeschichteView(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String displayName
) {}
/** Summarised person — exposes only id, firstName, and lastName. No admin-only fields. */
public record PersonView(
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id,
String firstName,
String lastName
) {}
}

View File

@@ -107,7 +107,7 @@ class GeschichteServiceIntegrationTest {
assertThat(geschichteService.list(null, List.of(franz.getId()), 50)).hasSize(1);
GeschichteView fetched = geschichteService.getById(draftId);
assertThat(fetched.title()).isEqualTo("Erinnerung an Opa Franz");
assertThat(fetched.persons()).extracting(Person::getId).containsExactly(franz.getId());
assertThat(fetched.persons()).extracting(GeschichteView.PersonView::id).containsExactly(franz.getId());
// Delete as writer; join rows go with it
authenticateAs(writer, Permission.BLOG_WRITE);

View File

@@ -153,6 +153,26 @@ class GeschichteServiceTest {
assertThat(result.author().displayName()).doesNotContain("secret@test");
}
@Test
void getById_persons_are_mapped_to_PersonView() {
authenticateAs(reader, Permission.READ_ALL);
UUID id = UUID.randomUUID();
UUID personId = UUID.randomUUID();
Geschichte published = published(id);
published.setPersons(new HashSet<>(List.of(
Person.builder().id(personId).firstName("Franz").lastName("Raddatz").build()
)));
when(geschichteRepository.findById(id)).thenReturn(Optional.of(published));
GeschichteView result = geschichteService.getById(id);
assertThat(result.persons()).hasSize(1);
GeschichteView.PersonView pv = result.persons().iterator().next();
assertThat(pv.id()).isEqualTo(personId);
assertThat(pv.firstName()).isEqualTo("Franz");
assertThat(pv.lastName()).isEqualTo("Raddatz");
}
@Test
void getById_items_come_from_journeyItemService() {
authenticateAs(reader, Permission.READ_ALL);