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

113 lines
4.6 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';
import { createRouter, createMemoryHistory } from 'vue-router';
// Мокаем api/auth до import'а ForgotPasswordView (внутри useAuthStore).
vi.mock('../../resources/js/api/auth', () => ({
login: vi.fn(),
register: vi.fn(),
me: vi.fn(),
logout: vi.fn(),
verifyTwoFactor: vi.fn(),
forgotPassword: 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 ForgotPasswordView from '../../resources/js/views/auth/ForgotPasswordView.vue';
const mountForgot = async () => {
const pinia = createPinia();
setActivePinia(pinia);
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/forgot', component: ForgotPasswordView },
{ path: '/login', name: 'login', component: { template: '<div>stub</div>' } },
],
});
await router.push('/forgot');
await router.isReady();
return mount(ForgotPasswordView, {
global: { plugins: [pinia, createVuetify(), router] },
});
};
describe('ForgotPasswordView.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('монтируется и содержит заголовок «Сброс пароля»', async () => {
const wrapper = await mountForgot();
expect(wrapper.text()).toContain('Сброс пароля');
});
it('содержит rate-limit alert с лимитом 5 попыток / 15 минут', async () => {
const wrapper = await mountForgot();
const text = wrapper.text();
expect(text).toContain('5 попыток в 15 минут');
});
it('содержит email-input и кнопку «Отправить ссылку»', async () => {
const wrapper = await mountForgot();
expect(wrapper.find('input[type="email"]').exists()).toBe(true);
expect(wrapper.text()).toContain('Отправить ссылку');
});
it('после успешного submit показывает success-state и скрывает форму', async () => {
vi.mocked(authApi.forgotPassword).mockResolvedValue({
message: 'Если такой email зарегистрирован — мы отправили ссылку для сброса пароля.',
});
const wrapper = await mountForgot();
const input = wrapper.find('input[type="email"]');
await input.setValue('user@example.ru');
await wrapper.find('form').trigger('submit.prevent');
// Дожидаемся async submit.
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="forgot-success"]').exists()).toBe(true);
// Форма скрыта после submit.
expect(wrapper.find('form').exists()).toBe(false);
expect(authApi.forgotPassword).toHaveBeenCalledWith('user@example.ru');
});
it('при 429 показывает lockout-alert через auth.lockoutSeconds', async () => {
const wrapper = await mountForgot();
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 мин');
});
it('A5: при не-валидационной ошибке (500/network) показывает generic fallback', async () => {
// forgotPassword отклоняется обычной ошибкой; extractValidationErrors и
// extractRateLimitRetry замоканы → null (см. vi.mock в шапке файла).
vi.mocked(authApi.forgotPassword).mockRejectedValue(new Error('Network Error'));
const wrapper = await mountForgot();
await wrapper.find('input[type="email"]').setValue('user@example.ru');
await wrapper.find('form').trigger('submit.prevent');
await flushPromises();
const messages = wrapper.findAll('.v-messages__message').map((m) => m.text());
expect(messages.join(' ')).toContain('Произошла ошибка. Попробуйте позже.');
});
});