import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { flushSync, mount, unmount } from 'svelte'; import { writable } from 'svelte/store'; import LoginPage from '../../src/routes/login/+page.svelte'; import UsersPage from '../../src/routes/settings/users/+page.svelte'; const page = writable({ url: new URL('https://ec.test/login?next=/hosts'), data: { user: { id: 'admin', username: 'administrator', role: 'admin' }, rememberedSession: false, setupRequired: false } }); vi.mock('$app/stores', () => ({ page: { subscribe: (run: Parameters[0]) => page.subscribe(run) } })); vi.mock('$app/navigation', () => ({ invalidateAll: vi.fn() })); let component: ReturnType | undefined; beforeEach(() => { page.update((value) => ({ ...value, data: { ...value.data, rememberedSession: false } })); }); afterEach(async () => { if (component) await unmount(component); component = undefined; document.body.replaceChildren(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); function enter(selector: string, value: string) { const input = document.querySelector(selector)!; input.value = value; input.dispatchEvent(new Event('input', { bubbles: true })); flushSync(); } it.each([false, true])( 'leaves remembering unchecked by default and submits the selected choice: %s', async (rememberMe) => { const fetch = vi .fn() .mockResolvedValue( new Response(JSON.stringify({ error: 'Try again' }), { status: 401 }) ); vi.stubGlobal('fetch', fetch); component = mount(LoginPage, { target: document.body }); flushSync(); const checkbox = document.querySelector('input[name="rememberMe"]')!; expect(checkbox.checked).toBe(false); expect(document.body.textContent).toContain('30 days'); enter('#username', 'administrator'); enter('#password', 'my current password'); checkbox.checked = rememberMe; checkbox.dispatchEvent(new Event('change', { bubbles: true })); flushSync(); document .querySelector('form')! .dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); await vi.waitFor(() => expect(fetch).toHaveBeenCalledOnce()); expect(JSON.parse(fetch.mock.calls[0][1].body)).toEqual({ username: 'administrator', password: 'my current password', rememberMe }); await vi.waitFor(() => expect(document.body.textContent).toContain('Try again')); expect(document.querySelector('#password')!.value).toBe(''); expect(checkbox.checked).toBe(rememberMe); } ); it.each([false, true])( 'asks remembered administrators for their own password before creating an account: %s', async (rememberedSession) => { page.update((value) => ({ ...value, data: { ...value.data, rememberedSession } })); const fetch = vi.fn(async (path: string, options?: RequestInit) => { if (options?.method === 'POST') return new Response(JSON.stringify({ error: 'Could not create user' }), { status: 403 }); return new Response( JSON.stringify(path.endsWith('/audit') ? { events: [] } : { users: [] }) ); }); vi.stubGlobal('fetch', fetch); component = mount(UsersPage, { target: document.body }); flushSync(); const add = Array.from(document.querySelectorAll('button')).find( (button) => button.textContent?.trim() === 'Add user' )!; add.click(); flushSync(); const confirmation = document.querySelector('#create-current-password'); expect(!!confirmation).toBe(rememberedSession); if (rememberedSession) { expect(confirmation!.required).toBe(true); enter('#create-current-password', 'admin secret'); } enter('#new-username', 'viewer'); enter('#new-name', 'Viewer'); enter('#temporary-password', 'temporary secret'); document .querySelector('form')! .dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); await vi.waitFor(() => expect(fetch.mock.calls.some(([, options]) => options?.method === 'POST')).toBe(true) ); const [, options] = fetch.mock.calls.find(([, options]) => options?.method === 'POST')!; expect(JSON.parse(String(options!.body))).toMatchObject({ password: 'temporary secret', currentPassword: rememberedSession ? 'admin secret' : '' }); await vi.waitFor(() => expect(document.body.textContent).toContain('Could not create user') ); if (confirmation) expect(confirmation.value).toBe(''); } );