feat(geschichten): resolve document title in loader, return documentFilter object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 09:05:53 +02:00
parent 7b858e5afd
commit 03e0dae5aa
2 changed files with 135 additions and 49 deletions

View File

@@ -14,16 +14,19 @@ export const load: PageServerLoad = async ({ url, fetch }) => {
const rawDocumentId = url.searchParams.get('documentId'); const rawDocumentId = url.searchParams.get('documentId');
const documentId = rawDocumentId && UUID_PATTERN.test(rawDocumentId) ? rawDocumentId : null; const documentId = rawDocumentId && UUID_PATTERN.test(rawDocumentId) ? rawDocumentId : null;
const [listResult, ...personResults] = await Promise.all([ const [listResult, docResult, ...personResults] = await Promise.all([
api.GET('/api/geschichten', { api.GET('/api/geschichten', {
params: { params: {
query: { query: {
status: 'PUBLISHED', status: 'PUBLISHED',
personId: personIds.length ? personIds : undefined, personId: personIds.length ? personIds : undefined,
documentId: documentId ?? undefined documentId: rawDocumentId ?? undefined
} }
} }
}), }),
documentId
? api.GET('/api/documents/{id}', { params: { path: { id: documentId } } })
: Promise.resolve(null),
...personIds.map((id) => api.GET('/api/persons/{id}', { params: { path: { id } } })) ...personIds.map((id) => api.GET('/api/persons/{id}', { params: { path: { id } } }))
]); ]);
@@ -35,9 +38,22 @@ export const load: PageServerLoad = async ({ url, fetch }) => {
.filter((r) => r && r.response.ok && r.data) .filter((r) => r && r.response.ok && r.data)
.map((r) => r!.data!) as Person[]; .map((r) => r!.data!) as Person[];
let documentFilter: { id: string; title: string | null } | null = null;
if (documentId) {
if (docResult && docResult.response.ok && docResult.data) {
const doc = docResult.data;
documentFilter = {
id: documentId,
title: doc.title || doc.originalFilename || null
};
} else {
documentFilter = { id: documentId, title: null };
}
}
return { return {
geschichten: listResult.data ?? [], geschichten: listResult.data ?? [],
personFilters, personFilters,
documentIdFilter: documentId documentFilter
}; };
}; };

View File

