Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m0s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m54s
CI / fail2ban Regex (pull_request) Successful in 47s
SDD Gate / RTM Check (pull_request) Successful in 15s
SDD Gate / Contract Validate (pull_request) Successful in 22s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
SDD Gate / Constitution Impact (pull_request) Successful in 16s
DocumentOption's metaDatePrecision/metaDateEnd are now optional so a TimelineEvent DocumentRef (id/title/documentDate only) maps cleanly into a picker chip — formatDocumentOption already degrades gracefully when precision is absent. The server specs read fail()'s union data via a small failData() cast that TS cannot narrow. svelte-check shows zero new errors in the #781 files. Refs #781 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
|
|
vi.mock('$lib/shared/api.server', () => ({
|
|
createApiClient: vi.fn(),
|
|
extractErrorCode: (e: unknown) => (e as { code?: string } | undefined)?.code
|
|
}));
|
|
|
|
import { createApiClient } from '$lib/shared/api.server';
|
|
import { load, actions } from './+page.server';
|
|
|
|
const mockFetch = vi.fn() as unknown as typeof fetch;
|
|
|
|
// fail() returns a union type that TS won't narrow; read its data loosely.
|
|
const failData = (r: unknown) => (r as { data: Record<string, unknown> }).data;
|
|
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
function localsWith(perms: string[] | null) {
|
|
if (perms === null) return { user: null };
|
|
return { user: { groups: [{ permissions: perms }] } };
|
|
}
|
|
|
|
function loadEvent(perms: string[] | null, id = 'e1') {
|
|
const url = new URL(`http://localhost/zeitstrahl/events/${id}/edit`);
|
|
return {
|
|
locals: localsWith(perms),
|
|
url,
|
|
params: { id },
|
|
fetch: mockFetch,
|
|
request: new Request(url),
|
|
route: { id: '/zeitstrahl/events/[id]/edit' }
|
|
} as never;
|
|
}
|
|
|
|
function actionEvent(
|
|
method: 'save' | 'delete',
|
|
fields: Record<string, string | string[]>,
|
|
id = 'e1'
|
|
) {
|
|
const fd = new FormData();
|
|
for (const [k, v] of Object.entries(fields)) {
|
|
if (Array.isArray(v)) v.forEach((x) => fd.append(k, x));
|
|
else fd.set(k, v);
|
|
}
|
|
return {
|
|
request: new Request(`http://localhost/zeitstrahl/events/${id}/edit?/${method}`, {
|
|
method: 'POST',
|
|
body: fd
|
|
}),
|
|
params: { id },
|
|
fetch: mockFetch,
|
|
route: { id: '/zeitstrahl/events/[id]/edit' }
|
|
} as never;
|
|
}
|
|
|
|
const EVENT_VIEW = {
|
|
id: 'e1',
|
|
title: 'Umzug',
|
|
type: 'PERSONAL',
|
|
eventDate: '1925-04-01',
|
|
precision: 'DAY',
|
|
version: 2,
|
|
createdBy: 'u1',
|
|
createdAt: '2026-01-01T00:00:00Z',
|
|
updatedBy: 'u1',
|
|
updatedAt: '2026-01-01T00:00:00Z',
|
|
persons: [],
|
|
documents: []
|
|
};
|
|
|
|
describe('zeitstrahl/events/[id]/edit load — gating (REQ-002/003)', () => {
|
|
it('throws 403 for an unauthenticated (null) user', async () => {
|
|
await expect(load(loadEvent(null))).rejects.toMatchObject({ status: 403 });
|
|
});
|
|
|
|
it('throws 403 for a user without WRITE_ALL', async () => {
|
|
await expect(load(loadEvent(['READ_ALL']))).rejects.toMatchObject({ status: 403 });
|
|
});
|
|
});
|
|
|
|
describe('zeitstrahl/events/[id]/edit load — fail closed (REQ-012)', () => {
|
|
it('throws 404 when the GET is not ok (unknown or derived id)', async () => {
|
|
vi.mocked(createApiClient).mockReturnValue({
|
|
GET: vi.fn().mockResolvedValue({ response: { ok: false, status: 404 }, data: undefined })
|
|
} as never);
|
|
await expect(load(loadEvent(['WRITE_ALL']))).rejects.toMatchObject({ status: 404 });
|
|
});
|
|
|
|
it('seeds the form with the event on an ok GET', async () => {
|
|
vi.mocked(createApiClient).mockReturnValue({
|
|
GET: vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: EVENT_VIEW })
|
|
} as never);
|
|
const result = await load(loadEvent(['WRITE_ALL']));
|
|
expect(result.event).toMatchObject({ id: 'e1', title: 'Umzug' });
|
|
});
|
|
});
|
|
|
|
describe('zeitstrahl/events/[id]/edit save action (REQ-005/013)', () => {
|
|
it('updates via PUT (with version) and redirects on success', async () => {
|
|
const put = vi
|
|
.fn()
|
|
.mockResolvedValue({ response: { ok: true, status: 200 }, data: EVENT_VIEW });
|
|
vi.mocked(createApiClient).mockReturnValue({ PUT: put } as never);
|
|
|
|
await expect(
|
|
actions.save(
|
|
actionEvent('save', {
|
|
title: 'Umzug II',
|
|
type: 'PERSONAL',
|
|
eventDate: '1925-04-01',
|
|
version: '2'
|
|
})
|
|
)
|
|
).rejects.toMatchObject({ status: 303, location: '/zeitstrahl' });
|
|
|
|
expect(put).toHaveBeenCalledTimes(1);
|
|
expect(put.mock.calls[0][1].params.path.id).toBe('e1');
|
|
expect(put.mock.calls[0][1].body).toMatchObject({ title: 'Umzug II', version: 2 });
|
|
});
|
|
|
|
it('maps a 409 conflict and does not redirect', async () => {
|
|
const put = vi
|
|
.fn()
|
|
.mockResolvedValue({ response: { ok: false, status: 409 }, error: { code: 'CONFLICT' } });
|
|
vi.mocked(createApiClient).mockReturnValue({ PUT: put } as never);
|
|
|
|
const result = await actions.save(
|
|
actionEvent('save', { title: 'Umzug', type: 'PERSONAL', eventDate: '1925-04-01' })
|
|
);
|
|
expect(result).toMatchObject({ status: 409 });
|
|
expect(failData(result).error).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('zeitstrahl/events/[id]/edit delete action (REQ-006/007)', () => {
|
|
const validUuid = '22222222-2222-2222-2222-222222222222';
|
|
|
|
it('deletes via DELETE and redirects to the resolved target on success', async () => {
|
|
const del = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 } });
|
|
vi.mocked(createApiClient).mockReturnValue({ DELETE: del } as never);
|
|
|
|
await expect(
|
|
actions.delete(actionEvent('delete', { originPersonId: validUuid }))
|
|
).rejects.toMatchObject({ status: 303, location: `/persons/${validUuid}` });
|
|
expect(del.mock.calls[0][1].params.path.id).toBe('e1');
|
|
});
|
|
|
|
it('returns fail(status) and does not redirect when DELETE is not ok', async () => {
|
|
const del = vi
|
|
.fn()
|
|
.mockResolvedValue({ response: { ok: false, status: 500 }, error: { code: 'INTERNAL' } });
|
|
vi.mocked(createApiClient).mockReturnValue({ DELETE: del } as never);
|
|
|
|
const result = await actions.delete(actionEvent('delete', {}));
|
|
expect(result).toMatchObject({ status: 500 });
|
|
expect(failData(result).error).toBeTruthy();
|
|
});
|
|
});
|