import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import request from 'supertest'; import { App } from 'supertest/types'; import { AppModule } from './../src/app.module'; import { StructuredLogger } from './../src/common/logging/structured-logger.service'; import { configureApp } from './../src/main'; import { PrismaService } from './../src/prisma/prisma.service'; import { AuthService } from './../src/auth/auth.service'; type Authentication = { user: { email: string }; accessToken: string }; type Problem = { code: string }; describe('Authentication (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); configureApp(app); await app.init(); await app.get(PrismaService).session.deleteMany(); await app.get(PrismaService).tbankConnection.deleteMany(); await app.get(PrismaService).user.deleteMany(); }); afterEach(() => app.close()); it('registers, guards profile, rotates refresh and logs out', async () => { const agent = request.agent(app.getHttpServer()); const registration = await agent .post('/api/v1/auth/register') .send({ email: ' User@Example.com ', password: 'a secure password' }) .expect(201); const authentication = registration.body as Authentication; expect(authentication.user.email).toBe('user@example.com'); const accessToken = authentication.accessToken; await request(app.getHttpServer()).get('/api/v1/auth/me').expect(401); await agent .get('/api/v1/auth/me') .set('Authorization', `Bearer ${accessToken}`) .expect(200) .expect(({ body }: { body: unknown }) => expect((body as { email: string }).email).toBe('user@example.com'), ); const firstCookie = registration.headers['set-cookie'][0]; await agent.post('/api/v1/auth/refresh').expect(201); await request(app.getHttpServer()) .post('/api/v1/auth/refresh') .set('Cookie', firstCookie) .expect(401) .expect('Content-Type', /application\/problem\+json/); await agent.post('/api/v1/auth/logout').expect(204); await agent.post('/api/v1/auth/logout').expect(204); }); it('rejects duplicate registration and invalid credentials', async () => { await request(app.getHttpServer()) .post('/api/v1/auth/register') .send({ email: 'user@example.com', password: 'a secure password' }) .expect(201); await request(app.getHttpServer()) .post('/api/v1/auth/register') .send({ email: 'USER@example.com', password: 'a secure password' }) .expect(409) .expect(({ body }: { body: unknown }) => expect((body as Problem).code).toBe('EMAIL_ALREADY_REGISTERED'), ); await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'user@example.com', password: 'wrong password' }) .expect(401) .expect(({ body }: { body: unknown }) => expect((body as Problem).code).toBe('INVALID_CREDENTIALS'), ); }); it('guards T-Bank connection and never returns a token', async () => { const registration = await request(app.getHttpServer()) .post('/api/v1/auth/register') .send({ email: 'connection@example.com', password: 'a secure password' }) .expect(201); const accessToken = (registration.body as Authentication).accessToken; await request(app.getHttpServer()) .get('/api/v1/tbank-connection') .expect(401); await request(app.getHttpServer()) .get('/api/v1/tbank-connection') .set('Authorization', `Bearer ${accessToken}`) .expect(200) .expect(({ body }: { body: unknown }) => expect(body).toEqual({ connected: false, connectedAt: null }), ); }); it('guards brokerage accounts and reports a missing connection', async () => { const registration = await request(app.getHttpServer()) .post('/api/v1/auth/register') .send({ email: 'accounts@example.com', password: 'a secure password' }) .expect(201); const accessToken = (registration.body as Authentication).accessToken; await request(app.getHttpServer()) .get('/api/v1/brokerage-accounts') .expect(401); await request(app.getHttpServer()) .get('/api/v1/brokerage-accounts') .set('Authorization', `Bearer ${accessToken}`) .expect(409) .expect(({ body }: { body: unknown }) => expect((body as Problem).code).toBe('TBANK_NOT_CONNECTED'), ); }); it('guards portfolio and reports a missing connection', async () => { const registration = await request(app.getHttpServer()) .post('/api/v1/auth/register') .send({ email: 'portfolio@example.com', password: 'a secure password' }) .expect(201); const accessToken = (registration.body as Authentication).accessToken; await request(app.getHttpServer()) .get('/api/v1/brokerage-accounts/account/portfolio') .expect(401); await request(app.getHttpServer()) .get('/api/v1/brokerage-accounts/account/portfolio') .set('Authorization', `Bearer ${accessToken}`) .expect(409) .expect(({ body }: { body: unknown }) => expect((body as Problem).code).toBe('TBANK_NOT_CONNECTED'), ); }); it('exposes a server request ID and logs one completion event', async () => { const logger = app.get(StructuredLogger); const event = jest.spyOn(logger, 'event'); const response = await request(app.getHttpServer()) .get('/api/v1/auth/me') .set('Origin', 'http://localhost:5173') .expect(401); expect(response.headers['x-request-id']).toMatch(/^[0-9a-f-]{36}$/i); expect(response.headers['access-control-expose-headers']).toContain( 'X-Request-ID', ); expect(event).toHaveBeenCalledTimes(1); expect(event).toHaveBeenCalledWith( 'warn', 'http.request.completed', expect.objectContaining({ status: 401, code: 'UNAUTHORIZED' }), ); }); it('records the public code and stack for an unhandled error', async () => { const logger = app.get(StructuredLogger); const event = jest.spyOn(logger, 'event'); jest .spyOn(app.get(AuthService), 'login') .mockRejectedValue(new Error('password: secret')); await request(app.getHttpServer()) .post('/api/v1/auth/login') .send({ email: 'user@example.com', password: 'a secure password' }) .expect(500) .expect(({ body }: { body: unknown }) => expect(body).toEqual({ status: 500, code: 'INTERNAL_SERVER_ERROR' }), ); expect(event).toHaveBeenCalledWith( 'error', 'http.request.completed', expect.objectContaining({ code: 'INTERNAL_SERVER_ERROR', }), ); }); });