import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe, ExecutionContext } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; import { GlobalExceptionFilter } from '../../src/shared/filters/global-exception.filter'; import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service'; import { JwtAuthGuard } from '../../src/shared/guards/jwt-auth.guard'; describe('Analytics API (E2E)', () => { let app: INestApplication; let prisma: PrismaService; const mockJwtToken = 'test-jwt-token'; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: (context: ExecutionContext) => { const req = context.switchToHttp().getRequest(); req.user = { userId: '12345' }; return true; }, }) .compile(); app = moduleFixture.createNestApplication(); app.useGlobalFilters(new GlobalExceptionFilter()); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, }), ); app.setGlobalPrefix('api/v1'); await app.init(); prisma = moduleFixture.get(PrismaService); }); afterAll(async () => { await app.close(); }); describe('POST /api/v1/analytics/events', () => { it('should accept batch events', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/analytics/events') .send({ events: [ { installId: 'test-install-id-12345', eventName: 'app_session_start', clientTs: Math.floor(Date.now() / 1000), properties: { os: 'iOS', osVersion: '17.0', appVersion: '1.0.0', }, }, ], }) .expect(201); expect(response.body).toHaveProperty('accepted'); expect(response.body.accepted).toBeGreaterThanOrEqual(0); }); it('should accept multiple events in batch', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/analytics/events') .send({ events: [ { installId: 'test-install-id-12345', eventName: 'app_session_start', clientTs: Math.floor(Date.now() / 1000), properties: { os: 'iOS' }, }, { installId: 'test-install-id-12345', eventName: 'presence_heartbeat', clientTs: Math.floor(Date.now() / 1000), properties: { os: 'iOS' }, }, { installId: 'test-install-id-12345', eventName: 'app_session_end', clientTs: Math.floor(Date.now() / 1000), properties: { os: 'iOS' }, }, ], }) .expect(201); expect(response.body.accepted).toBeGreaterThanOrEqual(0); }); it('should validate event name format', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/analytics/events') .send({ events: [ { installId: 'test-install-id-12345', eventName: '123_invalid', // Invalid: starts with number clientTs: Math.floor(Date.now() / 1000), }, ], }); // May return 201 with failed count or 400 depending on implementation // Check that validation occurs }); it('should validate installId format', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/analytics/events') .send({ events: [ { installId: 'short', // Invalid: too short eventName: 'app_session_start', clientTs: Math.floor(Date.now() / 1000), }, ], }); // May return 201 with failed count or 400 depending on implementation }); it('should handle empty events array', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/analytics/events') .send({ events: [], }) .expect(201); expect(response.body.accepted).toBe(0); }); }); describe('GET /api/v1/analytics/dau', () => { it('should return DAU statistics', async () => { const startDate = '2025-01-01'; const endDate = '2025-01-15'; const response = await request(app.getHttpServer()) .get('/api/v1/analytics/dau') .query({ startDate, endDate }) .set('Authorization', `Bearer ${mockJwtToken}`) .expect(200); expect(response.body).toHaveProperty('data'); expect(Array.isArray(response.body.data)).toBe(true); expect(response.body).toHaveProperty('total'); }); it('should validate date format', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/analytics/dau') .query({ startDate: 'invalid-date', endDate: '2025-01-15', }) .set('Authorization', `Bearer ${mockJwtToken}`) .expect(400); expect(response.body.statusCode).toBe(400); }); it('should require startDate parameter', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/analytics/dau') .query({ endDate: '2025-01-15' }) .set('Authorization', `Bearer ${mockJwtToken}`) .expect(400); expect(response.body.statusCode).toBe(400); }); it('should require endDate parameter', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/analytics/dau') .query({ startDate: '2025-01-01' }) .set('Authorization', `Bearer ${mockJwtToken}`) .expect(400); expect(response.body.statusCode).toBe(400); }); }); });