59299d3c2b
- axios@^1.16 + pinia@^3.0 (--legacy-peer-deps). - api/client.ts: axios с withCredentials+withXSRFToken (Sanctum SPA auto-XSRF). ensureCsrfCookie() + extractValidationErrors/Message helpers. - api/auth.ts: типизированные login/register/me/logout с AuthUser interface. - stores/auth.ts: Pinia composition-store (user/loading/requires2fa + isAuthenticated computed + login/register/fetchMe/logout actions). logout() catch-swallow - UI всегда выходит локально. - LoginView/RegisterView: useAuthStore интеграция, real POST через store, errors из 422 на v-text-fields, redirect на /dashboard или /2fa, :loading на btn'ах. - Auth-guard в router.beforeEach: meta.requiresAuth на 10 routes (6 app + 4 admin), meta.guestOnly на login/register/forgot. При первом переходе fetchMe() restore-session. Unauth → /login?redirect=<original>. - / redirect → /dashboard (auth-guard перехватит если не залогинен). - Pinia в app.ts через app.use(createPinia()). - cspell-words.txt: мокаем. Vitest +10 (всего 139/139 за 10.11s): - auth-store 7 (initial state + login success/reject + register + fetchMe success/401 + logout swallow). - router 5 переписан (login.guestOnly + 6 protected + admin layout + 3 error без auth + unauth /dashboard → /login?redirect). - LoginView/RegisterView/router тесты получили createPinia в plugins. - vi.mock api/auth в router+auth-store specs. Регресс: lint+type+format OK; vitest 139/139; vite build (main app-chunk 105→153.64 KB +axios+pinia+auth gzipped 54.54 KB) 806ms; story:build 21/28 за 31.73s; Pest 61/61 за 5.86s. CLAUDE.md v1.32->v1.33, реестр Открытых_вопросов v1.41->v1.42. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
63 lines
2.7 KiB
TypeScript
63 lines
2.7 KiB
TypeScript
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
||
import { createPinia, setActivePinia } from 'pinia';
|
||
|
||
// Мокаем api/auth, чтобы router beforeEach guard не делал реальных HTTP-вызовов.
|
||
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';
|
||
|
||
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');
|
||
});
|
||
});
|