From a6184fa12141392773a48429f317705b9efefd1f Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 11 Jun 2026 13:12:43 +0200 Subject: [PATCH] test(geschichte): add 403, catch-path, and CSRF header coverage for StoryDocumentPanel (#795) Co-Authored-By: Claude Sonnet 4.6 --- .../StoryDocumentPanel.svelte.spec.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/frontend/src/lib/geschichte/StoryDocumentPanel.svelte.spec.ts b/frontend/src/lib/geschichte/StoryDocumentPanel.svelte.spec.ts index 2aa19803..068f416f 100644 --- a/frontend/src/lib/geschichte/StoryDocumentPanel.svelte.spec.ts +++ b/frontend/src/lib/geschichte/StoryDocumentPanel.svelte.spec.ts @@ -210,6 +210,62 @@ describe('StoryDocumentPanel — add', () => { m.geschichte_documents_added_announce({ title: 'Brief von Eugenie' }) ); }); + + it('routes a 403 response through getErrorMessage on POST', async () => { + stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], { + ok: false, + status: 403, + body: { code: 'FORBIDDEN' } + }); + render(StoryDocumentPanel, defaultProps()); + + await addViaPicker(/Brief von Eugenie/i); + + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + const alertText = page.getByRole('alert').element().textContent ?? ''; + expect(alertText).not.toBe(''); + expect(alertText).not.toContain('FORBIDDEN'); + }); + + it('shows the generic reload message when POST throws a network error', async () => { + stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')]); + vi.stubGlobal( + 'fetch', + vi.fn((input: RequestInfo | URL, init?: RequestInit) => { + if ((init?.method ?? 'GET').toUpperCase() === 'GET') { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ items: [makeSearchResultItem('d1', 'Brief von Eugenie')] }) + }); + } + return Promise.reject(new Error('Network error')); + }) + ); + render(StoryDocumentPanel, defaultProps()); + + await addViaPicker(/Brief von Eugenie/i); + + await expect + .element(page.getByRole('alert')) + .toHaveTextContent(m.journey_mutation_error_reload()); + }); + + it('attaches X-XSRF-TOKEN header from cookie on POST', async () => { + document.cookie = 'XSRF-TOKEN=test-csrf-token'; + const fetchMock = stubFetch([makeSearchResultItem('d1', 'Brief von Eugenie')], { + ok: true, + body: makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie')) + }); + render(StoryDocumentPanel, defaultProps()); + + await addViaPicker(/Brief von Eugenie/i); + + const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST'); + const headers = post?.[1]?.headers as Headers; + expect(headers.get('X-XSRF-TOKEN')).toBe('test-csrf-token'); + document.cookie = 'XSRF-TOKEN=; Max-Age=0'; + }); }); describe('StoryDocumentPanel — remove', () => { @@ -348,4 +404,45 @@ describe('StoryDocumentPanel — remove', () => { m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' }) ); }); + + it('shows the generic reload message when DELETE throws a network error', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(() => Promise.reject(new Error('Network error'))) + ); + render( + StoryDocumentPanel, + defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] }) + ); + + await userEvent.click( + page.getByRole('button', { + name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' }) + }) + ); + + await expect + .element(page.getByRole('alert')) + .toHaveTextContent(m.journey_mutation_error_reload()); + }); + + it('attaches X-XSRF-TOKEN header from cookie on DELETE', async () => { + document.cookie = 'XSRF-TOKEN=test-csrf-token'; + const fetchMock = stubFetch([], { ok: true, body: {} }); + render( + StoryDocumentPanel, + defaultProps({ items: [makeItem('i1', 10, docSummary('d1', 'Brief von Eugenie'))] }) + ); + + await userEvent.click( + page.getByRole('button', { + name: m.geschichte_documents_remove_label({ title: 'Brief von Eugenie' }) + }) + ); + + const del = fetchMock.mock.calls.find(([, init]) => init?.method === 'DELETE'); + const headers = del?.[1]?.headers as Headers; + expect(headers.get('X-XSRF-TOKEN')).toBe('test-csrf-token'); + document.cookie = 'XSRF-TOKEN=; Max-Age=0'; + }); });