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);
|
|
});
|
|
});
|