3 positive + 2 negative Vitest tests covering CR-01/02/03 toast UX: - SettingsAccountTab: toast appears/hidden after changePassword (CR-01) - SettingsAccountTab: toast appears after disableTotp (CR-03) - TotpEnrollment: inline alert appears/hidden after enable_totp (CR-02) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209 lines
7.0 KiB
JavaScript
209 lines
7.0 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
|
|
|
// Mock auth store before any imports
|
|
vi.mock('../../../stores/auth.js', () => ({
|
|
useAuthStore: vi.fn(),
|
|
}))
|
|
|
|
// Mock api/client to avoid HTTP calls
|
|
vi.mock('../../../api/client.js', () => ({
|
|
changePassword: vi.fn(),
|
|
totpDisable: vi.fn(),
|
|
}))
|
|
|
|
import { useAuthStore } from '../../../stores/auth.js'
|
|
import { changePassword as changePasswordMock, totpDisable as totpDisableMock } from '../../../api/client.js'
|
|
import SettingsAccountTab from '../SettingsAccountTab.vue'
|
|
|
|
// Minimal router — required because the component calls useRouter() in script setup
|
|
const testRouter = createRouter({
|
|
history: createMemoryHistory(),
|
|
routes: [
|
|
{ path: '/', component: { template: '<div />' } },
|
|
{ path: '/login', component: { template: '<div />' } },
|
|
{ path: '/settings', component: { template: '<div />' } },
|
|
],
|
|
})
|
|
|
|
const globalStubs = {
|
|
TotpEnrollment: { template: '<div data-stub="TotpEnrollment" />' },
|
|
ConfirmBlock: { template: '<div data-stub="ConfirmBlock" />', props: ['message', 'confirmLabel', 'cancelLabel'] },
|
|
PasswordStrengthBar: { template: '<div data-stub="PasswordStrengthBar" />', props: ['password'] },
|
|
AppSpinner: { template: '<span />' },
|
|
}
|
|
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
// ─── GAP 1 & 2: Sessions revoked toast (CR-01, CR-03) ──────────────────────
|
|
|
|
describe('SettingsAccountTab — sessions revoked toast (CR-01, CR-03)', () => {
|
|
// Stub for ConfirmBlock that renders a button triggering @confirmed emission
|
|
const ConfirmBlockEmitter = {
|
|
name: 'ConfirmBlock',
|
|
template: '<div data-stub="ConfirmBlock"><button data-action="confirm" @click="$emit(\'confirmed\')">confirm</button></div>',
|
|
props: ['message', 'confirmLabel', 'cancelLabel'],
|
|
emits: ['confirmed', 'cancelled'],
|
|
}
|
|
|
|
it('shows "Other sessions have been terminated." toast after changePassword returns sessions_revoked > 0 (CR-01)', async () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
vi.mocked(changePasswordMock).mockResolvedValueOnce({
|
|
message: 'Password updated',
|
|
sessions_revoked: 2,
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: { ...globalStubs, ConfirmBlock: ConfirmBlockEmitter },
|
|
},
|
|
})
|
|
|
|
// Submit the change-password form to invoke changePassword()
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Other sessions have been terminated.')
|
|
})
|
|
|
|
it('does NOT show toast after changePassword when sessions_revoked is 0 (CR-01 negative)', async () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
vi.mocked(changePasswordMock).mockResolvedValueOnce({
|
|
message: 'Password updated',
|
|
sessions_revoked: 0,
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: { ...globalStubs, ConfirmBlock: ConfirmBlockEmitter },
|
|
},
|
|
})
|
|
|
|
await wrapper.find('form').trigger('submit')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).not.toContain('Other sessions have been terminated.')
|
|
})
|
|
|
|
it('shows "Other sessions have been terminated." toast after disableTotp returns sessions_revoked > 0 (CR-03)', async () => {
|
|
const user = { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: true }
|
|
useAuthStore.mockReturnValue({
|
|
user,
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
vi.mocked(totpDisableMock).mockResolvedValueOnce({
|
|
message: 'TOTP disabled',
|
|
sessions_revoked: 1,
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: { ...globalStubs, ConfirmBlock: ConfirmBlockEmitter },
|
|
},
|
|
})
|
|
|
|
// Click "Disable 2FA" to reveal the ConfirmBlock
|
|
const disableBtn = wrapper.findAll('button').find(b => b.text().includes('Disable 2FA'))
|
|
await disableBtn.trigger('click')
|
|
|
|
// Click the confirm button rendered by the ConfirmBlockEmitter stub
|
|
await wrapper.find('[data-action="confirm"]').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.text()).toContain('Other sessions have been terminated.')
|
|
})
|
|
})
|
|
|
|
describe('SettingsAccountTab — four section headings (AUTH-03, AUTH-04)', () => {
|
|
it('renders all 4 required section headings when totp_enabled is false', () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
|
|
const text = wrapper.text()
|
|
expect(text).toContain('Account information')
|
|
expect(text).toContain('Two-factor authentication')
|
|
expect(text).toContain('Change password')
|
|
expect(text).toContain('Sessions')
|
|
})
|
|
|
|
it('renders TotpEnrollment stub when totp_enabled is false', () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: false },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.find('[data-stub="TotpEnrollment"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows "Enabled" status and "Disable 2FA" button when totp_enabled is true', () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: true },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
|
|
// "Enabled" text must appear (inside the 2FA section)
|
|
expect(wrapper.text()).toContain('Enabled')
|
|
|
|
// "Disable 2FA" button must be present
|
|
const buttons = wrapper.findAll('button')
|
|
const disableBtn = buttons.find(b => b.text().includes('Disable 2FA'))
|
|
expect(disableBtn).toBeDefined()
|
|
})
|
|
|
|
it('does NOT render TotpEnrollment stub when totp_enabled is true', () => {
|
|
useAuthStore.mockReturnValue({
|
|
user: { email: 'test@example.com', handle: 'testuser', role: 'user', totp_enabled: true },
|
|
logoutAll: vi.fn(),
|
|
})
|
|
|
|
const wrapper = mount(SettingsAccountTab, {
|
|
global: {
|
|
plugins: [createPinia(), testRouter],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.find('[data-stub="TotpEnrollment"]').exists()).toBe(false)
|
|
})
|
|
})
|