import { describe, it, expect, vi, beforeEach } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' // Mock qrcode before any imports — avoids canvas rendering in happy-dom vi.mock('qrcode', () => ({ default: { toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,fakeqr'), }, })) // Mock API client vi.mock('../../../api/client.js', () => ({ totpSetup: vi.fn().mockResolvedValue({ provisioning_uri: 'otpauth://totp/DocuVault:test@example.com?secret=JBSWY3DPEHPK3PXP', secret: 'JBSWY3DPEHPK3PXP', }), totpEnable: vi.fn(), })) import { totpEnable as totpEnableMock } from '../../../api/client.js' import TotpEnrollment from '../TotpEnrollment.vue' beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() }) describe('TotpEnrollment — QR code rendering (AUTH-03)', () => { it('renders an tag with a data:image/ src after startSetup, not an otpauth:// link', async () => { const wrapper = mount(TotpEnrollment, { global: { plugins: [createPinia()], stubs: { AppSpinner: { template: '' }, BackupCodesDisplay: { template: '
', props: ['codes'] }, }, }, }) // Initial state — verify step not shown yet expect(wrapper.find('img').exists()).toBe(false) // Find and click the setup button const setupButton = wrapper.find('button') expect(setupButton.exists()).toBe(true) expect(setupButton.text()).toContain('Set up two-factor authentication') await setupButton.trigger('click') // Wait for all async operations (totpSetup + QRCode.toDataURL) await flushPromises() // Assert: must exist with a data:image/ src const img = wrapper.find('img') expect(img.exists()).toBe(true) expect(img.attributes('src')).toMatch(/^data:image\//) // Assert: no tag with href starting with otpauth:// (the old link-based approach) const links = wrapper.findAll('a') const otpauthLinks = links.filter(a => (a.attributes('href') || '').startsWith('otpauth://') ) expect(otpauthLinks).toHaveLength(0) }) it('displays the manual secret key after startSetup', async () => { const wrapper = mount(TotpEnrollment, { global: { plugins: [createPinia()], stubs: { AppSpinner: { template: '' }, BackupCodesDisplay: { template: '
', props: ['codes'] }, }, }, }) await wrapper.find('button').trigger('click') await flushPromises() // The secret JBSWY3DPEHPK3PXP must appear in the rendered output expect(wrapper.text()).toContain('JBSWY3DPEHPK3PXP') }) }) // ─── GAP 3: Sessions revoked inline alert (CR-02) ─────────────────────────── describe('TotpEnrollment — sessions revoked toast (CR-02)', () => { const mountStubs = { AppSpinner: { template: '' }, BackupCodesDisplay: { template: '
', props: ['codes'] }, } it('renders "Other sessions have been terminated." after totpEnable returns sessions_revoked > 0 (CR-02)', async () => { vi.mocked(totpEnableMock).mockResolvedValueOnce({ backup_codes: ['CODE1', 'CODE2'], sessions_revoked: 1, }) const wrapper = mount(TotpEnrollment, { global: { plugins: [createPinia()], stubs: mountStubs, }, }) // Move component from 'setup' step → 'verify' step by clicking the setup button await wrapper.find('button').trigger('click') await flushPromises() // Now in 'verify' step — find the code input and set a 6-digit value const codeInput = wrapper.find('input[inputmode="numeric"]') expect(codeInput.exists()).toBe(true) await codeInput.setValue('123456') // Click "Verify code" button to trigger confirmEnrollment() const verifyBtn = wrapper.findAll('button').find(b => b.text().includes('Verify code')) expect(verifyBtn).toBeDefined() await verifyBtn.trigger('click') await flushPromises() expect(wrapper.text()).toContain('Other sessions have been terminated.') }) it('does NOT render the alert when totpEnable returns sessions_revoked is 0 (CR-02 negative)', async () => { vi.mocked(totpEnableMock).mockResolvedValueOnce({ backup_codes: [], sessions_revoked: 0, }) const wrapper = mount(TotpEnrollment, { global: { plugins: [createPinia()], stubs: mountStubs, }, }) await wrapper.find('button').trigger('click') await flushPromises() const codeInput = wrapper.find('input[inputmode="numeric"]') await codeInput.setValue('123456') const verifyBtn = wrapper.findAll('button').find(b => b.text().includes('Verify code')) await verifyBtn.trigger('click') await flushPromises() expect(wrapper.text()).not.toContain('Other sessions have been terminated.') }) })