import { describe, expect, it, vi, beforeEach } from 'vitest'; vi.mock('$lib/shared/api.server', () => ({ createApiClient: vi.fn() })); import { load } from './+page.server'; import { createApiClient } from '$lib/shared/api.server'; beforeEach(() => vi.clearAllMocks()); function makeUrl(params: Record = {}) { const url = new URL('http://localhost/'); for (const [key, value] of Object.entries(params)) { if (Array.isArray(value)) { value.forEach((v) => url.searchParams.append(key, v)); } else { url.searchParams.set(key, value); } } return url; } function contributorParent() { return vi.fn().mockResolvedValue({ canWrite: true, canAnnotate: false, canBlogWrite: false }); } // ─── always-dashboard behaviour ─────────────────────────────────────────────── it('never calls /api/documents/search regardless of URL params', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl({ q: 'Urlaub', from: '2020-01-01' }), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); const calledEndpoints = mockGet.mock.calls.map((c: unknown[]) => c[0]); expect(calledEndpoints).not.toContain('/api/documents/search'); }); it('always fetches dashboard data regardless of URL params', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl({ q: 'Urlaub' }), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); const calledEndpoints = mockGet.mock.calls.map((c: unknown[]) => c[0]); expect(calledEndpoints).toContain('/api/stats'); expect(calledEndpoints).toContain('/api/dashboard/resume'); expect(calledEndpoints).toContain('/api/dashboard/pulse'); expect(calledEndpoints).toContain('/api/dashboard/activity'); }); // ─── dashboard mode ──────────────────────────────────────────────────────────── describe('home page load — dashboard', () => { it('fetches stats, resume, pulse, and activity APIs', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockResolvedValueOnce({ response: { ok: true }, data: { totalDocuments: 42, totalPersons: 7 } }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: { documentId: 'd1', title: 'T', caption: '', excerpt: '', totalBlocks: 2, pct: 50, collaborators: [] } }) // resume .mockResolvedValueOnce({ response: { ok: true }, data: { pages: 5, annotated: 1, transcribed: 2, uploaded: 1, yourPages: 3, contributors: [] } }) // pulse .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // activity .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // segmentation-queue .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // transcription-queue .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // ready-to-read .mockResolvedValueOnce({ response: { ok: true }, data: { segmentationCount: 0, transcriptionCount: 0, readyCount: 0 } }) // weekly-stats .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // incomplete .mockResolvedValueOnce({ response: { ok: true }, data: { count: 0 } }); // incomplete-count vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.stats).toEqual({ totalDocuments: 42, totalPersons: 7 }); expect(result.resumeDoc).not.toBeNull(); expect(result.resumeDoc?.totalBlocks).toBe(2); expect(result.pulse).not.toBeNull(); expect(result.activityFeed).toEqual([]); }); it('returns stats with totalDocuments from /api/stats', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockResolvedValueOnce({ response: { ok: true }, data: { totalDocuments: 248, totalPersons: 34 } }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: null }) // resume .mockResolvedValueOnce({ response: { ok: true }, data: null }) // pulse .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // activity .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // segmentation-queue .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // transcription-queue .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // ready-to-read .mockResolvedValueOnce({ response: { ok: true }, data: { segmentationCount: 0, transcriptionCount: 0, readyCount: 0 } }) // weekly-stats .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // incomplete .mockResolvedValueOnce({ response: { ok: true }, data: { count: 0 } }); // incomplete-count vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.stats?.totalDocuments).toBe(248); expect(result.stats?.totalPersons).toBe(34); }); it('returns stats: null when /api/stats rejects', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockRejectedValueOnce(new Error('network')) // stats .mockResolvedValueOnce({ response: { ok: true }, data: [] }) // resume .mockResolvedValueOnce({ response: { ok: true }, data: [] }); // pulse vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.stats).toBeNull(); }); it('defaults resumeDoc to null when resume API rejects', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockResolvedValueOnce({ response: { ok: true }, data: { content: [] } }) // stats .mockRejectedValueOnce(new Error('network')) // resume .mockResolvedValueOnce({ response: { ok: true }, data: null }) // pulse .mockResolvedValueOnce({ response: { ok: true }, data: [] }); // activity vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.resumeDoc).toBeNull(); }); it('defaults activityFeed to [] when activity API rejects', async () => { const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // persons .mockResolvedValueOnce({ response: { ok: true }, data: { totalDocuments: 0, totalPersons: 0 } }) // stats .mockResolvedValueOnce({ response: { ok: true }, data: null }) // resume .mockResolvedValueOnce({ response: { ok: true }, data: null }) // pulse .mockRejectedValueOnce(new Error('network')); // activity vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.activityFeed).toEqual([]); }); }); // ─── 401 redirect ───────────────────────────────────────────────────────────── describe('home page load — auth redirect', () => { it('redirects to /login when persons API returns 401', async () => { vi.mocked(createApiClient).mockReturnValue({ GET: vi.fn().mockResolvedValueOnce({ response: { ok: false, status: 401 }, data: null }) } as ReturnType); await expect( load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]) ).rejects.toMatchObject({ location: '/login' }); }); }); // ─── network error fallback ─────────────────────────────────────────────────── describe('home page load — network error fallback', () => { it('returns error string instead of throwing when API call throws', async () => { vi.mocked(createApiClient).mockReturnValue({ GET: vi.fn().mockRejectedValue(new Error('Network failure')) } as ReturnType); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: contributorParent() } as Parameters[0]); expect(result.error).toBe('Daten konnten nicht geladen werden.'); }); }); // ─── reader branch ───────────────────────────────────────────────────────────── describe('home page load — reader branch (isReader = !canWrite && !canAnnotate)', () => { it('does not call /api/transcription/* endpoints for a read-only user', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() .mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); const calledEndpoints = mockGet.mock.calls.map((c: unknown[]) => c[0] as string); const transcriptionCalls = calledEndpoints.filter((ep: string) => ep.startsWith('/api/transcription') ); expect(transcriptionCalls).toHaveLength(0); }); it('calls /api/stats, /api/persons, /api/documents/search, /api/geschichten for a read-only user', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() .mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); const calledEndpoints = mockGet.mock.calls.map((c: unknown[]) => c[0] as string); expect(calledEndpoints).toContain('/api/stats'); expect(calledEndpoints).toContain('/api/persons'); expect(calledEndpoints).toContain('/api/documents/search'); expect(calledEndpoints).toContain('/api/geschichten'); }); it('does not call /api/geschichten with status=DRAFT when canBlogWrite is false', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() .mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); const draftCalls = mockGet.mock.calls.filter( (c: unknown[]) => c[0] === '/api/geschichten' && (c[1] as { params?: { query?: { status?: string } } })?.params?.query?.status === 'DRAFT' ); expect(draftCalls).toHaveLength(0); }); it('calls /api/geschichten with status=DRAFT when canBlogWrite is true', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi.fn().mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: true }) } as Parameters[0]); const draftCalls = mockGet.mock.calls.filter( (c: unknown[]) => c[0] === '/api/geschichten' && (c[1] as { params?: { query?: { status?: string } } })?.params?.query?.status === 'DRAFT' ); expect(draftCalls).toHaveLength(1); }); it('returns isReader: true for read-only user', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() .mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); expect(result.isReader).toBe(true); }); it('returns isReader: false for contributor with WRITE_ALL', async () => { const mockGet = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: null }); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi.fn().mockResolvedValue({ canWrite: true, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); expect(result.isReader).toBe(false); }); it('returns topPersons=[] when topPersons fetch fails, rest of data still loads', async () => { const okStats = { response: { ok: true, status: 200 }, data: { totalDocuments: 5, totalPersons: 2, totalStories: 1 } }; const failPersons = Promise.reject(new Error('timeout')); const okSearch = { response: { ok: true, status: 200 }, data: { items: [] } }; const okStories = { response: { ok: true, status: 200 }, data: [] }; const mockGet = vi .fn() .mockResolvedValueOnce({ response: { ok: true, status: 200 }, data: [] }) // initial persons check .mockResolvedValueOnce(okStats) .mockReturnValueOnce(failPersons) .mockResolvedValueOnce(okSearch) .mockResolvedValueOnce(okStories); vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType< typeof createApiClient >); const result = await load({ url: makeUrl(), fetch: vi.fn() as unknown as typeof fetch, parent: vi .fn() .mockResolvedValue({ canWrite: false, canAnnotate: false, canBlogWrite: false }) } as Parameters[0]); expect(result.isReader).toBe(true); if (result.isReader) { expect(result.topPersons).toEqual([]); expect(result.readerStats?.totalDocuments).toBe(5); } }); });