Files
portal/app/tests/Frontend/LoginView.spec.ts
T
Дмитрий 80de6ecbbd fix/auth: косяк 05 — неподтверждённая почта зовёт подтвердить, а не «заблокирован»
При входе с email_verified_at=null (новый клиент не ввёл код из письма)
AuthController возвращает дружелюбное «Подтвердите почту — мы отправили код
на ...» с флагом email_not_confirmed, а не пугающее «Аккаунт заблокирован».
Реальная блокировка админом (verified + is_active=false) по-прежнему даёт
«Аккаунт заблокирован». Фронт по флагу уводит на /confirm-email с переносом
email в store (там кнопка «Отправить код повторно»).
TDD: Pest 2 кейса (неподтверждённый vs заблокирован), vitest redirect-кейс,
все GREEN. Проверено глазами на 8000.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:15:06 +03:00

132 lines
6.2 KiB
TypeScript

import { describe, it, expect, 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';
import LoginView from '../../resources/js/views/auth/LoginView.vue';
import { useAuthStore } from '../../resources/js/stores/auth';
// Smoke-тесты LoginView: монтируется внутри router-context, содержит
// все три ключевых элемента из v8_login.html секция #form-login —
// заголовок, кнопку «Войти», SSO Yandex 360.
const mountLoginView = async () => {
const pinia = createPinia();
setActivePinia(pinia);
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/login', name: 'login', component: LoginView },
{ path: '/register', name: 'register', component: { template: '<div>stub</div>' } },
{ path: '/forgot', name: 'forgot', component: { template: '<div>stub</div>' } },
],
});
await router.push('/login');
await router.isReady();
return mount(LoginView, {
global: {
plugins: [pinia, createVuetify(), router],
},
});
};
describe('LoginView.vue', () => {
it('монтируется без ошибок', async () => {
const wrapper = await mountLoginView();
expect(wrapper.exists()).toBe(true);
});
it('содержит заголовок «Вход в Лидерру»', async () => {
const wrapper = await mountLoginView();
expect(wrapper.text()).toContain('Вход в Лидерру');
});
it('содержит кнопку «Войти» и Yandex 360 SSO', async () => {
const wrapper = await mountLoginView();
expect(wrapper.text()).toContain('Войти');
expect(wrapper.text()).toContain('Yandex 360');
});
it('содержит поля email и пароль с правильным autocomplete', async () => {
const wrapper = await mountLoginView();
const emailInput = wrapper.find('input[type="email"]');
expect(emailInput.exists()).toBe(true);
expect(emailInput.attributes('autocomplete')).toBe('email');
const passwordInput = wrapper.find('input[type="password"]');
expect(passwordInput.exists()).toBe(true);
expect(passwordInput.attributes('autocomplete')).toBe('current-password');
});
it('содержит ссылки на регистрацию и сброс пароля', async () => {
const wrapper = await mountLoginView();
const links = wrapper.findAll('a').map((a) => a.text());
expect(links.some((t) => t.includes('Зарегистрируйтесь'))).toBe(true);
expect(links.some((t) => t.includes('Забыли пароль'))).toBe(true);
});
it('показывает lockout-alert когда auth.lockoutSeconds установлен (rate-limit 429)', async () => {
const wrapper = await mountLoginView();
// По умолчанию alert не отображается.
expect(wrapper.find('[data-testid="lockout-alert"]').exists()).toBe(false);
const auth = useAuthStore();
auth.lockoutSeconds = 600; // 10 минут
await wrapper.vm.$nextTick();
const alert = wrapper.find('[data-testid="lockout-alert"]');
expect(alert.exists()).toBe(true);
// 600 / 60 = 10 мин (округление вверх).
expect(alert.text()).toContain('10 мин');
expect(alert.text()).toContain('Слишком много попыток');
});
it('косяк 05: при email_not_confirmed уводит на /confirm-email и кладёт email в store', async () => {
const pinia = createPinia();
setActivePinia(pinia);
const router = createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/login', name: 'login', component: LoginView },
{ path: '/register', name: 'register', component: { template: '<div>stub</div>' } },
{ path: '/forgot', name: 'forgot', component: { template: '<div>stub</div>' } },
{ path: '/confirm-email', name: 'confirm-email', component: { template: '<div>confirm</div>' } },
],
});
await router.push('/login');
await router.isReady();
const wrapper = mount(LoginView, { global: { plugins: [pinia, createVuetify(), router] } });
const auth = useAuthStore();
auth.login = vi.fn().mockRejectedValue({ response: { data: { email_not_confirmed: true } } });
await wrapper.find('input[type="email"]').setValue('unconfirmed@example.ru');
await wrapper.find('input[type="password"]').setValue('password12');
await wrapper.find('form').trigger('submit.prevent');
await flushPromises();
expect(router.currentRoute.value.path).toBe('/confirm-email');
expect(auth.pendingEmail).toBe('unconfirmed@example.ru');
});
it('A1: SSO Yandex 360 — кнопка disabled до подключения Б-1', async () => {
const wrapper = await mountLoginView();
const ssoBtn = wrapper.findAll('button').find((b) => b.text().includes('Yandex 360'));
expect(ssoBtn).toBeDefined();
expect(ssoBtn!.classes()).toContain('v-btn--disabled');
});
it('A9: переключатель видимости пароля имеет accessible-name и работает', async () => {
const wrapper = await mountLoginView();
const toggle = wrapper.find('[aria-label="Показать пароль"]');
expect(toggle.exists()).toBe(true);
expect(toggle.attributes('role')).toBe('button');
await toggle.trigger('click');
expect(wrapper.find('[aria-label="Скрыть пароль"]').exists()).toBe(true);
// keyboard activation (Enter) — toggle back
await wrapper.find('[aria-label="Скрыть пароль"]').trigger('keydown', { key: 'Enter' });
expect(wrapper.find('[aria-label="Показать пароль"]').exists()).toBe(true);
});
});