From 07415a5b2b16f133903436fefb38b244124e1497 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 10:27:11 +0200 Subject: [PATCH] fix(tests): add missing Sentry mock event fields across 14 spec files; fix test:coverage semicolon `@sentry/sveltekit` wraps load functions and reads `event.request.method` and `event.url.pathname`. Mock events that omitted `request` or `url` threw `TypeError: Cannot read properties of undefined` on every invocation, silently masking 86 test failures on main. Two root causes fixed: - Added `request: new Request(...)` (and `url: new URL(...)` where absent) to all mock event objects in 14 `*.server.spec.ts` files - Changed `;` to `&&` in the `test:coverage` npm script so a failing server run propagates its exit code instead of being swallowed by the client run All 576 server-project tests now pass. Co-Authored-By: Claude Sonnet 4.6 --- frontend/package.json | 2 +- .../routes/admin/groups/layout.server.spec.ts | 18 +++++- .../src/routes/admin/layout.server.spec.ts | 30 ++++++++-- .../admin/ocr/[personId]/page.server.spec.ts | 14 ++++- .../admin/ocr/global/page.server.spec.ts | 14 ++++- .../src/routes/admin/ocr/page.server.spec.ts | 14 ++++- .../admin/tags/[id]/page.server.spec.ts | 2 + .../routes/admin/tags/layout.server.spec.ts | 36 +++++++++-- .../routes/aktivitaeten/page.server.spec.ts | 60 +++++++++++++++---- .../routes/briefwechsel/page.server.spec.ts | 6 ++ .../routes/documents/[id]/page.server.spec.ts | 25 ++++++-- .../documents/bulk-edit/page.server.spec.ts | 30 ++++++++-- .../src/routes/documents/page.server.spec.ts | 21 ++++++- frontend/src/routes/page.server.spec.ts | 16 +++++ .../routes/persons/[id]/page.server.spec.ts | 40 +++++++++++-- 15 files changed, 282 insertions(+), 46 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7ef2ce7d..bc0c2da5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "lint:boundary-demo": "eslint src/lib/tag/__fixtures__/", "test:unit": "vitest", "test": "npm run test:unit -- --run", - "test:coverage": "vitest run --coverage --project=server; vitest run -c vitest.client-coverage.config.ts --coverage", + "test:coverage": "vitest run --coverage --project=server && vitest run -c vitest.client-coverage.config.ts --coverage", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", "test:e2e:ui": "playwright test --ui", diff --git a/frontend/src/routes/admin/groups/layout.server.spec.ts b/frontend/src/routes/admin/groups/layout.server.spec.ts index 8a7df9a3..08cff99c 100644 --- a/frontend/src/routes/admin/groups/layout.server.spec.ts +++ b/frontend/src/routes/admin/groups/layout.server.spec.ts @@ -19,14 +19,22 @@ describe('admin/groups layout load', () => { { id: 'g1', name: 'Admins', permissions: ['ADMIN'] }, { id: 'g2', name: 'Editors', permissions: ['WRITE_ALL'] } ]); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/groups'), + url: new URL('http://localhost/admin/groups') + }); expect(result.groups).toHaveLength(2); expect(result.groups[0].name).toBe('Admins'); }); it('returns an empty array when the API returns nothing', async () => { mockApi([]); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/groups'), + url: new URL('http://localhost/admin/groups') + }); expect(result.groups).toEqual([]); }); @@ -35,7 +43,11 @@ describe('admin/groups layout load', () => { vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); - await load({ fetch: vi.fn() as unknown as typeof fetch }); + await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/groups'), + url: new URL('http://localhost/admin/groups') + }); expect(mockGet).toHaveBeenCalledWith('/api/groups'); }); }); diff --git a/frontend/src/routes/admin/layout.server.spec.ts b/frontend/src/routes/admin/layout.server.spec.ts index 1d744b6c..0fb89809 100644 --- a/frontend/src/routes/admin/layout.server.spec.ts +++ b/frontend/src/routes/admin/layout.server.spec.ts @@ -26,26 +26,46 @@ beforeEach(() => vi.clearAllMocks()); describe('admin layout load — permission check', () => { it('throws 403 when user has no admin permission', async () => { await expect( - load({ fetch: vi.fn() as unknown as typeof fetch, locals: { user: noPermUser } }) + load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin'), + url: new URL('http://localhost/admin'), + locals: { user: noPermUser } + }) ).rejects.toMatchObject({ status: 403 }); }); it('throws 403 when user is undefined', async () => { await expect( - load({ fetch: vi.fn() as unknown as typeof fetch, locals: { user: undefined } }) + load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin'), + url: new URL('http://localhost/admin'), + locals: { user: undefined } + }) ).rejects.toMatchObject({ status: 403 }); }); it('throws 403 when user has no groups', async () => { await expect( - load({ fetch: vi.fn() as unknown as typeof fetch, locals: { user: { groups: [] } } }) + load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin'), + url: new URL('http://localhost/admin'), + locals: { user: { groups: [] } } + }) ).rejects.toMatchObject({ status: 403 }); }); it('allows access for a user with ADMIN_TAG only', async () => { mockApi([], [], []); await expect( - load({ fetch: vi.fn() as unknown as typeof fetch, locals: { user: tagAdminUser } }) + load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin'), + url: new URL('http://localhost/admin'), + locals: { user: tagAdminUser } + }) ).resolves.toBeDefined(); }); @@ -63,6 +83,8 @@ describe('admin layout load — permission check', () => { const result = await load({ fetch: mockFetch as unknown as typeof fetch, + request: new Request('http://localhost/admin'), + url: new URL('http://localhost/admin'), locals: { user: adminUser } }); diff --git a/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts b/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts index 2e93d323..4d2f6ef7 100644 --- a/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts +++ b/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts @@ -15,7 +15,12 @@ describe('admin/ocr/[personId] — load', () => { data: { runs: [], personNames: { [personId]: 'Anna Müller' } } }); - const result = (await load({ params: { personId }, fetch } as never))!; + const result = (await load({ + params: { personId }, + fetch, + request: new Request('http://localhost/admin/ocr/123'), + url: new URL('http://localhost/admin/ocr/123') + } as never))!; expect(result.history.personNames?.[personId]).toBe('Anna Müller'); }); @@ -27,7 +32,12 @@ describe('admin/ocr/[personId] — load', () => { }); await expect( - load({ params: { personId: 'unknown-id' }, fetch } as never) + load({ + params: { personId: 'unknown-id' }, + fetch, + request: new Request('http://localhost/admin/ocr/unknown-id'), + url: new URL('http://localhost/admin/ocr/unknown-id') + } as never) ).rejects.toMatchObject({ status: 404 }); }); }); diff --git a/frontend/src/routes/admin/ocr/global/page.server.spec.ts b/frontend/src/routes/admin/ocr/global/page.server.spec.ts index 9aa48f64..b96438de 100644 --- a/frontend/src/routes/admin/ocr/global/page.server.spec.ts +++ b/frontend/src/routes/admin/ocr/global/page.server.spec.ts @@ -14,7 +14,11 @@ describe('admin/ocr/global — load', () => { data: { runs: [{ id: 'run1' }], personNames: {} } }); - const result = (await load({ fetch } as never))!; + const result = (await load({ + fetch, + request: new Request('http://localhost/admin/ocr/global'), + url: new URL('http://localhost/admin/ocr/global') + } as never))!; expect(result.history.runs).toHaveLength(1); }); @@ -22,6 +26,12 @@ describe('admin/ocr/global — load', () => { it('throws error when API call fails', async () => { mockApi.GET.mockResolvedValue({ response: { ok: false, status: 500 }, error: {} }); - await expect(load({ fetch } as never)).rejects.toMatchObject({ status: 500 }); + await expect( + load({ + fetch, + request: new Request('http://localhost/admin/ocr/global'), + url: new URL('http://localhost/admin/ocr/global') + } as never) + ).rejects.toMatchObject({ status: 500 }); }); }); diff --git a/frontend/src/routes/admin/ocr/page.server.spec.ts b/frontend/src/routes/admin/ocr/page.server.spec.ts index b2abe869..a7c4f019 100644 --- a/frontend/src/routes/admin/ocr/page.server.spec.ts +++ b/frontend/src/routes/admin/ocr/page.server.spec.ts @@ -14,7 +14,11 @@ describe('admin/ocr — load', () => { data: { availableBlocks: 10, ocrServiceAvailable: true, senderModels: [] } }); - const result = (await load({ fetch } as never))!; + const result = (await load({ + fetch, + request: new Request('http://localhost/admin/ocr'), + url: new URL('http://localhost/admin/ocr') + } as never))!; expect(result.trainingInfo.availableBlocks).toBe(10); expect(result.trainingInfo.ocrServiceAvailable).toBe(true); @@ -23,6 +27,12 @@ describe('admin/ocr — load', () => { it('throws 503 when OCR API call fails', async () => { mockApi.GET.mockResolvedValue({ response: { ok: false, status: 503 }, error: {} }); - await expect(load({ fetch } as never)).rejects.toMatchObject({ status: 503 }); + await expect( + load({ + fetch, + request: new Request('http://localhost/admin/ocr'), + url: new URL('http://localhost/admin/ocr') + } as never) + ).rejects.toMatchObject({ status: 503 }); }); }); diff --git a/frontend/src/routes/admin/tags/[id]/page.server.spec.ts b/frontend/src/routes/admin/tags/[id]/page.server.spec.ts index a4614eae..c8b724c0 100644 --- a/frontend/src/routes/admin/tags/[id]/page.server.spec.ts +++ b/frontend/src/routes/admin/tags/[id]/page.server.spec.ts @@ -21,6 +21,7 @@ describe('tags/[id] — load function', () => { const result = await load({ params: { id: 't1' }, parent: async () => ({ tags: [{ id: 't1', name: 'Test', documentCount: 0 }] }), + request: new Request('http://localhost/admin/tags/t1'), url } as never); expect((result as { mergeSuccess: boolean }).mergeSuccess).toBe(true); @@ -31,6 +32,7 @@ describe('tags/[id] — load function', () => { const result = await load({ params: { id: 't1' }, parent: async () => ({ tags: [{ id: 't1', name: 'Test', documentCount: 0 }] }), + request: new Request('http://localhost/admin/tags/t1'), url } as never); expect((result as { mergeSuccess: boolean }).mergeSuccess).toBe(false); diff --git a/frontend/src/routes/admin/tags/layout.server.spec.ts b/frontend/src/routes/admin/tags/layout.server.spec.ts index 80879593..d50440f5 100644 --- a/frontend/src/routes/admin/tags/layout.server.spec.ts +++ b/frontend/src/routes/admin/tags/layout.server.spec.ts @@ -44,14 +44,22 @@ const sampleTree = [ describe('admin/tags layout load', () => { it('returns the tree list', async () => { mockTreeApi(sampleTree); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); expect(result.tree).toHaveLength(2); expect(result.tree[0].name).toBe('Familie'); }); it('returns an empty tree when the API returns nothing', async () => { mockTreeApi([]); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); expect(result.tree).toEqual([]); }); @@ -60,13 +68,21 @@ describe('admin/tags layout load', () => { vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); - await load({ fetch: vi.fn() as unknown as typeof fetch }); + await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); expect(mockGet).toHaveBeenCalledWith('/api/tags/tree'); }); it('flattens the tree into a flat tags array', async () => { mockTreeApi(sampleTree); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); // Both parent and child should be in the flat array expect(result.tags).toHaveLength(3); expect(result.tags.map((t) => t.name)).toContain('Eltern'); @@ -74,14 +90,22 @@ describe('admin/tags layout load', () => { it('preserves parentId on child tags in the flat array', async () => { mockTreeApi(sampleTree); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); const child = result.tags.find((t) => t.name === 'Eltern'); expect(child?.parentId).toBe('parent1'); }); it('sets parentId to undefined on root tags in the flat array', async () => { mockTreeApi(sampleTree); - const result = await load({ fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + fetch: vi.fn() as unknown as typeof fetch, + request: new Request('http://localhost/admin/tags'), + url: new URL('http://localhost/admin/tags') + }); const root = result.tags.find((t) => t.name === 'Familie'); expect(root?.parentId).toBeUndefined(); }); diff --git a/frontend/src/routes/aktivitaeten/page.server.spec.ts b/frontend/src/routes/aktivitaeten/page.server.spec.ts index 91f0d31f..31a1619f 100644 --- a/frontend/src/routes/aktivitaeten/page.server.spec.ts +++ b/frontend/src/routes/aktivitaeten/page.server.spec.ts @@ -29,7 +29,11 @@ beforeEach(() => { describe('aktivitaeten/load — core', () => { it('requests only unread notifications for Für-dich', async () => { mockSuccess(); - await load({ fetch, url: buildUrl() } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl() + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/notifications', { params: { query: { read: false, page: 0, size: 20 } } }); @@ -45,7 +49,11 @@ describe('aktivitaeten/load — core', () => { return Promise.resolve({ response: { ok: true }, data: { content: unread } }); }); - const result = await load({ fetch, url: buildUrl() } as never); + const result = await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl() + } as never); expect(result.activityFeed).toEqual(feed); expect(result.unreadNotifications).toEqual(unread); @@ -61,7 +69,11 @@ describe('aktivitaeten/load — core', () => { return Promise.resolve({ response: { ok: true }, data: { content: [] } }); }); - const result = await load({ fetch, url: buildUrl() } as never); + const result = await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl() + } as never); expect(result.loadError).toBe('activity'); expect(result.activityFeed).toEqual([]); @@ -69,11 +81,19 @@ describe('aktivitaeten/load — core', () => { it('parses the filter query param, falling back to "alle" for invalid values', async () => { mockApi.GET.mockResolvedValue({ response: { ok: true }, data: [] }); - const validResult = await load({ fetch, url: buildUrl('?filter=fuer-dich') } as never); + const validResult = await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=fuer-dich') + } as never); expect(validResult.filter).toBe('fuer-dich'); mockApi.GET.mockResolvedValue({ response: { ok: true }, data: [] }); - const invalidResult = await load({ fetch, url: buildUrl('?filter=bogus') } as never); + const invalidResult = await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=bogus') + } as never); expect(invalidResult.filter).toBe('alle'); }); }); @@ -81,7 +101,11 @@ describe('aktivitaeten/load — core', () => { describe('aktivitaeten/load — kinds param per filter', () => { it('omits kinds for filter=alle (server defaults to ROLLUP_ELIGIBLE)', async () => { mockSuccess(); - await load({ fetch, url: buildUrl() } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl() + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40 } } }); @@ -89,7 +113,11 @@ describe('aktivitaeten/load — kinds param per filter', () => { it('omits kinds for filter=fuer-dich (client-side filtering on youMentioned/youParticipated)', async () => { mockSuccess(); - await load({ fetch, url: buildUrl('?filter=fuer-dich') } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=fuer-dich') + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40 } } }); @@ -97,7 +125,11 @@ describe('aktivitaeten/load — kinds param per filter', () => { it('sends kinds=FILE_UPLOADED for filter=hochgeladen', async () => { mockSuccess(); - await load({ fetch, url: buildUrl('?filter=hochgeladen') } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=hochgeladen') + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { limit: 40, kinds: ['FILE_UPLOADED'] } } }); @@ -105,7 +137,11 @@ describe('aktivitaeten/load — kinds param per filter', () => { it('sends TEXT_SAVED, BLOCK_REVIEWED, ANNOTATION_CREATED for filter=transkription', async () => { mockSuccess(); - await load({ fetch, url: buildUrl('?filter=transkription') } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=transkription') + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { @@ -120,7 +156,11 @@ describe('aktivitaeten/load — kinds param per filter', () => { it('sends COMMENT_ADDED, MENTION_CREATED for filter=kommentare', async () => { mockSuccess(); - await load({ fetch, url: buildUrl('?filter=kommentare') } as never); + await load({ + fetch, + request: new Request('http://localhost/aktivitaeten'), + url: buildUrl('?filter=kommentare') + } as never); expect(mockApi.GET).toHaveBeenCalledWith('/api/dashboard/activity', { params: { query: { diff --git a/frontend/src/routes/briefwechsel/page.server.spec.ts b/frontend/src/routes/briefwechsel/page.server.spec.ts index 16ad7637..1c8232ef 100644 --- a/frontend/src/routes/briefwechsel/page.server.spec.ts +++ b/frontend/src/routes/briefwechsel/page.server.spec.ts @@ -40,6 +40,7 @@ describe('korrespondenz load — no senderId', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: readUser } }); @@ -69,6 +70,7 @@ describe('korrespondenz load — senderId set, no receiverId', () => { const result = await load({ url: makeUrl({ senderId: 'p1' }), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: readUser } }); @@ -108,6 +110,7 @@ describe('korrespondenz load — senderId and receiverId set', () => { const result = await load({ url: makeUrl({ senderId: 'p1', receiverId: 'p2' }), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: readUser } }); @@ -137,6 +140,7 @@ describe('korrespondenz load — canWrite', () => { const result = await load({ url: makeUrl({ senderId: 'p1' }), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: writeUser } }); @@ -160,6 +164,7 @@ describe('korrespondenz load — canWrite', () => { const result = await load({ url: makeUrl({ senderId: 'p1' }), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: readUser } }); @@ -188,6 +193,7 @@ describe('korrespondenz load — backend error', () => { await expect( load({ url: makeUrl({ senderId: 'p1' }), + request: new Request('http://localhost/briefwechsel'), fetch: vi.fn() as unknown as typeof fetch, locals: { user: readUser } }) diff --git a/frontend/src/routes/documents/[id]/page.server.spec.ts b/frontend/src/routes/documents/[id]/page.server.spec.ts index 591a229d..f2735a9e 100644 --- a/frontend/src/routes/documents/[id]/page.server.spec.ts +++ b/frontend/src/routes/documents/[id]/page.server.spec.ts @@ -23,7 +23,9 @@ describe('document detail load — happy path', () => { const result = await load({ params: { id: '123' }, - fetch: mockFetch as unknown as typeof fetch + fetch: mockFetch as unknown as typeof fetch, + request: new Request('http://localhost/documents/123'), + url: new URL('http://localhost/documents/123') }); expect(result.document.title).toBe('Testbrief'); @@ -44,7 +46,12 @@ describe('document detail load — error paths', () => { const mockFetch = vi.fn(); await expect( - load({ params: { id: 'missing' }, fetch: mockFetch as unknown as typeof fetch }) + load({ + params: { id: 'missing' }, + fetch: mockFetch as unknown as typeof fetch, + request: new Request('http://localhost/documents/123'), + url: new URL('http://localhost/documents/123') + }) ).rejects.toMatchObject({ status: 404 }); }); @@ -59,7 +66,12 @@ describe('document detail load — error paths', () => { const mockFetch = vi.fn(); await expect( - load({ params: { id: 'secret' }, fetch: mockFetch as unknown as typeof fetch }) + load({ + params: { id: 'secret' }, + fetch: mockFetch as unknown as typeof fetch, + request: new Request('http://localhost/documents/123'), + url: new URL('http://localhost/documents/123') + }) ).rejects.toMatchObject({ status: 403 }); }); @@ -74,7 +86,12 @@ describe('document detail load — error paths', () => { const mockFetch = vi.fn(); await expect( - load({ params: { id: 'any' }, fetch: mockFetch as unknown as typeof fetch }) + load({ + params: { id: 'any' }, + fetch: mockFetch as unknown as typeof fetch, + request: new Request('http://localhost/documents/123'), + url: new URL('http://localhost/documents/123') + }) ).rejects.toMatchObject({ location: '/login' }); }); }); diff --git a/frontend/src/routes/documents/bulk-edit/page.server.spec.ts b/frontend/src/routes/documents/bulk-edit/page.server.spec.ts index d43d2248..2bca1032 100644 --- a/frontend/src/routes/documents/bulk-edit/page.server.spec.ts +++ b/frontend/src/routes/documents/bulk-edit/page.server.spec.ts @@ -6,7 +6,11 @@ describe('/documents/bulk-edit +page.server.ts', () => { const locals = { user: { groups: [{ permissions: ['READ_ALL'] }] } }; try { // @ts-expect-error — partial event shape sufficient for this guard - await load({ locals }); + await load({ + locals, + request: new Request('http://localhost/documents/bulk-edit'), + url: new URL('http://localhost/documents/bulk-edit') + }); throw new Error('expected redirect to be thrown'); } catch (e) { const err = e as { status?: number; location?: string }; @@ -19,7 +23,11 @@ describe('/documents/bulk-edit +page.server.ts', () => { const locals = { user: { groups: [] } }; try { // @ts-expect-error — partial event shape sufficient for this guard - await load({ locals }); + await load({ + locals, + request: new Request('http://localhost/documents/bulk-edit'), + url: new URL('http://localhost/documents/bulk-edit') + }); throw new Error('expected redirect'); } catch (e) { expect((e as { status?: number }).status).toBe(303); @@ -30,7 +38,11 @@ describe('/documents/bulk-edit +page.server.ts', () => { const locals = {}; try { // @ts-expect-error — partial event shape sufficient for this guard - await load({ locals }); + await load({ + locals, + request: new Request('http://localhost/documents/bulk-edit'), + url: new URL('http://localhost/documents/bulk-edit') + }); throw new Error('expected redirect'); } catch (e) { expect((e as { status?: number }).status).toBe(303); @@ -40,7 +52,11 @@ describe('/documents/bulk-edit +page.server.ts', () => { it('returns canWrite=true for a WRITE_ALL user', async () => { const locals = { user: { groups: [{ permissions: ['WRITE_ALL', 'READ_ALL'] }] } }; // @ts-expect-error — partial event shape sufficient for this guard - const result = await load({ locals }); + const result = await load({ + locals, + request: new Request('http://localhost/documents/bulk-edit'), + url: new URL('http://localhost/documents/bulk-edit') + }); expect(result).toEqual({ canWrite: true }); }); @@ -52,7 +68,11 @@ describe('/documents/bulk-edit +page.server.ts', () => { }; try { // @ts-expect-error — partial event shape sufficient for this guard - await load({ locals }); + await load({ + locals, + request: new Request('http://localhost/documents/bulk-edit'), + url: new URL('http://localhost/documents/bulk-edit') + }); throw new Error('expected redirect'); } catch (e) { expect((e as { status?: number }).status).toBe(303); diff --git a/frontend/src/routes/documents/page.server.spec.ts b/frontend/src/routes/documents/page.server.spec.ts index a77d2c3e..ac71972f 100644 --- a/frontend/src/routes/documents/page.server.spec.ts +++ b/frontend/src/routes/documents/page.server.spec.ts @@ -33,6 +33,7 @@ describe('documents page load — search params', () => { await load({ url: makeUrl({ q: 'Urlaub', from: '1920-01-01', to: '1950-12-31' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -57,6 +58,7 @@ describe('documents page load — search params', () => { await load({ url: makeUrl({ senderId: 'p-1', receiverId: 'p-2' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -81,6 +83,7 @@ describe('documents page load — search params', () => { await load({ url: makeUrl({ sort: 'TITLE', dir: 'asc', tagQ: 'fam' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -111,6 +114,7 @@ describe('documents page load — search params', () => { const result = await load({ url: makeUrl({ q: 'test' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -129,6 +133,7 @@ describe('documents page load — search params', () => { const result = await load({ url: makeUrl({ q: 'Urlaub', from: '1920-01-01', sort: 'TITLE', dir: 'asc' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -148,7 +153,11 @@ describe('documents page load — auth redirect', () => { } as ReturnType); await expect( - load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch }) + load({ + url: makeUrl(), + request: new Request('http://localhost/documents'), + fetch: vi.fn() as unknown as typeof fetch + }) ).rejects.toMatchObject({ location: '/login' }); }); }); @@ -161,7 +170,11 @@ describe('documents page load — network error fallback', () => { GET: vi.fn().mockRejectedValue(new Error('Network failure')) } as ReturnType); - const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch }); + const result = await load({ + url: makeUrl(), + request: new Request('http://localhost/documents'), + fetch: vi.fn() as unknown as typeof fetch + }); expect(result.error).toBeTruthy(); expect(result.items).toEqual([]); @@ -199,6 +212,7 @@ describe('documents page load — person name resolution', () => { const result = await load({ url: makeUrl({ senderId: '11111111-1111-1111-1111-111111111111' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -210,6 +224,7 @@ describe('documents page load — person name resolution', () => { const result = await load({ url: makeUrl({ receiverId: '22222222-2222-2222-2222-222222222222' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -221,6 +236,7 @@ describe('documents page load — person name resolution', () => { const result = await load({ url: makeUrl({ senderId: 'not-a-uuid' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); @@ -234,6 +250,7 @@ describe('documents page load — person name resolution', () => { const result = await load({ url: makeUrl({ senderId: '11111111-1111-1111-1111-111111111111' }), + request: new Request('http://localhost/documents'), fetch: vi.fn() as unknown as typeof fetch }); diff --git a/frontend/src/routes/page.server.spec.ts b/frontend/src/routes/page.server.spec.ts index b51540c3..9ffae2b9 100644 --- a/frontend/src/routes/page.server.spec.ts +++ b/frontend/src/routes/page.server.spec.ts @@ -33,6 +33,7 @@ it('never calls /api/documents/search regardless of URL params', async () => { await load({ url: makeUrl({ q: 'Urlaub', from: '2020-01-01' }), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -49,6 +50,7 @@ it('always fetches dashboard data regardless of URL params', async () => { await load({ url: makeUrl({ q: 'Urlaub' }), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -110,6 +112,7 @@ describe('home page load — dashboard', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -147,6 +150,7 @@ describe('home page load — dashboard', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -168,6 +172,7 @@ describe('home page load — dashboard', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -189,6 +194,7 @@ describe('home page load — dashboard', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -213,6 +219,7 @@ describe('home page load — dashboard', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -232,6 +239,7 @@ describe('home page load — auth redirect', () => { await expect( load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]) @@ -249,6 +257,7 @@ describe('home page load — network error fallback', () => { const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); @@ -268,6 +277,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() @@ -289,6 +299,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() @@ -310,6 +321,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() @@ -332,6 +344,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi.fn().mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: true }) } as Parameters[0]); @@ -352,6 +365,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() @@ -369,6 +383,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi.fn().mockResolvedValue({ canWrite: true, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); @@ -398,6 +413,7 @@ describe('home page load — reader branch (isReader = !canWrite && !canAnnotate const result = await load({ url: makeUrl(), + request: new Request('http://localhost/'), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() diff --git a/frontend/src/routes/persons/[id]/page.server.spec.ts b/frontend/src/routes/persons/[id]/page.server.spec.ts index 45d60ce6..c2fc6dea 100644 --- a/frontend/src/routes/persons/[id]/page.server.spec.ts +++ b/frontend/src/routes/persons/[id]/page.server.spec.ts @@ -32,7 +32,13 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); - const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocals }); + const result = await load({ + params: { id: 'p1' }, + fetch: mockFetch, + request: new Request('http://localhost/persons/p1'), + url: new URL('http://localhost/persons/p1'), + locals: mockLocals + }); expect(result.person.firstName).toBe('Hans'); expect(result.sentDocuments).toHaveLength(1); @@ -55,7 +61,13 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); - const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocalsWriter }); + const result = await load({ + params: { id: 'p1' }, + fetch: mockFetch, + request: new Request('http://localhost/persons/p1'), + url: new URL('http://localhost/persons/p1'), + locals: mockLocalsWriter + }); expect(result.canWrite).toBe(true); }); @@ -76,7 +88,13 @@ describe('person detail load — happy path', () => { .mockResolvedValueOnce({ response: { ok: true }, data: [] }) } as ReturnType); - const result = await load({ params: { id: 'p1' }, fetch: mockFetch, locals: mockLocals }); + const result = await load({ + params: { id: 'p1' }, + fetch: mockFetch, + request: new Request('http://localhost/persons/p1'), + url: new URL('http://localhost/persons/p1'), + locals: mockLocals + }); expect(result.sentDocuments).toEqual([]); expect(result.receivedDocuments).toEqual([]); @@ -100,7 +118,13 @@ describe('person detail load — error paths', () => { } as ReturnType); await expect( - load({ params: { id: 'missing' }, fetch: mockFetch, locals: mockLocals }) + load({ + params: { id: 'missing' }, + fetch: mockFetch, + request: new Request('http://localhost/persons/p1'), + url: new URL('http://localhost/persons/p1'), + locals: mockLocals + }) ).rejects.toMatchObject({ status: 404 }); @@ -120,7 +144,13 @@ describe('person detail load — error paths', () => { } as ReturnType); await expect( - load({ params: { id: 'forbidden' }, fetch: mockFetch, locals: mockLocals }) + load({ + params: { id: 'forbidden' }, + fetch: mockFetch, + request: new Request('http://localhost/persons/p1'), + url: new URL('http://localhost/persons/p1'), + locals: mockLocals + }) ).rejects.toMatchObject({ status: 403 });