This commit is contained in:
2025-12-04 11:54:42 +01:00
parent 9d5caa87f3
commit b7d5551ea7
223 changed files with 5473 additions and 885 deletions

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
/**
* Auth + caching behavior for RootLayout and Dashboard.
*
* These tests assert that:
* - RootLayout is marked dynamic so it re-evaluates cookies per request.
* - DashboardPage is also dynamic (no static caching of auth state).
*/
describe('RootLayout auth caching behavior', () => {
it('is configured as dynamic to avoid static auth caching', async () => {
const layoutModule = await import('../../../../apps/website/app/layout');
// Next.js dynamic routing flag
const dynamic = (layoutModule as any).dynamic;
expect(dynamic).toBe('force-dynamic');
});
});
describe('Dashboard auth caching behavior', () => {
it('is configured as dynamic to evaluate auth per request', async () => {
const dashboardModule = await import('../../../../apps/website/app/dashboard/page');
const dynamic = (dashboardModule as any).dynamic;
expect(dynamic).toBe('force-dynamic');
});
});

View File

@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const cookieStore = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
};
vi.mock('next/headers', () => ({
cookies: () => cookieStore,
}));
import { InMemoryAuthService } from '../../../../apps/website/lib/auth/InMemoryAuthService';
describe('InMemoryAuthService', () => {
beforeEach(() => {
cookieStore.get.mockReset();
cookieStore.set.mockReset();
cookieStore.delete.mockReset();
});
it('startIracingAuthRedirect returns redirectUrl with returnTo and state without touching cookies', async () => {
const service = new InMemoryAuthService();
const { redirectUrl, state } = await service.startIracingAuthRedirect('some');
expect(typeof state).toBe('string');
expect(state.length).toBeGreaterThan(0);
const url = new URL(redirectUrl, 'http://localhost');
expect(url.pathname).toBe('/auth/iracing/callback');
expect(url.searchParams.get('returnTo')).toBe('some');
expect(url.searchParams.get('state')).toBe(state);
expect(url.searchParams.get('code')).toBeTruthy();
expect(cookieStore.get).not.toHaveBeenCalled();
expect(cookieStore.set).not.toHaveBeenCalled();
expect(cookieStore.delete).not.toHaveBeenCalled();
});
it('loginWithIracingCallback returns deterministic demo session', async () => {
const service = new InMemoryAuthService();
const session = await service.loginWithIracingCallback({
code: 'dummy-code',
state: 'any-state',
});
expect(session.user.id).toBe('demo-user');
expect(session.user.primaryDriverId).toBeDefined();
expect(session.user.primaryDriverId).not.toBe('');
});
it('logout does not attempt to modify cookies directly', async () => {
const service = new InMemoryAuthService();
await service.logout();
expect(cookieStore.get).not.toHaveBeenCalled();
expect(cookieStore.set).not.toHaveBeenCalled();
expect(cookieStore.delete).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
describe('IracingAuthPage imports', () => {
it('does not import cookies or getAuthService', () => {
const filePath = path.resolve(
__dirname,
'../../../../apps/website/app/auth/iracing/page.tsx',
);
const source = fs.readFileSync(filePath, 'utf-8');
expect(source.includes("from 'next/headers'")).toBe(false);
expect(source.includes('cookies(')).toBe(false);
expect(source.includes('getAuthService')).toBe(false);
});
});

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const cookieStore = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
};
vi.mock('next/headers', () => {
return {
cookies: () => cookieStore,
};
});
import { GET as startGet } from '../../../../apps/website/app/auth/iracing/start/route';
import { GET as callbackGet } from '../../../../apps/website/app/auth/iracing/callback/route';
import { POST as logoutPost } from '../../../../apps/website/app/auth/logout/route';
describe('iRacing auth route handlers', () => {
beforeEach(() => {
cookieStore.get.mockReset();
cookieStore.set.mockReset();
cookieStore.delete.mockReset();
});
it('start route redirects to auth URL and sets state cookie', async () => {
const req = new Request('http://localhost/auth/iracing/start?returnTo=/dashboard');
const res = await startGet(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location') ?? '';
expect(location).toContain('/auth/iracing/callback');
expect(location).toContain('returnTo=%2Fdashboard');
expect(location).toMatch(/state=/);
expect(cookieStore.set).toHaveBeenCalled();
const [name] = cookieStore.set.mock.calls[0];
expect(name).toBe('gp_demo_auth_state');
});
it('callback route creates session cookie and redirects to returnTo', async () => {
cookieStore.get.mockImplementation((name: string) => {
if (name === 'gp_demo_auth_state') {
return { value: 'valid-state' };
}
return undefined;
});
const req = new Request(
'http://localhost/auth/iracing/callback?code=demo-code&state=valid-state&returnTo=/dashboard',
);
const res = await callbackGet(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location');
expect(location).toBe('http://localhost/dashboard');
expect(cookieStore.set).toHaveBeenCalled();
const [sessionName, sessionValue] = cookieStore.set.mock.calls[0];
expect(sessionName).toBe('gp_demo_session');
expect(typeof sessionValue).toBe('string');
expect(cookieStore.delete).toHaveBeenCalledWith('gp_demo_auth_state');
});
it('logout route deletes session cookie and redirects home using request origin', async () => {
const req = new Request('http://example.com/auth/logout', {
method: 'POST',
});
const res = await logoutPost(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location');
expect(location).toBe('http://example.com/');
expect(cookieStore.delete).toHaveBeenCalledWith('gp_demo_session');
});
});