Files
portal/app/tests/Frontend/TwoFactorCard.spec.ts
T
Дмитрий c09bff3799 test(security): Q.DEFER.003 sub-B — TwoFactorCard 9 own-spec tests
Coverage uplift от 28% to 80%+: enable button visibility / disable button
visibility / chip status / setup wizard openSetup→confirm→codes / invalid
code error / disable flow valid+invalid password / closeSetup state reset.
vi.mock authApi для 3 endpoint'ов (init/confirm/disable).
2026-05-13 01:46:30 +03:00

176 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/auth', () => ({
twoFactorInit: vi.fn(),
twoFactorConfirm: vi.fn(),
twoFactorDisable: vi.fn(),
}));
import TwoFactorCard from '../../resources/js/components/settings/security/TwoFactorCard.vue';
import * as authApi from '../../resources/js/api/auth';
import { useAuthStore } from '../../resources/js/stores/auth';
const vuetify = createVuetify();
type CardVm = {
setupOpen: boolean;
setupStep: 'init' | 'confirm' | 'codes';
setupSecret: string;
setupQrUrl: string;
setupCode: string;
setupRecoveryCodes: string[];
setupError: string;
setupBusy: boolean;
disableOpen: boolean;
disablePassword: string;
disableError: string;
openSetup: () => Promise<void>;
confirmSetup: () => Promise<void>;
closeSetup: () => void;
confirmDisable: () => Promise<void>;
};
describe('TwoFactorCard (Q.DEFER.003 sub-B)', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
const factory = (totpEnabled: boolean) => {
// Set auth state BEFORE mount — иначе computed has2fa зафиксируется false
// на первом render и кнопка не появится reactive'но (Pinia store reactivity quirk,
// see Task 2 RecoveryCodesCard.spec.ts learning).
const auth = useAuthStore();
auth.user = {
id: 1,
email: 'test@demo.local',
first_name: 'Test',
last_name: 'User',
tenant_id: 1,
totp_enabled: totpEnabled,
last_login_at: null,
} as unknown as ReturnType<typeof useAuthStore>['user'];
const wrapper = mount(TwoFactorCard, {
global: { plugins: [vuetify], stubs: { VDialog: true } },
});
return wrapper;
};
it('shows «Включить 2FA» button when 2FA disabled', async () => {
const wrapper = factory(false);
await flushPromises();
const btn = wrapper.find('[data-testid="enable-2fa-btn"]');
expect(btn.exists()).toBe(true);
expect(btn.text()).toContain('Включить 2FA');
});
it('shows «Отключить 2FA» button when 2FA enabled', async () => {
const wrapper = factory(true);
await flushPromises();
const btn = wrapper.find('[data-testid="disable-2fa-btn"]');
expect(btn.exists()).toBe(true);
expect(btn.text()).toContain('Отключить 2FA');
});
it('shows «включена» chip when 2FA enabled', async () => {
const wrapper = factory(true);
await flushPromises();
expect(wrapper.text()).toContain('включена');
});
it('openSetup() initializes wizard and transitions init→confirm with QR secret', async () => {
(authApi.twoFactorInit as ReturnType<typeof vi.fn>).mockResolvedValue({
secret: 'JBSWY3DPEHPK3PXP',
qr_url: 'otpauth://totp/test',
});
const wrapper = factory(false);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
await vm.openSetup();
await flushPromises();
expect(vm.setupOpen).toBe(true);
expect(vm.setupStep).toBe('confirm');
expect(vm.setupSecret).toBe('JBSWY3DPEHPK3PXP');
expect(vm.setupQrUrl).toBe('otpauth://totp/test');
expect(authApi.twoFactorInit).toHaveBeenCalled();
});
it('confirmSetup() valid code → step=codes + 8 recovery codes + auth.totp_enabled=true', async () => {
const codes = ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8'];
(authApi.twoFactorConfirm as ReturnType<typeof vi.fn>).mockResolvedValue({
recovery_codes: codes,
});
const wrapper = factory(false);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
vm.setupCode = '123456';
await vm.confirmSetup();
await flushPromises();
expect(vm.setupStep).toBe('codes');
expect(vm.setupRecoveryCodes).toEqual(codes);
const auth = useAuthStore();
expect(auth.user?.totp_enabled).toBe(true);
expect(authApi.twoFactorConfirm).toHaveBeenCalledWith('123456');
});
it('confirmSetup() invalid code → setupError set, step stays confirm', async () => {
(authApi.twoFactorConfirm as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('422'));
const wrapper = factory(false);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
vm.setupStep = 'confirm';
vm.setupCode = '999999';
await vm.confirmSetup();
await flushPromises();
expect(vm.setupError).toBe('Неверный код. Проверьте время на устройстве.');
expect(vm.setupStep).toBe('confirm');
});
it('closeSetup() resets all setup state', async () => {
const wrapper = factory(false);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
vm.setupOpen = true;
vm.setupSecret = 'SECRET';
vm.setupQrUrl = 'url';
vm.setupCode = '123';
vm.closeSetup();
expect(vm.setupOpen).toBe(false);
expect(vm.setupSecret).toBe('');
expect(vm.setupQrUrl).toBe('');
expect(vm.setupCode).toBe('');
});
it('confirmDisable() valid password auth.totp_enabled=false + dialog closes', async () => {
(authApi.twoFactorDisable as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
const wrapper = factory(true);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
vm.disableOpen = true;
vm.disablePassword = 'correctPass';
await vm.confirmDisable();
await flushPromises();
const auth = useAuthStore();
expect(auth.user?.totp_enabled).toBe(false);
expect(vm.disableOpen).toBe(false);
expect(vm.disablePassword).toBe('');
expect(authApi.twoFactorDisable).toHaveBeenCalledWith('correctPass');
});
it('confirmDisable() invalid password disableError set, totp stays enabled', async () => {
(authApi.twoFactorDisable as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('401'));
const wrapper = factory(true);
await flushPromises();
const vm = wrapper.vm as unknown as CardVm;
vm.disablePassword = 'wrongPass';
await vm.confirmDisable();
await flushPromises();
expect(vm.disableError).toBe('Неверный пароль.');
const auth = useAuthStore();
expect(auth.user?.totp_enabled).toBe(true);
});
});