import { beforeEach, describe, expect, it, vi } from 'vitest' import { readConfig } from '../src/config' import { mountStagingLogin } from '../src/login-staging' import type { DpopKey } from '../src/dpop' const cfg = readConfig({ protocol: 'https:', host: 'alice.term.example.com', hostname: 'alice.term.example.com', }) /** Deterministic DPoP key so the login test never touches WebCrypto. */ const fakeKey = (jkt: string): DpopKey => ({ jkt, proof: async () => 'h.p.s' }) const FAKE_JKT = 'A'.repeat(43) function mintFetch(body: unknown, ok = true, status = 200): typeof fetch { return vi.fn(async () => ({ ok, status, json: async () => body }) as Response) as unknown as typeof fetch } describe('mountStagingLogin (B6) — operator password → POST /auth/mint', () => { let root: HTMLElement beforeEach(() => { root = document.createElement('div') document.body.append(root) }) it('correct password → "ok" and POSTs {password, jkt, subdomain} (INV3: a NAME, not an accountId)', async () => { const fetchImpl = mintFetch({ token: 'CAPTOKEN' }) const onSuccess = vi.fn() const login = mountStagingLogin(root, cfg, { fetchImpl, onSuccess, createKey: async () => fakeKey(FAKE_JKT), }) await expect(login.submit('s3cret')).resolves.toBe('ok') const call = (fetchImpl as unknown as ReturnType).mock.calls[0]! expect(call[0]).toBe('/auth/mint') const init = call[1] as RequestInit expect(init.method).toBe('POST') expect(JSON.parse(init.body as string)).toEqual({ password: 's3cret', jkt: FAKE_JKT, subdomain: 'alice', }) // onSuccess receives the token + aud + the SAME dpop key whose jkt was minted. const session = onSuccess.mock.calls[0]![0] expect(session.token).toBe('CAPTOKEN') expect(session.aud).toBe('alice') expect(session.dpop.jkt).toBe(FAKE_JKT) }) it('wrong password (401) → "rejected", no onSuccess, error via textContent (no innerHTML)', async () => { const onSuccess = vi.fn() const login = mountStagingLogin(root, cfg, { fetchImpl: mintFetch({}, false, 401), onSuccess, createKey: async () => fakeKey(FAKE_JKT), }) await expect(login.submit('nope')).resolves.toBe('rejected') expect(onSuccess).not.toHaveBeenCalled() const err = root.querySelector('.login-error') as HTMLElement expect(err.textContent).toBe('Invalid operator password.') expect(err.innerHTML).toBe('Invalid operator password.') // textContent-set, no markup }) it('empty input keeps submit disabled and submit("") rejects without a fetch or a key', async () => { const fetchImpl = mintFetch({ token: 'X' }) const createKey = vi.fn(async () => fakeKey(FAKE_JKT)) const login = mountStagingLogin(root, cfg, { fetchImpl, createKey }) expect((root.querySelector('button') as HTMLButtonElement).disabled).toBe(true) await expect(login.submit('')).resolves.toBe('rejected') expect(fetchImpl).not.toHaveBeenCalled() expect(createKey).not.toHaveBeenCalled() }) it('network error → "rejected" with a retry message', async () => { const fetchImpl = vi.fn(async () => { throw new Error('offline') }) as unknown as typeof fetch const login = mountStagingLogin(root, cfg, { fetchImpl, createKey: async () => fakeKey(FAKE_JKT) }) await expect(login.submit('s3cret')).resolves.toBe('rejected') expect((root.querySelector('.login-error') as HTMLElement).textContent).toBe( 'Network error — please retry.', ) }) it('malformed mint response (no token) → "rejected", no onSuccess', async () => { const onSuccess = vi.fn() const login = mountStagingLogin(root, cfg, { fetchImpl: mintFetch({ nope: true }), onSuccess, createKey: async () => fakeKey(FAKE_JKT), }) await expect(login.submit('s3cret')).resolves.toBe('rejected') expect(onSuccess).not.toHaveBeenCalled() expect((root.querySelector('.login-error') as HTMLElement).textContent).toBe( 'Malformed response from the relay.', ) }) it('crypto failure (key gen throws) → "rejected", no fetch issued', async () => { const fetchImpl = mintFetch({ token: 'X' }) const login = mountStagingLogin(root, cfg, { fetchImpl, createKey: async () => { throw new Error('no webcrypto') }, }) await expect(login.submit('s3cret')).resolves.toBe('rejected') expect(fetchImpl).not.toHaveBeenCalled() expect((root.querySelector('.login-error') as HTMLElement).textContent).toBe( 'Secure crypto is unavailable in this browser.', ) }) it('does not persist the password to localStorage', async () => { const login = mountStagingLogin(root, cfg, { fetchImpl: mintFetch({ token: 'X' }), createKey: async () => fakeKey(FAKE_JKT), }) await login.submit('s3cret') expect(JSON.stringify(localStorage)).not.toContain('s3cret') }) })