Files
portal/app/tests/Frontend/ResetPasswordView.spec.ts
T
Дмитрий 9c488122a1 phase2(reset-password): POST /api/auth/reset-password + ResetPasswordView + DB timezone fix
- AuthController::resetPassword через Password::reset() (callback пишет password_hash)
- ResetPasswordRequest: token + email + password (min 10 по ТЗ §22.4.1) + confirmed
- Rate-limit auth:reset:{sha256(token)[0..16]}|{ip} (5/15мин)
- ResetPasswordView для deep-link /reset/:token?email=...; pre-fill email из query; success → redirect /login через 3 сек
- Vue Router /reset/:token (guestOnly); web.php /reset SPA-path
- DB FIX: config/database.php pgsql.timezone=UTC — без него PG TIMESTAMPTZ +03 терялся при Carbon::parse и tokenExpired ошибочно срабатывал
- Pest +6 ResetPasswordTest (85/85 за 11.50с, 291 assertions)
- Vitest +7 (160/160 за 11.02с)
- Регресс: lint+type+format OK; build 784ms; story:build 21/28 за 30.74с; Pint+Stan passed
- CLAUDE.md v1.37→v1.38, реестр v1.46→v1.47

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 03:36:27 +03:00

107 lines
4.1 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 } 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(),
}));
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 ResetPasswordView from '../../resources/js/views/auth/ResetPasswordView.vue';
const mountReset = async (path = '/reset/abc-token-xyz?email=user@example.ru') => {
const pinia = createPinia();
setActivePinia(pinia);
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/reset/:token', name: 'reset-password', component: ResetPasswordView },
{ path: '/login', name: 'login', component: { template: '<div>stub</div>' } },
],
});
await router.push(path);
await router.isReady();
return mount(ResetPasswordView, {
global: { plugins: [pinia, createVuetify(), router] },
});
};
describe('ResetPasswordView.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
it('монтируется и содержит заголовок «Новый пароль»', async () => {
const wrapper = await mountReset();
expect(wrapper.text()).toContain('Новый пароль');
});
it('предзаполняет email из query-параметра', async () => {
const wrapper = await mountReset('/reset/some-token?email=preset@example.ru');
const emailInput = wrapper.find('input[type="email"]');
expect((emailInput.element as HTMLInputElement).value).toBe('preset@example.ru');
});
it('содержит поля пароля + подтверждения с autocomplete=new-password', async () => {
const wrapper = await mountReset();
const passwordInputs = wrapper.findAll('input[type="password"]');
expect(passwordInputs.length).toBe(2);
passwordInputs.forEach((input) => {
expect(input.attributes('autocomplete')).toBe('new-password');
});
});
it('после успешного submit показывает success-state и скрывает форму', async () => {
vi.mocked(authApi.resetPassword).mockResolvedValue({ message: 'Пароль успешно изменён.' });
const wrapper = await mountReset('/reset/valid-token?email=user@example.ru');
await wrapper.findAll('input[type="password"]')[0].setValue('new-strong-pass-1234');
await wrapper.findAll('input[type="password"]')[1].setValue('new-strong-pass-1234');
await wrapper.find('form').trigger('submit.prevent');
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
expect(wrapper.find('[data-testid="reset-success"]').exists()).toBe(true);
expect(wrapper.find('form').exists()).toBe(false);
expect(authApi.resetPassword).toHaveBeenCalledWith(
expect.objectContaining({
token: 'valid-token',
email: 'user@example.ru',
password: 'new-strong-pass-1234',
password_confirmation: 'new-strong-pass-1234',
}),
);
});
it('при 429 показывает lockout-alert через auth.lockoutSeconds', async () => {
const wrapper = await mountReset();
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 мин');
});
});