Files
portal/app/tests/Frontend/SettingsView.spec.ts
T
Дмитрий 73e64128dc phase2(2fa-setup): wizard init+confirm+disable+regenerate в SettingsView/SecurityTab
- TwoFactorSetupController (auth:sanctum): /api/2fa/{init,confirm,disable,regenerate-recovery-codes}
- init секрет в session (не в БД), QR-URL otpauth://; confirm активирует 2FA + 8 recovery codes
- disable/regenerate требуют password-confirmation
- User.casts: totp_secret => encrypted

Schema v8.7→v8.8: users.totp_secret VARCHAR(255) → TEXT (encrypted ~256 chars)
Migration fix: explicit ALTER TABLE webhook_dedup_keys ADD FK после DB::unprepared (PDO глотал FK на partitioned)
PartitionsCreateMonthsTest fix: DETACH PARTITION + DROP вместо DROP CASCADE

Frontend: SecurityTab реальная логика (setup wizard 3 шага, disable, regenerate dialogs)

- Pest +10 (101/101 за 13.37с, 364 assertions)
- Vitest 166/166
- CLAUDE.md v1.39→v1.40, реестр v1.48→v1.49, schema v8.7→v8.8

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

96 lines
4.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createPinia } from 'pinia';
import { createVuetify } from 'vuetify';
import SettingsView from '../../resources/js/views/SettingsView.vue';
describe('SettingsView.vue', () => {
const factory = () =>
mount(SettingsView, {
global: { plugins: [createPinia(), createVuetify()] },
});
it('монтируется и содержит заголовок «Настройки»', () => {
const wrapper = factory();
expect(wrapper.find('h1').text()).toBe('Настройки');
});
it('содержит ровно 8 nav-tabs', () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
expect(items.length).toBe(8);
});
it('содержит все 8 названий вкладок', () => {
const wrapper = factory();
const text = wrapper.text();
const labels = [
'Профиль',
'Безопасность',
'Проекты',
'Команда',
'API и Webhook',
'Интеграции',
'Тихие часы',
'Уведомления',
];
labels.forEach((l) => expect(text).toContain(l));
});
it('по умолчанию показывает вкладку «Профиль»', () => {
const wrapper = factory();
const text = wrapper.text();
// ProfileTab содержит «Полное имя» и поле email.
expect(text).toContain('Полное имя');
expect(text).toContain('Тайм-зона');
});
it('placeholder-вкладки показывают «В разработке»', async () => {
const wrapper = factory();
// Кликаем по «Проекты» — placeholder-вкладка.
const items = wrapper.findAll('.tabs-rail .v-list-item');
const projectsItem = items.find((i) => i.text().includes('Проекты'));
expect(projectsItem).toBeDefined();
await projectsItem!.trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('В разработке');
});
it('переключение на «Уведомления» показывает матрицу 8×3', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const notifItem = items.find((i) => i.text().includes('Уведомления'));
await notifItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('События × каналы');
// 8 типов событий из матрицы.
['Новый лид', 'Дубликат', 'Низкий баланс', 'Срок напоминания', 'Webhook упал'].forEach((e) =>
expect(text).toContain(e),
);
});
it('переключение на «Безопасность» показывает 2FA и сессии', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const secItem = items.find((i) => i.text().includes('Безопасность'));
await secItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('Двухфакторная авторизация');
expect(text).toContain('Активные сессии');
});
it('переключение на «API и Webhook» показывает API-ключ и signing secret', async () => {
const wrapper = factory();
const items = wrapper.findAll('.tabs-rail .v-list-item');
const apiItem = items.find((i) => i.text().includes('API'));
await apiItem!.trigger('click');
await wrapper.vm.$nextTick();
const text = wrapper.text();
expect(text).toContain('API-ключ');
expect(text).toContain('Signing secret');
expect(text).toContain('HMAC');
});
});