@@ -24,17 +24,6 @@ function makeUrl(params: Record<string, string | string[]> = {}) {
return url; return url;
} }
function mockApi() {
const mockGet = vi.fn().mockResolvedValue({
response: { ok: true, status: 200 },
data: []
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
return mockGet;
}
function callLoad(url: URL) { function callLoad(url: URL) {
return load({ return load({
url, url,
@@ -43,10 +32,119 @@ function callLoad(url: URL) {
}); });
} }
// ─── documentId filter forwarding ───────────────────────────────────────────── function mockApi(
opts: {
listData?: unknown[];
docOk?: boolean;
docData?: Record<string, unknown> | null;
} = {}
) {
const {
listData = [],
docOk = true,
docData = { id: VALID_UUID, title: 'Brief an Oma', originalFilename: 'brief.jpg' }
} = opts;
describe('geschichten page load — documentId filter', () => { const mockGet = vi.fn().mockImplementation((path: string) => {
it('passes a valid documentId to the geschichten API', async () => { if (path === '/api/documents/{id}') {
return Promise.resolve({
response: { ok: docOk, status: docOk ? 200 : 404 },
data: docOk ? docData : undefined
});
}
return Promise.resolve({
response: { ok: true, status: 200 },
data: listData
});
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
return mockGet;
}
describe('geschichten page load — documentFilter title resolution', () => {
it('resolves document title when documentId is a valid UUID and document exists', async () => {
mockApi({ docData: { id: VALID_UUID, title: 'Brief an Oma', originalFilename: 'brief.jpg' } });
const result = await callLoad(makeUrl({ documentId: VALID_UUID }));
expect(result.documentFilter).toEqual({ id: VALID_UUID, title: 'Brief an Oma' });
});
it('falls back to originalFilename when document title is null', async () => {
mockApi({ docData: { id: VALID_UUID, title: null, originalFilename: 'scan_001.jpg' } });
const result = await callLoad(makeUrl({ documentId: VALID_UUID }));
expect(result.documentFilter).toEqual({ id: VALID_UUID, title: 'scan_001.jpg' });
});
it('degrades to {id, title: null} on 404 without throwing (resolves, never rejects)', async () => {
// Explicit .resolves locks the no-throw guarantee — if error() were called, this would reject
mockApi({ docOk: false });
await expect(callLoad(makeUrl({ documentId: VALID_UUID }))).resolves.toMatchObject({
documentFilter: { id: VALID_UUID, title: null }
});
});
it('treats 403 identically to 404 — no oracle, loader still resolves', async () => {
// Permanent regression test: loader must not call getErrorMessage/throw on a forbidden title fetch.
// If it did, this assertion would fail with a rejection instead of a resolution.
const mockGet = vi.fn().mockImplementation((path: string) => {
if (path === '/api/documents/{id}') {
return Promise.resolve({ response: { ok: false, status: 403 }, data: undefined });
}
return Promise.resolve({ response: { ok: true, status: 200 }, data: [] });
});
vi.mocked(createApiClient).mockReturnValue({ GET: mockGet } as ReturnType<
typeof createApiClient
>);
await expect(callLoad(makeUrl({ documentId: VALID_UUID }))).resolves.toMatchObject({
documentFilter: { id: VALID_UUID, title: null }
});
});
it('list still populates when title fetch returns 404 (independent results)', async () => {
mockApi({
listData: [{ id: 'g1', title: 'Some Story' }],
docOk: false
});
const result = await callLoad(makeUrl({ documentId: VALID_UUID }));
expect(result.geschichten).toHaveLength(1);
expect(result.documentFilter).toEqual({ id: VALID_UUID, title: null });
});
it('returns null documentFilter when documentId is syntactically invalid', async () => {
mockApi();
const result = await callLoad(makeUrl({ documentId: 'not-a-uuid' }));
expect(result.documentFilter).toBeNull();
});
it('does not fetch document title when documentId is invalid', async () => {
const mockGet = mockApi();
await callLoad(makeUrl({ documentId: 'not-a-uuid' }));
expect(mockGet).not.toHaveBeenCalledWith('/api/documents/{id}', expect.anything());
});
it('returns null documentFilter when documentId is absent', async () => {
mockApi();
const result = await callLoad(makeUrl());
expect(result.documentFilter).toBeNull();
});
it('passes valid documentId to the geschichten API', async () => {
const mockGet = mockApi(); const mockGet = mockApi();
await callLoad(makeUrl({ documentId: VALID_UUID })); await callLoad(makeUrl({ documentId: VALID_UUID }));
@@ -61,38 +159,13 @@ describe('geschichten page load — documentId filter', () => {
); );
}); });
it('omits documentId from the API call when the value is not a UUID', async () => { it('passes invalid documentId to the list API without stripping (option B)', async () => {
const mockGet = mockApi(); const mockGet = mockApi();
await callLoad(makeUrl({ documentId: 'not-a-uuid' })); await callLoad(makeUrl({ documentId: 'not-a-uuid' }));
const query = mockGet.mock.calls[0][1].params.query; const listCall = mockGet.mock.calls.find((c) => c[0] === '/api/geschichten');
expect(query.documentId).toBeUndefined(); expect(listCall?.[1]?.params?.query?.documentId).toBe('not-a-uuid');
});
it('omits documentId from the API call when the param is absent', async () => {
const mockGet = mockApi();
await callLoad(makeUrl());
const query = mockGet.mock.calls[0][1].params.query;
expect(query.documentId).toBeUndefined();
});
it('returns documentIdFilter in page data when a valid documentId is given', async () => {
mockApi();
const result = await callLoad(makeUrl({ documentId: VALID_UUID }));
expect(result.documentIdFilter).toBe(VALID_UUID);
});
it('returns null documentIdFilter when documentId is invalid', async () => {
mockApi();
const result = await callLoad(makeUrl({ documentId: 'not-a-uuid' }));
expect(result.documentIdFilter).toBeNull();
}); });
it('keeps forwarding personId filters alongside documentId', async () => { it('keeps forwarding personId filters alongside documentId', async () => {
@@ -104,10 +177,7 @@ describe('geschichten page load — documentId filter', () => {
'/api/geschichten', '/api/geschichten',
expect.objectContaining({ expect.objectContaining({
params: expect.objectContaining({ params: expect.objectContaining({
query: expect.objectContaining({ query: expect.objectContaining({ documentId: VALID_UUID, personId: [VALID_UUID] })
documentId: VALID_UUID,
personId: [VALID_UUID]
})
}) })
}) })
); );