Files
portal/app/tests/Frontend/router.spec.ts
T
Дмитрий f2627e4d3e test(router): Q.DEFER.003 sub-C — 5 integration tests for guard branches
Coverage uplift router/index.ts от 33% Stmts / 7% Funcs к ~85% Funcs:
- authenticated /login (guestOnly) → /dashboard redirect
- authenticated /dashboard passes requiresAuth
- /no-such-path → 404 catch-all
- /admin → /admin/tenants redirect
- /reset/:token param exposure

Refactored vi.mock me() для conditional resolve/reject per test.
2026-05-13 01:55:06 +03:00

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