2026-05-08 19:59:43 +03:00
|
|
|
|
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
|
|
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
|
|
|
|
|
|
|
|
|
|
// Мокаем api/auth, чтобы router beforeEach guard не делал реальных HTTP-вызовов.
|
2026-05-13 01:55:06 +03:00
|
|
|
|
// `me()` возвращает AuthUser напрямую (см. resources/js/api/auth.ts:65-68).
|
2026-05-08 19:59:43 +03:00
|
|
|
|
vi.mock('../../resources/js/api/auth', () => ({
|
|
|
|
|
|
me: vi.fn(() => Promise.reject(new Error('not authenticated'))),
|
|
|
|
|
|
login: vi.fn(),
|
|
|
|
|
|
register: vi.fn(),
|
|
|
|
|
|
logout: vi.fn(),
|
|
|
|
|
|
}));
|
2026-05-08 16:59:00 +03:00
|
|
|
|
|
2026-05-08 19:59:43 +03:00
|
|
|
|
import { router } from '../../resources/js/router';
|
2026-05-13 01:55:06 +03:00
|
|
|
|
import { useAuthStore } from '../../resources/js/stores/auth';
|
2026-05-08 16:59:00 +03:00
|
|
|
|
|
|
|
|
|
|
describe('router/index.ts', () => {
|
2026-05-08 19:59:43 +03:00
|
|
|
|
beforeEach(() => {
|
|
|
|
|
|
setActivePinia(createPinia());
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('содержит маршрут /login с layout=auth + guestOnly', () => {
|
2026-05-08 16:59:00 +03:00
|
|
|
|
const loginRoute = router.getRoutes().find((r) => r.name === 'login');
|
|
|
|
|
|
expect(loginRoute).toBeDefined();
|
|
|
|
|
|
expect(loginRoute?.path).toBe('/login');
|
|
|
|
|
|
expect(loginRoute?.meta.layout).toBe('auth');
|
2026-05-08 19:59:43 +03:00
|
|
|
|
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');
|
|
|
|
|
|
});
|
2026-05-08 16:59:00 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-08 19:59:43 +03:00
|
|
|
|
it('гость, идущий на /dashboard без auth, редиректится на /login', async () => {
|
|
|
|
|
|
await router.push('/dashboard');
|
2026-05-08 16:59:00 +03:00
|
|
|
|
await router.isReady();
|
2026-05-08 19:59:43 +03:00
|
|
|
|
// beforeEach guard делает fetchMe (мок отвергает) → user=null → redirect /login.
|
2026-05-08 16:59:00 +03:00
|
|
|
|
expect(router.currentRoute.value.path).toBe('/login');
|
2026-05-08 19:59:43 +03:00
|
|
|
|
// Сохраняется ?redirect=/dashboard для возврата после login.
|
|
|
|
|
|
expect(router.currentRoute.value.query.redirect).toBe('/dashboard');
|
2026-05-08 16:59:00 +03:00
|
|
|
|
});
|
2026-05-13 01:55:06 +03:00
|
|
|
|
|
|
|
|
|
|
// ---------- 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');
|
|
|
|
|
|
});
|
2026-05-08 16:59:00 +03:00
|
|
|
|
});
|