import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { PrismaService } from '../src/prisma/prisma.service'; import { createTestApp } from './utils/create-test-app'; import { AccessTokenBody, extractCookie, login } from './utils/api'; describe('Auth (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const email = `auth-e2e-${Date.now()}@example.com`; const password = 'SuperSecret123!'; let userId: string; beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const passwordHash = await bcrypt.hash(password, 12); const user = await prisma.user.create({ data: { email, passwordHash, name: 'Auth E2E User', legacyRole: 'ADMIN', role: { connect: { key: 'ADMIN' } }, }, }); userId = user.id; }); afterAll(async () => { await prisma.refreshToken.deleteMany({ where: { userId } }); await prisma.auditLog.deleteMany({ where: { userId } }); await prisma.user.delete({ where: { id: userId } }); await app.close(); }); it('rejects login with wrong password', () => { return request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email, password: 'wrong-password' }) .expect(401); }); it('logs in, returns an access token, and sets an httpOnly refresh-token cookie', async () => { const tokens = await login(app, email, password); expect(tokens.accessToken).toEqual(expect.any(String)); expect(tokens.refreshTokenCookie).toMatch(/^refreshToken=/); }); it('rejects a protected route without a token', () => { return request(app.getHttpServer()).get('/api/v1/users').expect(401); }); it('accepts a protected route with a valid access token', async () => { const tokens = await login(app, email, password); await request(app.getHttpServer()) .get('/api/v1/users') .set('Authorization', `Bearer ${tokens.accessToken}`) .expect(200); }); it('rejects refresh when no refresh-token cookie is present', () => { return request(app.getHttpServer()) .post('/api/v1/auth/refresh') .expect(401); }); it('rotates the refresh-token cookie and rejects reuse of the old one', async () => { const tokens = await login(app, email, password); const refreshResponse = await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .set('Cookie', tokens.refreshTokenCookie) .expect(200); const rotatedCookie = extractCookie(refreshResponse, 'refreshToken'); expect(rotatedCookie).not.toEqual(tokens.refreshTokenCookie); expect((refreshResponse.body as AccessTokenBody).accessToken).toEqual( expect.any(String), ); await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .set('Cookie', tokens.refreshTokenCookie) .expect(401); }); it('logs out and invalidates the refresh-token cookie', async () => { const tokens = await login(app, email, password); await request(app.getHttpServer()) .post('/api/v1/auth/logout') .set('Cookie', tokens.refreshTokenCookie) .expect(204); await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .set('Cookie', tokens.refreshTokenCookie) .expect(401); }); });