Add optional 30-day remembered sessions
CI / checks (push) Successful in 6m29s

This commit is contained in:
2026-10-13 14:42:53 -04:00
parent e85dc58130
commit 82e07ea33e
13 changed files with 677 additions and 72 deletions
+119
View File
@@ -0,0 +1,119 @@
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<typeof page.subscribe>[0]) => page.subscribe(run) }
}));
vi.mock('$app/navigation', () => ({ invalidateAll: vi.fn() }));
let component: ReturnType<typeof mount> | 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<HTMLInputElement>(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<HTMLInputElement>('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<HTMLInputElement>('#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<HTMLInputElement>('#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('');
}
);