/** * Tests for security/auth-validation.js * * window.validatePasswordViaAPI must NEVER block submit on transport failures — * server-side validation is the source of truth, this is just a UX hint. So * every failure mode (non-OK response, network error) should resolve to * { valid: true, errors: [] }. */ import '@js/security/auth-validation.js'; describe('validatePasswordViaAPI', () => { let originalFetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); it('returns the parsed JSON on a 200 response', async () => { const apiResult = { valid: false, errors: ['too short', 'no digits'] }; globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(apiResult), }); const r = await window.validatePasswordViaAPI('weak', 'csrf-abc'); expect(r).toEqual(apiResult); }); it('returns valid:true on 403 (CSRF expiry) so submit is not blocked', async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 403, json: () => Promise.resolve({ error: 'CSRF expired' }), }); const r = await window.validatePasswordViaAPI('whatever', 'stale'); expect(r).toEqual({ valid: true, errors: [] }); }); it('returns valid:true on 500 (server error)', async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 500, json: () => Promise.resolve({}), }); const r = await window.validatePasswordViaAPI('x', 't'); expect(r).toEqual({ valid: true, errors: [] }); }); it('returns valid:true when fetch rejects (network error)', async () => { globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network down')); const r = await window.validatePasswordViaAPI('x', 't'); expect(r).toEqual({ valid: true, errors: [] }); }); it('POSTs FormData with password and csrf_token fields', async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ valid: true, errors: [] }), }); await window.validatePasswordViaAPI('hunter2', 'token-xyz'); expect(globalThis.fetch).toHaveBeenCalledTimes(1); const [url, opts] = globalThis.fetch.mock.calls[0]; expect(url).toBe('/auth/validate-password'); expect(opts.method).toBe('POST'); expect(opts.body).toBeInstanceOf(FormData); expect(opts.body.get('password')).toBe('hunter2'); expect(opts.body.get('csrf_token')).toBe('token-xyz'); }); it('does not set Content-Type header (FormData sets its own multipart boundary)', async () => { globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ valid: true, errors: [] }), }); await window.validatePasswordViaAPI('x', 't'); const [, opts] = globalThis.fetch.mock.calls[0]; // Either no headers at all, or no Content-Type override expect(opts.headers).toBeUndefined(); }); it('returns valid:true when JSON parsing throws on a 200 response', async () => { // If the server returns 200 but body is malformed, json() throws // and the catch returns the safe default globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.reject(new Error('bad JSON')), }); const r = await window.validatePasswordViaAPI('x', 't'); expect(r).toEqual({ valid: true, errors: [] }); }); });