199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
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>(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);
|
|
});
|
|
});
|
|
});
|