All checks were successful
Use untrack() for intentional one-time prop seed in UserGroupsSection. Add explicit LoadData type alias in page.server.test to avoid void|Record<string,any> union. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
4.8 KiB
TypeScript
156 lines
4.8 KiB
TypeScript
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([]);
|
|
});
|
|
});
|