Files
AirCardIOS/web/src/pages/reset-page.test.ts
T
ClaudeandClaude Opus 5.5 10c55ca933 feat(account): Login, Registrierung, Passwort zurücksetzen
- appwrite 22.4.1 (exakt gepinnt); Client mit Endpoint aus VITE_APPWRITE_ENDPOINT
  bzw. location.origin + /v1, Projekt aus VITE_APPWRITE_PROJECT_ID bzw. aircard
- auth.ts: getCurrentUser (401 -> null, ungültige Antwort -> Fehler), login, register,
  logout, Passwort-Link anfordern/abschließen, onAuthChange, validatePassword
- errors.ts: deutsche Meldungen nach type, dann code (Tabelle aus dem Plan)
- Login-Dialog (Anmelden/Registrieren/Passwort vergessen) als gemeinsames <dialog>,
  Konto-Menü im Seitenkopf, Seite /passwort-neu
- Tests: errors, auth, Dialog, Passwort-neu-Seite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 08:54:10 +00:00

154 lines
6.2 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
get: vi.fn(),
create: vi.fn(),
createEmailPasswordSession: vi.fn(),
deleteSession: vi.fn(),
createRecovery: vi.fn(),
updateRecovery: vi.fn(),
}));
vi.mock('../account/appwrite', () => ({ account: mocks }));
const fail = (code: number, type: string, message = '') => Object.assign(new Error(message), { code, type });
type Page = typeof import('./reset-page');
let mountResetPage: Page['mountResetPage'];
let root: HTMLElement;
beforeEach(async () => {
for (const fn of Object.values(mocks)) fn.mockReset();
vi.resetModules();
document.body.innerHTML = '<main id="app-root"></main>';
root = document.getElementById('app-root')!;
history.replaceState(null, '', '/passwort-neu');
({ mountResetPage } = await import('./reset-page'));
});
const withLink = () => history.replaceState(null, '', '/passwort-neu?userId=u1&secret=s3cret');
const input = (name: string) => root.querySelector<HTMLInputElement>(`input[name="${name}"]`)!;
const alertText = () => root.querySelector('[role="alert"]')!.textContent;
const submit = () => root.querySelector('form')!.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
const fill = (password: string, repeat = password) => {
input('password').value = password;
input('password-repeat').value = repeat;
};
const button = (text: string) => [...root.querySelectorAll('button')].find((b) => b.textContent === text)!;
describe('ohne gültigen Link', () => {
it('ohne Parameter: Hinweis und Knopf „Neuen Link anfordern“, der den Dialog öffnet', () => {
mountResetPage(root);
expect(root.querySelector('h1')!.textContent).toBe('Passwort neu setzen');
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
expect(root.querySelector('form')).toBeNull();
button('Neuen Link anfordern').click();
const dialog = document.querySelector<HTMLDialogElement>('dialog.auth-dialog')!;
expect(dialog.open).toBe(true);
expect(dialog.querySelector('h2')!.textContent).toBe('Passwort vergessen');
});
it('mit nur einem der beiden Parameter ebenso', () => {
history.replaceState(null, '', '/passwort-neu?userId=u1');
mountResetPage(root);
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
});
});
describe('mit Link', () => {
it('zeigt zwei Passwortfelder mit new-password', () => {
withLink();
mountResetPage(root);
expect(input('password').autocomplete).toBe('new-password');
expect(input('password-repeat').autocomplete).toBe('new-password');
});
it('prüft vor dem Senden', () => {
withLink();
mountResetPage(root);
fill('geheim123', 'geheim124');
submit();
expect(alertText()).toBe('Die Passwörter stimmen nicht überein.');
fill('kurz');
submit();
expect(alertText()).toBe('Das Passwort muss 8 bis 256 Zeichen lang sein.');
expect(input('password').getAttribute('aria-invalid')).toBe('true');
expect(mocks.updateRecovery).not.toHaveBeenCalled();
});
it('setzt das Passwort, zeigt den Erfolg, entfernt das Geheimnis aus der Adresse und bietet „Anmelden“', async () => {
withLink();
mocks.updateRecovery.mockResolvedValue({});
mountResetPage(root);
fill('neuesPasswort1');
submit();
await vi.waitFor(() =>
expect(root.querySelector('[role="status"]')!.textContent).toBe('Passwort geändert. Du kannst dich jetzt anmelden.'),
);
expect(mocks.updateRecovery).toHaveBeenCalledWith({ userId: 'u1', secret: 's3cret', password: 'neuesPasswort1' });
expect(location.search).toBe('');
button('Anmelden').click();
const dialog = document.querySelector<HTMLDialogElement>('dialog.auth-dialog')!;
expect(dialog.open).toBe(true);
expect(dialog.querySelector('h2')!.textContent).toBe('Anmelden');
});
it('sperrt das Formular, solange die Anfrage läuft', async () => {
withLink();
let release!: () => void;
mocks.updateRecovery.mockReturnValue(new Promise<void>((resolve) => (release = resolve)));
mountResetPage(root);
fill('neuesPasswort1');
submit();
submit();
expect(mocks.updateRecovery).toHaveBeenCalledTimes(1);
expect(root.querySelector<HTMLButtonElement>('button[type="submit"]')!.disabled).toBe(true);
release();
await vi.waitFor(() => expect(root.querySelector('[role="status"]')).not.toBeNull());
});
it.each([
['user_invalid_token', 401, ''],
['user_not_found', 404, ''],
['general_argument_invalid', 400, 'Invalid `userId` param: Parameter must be a valid number'],
['general_argument_invalid', 400, 'Invalid `secret` param: Value must be a valid string'],
])('Link-Fehler %s (%s) zeigt „Link ungültig“ und den Knopf „Neuen Link anfordern“', async (type, code, message) => {
withLink();
mocks.updateRecovery.mockRejectedValue(fail(code, type, message));
mountResetPage(root);
fill('neuesPasswort1');
submit();
await vi.waitFor(() => expect(root.querySelector('form')).toBeNull());
expect(alertText()).toBe('Der Link ist ungültig oder abgelaufen.');
expect(button('Neuen Link anfordern')).toBeDefined();
});
it('ein Passwortfehler vom Server steht am Feld, das Formular bleibt', async () => {
withLink();
mocks.updateRecovery.mockRejectedValue(
fail(
400,
'general_argument_invalid',
'Invalid `password` param: Password must be at least 8 characters and should not be one of the commonly used password.',
),
);
mountResetPage(root);
fill('password123');
submit();
await vi.waitFor(() => expect(alertText()).toBe('Dieses Passwort ist zu verbreitet. Bitte wähle ein anderes.'));
expect(root.querySelector('form')).not.toBeNull();
expect(input('password').getAttribute('aria-invalid')).toBe('true');
expect(root.querySelector<HTMLButtonElement>('button[type="submit"]')!.disabled).toBe(false);
});
it('Netzfehler: Meldung, Formular bleibt', async () => {
withLink();
mocks.updateRecovery.mockRejectedValue(new TypeError('Failed to fetch'));
mountResetPage(root);
fill('neuesPasswort1');
submit();
await vi.waitFor(() => expect(alertText()).toBe('Keine Verbindung zum Server.'));
expect(root.querySelector('form')).not.toBeNull();
});
});