c09bff3799
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).
176 lines
6.8 KiB
TypeScript
176 lines
6.8 KiB
TypeScript
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);
|
||
});
|
||
});
|