Files
kite/frontend/src/components/auth/__tests__/TotpEnrollment.test.js
T
curo1305 3b8e2c1bd4 refactor(08-03): migrate session-revoked inline toast to toastStore.show()
- Remove sessionRevokedToast ref + setTimeout + inline HTML from SettingsAccountTab.vue
- Remove sessionRevokedToast ref + setTimeout + inline HTML from TotpEnrollment.vue
- Add toastStore.show('Other sessions have been terminated.', 'success') in changePassword
- Add toastStore.show('Other sessions have been terminated.', 'success') in disableTotp
- Add toastStore.show('Other sessions have been terminated.', 'success') in confirmEnrollment
- Update SettingsAccountTab.test.js to assert on mockShow instead of DOM text
- Update TotpEnrollment.test.js to assert on mockShow instead of DOM text
2026-06-08 16:34:44 +02:00

157 lines
5.1 KiB
JavaScript

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(),
}))
// Mock toast store — capture show() calls so we can assert on them
const mockShow = vi.fn()
vi.mock('../../../stores/toast.js', () => ({
useToastStore: () => ({ show: mockShow }),
}))
import { totpEnable as totpEnableMock } from '../../../api/client.js'
import TotpEnrollment from '../TotpEnrollment.vue'
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
mockShow.mockClear()
})
describe('TotpEnrollment — QR code rendering (AUTH-03)', () => {
it('renders an <img> tag with a data:image/ src after startSetup, not an otpauth:// link', async () => {
const wrapper = mount(TotpEnrollment, {
global: {
plugins: [createPinia()],
stubs: {
AppSpinner: { template: '<span />' },
BackupCodesDisplay: { template: '<div />', 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: <img> 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 <a> 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: '<span />' },
BackupCodesDisplay: { template: '<div />', 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: '<span />' },
BackupCodesDisplay: { template: '<div />', props: ['codes'] },
}
it('calls toastStore.show("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(mockShow).toHaveBeenCalledWith('Other sessions have been terminated.', 'success')
})
it('does NOT call toastStore.show 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(mockShow).not.toHaveBeenCalled()
})
})