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(), extractErrorCode: (e: unknown) => (e as { code?: string } | undefined)?.code })); import { load, actions } from './+page.server'; import { createApiClient } from '$lib/shared/api.server'; function mockApi(methods: Partial>>) { vi.mocked(createApiClient).mockReturnValue(methods as ReturnType); } // ─── 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 = {}) { const fd = new FormData(); const defaults: Record = { 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 }); }); });