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', () => ({ twoFactorRegenerateRecoveryCodes: vi.fn(), })); import RecoveryCodesCard from '../../resources/js/components/settings/security/RecoveryCodesCard.vue'; import * as authApi from '../../resources/js/api/auth'; import { useAuthStore } from '../../resources/js/stores/auth'; const vuetify = createVuetify(); describe('RecoveryCodesCard (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). 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['user']; const wrapper = mount(RecoveryCodesCard, { global: { plugins: [vuetify], stubs: { VDialog: true } }, }); return wrapper; }; it('does NOT render button when 2FA disabled (auth.user.totp_enabled=false)', () => { const wrapper = factory(false); expect(wrapper.find('[data-testid="regen-codes-btn"]').exists()).toBe(false); }); it('renders button when 2FA enabled', async () => { const wrapper = factory(true); await flushPromises(); const btn = wrapper.find('[data-testid="regen-codes-btn"]'); expect(btn.exists()).toBe(true); expect(btn.text()).toContain('Перегенерировать резервные коды'); }); it('clicking regen-codes-btn opens dialog (sets regenOpen)', async () => { const wrapper = factory(true); await flushPromises(); await wrapper.find('[data-testid="regen-codes-btn"]').trigger('click'); // VDialog is stubbed — check internal state via component instance expect((wrapper.vm as unknown as { regenOpen: boolean }).regenOpen).toBe(true); }); it('confirmRegen() with valid password fetches 8 codes and clears password', async () => { const newCodes = [ 'AAAA-1111', 'BBBB-2222', 'CCCC-3333', 'DDDD-4444', 'EEEE-5555', 'FFFF-6666', 'GGGG-7777', 'HHHH-8888', ]; (authApi.twoFactorRegenerateRecoveryCodes as ReturnType).mockResolvedValue({ recovery_codes: newCodes, }); const wrapper = factory(true); await flushPromises(); const vm = wrapper.vm as unknown as { regenOpen: boolean; regenPassword: string; regenCodes: string[]; regenError: string; confirmRegen: () => Promise; }; vm.regenOpen = true; vm.regenPassword = 'correctPass'; await vm.confirmRegen(); await flushPromises(); expect(vm.regenCodes).toEqual(newCodes); expect(vm.regenPassword).toBe(''); expect(vm.regenError).toBe(''); expect(authApi.twoFactorRegenerateRecoveryCodes).toHaveBeenCalledWith('correctPass'); }); it('confirmRegen() with invalid password sets error and keeps regenCodes empty', async () => { (authApi.twoFactorRegenerateRecoveryCodes as ReturnType).mockRejectedValue(new Error('401')); const wrapper = factory(true); await flushPromises(); const vm = wrapper.vm as unknown as { regenPassword: string; regenCodes: string[]; regenError: string; confirmRegen: () => Promise; }; vm.regenPassword = 'wrongPass'; await vm.confirmRegen(); await flushPromises(); expect(vm.regenError).toBe('Неверный пароль.'); expect(vm.regenCodes).toEqual([]); }); it('closeRegen() clears codes and closes dialog', async () => { const wrapper = factory(true); await flushPromises(); const vm = wrapper.vm as unknown as { regenOpen: boolean; regenCodes: string[]; closeRegen: () => void; }; vm.regenOpen = true; vm.regenCodes = ['AAAA-1111']; vm.closeRegen(); expect(vm.regenOpen).toBe(false); expect(vm.regenCodes).toEqual([]); }); });