/** * webauthnConfig.test.ts * * The RP-ID / allowed-origin resolver is the single highest-risk piece of the * passkey feature: a wrong RP ID permanently bricks every enrolled credential. * These tests pin the security-relevant rules — config wins over APP_URL, bare * IPs are rejected, localhost dev uses the browser (Vite) origin, and the * resolver NEVER reads request headers. */ const { settingsStore, appUrlRef } = vi.hoisted(() => ({ settingsStore: new Map(), appUrlRef: { value: '' }, })); vi.mock('../../../src/db/database', () => ({ db: { prepare: (_sql: string) => ({ get: (key: string) => { const v = settingsStore.get(key); return v === undefined ? undefined : { value: v }; }, }), }, })); vi.mock('../../../src/services/notifications', () => ({ getAppUrl: () => appUrlRef.value, })); import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { resolveWebauthnConfig, isPasskeyConfigured } from '../../../src/services/webauthnConfig'; beforeEach(() => { settingsStore.clear(); appUrlRef.value = ''; }); afterEach(() => { vi.unstubAllEnvs(); }); describe('resolveWebauthnConfig', () => { it('WAC-001: derives the RP ID and single origin from a real APP_URL domain', () => { appUrlRef.value = 'https://trek.example.org'; const cfg = resolveWebauthnConfig(); expect(cfg).not.toBeNull(); expect(cfg!.rpID).toBe('trek.example.org'); expect(cfg!.origins).toEqual(['https://trek.example.org']); expect(isPasskeyConfigured()).toBe(true); }); it('WAC-002: returns null for a bare-IP host (IPs are not valid RP IDs)', () => { appUrlRef.value = 'http://192.168.1.50:3001'; expect(resolveWebauthnConfig()).toBeNull(); expect(isPasskeyConfigured()).toBe(false); }); it('WAC-003: returns null when nothing is configured', () => { expect(resolveWebauthnConfig()).toBeNull(); expect(isPasskeyConfigured()).toBe(false); }); it('WAC-004: localhost dev uses the browser (Vite :5173) origin, not just the API port', () => { appUrlRef.value = 'http://localhost:3001'; const cfg = resolveWebauthnConfig(); expect(cfg!.rpID).toBe('localhost'); expect(cfg!.origins).toContain('http://localhost:5173'); expect(cfg!.origins).toContain('http://localhost:3001'); }); it('WAC-005: an explicit webauthn_rp_id app-setting overrides APP_URL', () => { appUrlRef.value = 'https://internal.example.org'; settingsStore.set('webauthn_rp_id', 'public.example.org'); settingsStore.set('webauthn_origins', 'https://public.example.org'); const cfg = resolveWebauthnConfig(); expect(cfg!.rpID).toBe('public.example.org'); expect(cfg!.origins).toEqual(['https://public.example.org']); }); it('WAC-006: webauthn_origins is parsed as a comma-separated, trimmed list', () => { settingsStore.set('webauthn_rp_id', 'example.org'); settingsStore.set('webauthn_origins', 'https://a.example.org , https://b.example.org/'); const cfg = resolveWebauthnConfig(); expect(cfg!.origins).toEqual(['https://a.example.org', 'https://b.example.org']); }); it('WAC-007: the WEBAUTHN_RP_ID env var takes priority', () => { vi.stubEnv('WEBAUTHN_RP_ID', 'env.example.org'); vi.stubEnv('WEBAUTHN_ORIGINS', 'https://env.example.org'); appUrlRef.value = 'https://ignored.example.org'; const cfg = resolveWebauthnConfig(); expect(cfg!.rpID).toBe('env.example.org'); expect(cfg!.origins).toEqual(['https://env.example.org']); }); it('WAC-008: a configured RP ID with no origins falls back to the APP_URL origin', () => { appUrlRef.value = 'https://trek.example.org'; settingsStore.set('webauthn_rp_id', 'trek.example.org'); const cfg = resolveWebauthnConfig(); expect(cfg!.origins).toEqual(['https://trek.example.org']); }); });