import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import request from 'supertest'; import { DataSource, Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { AppModule } from '../src/app.module'; import { Segment } from '../src/segments/segment.entity'; import { AggregatedMetric } from '../src/entities/aggregated-metric.entity'; import { createMockDataSource, createMockRepository, type MockDataSource, type MockRepository, } from './mocks'; import { createAcquisitionOverviewResult, createChannelResults, createSourceResults, createCampaignResults, createReferrerResults, createEngagementOverviewResult, createUniqueViewsResult, createPageResults, createEventResults, createScrollDepthResults, createUserFlowResults, createAudienceOverviewResult, createDemographicsResults, createDeviceResults, createBrowserResults, createOSResults, createGeoResults, createLanguageResults, createNewVsReturningResults, createSessionListResults, createSessionCountResult, createSessionMetricsResult, createSegmentMetricsResult, createAggregatedMetricSeries, } from './fixtures/aggregated-metric.fixture'; import { SegmentOperator, ConditionOperator } from '../src/segments/dto/segment.dto'; describe('Analytics API (E2E)', () => { let app: INestApplication; let mockDataSource: MockDataSource; let mockSegmentRepo: MockRepository; let mockMetricRepo: MockRepository; beforeAll(async () => { mockDataSource = createMockDataSource(); mockSegmentRepo = createMockRepository(); mockMetricRepo = createMockRepository(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(DataSource) .useValue(mockDataSource) .overrideProvider(getRepositoryToken(Segment)) .useValue(mockSegmentRepo) .overrideProvider(getRepositoryToken(AggregatedMetric)) .useValue(mockMetricRepo) .compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, forbidNonWhitelisted: true, }), ); await app.init(); }); afterAll(async () => { await app.close(); }); describe('Health', () => { it('should return health status', async () => { const response = await request(app.getHttpServer()) .get('/health') .expect(200); expect(response.body).toHaveProperty('status'); }); }); describe('Acquisition', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /acquisition/overview', () => { it('should return acquisition overview', async () => { mockDataSource.query.mockResolvedValue(createAcquisitionOverviewResult()); const response = await request(app.getHttpServer()) .get('/acquisition/overview') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('totalSessions'); expect(response.body).toHaveProperty('totalUsers'); expect(response.body).toHaveProperty('newUsers'); expect(response.body).toHaveProperty('returningUsers'); }); it('should reject missing startDate', async () => { await request(app.getHttpServer()) .get('/acquisition/overview') .query({ endDate: '2026-01-28' }) .expect(400); }); it('should reject missing endDate', async () => { await request(app.getHttpServer()) .get('/acquisition/overview') .query({ startDate: '2026-01-01' }) .expect(400); }); it('should reject invalid date format', async () => { await request(app.getHttpServer()) .get('/acquisition/overview') .query({ startDate: 'invalid', endDate: '2026-01-28' }) .expect(400); }); }); describe('GET /acquisition/channels', () => { it('should return channel breakdown', async () => { mockDataSource.query.mockResolvedValue(createChannelResults()); const response = await request(app.getHttpServer()) .get('/acquisition/channels') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBeGreaterThan(0); expect(response.body[0]).toHaveProperty('channel'); expect(response.body[0]).toHaveProperty('sessions'); expect(response.body[0]).toHaveProperty('users'); }); }); describe('GET /acquisition/sources', () => { it('should return source breakdown', async () => { mockDataSource.query.mockResolvedValue(createSourceResults()); const response = await request(app.getHttpServer()) .get('/acquisition/sources') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body.length).toBeGreaterThan(0); expect(response.body[0]).toHaveProperty('source'); expect(response.body[0]).toHaveProperty('medium'); }); it('should filter by channel', async () => { mockDataSource.query.mockResolvedValue(createSourceResults()); const response = await request(app.getHttpServer()) .get('/acquisition/sources') .query({ startDate: '2026-01-01', endDate: '2026-01-28', channel: 'organic', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /acquisition/campaigns', () => { it('should return campaign metrics', async () => { mockDataSource.query.mockResolvedValue(createCampaignResults()); const response = await request(app.getHttpServer()) .get('/acquisition/campaigns') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('campaign'); expect(response.body[0]).toHaveProperty('source'); expect(response.body[0]).toHaveProperty('medium'); } }); }); describe('GET /acquisition/referrers', () => { it('should return top referrers', async () => { mockDataSource.query.mockResolvedValue(createReferrerResults()); const response = await request(app.getHttpServer()) .get('/acquisition/referrers') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should respect limit parameter', async () => { mockDataSource.query.mockResolvedValue(createReferrerResults()); const response = await request(app.getHttpServer()) .get('/acquisition/referrers') .query({ startDate: '2026-01-01', endDate: '2026-01-28', limit: 5, }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /acquisition/compare', () => { it('should compare two periods', async () => { mockDataSource.query .mockResolvedValueOnce(createAcquisitionOverviewResult()) .mockResolvedValueOnce(createAcquisitionOverviewResult({ total_sessions: '1200', total_users: '750', })); const response = await request(app.getHttpServer()) .get('/acquisition/compare') .query({ startDate: '2026-01-01', endDate: '2026-01-28', compareStartDate: '2025-12-01', compareEndDate: '2025-12-28', }) .expect(200); expect(response.body).toHaveProperty('current'); expect(response.body).toHaveProperty('previous'); expect(response.body).toHaveProperty('change'); }); it('should reject missing compare dates', async () => { await request(app.getHttpServer()) .get('/acquisition/compare') .query({ startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(400); }); }); }); describe('Engagement', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /engagement/overview', () => { it('should return engagement overview', async () => { mockDataSource.query.mockResolvedValue(createEngagementOverviewResult()); const response = await request(app.getHttpServer()) .get('/engagement/overview') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('totalSessions'); expect(response.body).toHaveProperty('engagedSessions'); expect(response.body).toHaveProperty('avgDuration'); }); }); describe('GET /engagement/pages', () => { it('should return page metrics', async () => { mockDataSource.query .mockResolvedValueOnce(createPageResults()) .mockResolvedValueOnce(createUniqueViewsResult()); const response = await request(app.getHttpServer()) .get('/engagement/pages') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('path'); expect(response.body[0]).toHaveProperty('views'); } }); it('should accept sortBy parameter', async () => { mockDataSource.query .mockResolvedValueOnce(createPageResults()) .mockResolvedValueOnce(createUniqueViewsResult()); await request(app.getHttpServer()) .get('/engagement/pages') .query({ startDate: '2026-01-01', endDate: '2026-01-28', sortBy: 'views', }) .expect(200); }); }); describe('GET /engagement/events', () => { it('should return event metrics', async () => { mockDataSource.query.mockResolvedValue(createEventResults()); const response = await request(app.getHttpServer()) .get('/engagement/events') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should filter by category', async () => { mockDataSource.query.mockResolvedValue(createEventResults()); await request(app.getHttpServer()) .get('/engagement/events') .query({ startDate: '2026-01-01', endDate: '2026-01-28', category: 'click', }) .expect(200); }); }); describe('GET /engagement/scroll-depth', () => { it('should return scroll depth metrics', async () => { mockDataSource.query.mockResolvedValue(createScrollDepthResults()); const response = await request(app.getHttpServer()) .get('/engagement/scroll-depth') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should filter by page', async () => { mockDataSource.query.mockResolvedValue(createScrollDepthResults()); await request(app.getHttpServer()) .get('/engagement/scroll-depth') .query({ startDate: '2026-01-01', endDate: '2026-01-28', page: '/', }) .expect(200); }); }); describe('GET /engagement/user-flow', () => { it('should return user flow data', async () => { mockDataSource.query.mockResolvedValue(createUserFlowResults()); const response = await request(app.getHttpServer()) .get('/engagement/user-flow') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should filter by start page', async () => { mockDataSource.query.mockResolvedValue(createUserFlowResults()); await request(app.getHttpServer()) .get('/engagement/user-flow') .query({ startDate: '2026-01-01', endDate: '2026-01-28', startPage: '/', }) .expect(200); }); }); }); describe('Audience', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /audience/overview', () => { it('should return audience overview', async () => { mockDataSource.query.mockResolvedValue(createAudienceOverviewResult()); const response = await request(app.getHttpServer()) .get('/audience/overview') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('totalUsers'); expect(response.body).toHaveProperty('newUsers'); expect(response.body).toHaveProperty('returningUsers'); }); }); describe('GET /audience/demographics', () => { it('should return demographic breakdown', async () => { mockDataSource.query.mockResolvedValue(createDemographicsResults()); const response = await request(app.getHttpServer()) .get('/audience/demographics') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /audience/devices', () => { it('should return device breakdown', async () => { mockDataSource.query.mockResolvedValue(createDeviceResults()); const response = await request(app.getHttpServer()) .get('/audience/devices') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('deviceType'); } }); }); describe('GET /audience/browsers', () => { it('should return browser breakdown', async () => { mockDataSource.query.mockResolvedValue(createBrowserResults()); const response = await request(app.getHttpServer()) .get('/audience/browsers') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('browser'); } }); }); describe('GET /audience/operating-systems', () => { it('should return OS breakdown', async () => { mockDataSource.query.mockResolvedValue(createOSResults()); const response = await request(app.getHttpServer()) .get('/audience/operating-systems') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('os'); } }); }); describe('GET /audience/geography', () => { it('should return geographic breakdown', async () => { mockDataSource.query.mockResolvedValue(createGeoResults()); const response = await request(app.getHttpServer()) .get('/audience/geography') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should accept granularity parameter', async () => { mockDataSource.query.mockResolvedValue(createGeoResults()); await request(app.getHttpServer()) .get('/audience/geography') .query({ startDate: '2026-01-01', endDate: '2026-01-28', granularity: 'country', }) .expect(200); }); }); describe('GET /audience/languages', () => { it('should return language breakdown', async () => { mockDataSource.query.mockResolvedValue(createLanguageResults()); const response = await request(app.getHttpServer()) .get('/audience/languages') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /audience/new-vs-returning', () => { it('should return new vs returning user trend', async () => { mockDataSource.query.mockResolvedValue(createNewVsReturningResults()); const response = await request(app.getHttpServer()) .get('/audience/new-vs-returning') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); }); describe('Sessions', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /sessions', () => { it('should return paginated session list', async () => { mockDataSource.query .mockResolvedValueOnce(createSessionListResults()) .mockResolvedValueOnce(createSessionCountResult(150)); const response = await request(app.getHttpServer()) .get('/sessions') .query({ startDate: '2026-01-01', endDate: '2026-01-28', limit: 10, offset: 0, }) .expect(200); expect(response.body).toHaveProperty('sessions'); expect(response.body).toHaveProperty('total'); expect(Array.isArray(response.body.sessions)).toBe(true); }); it('should accept pagination parameters', async () => { mockDataSource.query .mockResolvedValueOnce(createSessionListResults()) .mockResolvedValueOnce(createSessionCountResult(150)); await request(app.getHttpServer()) .get('/sessions') .query({ startDate: '2026-01-01', endDate: '2026-01-28', limit: 20, offset: 40, }) .expect(200); }); }); describe('GET /sessions/metrics', () => { it('should return session metrics', async () => { mockDataSource.query.mockResolvedValue(createSessionMetricsResult()); const response = await request(app.getHttpServer()) .get('/sessions/metrics') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('totalSessions'); expect(response.body).toHaveProperty('avgDuration'); expect(response.body).toHaveProperty('bounceRate'); }); }); describe('GET /sessions/:sessionId', () => { it('should return single session details', async () => { mockDataSource.query.mockResolvedValue(createSessionListResults().slice(0, 1)); const response = await request(app.getHttpServer()) .get('/sessions/sess-001') .expect(200); expect(response.body).toHaveProperty('sessionId'); }); it('should return 404 for non-existent session', async () => { mockDataSource.query.mockResolvedValue([]); await request(app.getHttpServer()) .get('/sessions/non-existent-id') .expect(404); }); }); }); describe('Segments', () => { beforeEach(() => { mockSegmentRepo.find.mockReset(); mockSegmentRepo.findOne.mockReset(); mockSegmentRepo.findOneBy.mockReset(); mockSegmentRepo.save.mockReset(); mockSegmentRepo.create.mockReset(); mockSegmentRepo.remove.mockReset(); mockDataSource.query.mockReset(); }); const mockSegment: Segment = { id: 'segment-001', name: 'Desktop Users', description: 'Users on desktop devices', conditions: [ { dimension: 'deviceType', operator: ConditionOperator.EQUALS, values: ['desktop'], }, ], operator: SegmentOperator.AND, isBuiltIn: false, createdBy: null, createdAt: new Date('2026-01-15T10:00:00Z'), updatedAt: new Date('2026-01-15T10:00:00Z'), }; describe('GET /segments', () => { it('should return all segments', async () => { mockSegmentRepo.find.mockResolvedValue([mockSegment]); const response = await request(app.getHttpServer()) .get('/segments') .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('id'); expect(response.body[0]).toHaveProperty('name'); expect(response.body[0]).toHaveProperty('conditions'); } }); }); describe('GET /segments/:id', () => { it('should return single segment', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(mockSegment); const response = await request(app.getHttpServer()) .get('/segments/segment-001') .expect(200); expect(response.body).toHaveProperty('id'); expect(response.body).toHaveProperty('name'); }); it('should return 404 for non-existent segment', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(null); await request(app.getHttpServer()) .get('/segments/non-existent') .expect(404); }); }); describe('POST /segments', () => { it('should create new segment', async () => { const newSegment = { name: 'Mobile Users', description: 'Users on mobile devices', conditions: [ { dimension: 'deviceType', operator: ConditionOperator.EQUALS, values: ['mobile'], }, ], operator: SegmentOperator.AND, }; mockSegmentRepo.create.mockReturnValue(mockSegment); mockSegmentRepo.save.mockResolvedValue(mockSegment); const response = await request(app.getHttpServer()) .post('/segments') .send(newSegment) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body).toHaveProperty('name'); }); it('should reject invalid segment data', async () => { await request(app.getHttpServer()) .post('/segments') .send({ name: 'Missing conditions' }) .expect(400); }); it('should reject invalid condition operator', async () => { await request(app.getHttpServer()) .post('/segments') .send({ name: 'Test', conditions: [ { dimension: 'deviceType', operator: 'invalid_operator', values: ['mobile'], }, ], }) .expect(400); }); }); describe('PUT /segments/:id', () => { it('should update segment', async () => { const updatedSegment = { ...mockSegment, name: 'Updated Name' }; mockSegmentRepo.findOneBy.mockResolvedValue(mockSegment); mockSegmentRepo.save.mockResolvedValue(updatedSegment); const response = await request(app.getHttpServer()) .put('/segments/segment-001') .send({ name: 'Updated Name' }) .expect(200); expect(response.body).toHaveProperty('name', 'Updated Name'); }); it('should return 404 for non-existent segment', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(null); await request(app.getHttpServer()) .put('/segments/non-existent') .send({ name: 'Updated' }) .expect(404); }); }); describe('DELETE /segments/:id', () => { it('should delete segment', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(mockSegment); mockSegmentRepo.remove.mockResolvedValue(mockSegment); await request(app.getHttpServer()) .delete('/segments/segment-001') .expect(200); }); it('should return 404 for non-existent segment', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(null); await request(app.getHttpServer()) .delete('/segments/non-existent') .expect(404); }); }); describe('GET /segments/:id/apply', () => { it('should apply segment and return metrics', async () => { mockSegmentRepo.findOneBy.mockResolvedValue(mockSegment); mockDataSource.query.mockResolvedValue(createSegmentMetricsResult()); const response = await request(app.getHttpServer()) .get('/segments/segment-001/apply') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('sessions'); expect(response.body).toHaveProperty('users'); }); it('should reject missing date range', async () => { await request(app.getHttpServer()) .get('/segments/segment-001/apply') .expect(400); }); }); describe('GET /segments/compare', () => { it('should compare multiple segments', async () => { mockSegmentRepo.findOneBy .mockResolvedValueOnce(mockSegment) .mockResolvedValueOnce({ ...mockSegment, id: 'segment-002' }); mockDataSource.query .mockResolvedValueOnce(createSegmentMetricsResult()) .mockResolvedValueOnce(createSegmentMetricsResult()); const response = await request(app.getHttpServer()) .get('/segments/compare') .query({ segmentIds: ['segment-001', 'segment-002'], startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should reject empty segment IDs', async () => { await request(app.getHttpServer()) .get('/segments/compare') .query({ segmentIds: [], startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(400); }); }); }); describe('Trends', () => { beforeEach(() => { mockMetricRepo.queryBuilder.getRawMany.mockReset(); }); describe('GET /trends', () => { it('should return trend data', async () => { mockMetricRepo.queryBuilder.getRawMany.mockResolvedValue( createAggregatedMetricSeries(7).map(m => ({ period: m.timestamp, value: String(m.value), })), ); const response = await request(app.getHttpServer()) .get('/trends') .query({ metric: 'page_views', startDate: '2026-01-15', endDate: '2026-01-22', granularity: 'day', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('period'); expect(response.body[0]).toHaveProperty('value'); } }); it('should reject invalid metric', async () => { await request(app.getHttpServer()) .get('/trends') .query({ metric: 'invalid_metric', startDate: '2026-01-15', endDate: '2026-01-22', granularity: 'day', }) .expect(400); }); it('should reject missing required params', async () => { await request(app.getHttpServer()) .get('/trends') .query({ metric: 'page_views' }) .expect(400); }); }); describe('GET /trends/compare', () => { it('should compare trends between periods', async () => { mockMetricRepo.queryBuilder.getRawMany .mockResolvedValueOnce( createAggregatedMetricSeries(7).map(m => ({ period: m.timestamp, value: String(m.value), })), ) .mockResolvedValueOnce( createAggregatedMetricSeries(7).map(m => ({ period: m.timestamp, value: String(m.value - 10), })), ); const response = await request(app.getHttpServer()) .get('/trends/compare') .query({ metric: 'page_views', startDate: '2026-01-15', endDate: '2026-01-22', compareStartDate: '2026-01-08', compareEndDate: '2026-01-15', granularity: 'day', }) .expect(200); expect(response.body).toHaveProperty('current'); expect(response.body).toHaveProperty('previous'); expect(Array.isArray(response.body.current)).toBe(true); expect(Array.isArray(response.body.previous)).toBe(true); }); }); }); describe('Funnels', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('POST /funnels/analyze', () => { it('should analyze funnel conversion', async () => { mockDataSource.query.mockResolvedValue([ { step: 1, step_name: 'Landing', users: '1000', conversion_rate: '1.00' }, { step: 2, step_name: 'Profile', users: '600', conversion_rate: '0.60' }, { step: 3, step_name: 'Contact', users: '200', conversion_rate: '0.20' }, ]); const response = await request(app.getHttpServer()) .post('/funnels/analyze') .send({ steps: [ { name: 'Landing', eventType: 'pageview', filter: { pageUrl: '/' } }, { name: 'Profile', eventType: 'pageview', filter: { pageUrl: '/profiles' } }, { name: 'Contact', eventType: 'click', filter: { metadata: { action: 'contact' } } }, ], startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); if (response.body.length > 0) { expect(response.body[0]).toHaveProperty('step'); expect(response.body[0]).toHaveProperty('stepName'); expect(response.body[0]).toHaveProperty('users'); } }); it('should reject invalid funnel steps', async () => { await request(app.getHttpServer()) .post('/funnels/analyze') .send({ steps: [], startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(400); }); it('should reject missing date range', async () => { await request(app.getHttpServer()) .post('/funnels/analyze') .send({ steps: [ { name: 'Landing', eventType: 'pageview', filter: { pageUrl: '/' } }, ], }) .expect(400); }); }); describe('GET /funnels/presets', () => { it('should return preset funnels', async () => { const response = await request(app.getHttpServer()) .get('/funnels/presets') .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); }); describe('Cohorts', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /cohorts/retention', () => { it('should return retention cohort analysis', async () => { mockDataSource.query.mockResolvedValue([ { cohort: '2026-01-15', day_0: '100', day_1: '60', day_7: '40', day_30: '25' }, { cohort: '2026-01-16', day_0: '110', day_1: '65', day_7: '42', day_30: null }, ]); const response = await request(app.getHttpServer()) .get('/cohorts/retention') .query({ startDate: '2026-01-01', endDate: '2026-01-28', granularity: 'day', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should reject invalid granularity', async () => { await request(app.getHttpServer()) .get('/cohorts/retention') .query({ startDate: '2026-01-01', endDate: '2026-01-28', granularity: 'invalid', }) .expect(400); }); }); describe('GET /cohorts/behavioral', () => { it('should return behavioral cohorts', async () => { mockDataSource.query.mockResolvedValue([ { segment: 'desktop', users: '450', avg_sessions: '2.5', retention_rate: '0.65' }, { segment: 'mobile', users: '350', avg_sessions: '1.8', retention_rate: '0.52' }, ]); const response = await request(app.getHttpServer()) .get('/cohorts/behavioral') .query({ startDate: '2026-01-01', endDate: '2026-01-28', segmentBy: 'device', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should reject missing segmentBy', async () => { await request(app.getHttpServer()) .get('/cohorts/behavioral') .query({ startDate: '2026-01-01', endDate: '2026-01-28', }) .expect(400); }); }); }); describe('Revenue', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('GET /revenue/summary', () => { it('should return revenue summary', async () => { mockDataSource.query.mockResolvedValue([{ total_revenue: '25000.00', total_transactions: '500', avg_order_value: '50.00', revenue_per_user: '27.78', }]); const response = await request(app.getHttpServer()) .get('/revenue/summary') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(response.body).toHaveProperty('totalRevenue'); expect(response.body).toHaveProperty('totalTransactions'); expect(response.body).toHaveProperty('avgOrderValue'); }); }); describe('GET /revenue/ltv', () => { it('should return lifetime value analysis', async () => { mockDataSource.query.mockResolvedValue([ { cohort: '2026-01', ltv: '150.00', users: '200' }, { cohort: '2025-12', ltv: '280.00', users: '180' }, ]); const response = await request(app.getHttpServer()) .get('/revenue/ltv') .query({ startDate: '2026-01-01', endDate: '2026-01-28' }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /revenue/arpu', () => { it('should return average revenue per user', async () => { mockDataSource.query.mockResolvedValue([ { period: new Date('2026-01-15'), arpu: '25.50' }, { period: new Date('2026-01-16'), arpu: '27.80' }, ]); const response = await request(app.getHttpServer()) .get('/revenue/arpu') .query({ startDate: '2026-01-01', endDate: '2026-01-28', granularity: 'day', }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /revenue/mrr', () => { it('should return monthly recurring revenue', async () => { mockDataSource.query.mockResolvedValue([{ mrr: '15000.00', subscribers: '300', churn_rate: '0.05', }]); const response = await request(app.getHttpServer()) .get('/revenue/mrr') .query({ month: '2026-01' }) .expect(200); expect(response.body).toHaveProperty('mrr'); expect(response.body).toHaveProperty('subscribers'); }); it('should reject invalid month format', async () => { await request(app.getHttpServer()) .get('/revenue/mrr') .query({ month: 'invalid' }) .expect(400); }); }); }); describe('GDPR', () => { beforeEach(() => { mockDataSource.query.mockReset(); }); describe('POST /gdpr/export/user/:userId', () => { it('should export user data', async () => { mockDataSource.query .mockResolvedValueOnce([{ sessionId: 'sess-001', userId: 'user-001' }]) .mockResolvedValueOnce([{ eventType: 'pageview', timestamp: '2026-01-15T10:00:00Z' }]); const response = await request(app.getHttpServer()) .post('/gdpr/export/user/user-001') .expect(200); expect(response.body).toHaveProperty('userId'); expect(response.body).toHaveProperty('sessions'); expect(response.body).toHaveProperty('events'); }); }); describe('POST /gdpr/export/session/:sessionId', () => { it('should export session data', async () => { mockDataSource.query .mockResolvedValueOnce([{ sessionId: 'sess-001' }]) .mockResolvedValueOnce([{ eventType: 'pageview' }]); const response = await request(app.getHttpServer()) .post('/gdpr/export/session/sess-001') .expect(200); expect(response.body).toHaveProperty('sessionId'); expect(response.body).toHaveProperty('events'); }); }); describe('DELETE /gdpr/erase/user/:userId', () => { it('should erase user data', async () => { const queryRunner = { connect: vi.fn(), startTransaction: vi.fn(), commitTransaction: vi.fn(), rollbackTransaction: vi.fn(), release: vi.fn(), manager: { delete: vi.fn().mockResolvedValue({ affected: 1 }), }, }; mockDataSource.createQueryRunner.mockReturnValue(queryRunner); await request(app.getHttpServer()) .delete('/gdpr/erase/user/user-001') .expect(200); }); }); describe('DELETE /gdpr/erase/session/:sessionId', () => { it('should erase session data', async () => { const queryRunner = { connect: vi.fn(), startTransaction: vi.fn(), commitTransaction: vi.fn(), rollbackTransaction: vi.fn(), release: vi.fn(), manager: { delete: vi.fn().mockResolvedValue({ affected: 1 }), }, }; mockDataSource.createQueryRunner.mockReturnValue(queryRunner); await request(app.getHttpServer()) .delete('/gdpr/erase/session/sess-001') .expect(200); }); }); describe('GET /gdpr/retention-policy', () => { it('should return retention policy', async () => { const response = await request(app.getHttpServer()) .get('/gdpr/retention-policy') .expect(200); expect(response.body).toHaveProperty('rawEvents'); expect(response.body).toHaveProperty('sessions'); expect(response.body).toHaveProperty('aggregatedMetrics'); }); }); describe('POST /gdpr/retention-policy', () => { it('should update retention policy', async () => { const response = await request(app.getHttpServer()) .post('/gdpr/retention-policy') .send({ rawEvents: 90, sessions: 365, aggregatedMetrics: 730, }) .expect(200); expect(response.body).toHaveProperty('rawEvents', 90); }); it('should reject invalid retention days', async () => { await request(app.getHttpServer()) .post('/gdpr/retention-policy') .send({ rawEvents: -1, }) .expect(400); }); }); describe('POST /gdpr/cleanup/execute', () => { it('should execute cleanup', async () => { const queryRunner = { connect: vi.fn(), startTransaction: vi.fn(), commitTransaction: vi.fn(), rollbackTransaction: vi.fn(), release: vi.fn(), manager: { delete: vi.fn().mockResolvedValue({ affected: 100 }), }, }; mockDataSource.createQueryRunner.mockReturnValue(queryRunner); const response = await request(app.getHttpServer()) .post('/gdpr/cleanup/execute') .expect(200); expect(response.body).toHaveProperty('deletedRecords'); }); }); describe('GET /gdpr/cleanup/preview', () => { it('should preview cleanup', async () => { mockDataSource.query.mockResolvedValue([{ count: '150' }]); const response = await request(app.getHttpServer()) .get('/gdpr/cleanup/preview') .expect(200); expect(response.body).toHaveProperty('rawEventsToDelete'); expect(response.body).toHaveProperty('sessionsToDelete'); }); }); describe('GET /gdpr/audit-log', () => { it('should return audit log', async () => { mockDataSource.query.mockResolvedValue([ { id: 'log-001', action: 'EXPORT', targetType: 'USER', targetId: 'user-001', timestamp: '2026-01-15T10:00:00Z', }, ]); const response = await request(app.getHttpServer()) .get('/gdpr/audit-log') .query({ limit: 50, offset: 0 }) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /gdpr/consent/:userId', () => { it('should return user consent status', async () => { mockDataSource.query.mockResolvedValue([ { userId: 'user-001', analytics: true, marketing: false, lastUpdated: '2026-01-15T10:00:00Z', }, ]); const response = await request(app.getHttpServer()) .get('/gdpr/consent/user-001') .expect(200); expect(response.body).toHaveProperty('userId'); expect(response.body).toHaveProperty('analytics'); }); }); describe('POST /gdpr/consent/:userId', () => { it('should update user consent', async () => { mockDataSource.query.mockResolvedValue({ affected: 1 }); const response = await request(app.getHttpServer()) .post('/gdpr/consent/user-001') .send({ analytics: true, marketing: false, }) .expect(200); expect(response.body).toHaveProperty('userId'); expect(response.body).toHaveProperty('analytics', true); }); it('should reject invalid consent data', async () => { await request(app.getHttpServer()) .post('/gdpr/consent/user-001') .send({ analytics: 'not-a-boolean', }) .expect(400); }); }); }); });