80de6ecbbd
При входе с 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>
132 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|