Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ac1984dd | ||
|
|
d1c6ae67c1 | ||
|
|
07415a5b2b | ||
|
|
741b2231f2 | ||
|
|
6576e1d376 | ||
|
|
08eec086a9 |
@@ -263,7 +263,7 @@ if (!result.response.ok) {
|
||||
return { person: result.data! }; // non-null assertion is safe after the ok check
|
||||
```
|
||||
|
||||
For multipart/form-data (file uploads): bypass the typed client and use raw `fetch` — the client cannot handle it.
|
||||
For multipart/form-data (file uploads): bypass the typed client and use `event.fetch` directly — never global `fetch`. The typed client cannot handle multipart bodies, but `event.fetch` is still required so that `handleFetch` injects the session cookie.
|
||||
|
||||
### Date handling
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@ public class InviteListItemDTO {
|
||||
private String status;
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createdAt;
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String shareableUrl;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { parseBackendError } from '$lib/shared/errors';
|
||||
import { createApiClient } from '$lib/shared/api.server';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
// The spec marks shareableUrl optional but the backend always populates it.
|
||||
// Keeping the required shape here avoids null-guarding throughout the page component.
|
||||
export interface InviteListItem {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -17,34 +19,40 @@ export interface InviteListItem {
|
||||
createdAt: string;
|
||||
shareableUrl: string;
|
||||
}
|
||||
|
||||
export type UserGroup = components['schemas']['UserGroup'];
|
||||
|
||||
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||
const status = url.searchParams.get('status') ?? 'active';
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const VALID_STATUSES = ['ACTIVE', 'REVOKED', 'EXPIRED'] as const;
|
||||
type InviteStatus = (typeof VALID_STATUSES)[number];
|
||||
|
||||
const [invitesRes, groupsRes] = await Promise.all([
|
||||
fetch(`${apiUrl}/api/invites?status=${encodeURIComponent(status)}`),
|
||||
fetch(`${apiUrl}/api/groups`)
|
||||
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||
const rawStatus = url.searchParams.get('status');
|
||||
const status: InviteStatus = VALID_STATUSES.includes(rawStatus as InviteStatus)
|
||||
? (rawStatus as InviteStatus)
|
||||
: 'ACTIVE';
|
||||
const api = createApiClient(fetch);
|
||||
|
||||
const [invitesResult, groupsResult] = await Promise.all([
|
||||
api.GET('/api/invites', { params: { query: { status } } }),
|
||||
api.GET('/api/groups')
|
||||
]);
|
||||
|
||||
let invites: InviteListItem[] = [];
|
||||
let loadError: string | null = null;
|
||||
if (!invitesRes.ok) {
|
||||
const backendError = await parseBackendError(invitesRes);
|
||||
loadError = backendError?.code ?? 'INTERNAL_ERROR';
|
||||
if (!invitesResult.response.ok) {
|
||||
const code = (invitesResult.error as unknown as { code?: string })?.code;
|
||||
loadError = code ?? 'INTERNAL_ERROR';
|
||||
} else {
|
||||
invites = await invitesRes.json();
|
||||
// TODO: remove cast after next npm run generate:api — shareableUrl is now @Schema(requiredMode=REQUIRED)
|
||||
invites = (invitesResult.data ?? []) as unknown as InviteListItem[];
|
||||
}
|
||||
|
||||
let groups: UserGroup[] = [];
|
||||
let groupsLoadError: string | null = null;
|
||||
if (!groupsRes.ok) {
|
||||
const backendError = await parseBackendError(groupsRes);
|
||||
groupsLoadError = backendError?.code ?? 'INTERNAL_ERROR';
|
||||
if (!groupsResult.response.ok) {
|
||||
const code = (groupsResult.error as unknown as { code?: string })?.code;
|
||||
groupsLoadError = code ?? 'INTERNAL_ERROR';
|
||||
} else {
|
||||
const raw: UserGroup[] = await groupsRes.json();
|
||||
const raw = groupsResult.data ?? [];
|
||||
groups = [...raw].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
@@ -63,42 +71,31 @@ export const actions = {
|
||||
const expiresAt = (formData.get('expiresAt') as string) || undefined;
|
||||
const groupIds = formData.getAll('groupIds') as string[];
|
||||
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const res = await fetch(`${apiUrl}/api/invites`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
label,
|
||||
maxUses,
|
||||
prefillFirstName,
|
||||
prefillLastName,
|
||||
prefillEmail,
|
||||
expiresAt,
|
||||
groupIds
|
||||
})
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.POST('/api/invites', {
|
||||
body: { label, maxUses, prefillFirstName, prefillLastName, prefillEmail, expiresAt, groupIds }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const backendError = await parseBackendError(res);
|
||||
return fail(res.status, { createError: backendError?.code ?? 'INTERNAL_ERROR' });
|
||||
if (!result.response.ok) {
|
||||
const code = (result.error as unknown as { code?: string })?.code;
|
||||
return fail(result.response.status, { createError: code ?? 'INTERNAL_ERROR' });
|
||||
}
|
||||
|
||||
const created: InviteListItem = await res.json();
|
||||
return { created };
|
||||
// TODO: remove cast after next npm run generate:api — shareableUrl is now @Schema(requiredMode=REQUIRED)
|
||||
return { created: result.data! as unknown as InviteListItem };
|
||||
},
|
||||
|
||||
revoke: async ({ request, fetch }) => {
|
||||
const formData = await request.formData();
|
||||
const id = formData.get('id') as string;
|
||||
const id = formData.get('id') as string | null;
|
||||
if (!id) return fail(400, { revokeError: getErrorMessage('VALIDATION_ERROR') });
|
||||
|
||||
const apiUrl = env.API_INTERNAL_URL || 'http://localhost:8080';
|
||||
const res = await fetch(`${apiUrl}/api/invites/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.DELETE('/api/invites/{id}', { params: { path: { id } } });
|
||||
|
||||
if (!res.ok) {
|
||||
const backendError = await parseBackendError(res);
|
||||
return fail(res.status, { revokeError: backendError?.code ?? 'INTERNAL_ERROR' });
|
||||
if (!result.response.ok) {
|
||||
const code = (result.error as unknown as { code?: string })?.code;
|
||||
return fail(result.response.status, { revokeError: code ?? 'INTERNAL_ERROR' });
|
||||
}
|
||||
|
||||
return { revoked: id };
|
||||
|
||||
284
frontend/src/routes/admin/invites/page.server.spec.ts
Normal file
284
frontend/src/routes/admin/invites/page.server.spec.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: { API_INTERNAL_URL: 'http://localhost:8080' }
|
||||
}));
|
||||
|
||||
import { load, actions } from './+page.server';
|
||||
import type { UserGroup } from './+page.server';
|
||||
|
||||
// PageServerLoad annotates the return as `void | (...)`. This explicit shape avoids
|
||||
// the void and the Record<string, any> from the generic constraint.
|
||||
type LoadData = {
|
||||
invites: unknown[];
|
||||
status: string;
|
||||
loadError: string | null;
|
||||
groups: UserGroup[];
|
||||
groupsLoadError: string | null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyFetch = (...args: any[]) => any;
|
||||
|
||||
function mockResponse(ok: boolean, body: unknown, status = 200) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: new Headers({ 'content-type': 'application/json' })
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
describe('admin/invites load()', () => {
|
||||
const mockFetch = vi.fn<AnyFetch>();
|
||||
|
||||
beforeEach(() => mockFetch.mockReset());
|
||||
|
||||
function event(status = 'active') {
|
||||
const url = new URL(`http://localhost/admin/invites?status=${status}`);
|
||||
return {
|
||||
url,
|
||||
request: new Request(url),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
it('returns groups array alongside invites when both succeed', async () => {
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
|
||||
mockResponse(true, [
|
||||
{ id: 'g-1', name: 'Familie', permissions: ['READ_ALL'] },
|
||||
{ id: 'g-2', name: 'Administratoren', permissions: ['ADMIN'] }
|
||||
])
|
||||
);
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups).toHaveLength(2);
|
||||
expect(result.groupsLoadError).toBeNull();
|
||||
});
|
||||
|
||||
it('returns groups sorted alphabetically by name', async () => {
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
|
||||
mockResponse(true, [
|
||||
{ id: 'g-1', name: 'Zebra', permissions: [] },
|
||||
{ id: 'g-2', name: 'Alfa', permissions: [] },
|
||||
{ id: 'g-3', name: 'Mitte', permissions: [] }
|
||||
])
|
||||
);
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups.map((g) => g.name)).toEqual(['Alfa', 'Mitte', 'Zebra']);
|
||||
});
|
||||
|
||||
it('returns groups: [] and non-null groupsLoadError when groups fetch is non-OK', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(false, { code: 'FORBIDDEN' }, 403));
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups).toEqual([]);
|
||||
expect(result.groupsLoadError).toBe('FORBIDDEN');
|
||||
});
|
||||
|
||||
it('falls back to INTERNAL_ERROR when groups error body has no code', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(false, null, 500));
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groupsLoadError).toBe('INTERNAL_ERROR');
|
||||
});
|
||||
|
||||
it('fetches invites and groups in parallel (both URLs called)', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(true, []));
|
||||
|
||||
await load(event());
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
// createApiClient calls fetch(Request, {}), not fetch(string, init)
|
||||
const urls = mockFetch.mock.calls.map((call) => (call[0] as Request).url);
|
||||
expect(urls).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('/api/invites'),
|
||||
expect.stringContaining('/api/groups')
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin/invites create action', () => {
|
||||
const mockFetch = vi.fn<AnyFetch>();
|
||||
|
||||
beforeEach(() => mockFetch.mockReset());
|
||||
|
||||
const successBody = {
|
||||
id: 'inv-1',
|
||||
code: 'ABCDE12345',
|
||||
displayCode: 'ABCDE-12345',
|
||||
status: 'active',
|
||||
revoked: false,
|
||||
useCount: 0,
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
shareableUrl: 'http://localhost/register?code=ABCDE12345'
|
||||
};
|
||||
|
||||
it('includes groupIds array in POST body when checkboxes are checked', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('groupIds', 'g-1');
|
||||
fd.append('groupIds', 'g-2');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
// createApiClient calls fetch(Request, {}), not fetch(string, init)
|
||||
const [req] = mockFetch.mock.calls[0] as [Request, unknown];
|
||||
expect(req).toBeInstanceOf(Request);
|
||||
expect(req.url).toContain('/api/invites');
|
||||
const sent = await req.json();
|
||||
expect(sent.groupIds).toEqual(['g-1', 'g-2']);
|
||||
});
|
||||
|
||||
it('sends groupIds: [] when no checkboxes are checked', async () => {
|
||||
const fd = new FormData();
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const [req] = mockFetch.mock.calls[0] as [Request, unknown];
|
||||
expect(req).toBeInstanceOf(Request);
|
||||
const sent = await req.json();
|
||||
expect(sent.groupIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns created invite on success', async () => {
|
||||
const fd = new FormData();
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
const result = await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(result).toMatchObject({ created: expect.objectContaining({ id: 'inv-1' }) });
|
||||
});
|
||||
|
||||
it('returns fail with backend error code when create returns non-OK', async () => {
|
||||
const fd = new FormData();
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(false, { code: 'FORBIDDEN' }, 403));
|
||||
|
||||
const result = await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(result).toMatchObject({ status: 403, data: { createError: 'FORBIDDEN' } });
|
||||
});
|
||||
|
||||
it('falls back to INTERNAL_ERROR when create error body has no code', async () => {
|
||||
const fd = new FormData();
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(false, null, 500));
|
||||
|
||||
const result = await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(result).toMatchObject({ status: 500, data: { createError: 'INTERNAL_ERROR' } });
|
||||
});
|
||||
|
||||
it('includes expiresAt in POST body when provided', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('expiresAt', '2026-12-31');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const [req] = mockFetch.mock.calls[0] as [Request, unknown];
|
||||
const sent = await req.json();
|
||||
expect(sent.expiresAt).toBe('2026-12-31');
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin/invites revoke action', () => {
|
||||
const mockFetch = vi.fn<AnyFetch>();
|
||||
|
||||
beforeEach(() => mockFetch.mockReset());
|
||||
|
||||
it('calls DELETE /api/invites/{id} via createApiClient', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('id', 'inv-abc');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, null, 200));
|
||||
|
||||
await actions.revoke({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const [req] = mockFetch.mock.calls[0] as [Request, unknown];
|
||||
expect(req).toBeInstanceOf(Request);
|
||||
expect(req.url).toContain('/api/invites/inv-abc');
|
||||
expect(req.method).toBe('DELETE');
|
||||
});
|
||||
|
||||
it('returns revoked id on success', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('id', 'inv-abc');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, null, 200));
|
||||
|
||||
const result = await actions.revoke({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(result).toEqual({ revoked: 'inv-abc' });
|
||||
});
|
||||
|
||||
it('returns fail with backend error code when revoke returns non-OK', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('id', 'inv-abc');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(false, { code: 'NOT_FOUND' }, 404));
|
||||
|
||||
const result = await actions.revoke({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(result).toMatchObject({ status: 404, data: { revokeError: 'NOT_FOUND' } });
|
||||
});
|
||||
|
||||
it('returns fail(400) when revoke id is missing', async () => {
|
||||
const result = await actions.revoke({
|
||||
request: new Request('http://localhost', { method: 'POST', body: new FormData() }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ status: 400 });
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: { API_INTERNAL_URL: 'http://localhost:8080' }
|
||||
}));
|
||||
|
||||
import { load, actions } from './+page.server';
|
||||
import type { UserGroup } from './+page.server';
|
||||
|
||||
// PageServerLoad annotates the return as `void | (...)`. This explicit shape avoids
|
||||
// the void and the Record<string, any> from the generic constraint.
|
||||
type LoadData = {
|
||||
invites: unknown[];
|
||||
status: string;
|
||||
loadError: string | null;
|
||||
groups: UserGroup[];
|
||||
groupsLoadError: string | null;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyFetch = (...args: any[]) => any;
|
||||
|
||||
function mockResponse(ok: boolean, body: unknown, status = 200) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: new Headers({ 'content-type': 'application/json' })
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
describe('admin/invites load()', () => {
|
||||
const mockFetch = vi.fn<AnyFetch>();
|
||||
|
||||
beforeEach(() => mockFetch.mockReset());
|
||||
|
||||
function event(status = 'active') {
|
||||
return {
|
||||
url: new URL(`http://localhost/admin/invites?status=${status}`),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
it('returns groups array alongside invites when both succeed', async () => {
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
|
||||
mockResponse(true, [
|
||||
{ id: 'g-1', name: 'Familie', permissions: ['READ_ALL'] },
|
||||
{ id: 'g-2', name: 'Administratoren', permissions: ['ADMIN'] }
|
||||
])
|
||||
);
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups).toHaveLength(2);
|
||||
expect(result.groupsLoadError).toBeNull();
|
||||
});
|
||||
|
||||
it('returns groups sorted alphabetically by name', async () => {
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, [])).mockResolvedValueOnce(
|
||||
mockResponse(true, [
|
||||
{ id: 'g-1', name: 'Zebra', permissions: [] },
|
||||
{ id: 'g-2', name: 'Alfa', permissions: [] },
|
||||
{ id: 'g-3', name: 'Mitte', permissions: [] }
|
||||
])
|
||||
);
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups.map((g) => g.name)).toEqual(['Alfa', 'Mitte', 'Zebra']);
|
||||
});
|
||||
|
||||
it('returns groups: [] and non-null groupsLoadError when groups fetch is non-OK', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(false, { code: 'FORBIDDEN' }, 403));
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groups).toEqual([]);
|
||||
expect(result.groupsLoadError).toBe('FORBIDDEN');
|
||||
});
|
||||
|
||||
it('falls back to INTERNAL_ERROR when groups error body has no code', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(false, null, 500));
|
||||
|
||||
const result = (await load(event())) as LoadData;
|
||||
|
||||
expect(result.groupsLoadError).toBe('INTERNAL_ERROR');
|
||||
});
|
||||
|
||||
it('fetches invites and groups in parallel (both URLs called)', async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce(mockResponse(true, []))
|
||||
.mockResolvedValueOnce(mockResponse(true, []));
|
||||
|
||||
await load(event());
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/api/invites'));
|
||||
expect(mockFetch).toHaveBeenCalledWith(expect.stringContaining('/api/groups'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin/invites create action', () => {
|
||||
const mockFetch = vi.fn<AnyFetch>();
|
||||
|
||||
beforeEach(() => mockFetch.mockReset());
|
||||
|
||||
const successBody = {
|
||||
id: 'inv-1',
|
||||
code: 'ABCDE12345',
|
||||
displayCode: 'ABCDE-12345',
|
||||
status: 'active',
|
||||
revoked: false,
|
||||
useCount: 0,
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
shareableUrl: 'http://localhost/register?code=ABCDE12345'
|
||||
};
|
||||
|
||||
it('includes groupIds array in POST body when checkboxes are checked', async () => {
|
||||
const fd = new FormData();
|
||||
fd.append('groupIds', 'g-1');
|
||||
fd.append('groupIds', 'g-2');
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
||||
const sent = JSON.parse(init.body as string);
|
||||
expect(sent.groupIds).toEqual(['g-1', 'g-2']);
|
||||
});
|
||||
|
||||
it('sends groupIds: [] when no checkboxes are checked', async () => {
|
||||
const fd = new FormData();
|
||||
mockFetch.mockResolvedValueOnce(mockResponse(true, successBody, 201));
|
||||
|
||||
await actions.create({
|
||||
request: new Request('http://localhost', { method: 'POST', body: fd }),
|
||||
fetch: mockFetch as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
|
||||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
||||
const sent = JSON.parse(init.body as string);
|
||||
expect(sent.groupIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { createApiClient } from '$lib/shared/api.server';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ params, fetch, locals }) => {
|
||||
const user = locals.user;
|
||||
@@ -45,21 +46,17 @@ export const actions: Actions = {
|
||||
groupIds: data.getAll('groupIds') as string[]
|
||||
};
|
||||
|
||||
const res = await fetch(`/api/users/${params.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
const api = createApiClient(fetch);
|
||||
const result = await api.PUT('/api/users/{id}', {
|
||||
params: { path: { id: params.id } },
|
||||
// Body may contain null for fields the user cleared; the backend treats
|
||||
// null as "clear this field". Cast to satisfy the optional-only spec type.
|
||||
body: body as components['schemas']['AdminUpdateUserRequest']
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let code: string | undefined;
|
||||
try {
|
||||
const json = await res.json();
|
||||
code = json?.code;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return fail(res.status, { error: getErrorMessage(code) });
|
||||
if (!result.response.ok) {
|
||||
const code = (result.error as unknown as { code?: string })?.code;
|
||||
return fail(result.response.status, { error: getErrorMessage(code) });
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
|
||||
199
frontend/src/routes/admin/users/[id]/page.server.spec.ts
Normal file
199
frontend/src/routes/admin/users/[id]/page.server.spec.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
vi.mock('$env/dynamic/private', () => ({
|
||||
env: { API_INTERNAL_URL: 'http://localhost:8080' }
|
||||
}));
|
||||
|
||||
vi.mock('$lib/shared/api.server', () => ({ createApiClient: vi.fn() }));
|
||||
|
||||
import { load, actions } from './+page.server';
|
||||
import { createApiClient } from '$lib/shared/api.server';
|
||||
|
||||
function mockApi(methods: Partial<Record<'GET' | 'PUT' | 'DELETE', ReturnType<typeof vi.fn>>>) {
|
||||
vi.mocked(createApiClient).mockReturnValue(methods as ReturnType<typeof createApiClient>);
|
||||
}
|
||||
|
||||
// ─── load() ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('admin/users/[id] load()', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
function makeEvent(permissions: string[] = ['ADMIN']) {
|
||||
return {
|
||||
params: { id: 'user-123' },
|
||||
fetch: vi.fn() as unknown as typeof fetch,
|
||||
locals: { user: { groups: [{ permissions }] } },
|
||||
request: new Request('http://localhost/admin/users/user-123'),
|
||||
url: new URL('http://localhost/admin/users/user-123')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
it('throws 403 when the user lacks the ADMIN permission', async () => {
|
||||
let thrown: unknown;
|
||||
try {
|
||||
await load(makeEvent(['READ_ALL']));
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
expect((thrown as { status: number }).status).toBe(403);
|
||||
});
|
||||
|
||||
it('throws 404 when the backend returns non-ok for the user lookup', async () => {
|
||||
const mockGet = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ response: { ok: false, status: 404 }, data: undefined })
|
||||
.mockResolvedValueOnce({ response: { ok: true }, data: [] });
|
||||
mockApi({ GET: mockGet });
|
||||
|
||||
let thrown: unknown;
|
||||
try {
|
||||
await load(makeEvent());
|
||||
} catch (e) {
|
||||
thrown = e;
|
||||
}
|
||||
|
||||
expect((thrown as { status: number }).status).toBe(404);
|
||||
});
|
||||
|
||||
it('returns editUser and groups on success', async () => {
|
||||
const editUser = { id: 'user-123', email: 'max@example.com', firstName: 'Max' };
|
||||
const groups = [
|
||||
{ id: 'g-1', name: 'Familie', permissions: ['READ_ALL'] },
|
||||
{ id: 'g-2', name: 'Administratoren', permissions: ['ADMIN'] }
|
||||
];
|
||||
const mockGet = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ response: { ok: true }, data: editUser })
|
||||
.mockResolvedValueOnce({ response: { ok: true }, data: groups });
|
||||
mockApi({ GET: mockGet });
|
||||
|
||||
const result = await load(makeEvent());
|
||||
|
||||
expect(result).toMatchObject({
|
||||
editUser: expect.objectContaining({ id: 'user-123' }),
|
||||
groups: expect.arrayContaining([expect.objectContaining({ id: 'g-1' })])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── update action ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('admin/users/[id] update action', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
function makeUpdateRequest(fields: Record<string, string | string[]> = {}) {
|
||||
const fd = new FormData();
|
||||
const defaults: Record<string, string> = {
|
||||
firstName: 'Max',
|
||||
lastName: 'Mustermann',
|
||||
email: 'max@example.com'
|
||||
};
|
||||
for (const [k, v] of Object.entries({ ...defaults, ...fields })) {
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach((item) => fd.append(k, item));
|
||||
} else {
|
||||
fd.append(k, v);
|
||||
}
|
||||
}
|
||||
return new Request('http://localhost', { method: 'POST', body: fd });
|
||||
}
|
||||
|
||||
function makeEvent(request: Request) {
|
||||
return {
|
||||
params: { id: 'user-123' },
|
||||
request,
|
||||
fetch: vi.fn() as unknown as typeof fetch
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
it('calls PUT /api/users/{id} and returns success: true on 200', async () => {
|
||||
const mockPut = vi.fn().mockResolvedValue({ response: { ok: true, status: 200 }, data: {} });
|
||||
mockApi({ PUT: mockPut });
|
||||
|
||||
const result = await actions.update(makeEvent(makeUpdateRequest()));
|
||||
|
||||
expect(mockPut).toHaveBeenCalledWith(
|
||||
'/api/users/{id}',
|
||||
expect.objectContaining({ params: { path: { id: 'user-123' } } })
|
||||
);
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('returns fail with backend error code when PUT returns non-OK', async () => {
|
||||
const mockPut = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ response: { ok: false, status: 403 }, error: { code: 'FORBIDDEN' } });
|
||||
mockApi({ PUT: mockPut });
|
||||
|
||||
const result = await actions.update(makeEvent(makeUpdateRequest()));
|
||||
|
||||
expect(result).toMatchObject({ status: 403 });
|
||||
});
|
||||
|
||||
it('returns fail with generic message when error body has no code field', async () => {
|
||||
const mockPut = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ response: { ok: false, status: 500 }, error: null });
|
||||
mockApi({ PUT: mockPut });
|
||||
|
||||
const result = await actions.update(makeEvent(makeUpdateRequest()));
|
||||
|
||||
expect(result).toMatchObject({ status: 500 });
|
||||
});
|
||||
|
||||
it('returns fail without calling backend when passwords do not match', async () => {
|
||||
const mockPut = vi.fn();
|
||||
mockApi({ PUT: mockPut });
|
||||
|
||||
const result = await actions.update(
|
||||
makeEvent(makeUpdateRequest({ newPassword: 'abc', confirmPassword: 'xyz' }))
|
||||
);
|
||||
|
||||
expect(mockPut).not.toHaveBeenCalled();
|
||||
expect(result).toMatchObject({ status: 400 });
|
||||
});
|
||||
});
|
||||
|
||||
// ─── delete action ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('admin/users/[id] delete action', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
function makeEvent() {
|
||||
return {
|
||||
params: { id: 'user-123' },
|
||||
fetch: vi.fn() as unknown as typeof fetch,
|
||||
request: new Request('http://localhost/admin/users/user-123')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any;
|
||||
}
|
||||
|
||||
it('redirects to /admin/users on successful delete', async () => {
|
||||
const mockDelete = vi.fn().mockResolvedValue({ response: { ok: true } });
|
||||
mockApi({ DELETE: mockDelete });
|
||||
|
||||
let redirectLocation: string | null = null;
|
||||
try {
|
||||
await actions.delete(makeEvent());
|
||||
} catch (e: unknown) {
|
||||
const r = e as { location?: string };
|
||||
redirectLocation = r.location ?? null;
|
||||
}
|
||||
|
||||
expect(redirectLocation).toBe('/admin/users');
|
||||
});
|
||||
|
||||
it('returns fail when delete returns non-OK', async () => {
|
||||
const mockDelete = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ response: { ok: false, status: 403 }, error: { code: 'FORBIDDEN' } });
|
||||
mockApi({ DELETE: mockDelete });
|
||||
|
||||
const result = await actions.delete(makeEvent());
|
||||
|
||||
expect(result).toMatchObject({ status: 403 });
|
||||
});
|
||||
});
|
||||
@@ -19,14 +19,24 @@ describe('admin/users layout load', () => {
|
||||
{ id: 'u1', email: 'alice@example.com' },
|
||||
{ id: 'u2', email: 'bob@example.com' }
|
||||
]);
|
||||
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/users'),
|
||||
url: new URL('http://localhost/admin/users')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
expect(result.users).toHaveLength(2);
|
||||
expect(result.users[0].email).toBe('alice@example.com');
|
||||
});
|
||||
|
||||
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/users'),
|
||||
url: new URL('http://localhost/admin/users')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
expect(result.users).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -35,7 +45,12 @@ describe('admin/users 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/users'),
|
||||
url: new URL('http://localhost/admin/users')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any);
|
||||
expect(mockGet).toHaveBeenCalledWith('/api/users');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof createApiClient>);
|
||||
|
||||
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<typeof createApiClient>);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
@@ -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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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<typeof load>[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()
|
||||
|
||||
@@ -32,7 +32,13 @@ describe('person detail load — happy path', () => {
|
||||
.mockResolvedValueOnce({ response: { ok: true }, data: [] })
|
||||
} as ReturnType<typeof createApiClient>);
|
||||
|
||||
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<typeof createApiClient>);
|
||||
|
||||
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<typeof createApiClient>);
|
||||
|
||||
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<typeof createApiClient>);
|
||||
|
||||
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<typeof createApiClient>);
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user