/** * Auth e2e — exercises the migrated /api/auth endpoints through the real * JwtAuthGuard/OptionalJwtGuard AND the real cookie service against a temp * SQLite db. Only the authService (credential/MFA logic) + audit/notifications * are mocked; this proves the httpOnly trek_session cookie is set on login and * cleared on logout, that /me requires a session, and that /app-config is * optional-auth. */ import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest'; import request from 'supertest'; import cookieParser from 'cookie-parser'; import type { Server } from 'http'; import { Test } from '@nestjs/testing'; import { seedUser, sessionCookie } from './harness'; const { db } = vi.hoisted(() => { // eslint-disable-next-line @typescript-eslint/no-require-imports const Database = require('better-sqlite3'); const tmp = new Database(':memory:'); tmp.exec('PRAGMA journal_mode = WAL'); tmp.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, email TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'user', password_version INTEGER NOT NULL DEFAULT 0);`); return { db: tmp }; }); vi.mock('../../src/db/database', () => ({ db, closeDb: () => {}, reinitialize: () => {} })); vi.mock('../../src/services/auditLog', () => ({ writeAudit: vi.fn(), getClientIp: vi.fn(() => '1.2.3.4') })); vi.mock('../../src/services/notifications', () => ({ getAppUrl: () => 'https://x', sendPasswordResetEmail: vi.fn().mockResolvedValue({ delivered: true }) })); const { authSvc } = vi.hoisted(() => ({ authSvc: { getAppConfig: vi.fn(), demoLogin: vi.fn(), validateInviteToken: vi.fn(), registerUser: vi.fn(), loginUser: vi.fn(), requestPasswordReset: vi.fn(), resetPassword: vi.fn(), verifyMfaLogin: vi.fn(), getCurrentUser: vi.fn(), changePassword: vi.fn(), deleteAccount: vi.fn(), updateMapsKey: vi.fn(), updateApiKeys: vi.fn(), updateSettings: vi.fn(), getSettings: vi.fn(), saveAvatar: vi.fn(), deleteAvatar: vi.fn(), listUsers: vi.fn(), validateKeys: vi.fn(), getAppSettings: vi.fn(), updateAppSettings: vi.fn(), getTravelStats: vi.fn(), setupMfa: vi.fn(), enableMfa: vi.fn(), disableMfa: vi.fn(), listMcpTokens: vi.fn(), createMcpToken: vi.fn(), deleteMcpToken: vi.fn(), createWsToken: vi.fn(), createResourceToken: vi.fn(), }, })); vi.mock('../../src/services/authService', () => authSvc); import { AuthModule } from '../../src/nest/auth/auth.module'; import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter'; describe('Auth e2e (real auth guard + real cookie service + temp SQLite)', () => { let server: Server; let app: Awaited>; async function build() { const moduleRef = await Test.createTestingModule({ imports: [AuthModule] }).compile(); const nest = moduleRef.createNestApplication(); nest.use(cookieParser()); nest.useGlobalFilters(new TrekExceptionFilter()); await nest.init(); return nest; } beforeAll(async () => { seedUser(db as never, { id: 1, email: 'u@example.test' }); app = await build(); server = app.getHttpServer(); authSvc.getAppConfig.mockReturnValue({ version: '3' }); authSvc.loginUser.mockReturnValue({ token: 'jwt.token.value', user: { id: 1 } }); authSvc.getCurrentUser.mockReturnValue({ id: 1, email: 'u@example.test' }); }); beforeEach(() => vi.clearAllMocks()); afterAll(async () => { await app.close(); }); it('GET /app-config is optional-auth (200 without a cookie)', async () => { authSvc.getAppConfig.mockReturnValue({ version: '3' }); const res = await request(server).get('/api/auth/app-config'); expect(res.status).toBe(200); expect(res.body).toEqual({ version: '3' }); }); it('GET /me requires a session (401 without a cookie)', async () => { expect((await request(server).get('/api/auth/me')).status).toBe(401); }); it('GET /me returns the user with a valid session', async () => { authSvc.getCurrentUser.mockReturnValue({ id: 1, email: 'u@example.test' }); const res = await request(server).get('/api/auth/me').set('Cookie', sessionCookie(1)); expect(res.status).toBe(200); expect(res.body).toEqual({ user: { id: 1, email: 'u@example.test' } }); }); it('POST /login sets the httpOnly trek_session cookie', async () => { authSvc.loginUser.mockReturnValue({ token: 'jwt.token.value', user: { id: 1 } }); const res = await request(server).post('/api/auth/login').send({ email: 'u@example.test', password: 'pw' }); expect(res.status).toBe(200); expect(res.body).toEqual({ token: 'jwt.token.value', user: { id: 1 } }); const setCookie = res.headers['set-cookie'] as unknown as string[]; expect(setCookie.some((c) => c.startsWith('trek_session=') && /HttpOnly/i.test(c))).toBe(true); }, 10000); it('POST /logout clears the session cookie', async () => { const res = await request(server).post('/api/auth/logout'); expect(res.status).toBe(200); expect(res.body).toEqual({ success: true }); const setCookie = res.headers['set-cookie'] as unknown as string[]; expect(setCookie.some((c) => c.startsWith('trek_session='))).toBe(true); }); });