Spaces:
Sleeping
Sleeping
File size: 6,543 Bytes
1f5ea39 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | /**
* 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<ReturnType<typeof build>>;
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 /login with remember_me sets a persistent cookie (Max-Age present)', async () => {
authSvc.loginUser.mockReturnValue({ token: 'jwt.token.value', user: { id: 1 }, remember: true });
const res = await request(server).post('/api/auth/login').send({ email: 'u@example.test', password: 'pw', remember_me: true });
expect(res.status).toBe(200);
const setCookie = res.headers['set-cookie'] as unknown as string[];
const cookie = setCookie.find((c) => c.startsWith('trek_session='))!;
expect(cookie).toMatch(/Max-Age=\d+/i);
// 30d default — well above the 24h (86400s) non-remember window.
const maxAge = Number(/Max-Age=(\d+)/i.exec(cookie)?.[1]);
expect(maxAge).toBeGreaterThan(86_400);
}, 10000);
it('POST /login without remember_me sets a session cookie (no Max-Age)', async () => {
authSvc.loginUser.mockReturnValue({ token: 'jwt.token.value', user: { id: 1 }, remember: false });
const res = await request(server).post('/api/auth/login').send({ email: 'u@example.test', password: 'pw' });
expect(res.status).toBe(200);
const setCookie = res.headers['set-cookie'] as unknown as string[];
const cookie = setCookie.find((c) => c.startsWith('trek_session='))!;
expect(cookie).not.toMatch(/Max-Age/i);
expect(cookie).not.toMatch(/Expires/i);
}, 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);
});
});
|