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 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(); 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(); 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([]); }); });