Files
portal/app/tests/Frontend/router.spec.ts
T

137 lines
5.9 KiB
TypeScript
Raw Normal View History

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');
});
});