diff --git a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSummary.java b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSummary.java index e4bf9453..d8fb9be1 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSummary.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteSummary.java @@ -37,10 +37,9 @@ public interface GeschichteSummary { String getBody(); + /** Author projection — names only; never email or group memberships (same rule as GeschichteView.AuthorView). */ interface AuthorSummary { String getFirstName(); String getLastName(); - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - String getEmail(); } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteListProjectionTest.java b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteListProjectionTest.java index 3ab95cc1..49c31bf8 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteListProjectionTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteListProjectionTest.java @@ -87,8 +87,9 @@ class GeschichteListProjectionTest { // ─── AuthorSummary nested projection ───────────────────────────────────── @Test - void findSummaries_exposes_nested_author_firstName_lastName_email() { + void findSummaries_exposes_nested_author_names_but_never_email() { AppUser richAuthor = appUserRepository.save(AppUser.builder() + .firstName("Franz").lastName("Raddatz") .email("franz@raddatz.de").password("pw").build()); geschichteRepository.save(published("Briefe aus der Front", richAuthor)); @@ -97,7 +98,13 @@ class GeschichteListProjectionTest { assertThat(result).hasSize(1); GeschichteSummary.AuthorSummary a = result.get(0).getAuthor(); - assertThat(a.getEmail()).isEqualTo("franz@raddatz.de"); + assertThat(a.getFirstName()).isEqualTo("Franz"); + assertThat(a.getLastName()).isEqualTo("Raddatz"); + // Design rule (GeschichteView.AuthorView javadoc): author projections never + // expose email or group memberships to readers. + assertThat(GeschichteSummary.AuthorSummary.class.getMethods()) + .extracting(java.lang.reflect.Method::getName) + .doesNotContain("getEmail"); } // ─── GeschichteType is exposed ──────────────────────────────────────────── diff --git a/frontend/src/lib/geschichte/GeschichteListRow.svelte.spec.ts b/frontend/src/lib/geschichte/GeschichteListRow.svelte.spec.ts index c0aa748d..b06f5ae8 100644 --- a/frontend/src/lib/geschichte/GeschichteListRow.svelte.spec.ts +++ b/frontend/src/lib/geschichte/GeschichteListRow.svelte.spec.ts @@ -12,7 +12,7 @@ const baseRow = (overrides = {}) => ({ body: '

Im Jahr 1923...

', type: 'STORY' as 'STORY' | 'JOURNEY', status: 'PUBLISHED' as 'PUBLISHED' | 'DRAFT', - author: { firstName: 'Anna', lastName: 'Schmidt', email: 'a@x' }, + author: { firstName: 'Anna', lastName: 'Schmidt' }, publishedAt: '2026-04-15T10:00:00Z', ...overrides }); diff --git a/frontend/src/lib/geschichte/GeschichtenCard.svelte b/frontend/src/lib/geschichte/GeschichtenCard.svelte index f7f71166..353909fe 100644 --- a/frontend/src/lib/geschichte/GeschichtenCard.svelte +++ b/frontend/src/lib/geschichte/GeschichtenCard.svelte @@ -27,7 +27,7 @@ function authorName(g: GeschichteSummary): string { const a = g.author; if (!a) return ''; const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim(); - return full || a.email || ''; + return full || '[Unbekannt]'; } diff --git a/frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts b/frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts index 2aeefc5a..fd1ddcbc 100644 --- a/frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts +++ b/frontend/src/lib/geschichte/GeschichtenCard.svelte.spec.ts @@ -16,7 +16,6 @@ const makeStory = (id: string, title: string, body: string | undefined = '

Bod items: [], author: { id: 'u1', - email: 'marcel@example.com', firstName: 'Marcel', lastName: 'Raddatz', enabled: true, diff --git a/frontend/src/lib/geschichte/utils.test.ts b/frontend/src/lib/geschichte/utils.test.ts index e5f78253..2d5bb6ac 100644 --- a/frontend/src/lib/geschichte/utils.test.ts +++ b/frontend/src/lib/geschichte/utils.test.ts @@ -3,21 +3,19 @@ import { formatAuthorName, formatAuthorDisplayName, formatPublishedAt } from './ describe('formatAuthorName', () => { it('joins firstName and lastName with a space', () => { - expect(formatAuthorName({ firstName: 'Anna', lastName: 'Schmidt', email: 'a@x' })).toBe( - 'Anna Schmidt' - ); + expect(formatAuthorName({ firstName: 'Anna', lastName: 'Schmidt' })).toBe('Anna Schmidt'); }); it('returns firstName alone when lastName is absent', () => { - expect(formatAuthorName({ firstName: 'Anna', email: 'a@x' })).toBe('Anna'); + expect(formatAuthorName({ firstName: 'Anna' })).toBe('Anna'); }); it('returns lastName alone when firstName is absent', () => { - expect(formatAuthorName({ lastName: 'Schmidt', email: 'a@x' })).toBe('Schmidt'); + expect(formatAuthorName({ lastName: 'Schmidt' })).toBe('Schmidt'); }); - it('falls back to email when both names are absent', () => { - expect(formatAuthorName({ email: 'fallback@example.com' })).toBe('fallback@example.com'); + it('falls back to [Unbekannt] when both names are absent', () => { + expect(formatAuthorName({})).toBe('[Unbekannt]'); }); it('returns empty string for null input', () => { diff --git a/frontend/src/lib/geschichte/utils.ts b/frontend/src/lib/geschichte/utils.ts index 36db9d05..495a9be5 100644 --- a/frontend/src/lib/geschichte/utils.ts +++ b/frontend/src/lib/geschichte/utils.ts @@ -1,12 +1,13 @@ import { formatDate } from '$lib/shared/utils/date'; -type AuthorSummary = { firstName?: string; lastName?: string; email: string }; +type AuthorSummary = { firstName?: string; lastName?: string }; type AuthorView = { displayName: string }; export function formatAuthorName(author: AuthorSummary | null | undefined): string { if (!author) return ''; const full = [author.firstName, author.lastName].filter(Boolean).join(' ').trim(); - return full || author.email || ''; + // Mirrors the server-side fallback in GeschichteService.toView — email is no longer exposed. + return full || '[Unbekannt]'; } export function formatAuthorDisplayName(author: AuthorView | null | undefined): string { diff --git a/frontend/src/routes/geschichten/[id]/page.svelte.test.ts b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts index 25d8b4cd..4f0a3a11 100644 --- a/frontend/src/routes/geschichten/[id]/page.svelte.test.ts +++ b/frontend/src/routes/geschichten/[id]/page.svelte.test.ts @@ -76,7 +76,7 @@ describe('geschichten/[id] page', () => { await expect.element(page.getByText(/Anna Schmidt/)).toBeVisible(); }); - it('falls back to author email when no name is set', async () => { + it('renders the server-computed author displayName verbatim', async () => { render(GeschichtePage, { context: new Map([[CONFIRM_KEY, createConfirmService()]]), props: { diff --git a/frontend/src/routes/geschichten/page.svelte.test.ts b/frontend/src/routes/geschichten/page.svelte.test.ts index 663859b9..0cb6b8a7 100644 --- a/frontend/src/routes/geschichten/page.svelte.test.ts +++ b/frontend/src/routes/geschichten/page.svelte.test.ts @@ -26,7 +26,7 @@ const baseData = (overrides: Record = {}) => ({ title: string; body?: string; publishedAt?: string; - author?: { firstName?: string; lastName?: string; email: string }; + author?: { firstName?: string; lastName?: string }; }>, personFilters: [] as { id?: string; displayName: string }[], documentFilter: null, @@ -127,7 +127,7 @@ describe('geschichten/+ page', () => { title: 'Reise nach Berlin', body: '

Im Jahr 1923...

', publishedAt: '2026-04-15T10:00:00Z', - author: { firstName: 'Anna', lastName: 'Schmidt', email: 'a@x' } + author: { firstName: 'Anna', lastName: 'Schmidt' } } ] }) @@ -139,7 +139,7 @@ describe('geschichten/+ page', () => { .toBeVisible(); }); - it('authorName falls back to email when first/last names are missing', async () => { + it('authorName falls back to [Unbekannt] when first/last names are missing', async () => { render(GeschichtenListPage, { props: { data: baseData({ @@ -147,14 +147,14 @@ describe('geschichten/+ page', () => { { id: 'g1', title: 'Anonym', - author: { email: 'anon@example.com' } + author: {} } ] }) } }); - expect(document.body.textContent).toContain('anon@example.com'); + expect(document.body.textContent).toContain('[Unbekannt]'); }); it('authorName renders empty when author is undefined', async () => { @@ -178,7 +178,7 @@ describe('geschichten/+ page', () => { { id: 'g1', title: 'Draft', - author: { firstName: 'Anna', lastName: 'Schmidt', email: 'a@x' } + author: { firstName: 'Anna', lastName: 'Schmidt' } } ] }) @@ -202,7 +202,7 @@ describe('geschichten/+ page', () => { id: 'g1', title: 'No Body', body: '', - author: { firstName: 'Anna', lastName: 'Schmidt', email: 'a@x' } + author: { firstName: 'Anna', lastName: 'Schmidt' } } ] })