Files
portal/app/tests/Frontend/router.spec.ts
T
Дмитрий de066145d3 feat(import): маршрут /import + сайдбар + инструкция H9
- router/index.ts: добавлен маршрут /import (name=import, layout=app,
  requiresAuth=true, transition=ld-route-fadeup, devIndex=29)
- AppSidebar.vue: пункт «Импорт данных» (mdi-database-import-outline)
  добавлен в группу «Работа» следом за Дашборд
- router.spec.ts: TDD-кейс маршрута /import (layout=app, requiresAuth=true)
- docs/Как_перенести_данные_из_crm-bp-gr.md: инструкция H9 (4 шага + таблица ошибок)
- cspell-words.txt: добавлены формы глагола «замапить»

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

145 lines
6.3 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 { beforeEach, describe, it, expect, vi } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
// Мокаем api/auth, чтобы router beforeEach guard не делал реальных HTTP-вызовов.
// `me()` возвращает AuthUser напрямую (см. resources/js/api/auth.ts:65-68).
vi.mock('../../resources/js/api/auth', () => ({
me: vi.fn(() => Promise.reject(new Error('not authenticated'))),
login: vi.fn(),
register: vi.fn(),
logout: vi.fn(),
}));
import { router } from '../../resources/js/router';
import { useAuthStore } from '../../resources/js/stores/auth';
describe('router/index.ts', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('содержит маршрут /login с layout=auth + guestOnly', () => {
const loginRoute = router.getRoutes().find((r) => r.name === 'login');
expect(loginRoute).toBeDefined();
expect(loginRoute?.path).toBe('/login');
expect(loginRoute?.meta.layout).toBe('auth');
expect(loginRoute?.meta.guestOnly).toBe(true);
});
it('защищённые маршруты помечены requiresAuth=true', () => {
const protectedNames = ['dashboard', 'deals', 'kanban', 'billing', 'settings', 'reports'];
const routes = router.getRoutes();
protectedNames.forEach((name) => {
const route = routes.find((r) => r.name === name);
expect(route, `route ${name} not found`).toBeDefined();
expect(route?.meta.requiresAuth, `route ${name} should require auth`).toBe(true);
});
});
it('admin-маршруты помечены requiresAuth=true и layout=admin', () => {
const routes = router.getRoutes();
const adminTenants = routes.find((r) => r.name === 'admin-tenants');
expect(adminTenants?.meta.requiresAuth).toBe(true);
expect(adminTenants?.meta.layout).toBe('admin');
});
it('error-маршруты НЕ требуют auth', () => {
const routes = router.getRoutes();
const errorNames = ['forbidden', 'server-error', 'not-found'];
errorNames.forEach((name) => {
const route = routes.find((r) => r.name === name);
expect(route?.meta.requiresAuth).toBeUndefined();
expect(route?.meta.layout).toBe('error');
});
});
it('гость, идущий на /dashboard без auth, редиректится на /login', async () => {
await router.push('/dashboard');
await router.isReady();
// beforeEach guard делает fetchMe (мок отвергает) → user=null → redirect /login.
expect(router.currentRoute.value.path).toBe('/login');
// Сохраняется ?redirect=/dashboard для возврата после login.
expect(router.currentRoute.value.query.redirect).toBe('/dashboard');
});
// ---------- Integration tests (Q.DEFER.003 sub-C) ----------
// Покрывают actual navigation flows через beforeEach guard.
// NB: `authInitialized` (router/index.ts:281) — module-level флаг, после первого
// теста выше остаётся true. Тесты ниже устанавливают auth.user напрямую через
// store mutation (bypass fetchMe path).
it('authenticated user attempting /login (guestOnly) → redirected to /dashboard', async () => {
const auth = useAuthStore();
auth.user = {
id: 1,
email: 'test@demo.local',
first_name: 'Test',
last_name: 'User',
tenant_id: 1,
totp_enabled: false,
last_login_at: '2026-05-13T00:00:00Z',
} as unknown as ReturnType<typeof useAuthStore>['user'];
await router.push('/login');
await router.isReady();
expect(router.currentRoute.value.path).toBe('/dashboard');
});
it('authenticated user navigating to /dashboard passes requiresAuth guard', async () => {
const auth = useAuthStore();
auth.user = {
id: 1,
email: 'test@demo.local',
first_name: 'Test',
last_name: 'User',
tenant_id: 1,
totp_enabled: false,
last_login_at: '2026-05-13T00:00:00Z',
} as unknown as ReturnType<typeof useAuthStore>['user'];
await router.push('/dashboard');
await router.isReady();
expect(router.currentRoute.value.path).toBe('/dashboard');
expect(router.currentRoute.value.name).toBe('dashboard');
});
it('unknown path /no-such-path resolves to 404 ErrorView', async () => {
await router.push('/no-such-path-xyz');
await router.isReady();
expect(router.currentRoute.value.name).toBe('not-found');
expect(router.currentRoute.value.meta.errorCode).toBe('404');
expect(router.currentRoute.value.meta.layout).toBe('error');
});
it('/admin redirects to /admin/tenants', async () => {
const auth = useAuthStore();
auth.user = {
id: 1,
email: 'test@demo.local',
first_name: 'Test',
last_name: 'User',
tenant_id: 1,
totp_enabled: false,
last_login_at: '2026-05-13T00:00:00Z',
} as unknown as ReturnType<typeof useAuthStore>['user'];
await router.push('/admin');
await router.isReady();
expect(router.currentRoute.value.path).toBe('/admin/tenants');
expect(router.currentRoute.value.name).toBe('admin-tenants');
});
it('/reset/:token resolves with token param exposed', async () => {
// auth.user остаётся null (beforeEach создал свежий Pinia), guestOnly пропускает.
await router.push('/reset/abc123-token-xyz');
await router.isReady();
expect(router.currentRoute.value.name).toBe('reset-password');
expect(router.currentRoute.value.params.token).toBe('abc123-token-xyz');
});
it('маршрут /import зарегистрирован с layout app', () => {
const importRoute = router.getRoutes().find((r) => r.name === 'import');
expect(importRoute, 'route import not found').toBeDefined();
expect(importRoute?.path).toBe('/import');
expect(importRoute?.meta.layout).toBe('app');
expect(importRoute?.meta.requiresAuth).toBe(true);
});
});