Files
portal/app/tests/Frontend/UseRecoveryCodeView.spec.ts
T

106 lines
3.8 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
vi.mock('../../resources/js/api/auth', () => ({
login: vi.fn(),
register: vi.fn(),
me: vi.fn(),
logout: vi.fn(),
verifyTwoFactor: vi.fn(),
forgotPassword: vi.fn(),
resetPassword: vi.fn(),
useRecoveryCode: vi.fn(),
}));
vi.mock('../../resources/js/api/client', () => ({
extractRateLimitRetry: vi.fn(() => null),
extractValidationErrors: vi.fn(() => null),
extractErrorMessage: vi.fn(() => 'Ошибка'),
apiClient: {},
ensureCsrfCookie: vi.fn(),
}));
import * as authApi from '../../resources/js/api/auth';
import { useAuthStore } from '../../resources/js/stores/auth';
import UseRecoveryCodeView from '../../resources/js/views/auth/UseRecoveryCodeView.vue';
const mountView = async () => {
const pinia = createPinia();
setActivePinia(pinia);
// Симулируем pending-2FA state — иначе onMounted редиректит на /login.
const auth = useAuthStore();
auth.requires2fa = true;
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/recovery-use', name: 'recovery-use', component: UseRecoveryCodeView },
{ path: '/login', name: 'login', component: { template: '<div>stub</div>' } },
{ path: '/2fa', name: '2fa', component: { template: '<div>stub</div>' } },
{ path: '/dashboard', name: 'dashboard', component: { template: '<div>stub</div>' } },
],
});
await router.push('/recovery-use');
await router.isReady();
return mount(UseRecoveryCodeView, {
global: { plugins: [pinia, createVuetify(), router] },
});
};
describe('UseRecoveryCodeView.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('монтируется и содержит заголовок «Резервный код»', async () => {
const wrapper = await mountView();
expect(wrapper.text()).toContain('Резервный код');
});
it('содержит input с label XXXX-XXXX и autocomplete=one-time-code', async () => {
const wrapper = await mountView();
const input = wrapper.find('input');
expect(input.exists()).toBe(true);
expect(input.attributes('autocomplete')).toBe('one-time-code');
});
it('успешный submit вызывает auth.useRecoveryCode и сохраняет remaining в sessionStorage', async () => {
vi.mocked(authApi.useRecoveryCode).mockResolvedValue({
user: {
id: 1,
email: 'r@example.ru',
first_name: 'R',
last_name: 'C',
tenant_id: 1,
totp_enabled: true,
last_login_at: null,
},
requires_2fa: false,
recovery_codes_remaining: 3,
});
const wrapper = await mountView();
await wrapper.find('input').setValue('ABCD-1234');
await wrapper.find('form').trigger('submit.prevent');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(authApi.useRecoveryCode).toHaveBeenCalledWith('ABCD-1234');
expect(window.sessionStorage.getItem('recovery_codes_remaining')).toBe('3');
});
it('при 429 показывает lockout-alert', async () => {
const wrapper = await mountView();
const auth = useAuthStore();
auth.lockoutSeconds = 600;
await wrapper.vm.$nextTick();
const alert = wrapper.find('[data-testid="lockout-alert"]');
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain('10 мин');
});
});