platform-codebase/features/platform-analytics/backend-api/test/analytics.e2e-spec.ts
Lilith dcae150ea6 chore: snapshot before monorepo consolidation
Capture current working state before converting platform-codebase
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:30 -08:00

470 lines
16 KiB
TypeScript

import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import request from 'supertest';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { RevenueModule } from '@/modules/revenue';
import { PnLModule } from '@/modules/pnl';
import { CostsModule } from '@/modules/costs';
import { PerformanceModule } from '@/modules/performance';
import { RealtimeModule } from '@/modules/realtime';
import {
Transaction,
TransactionType,
CostEntry,
CostType,
CostCategory,
ApiRequestMetric,
EngagementMetric,
} from '@/entities';
import {
createMockRepository,
createMockQueryBuilder,
type MockRepository,
type MockQueryBuilder,
} from './mocks';
import {
createTransactionAggregateResult,
createCostAggregateResult,
createPerformanceAggregateResult,
createTrendPoints,
} from './fixtures';
// Mock service registry
vi.mock('@lilith/service-registry', () => ({
buildDeploymentRegistry: () => ({
services: new Map(),
}),
}));
describe('Analytics API (E2E)', () => {
let app: INestApplication;
let transactionRepo: MockRepository<Transaction>;
let costRepo: MockRepository<CostEntry>;
let metricRepo: MockRepository<ApiRequestMetric>;
let engagementRepo: MockRepository<EngagementMetric>;
let transactionQb: MockQueryBuilder;
let costQb: MockQueryBuilder;
let metricQb: MockQueryBuilder;
let engagementQb: MockQueryBuilder;
beforeAll(async () => {
transactionRepo = createMockRepository<Transaction>();
costRepo = createMockRepository<CostEntry>();
metricRepo = createMockRepository<ApiRequestMetric>();
engagementRepo = createMockRepository<EngagementMetric>();
transactionQb = createMockQueryBuilder();
costQb = createMockQueryBuilder();
metricQb = createMockQueryBuilder();
engagementQb = createMockQueryBuilder();
transactionRepo.createQueryBuilder.mockReturnValue(transactionQb);
costRepo.createQueryBuilder.mockReturnValue(costQb);
metricRepo.createQueryBuilder.mockReturnValue(metricQb);
engagementRepo.createQueryBuilder.mockReturnValue(engagementQb);
// Setup default mock responses
transactionQb.getRawOne.mockResolvedValue(createTransactionAggregateResult());
transactionQb.getRawMany.mockResolvedValue([]);
costQb.getRawOne.mockResolvedValue(createCostAggregateResult());
costQb.getRawMany.mockResolvedValue([]);
metricQb.getRawOne.mockResolvedValue(createPerformanceAggregateResult());
metricQb.getRawMany.mockResolvedValue([]);
engagementQb.getRawOne.mockResolvedValue({ count: '100' });
engagementQb.getRawMany.mockResolvedValue([]);
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
ignoreEnvFile: true,
load: [() => ({ REDIS_URL: '' })],
}),
RevenueModule,
PnLModule,
CostsModule,
PerformanceModule,
RealtimeModule,
],
})
.overrideProvider(getRepositoryToken(Transaction))
.useValue(transactionRepo)
.overrideProvider(getRepositoryToken(CostEntry))
.useValue(costRepo)
.overrideProvider(getRepositoryToken(ApiRequestMetric))
.useValue(metricRepo)
.overrideProvider(getRepositoryToken(EngagementMetric))
.useValue(engagementRepo)
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
vi.clearAllMocks();
// Reset mock responses
transactionQb.getRawOne.mockResolvedValue(createTransactionAggregateResult());
transactionQb.getRawMany.mockResolvedValue([]);
costQb.getRawOne.mockResolvedValue(createCostAggregateResult());
costQb.getRawMany.mockResolvedValue([]);
metricQb.getRawOne.mockResolvedValue(createPerformanceAggregateResult());
metricQb.getRawMany.mockResolvedValue([]);
engagementQb.getRawOne.mockResolvedValue({ count: '100' });
engagementQb.getRawMany.mockResolvedValue([]);
});
describe('Revenue Endpoints', () => {
describe('GET /revenue/metrics', () => {
it('should return revenue metrics with default period', async () => {
const response = await request(app.getHttpServer())
.get('/revenue/metrics')
.expect(200);
expect(response.body).toHaveProperty('totalRevenue');
expect(response.body).toHaveProperty('recurringRevenue');
expect(response.body).toHaveProperty('oneTimeRevenue');
expect(response.body).toHaveProperty('averageTransactionValue');
expect(response.body).toHaveProperty('transactionCount');
expect(response.body).toHaveProperty('growthRate');
expect(response.body).toHaveProperty('cryptoRevenue');
expect(response.body).toHaveProperty('avgRevenuePerUser');
});
it('should accept period query parameter', async () => {
await request(app.getHttpServer())
.get('/revenue/metrics?period=7d')
.expect(200);
await request(app.getHttpServer())
.get('/revenue/metrics?period=90d')
.expect(200);
});
it('should return numeric values', async () => {
const response = await request(app.getHttpServer())
.get('/revenue/metrics')
.expect(200);
expect(typeof response.body.totalRevenue).toBe('number');
expect(typeof response.body.transactionCount).toBe('number');
expect(typeof response.body.growthRate).toBe('number');
});
});
describe('GET /revenue/trend', () => {
it('should return trend array', async () => {
transactionQb.getRawMany.mockResolvedValue(createTrendPoints(5, 'revenue'));
const response = await request(app.getHttpServer())
.get('/revenue/trend')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
it('should return trend point with correct shape', async () => {
transactionQb.getRawMany.mockResolvedValue(createTrendPoints(1, 'revenue'));
const response = await request(app.getHttpServer())
.get('/revenue/trend?period=30d')
.expect(200);
if (response.body.length > 0) {
expect(response.body[0]).toHaveProperty('date');
expect(response.body[0]).toHaveProperty('amount');
expect(response.body[0]).toHaveProperty('transactionCount');
expect(response.body[0]).toHaveProperty('recurring');
expect(response.body[0]).toHaveProperty('oneTime');
expect(response.body[0]).toHaveProperty('crypto');
}
});
});
describe('GET /revenue/breakdown', () => {
it('should return breakdown by type and source', async () => {
const byTypeData = [
{ type: TransactionType.SUBSCRIPTION, amount: '6000.00' },
];
const bySourceData = [{ source: 'web', amount: '7000.00' }];
let callCount = 0;
transactionQb.getRawMany.mockImplementation(() => {
callCount++;
if (callCount === 1) return Promise.resolve(byTypeData);
return Promise.resolve(bySourceData);
});
const response = await request(app.getHttpServer())
.get('/revenue/breakdown')
.expect(200);
expect(response.body).toHaveProperty('byType');
expect(response.body).toHaveProperty('bySource');
});
});
});
describe('P&L Endpoints', () => {
describe('GET /pnl/statement', () => {
it('should return complete P&L statement', async () => {
const revenueByType = [
{ type: TransactionType.SUBSCRIPTION, amount: '5000.00' },
];
const costData = [
{ category: 'INFRASTRUCTURE', costType: CostType.FIXED, amount: '1000.00' },
];
transactionQb.getRawMany.mockResolvedValue(revenueByType);
transactionQb.getRawOne.mockResolvedValue({ amount: '100.00' });
costQb.getRawMany.mockResolvedValue(costData);
const response = await request(app.getHttpServer())
.get('/pnl/statement')
.expect(200);
expect(response.body).toHaveProperty('revenue');
expect(response.body).toHaveProperty('costs');
expect(response.body).toHaveProperty('grossProfit');
expect(response.body).toHaveProperty('operatingExpenses');
expect(response.body).toHaveProperty('ebitda');
expect(response.body).toHaveProperty('netIncome');
expect(response.body).toHaveProperty('margins');
expect(response.body).toHaveProperty('breakdown');
});
it('should accept period query parameter', async () => {
await request(app.getHttpServer())
.get('/pnl/statement?period=90d')
.expect(200);
});
});
describe('GET /pnl/trend', () => {
it('should return P&L trend array', async () => {
const response = await request(app.getHttpServer())
.get('/pnl/trend')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('GET /pnl/reserve', () => {
it('should return reserve progress', async () => {
const response = await request(app.getHttpServer())
.get('/pnl/reserve')
.expect(200);
expect(response.body).toHaveProperty('current');
expect(response.body).toHaveProperty('goal');
expect(response.body).toHaveProperty('progress');
expect(response.body).toHaveProperty('monthlyContribution');
expect(response.body).toHaveProperty('projectedDate');
});
});
});
describe('Costs Endpoints', () => {
describe('GET /costs/metrics', () => {
it('should return cost metrics', async () => {
const response = await request(app.getHttpServer())
.get('/costs/metrics')
.expect(200);
expect(response.body).toHaveProperty('totalCosts');
expect(response.body).toHaveProperty('fixedCosts');
expect(response.body).toHaveProperty('variableCosts');
expect(response.body).toHaveProperty('cogs');
expect(response.body).toHaveProperty('averageMonthlyCost');
});
});
describe('GET /costs/breakdown', () => {
it('should return cost breakdown', async () => {
const byCategoryData = [
{ category: CostCategory.INFRASTRUCTURE, amount: '2000.00' },
];
const byTypeData = [{ type: CostType.FIXED, amount: '2000.00' }];
let callCount = 0;
costQb.getRawMany.mockImplementation(() => {
callCount++;
if (callCount === 1) return Promise.resolve(byCategoryData);
return Promise.resolve(byTypeData);
});
const response = await request(app.getHttpServer())
.get('/costs/breakdown')
.expect(200);
expect(response.body).toHaveProperty('byCategory');
expect(response.body).toHaveProperty('byType');
expect(Array.isArray(response.body.byCategory)).toBe(true);
expect(Array.isArray(response.body.byType)).toBe(true);
});
});
describe('GET /costs/trend', () => {
it('should return cost trend array', async () => {
costQb.getRawMany.mockResolvedValue(createTrendPoints(5, 'cost'));
const response = await request(app.getHttpServer())
.get('/costs/trend')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('GET /costs/budget', () => {
it('should return budget comparison', async () => {
const budgetData = [
{ category: CostCategory.INFRASTRUCTURE, actual: '1800.00', budgeted: '2000.00' },
];
costQb.getRawMany.mockResolvedValue(budgetData);
const response = await request(app.getHttpServer())
.get('/costs/budget')
.expect(200);
expect(response.body).toHaveProperty('budgeted');
expect(response.body).toHaveProperty('actual');
expect(response.body).toHaveProperty('variance');
expect(response.body).toHaveProperty('variancePercentage');
expect(response.body).toHaveProperty('byCategory');
});
});
});
describe('Performance Endpoints', () => {
describe('GET /performance/metrics', () => {
it('should return performance metrics', async () => {
const response = await request(app.getHttpServer())
.get('/performance/metrics')
.expect(200);
expect(response.body).toHaveProperty('avgResponseTime');
expect(response.body).toHaveProperty('p50ResponseTime');
expect(response.body).toHaveProperty('p95ResponseTime');
expect(response.body).toHaveProperty('p99ResponseTime');
expect(response.body).toHaveProperty('requestsPerSecond');
expect(response.body).toHaveProperty('errorRate');
expect(response.body).toHaveProperty('uptime');
expect(response.body).toHaveProperty('totalRequests');
expect(response.body).toHaveProperty('totalErrors');
});
});
describe('GET /performance/trend', () => {
it('should return performance trend array', async () => {
metricQb.getRawMany.mockResolvedValue(createTrendPoints(5, 'performance'));
const response = await request(app.getHttpServer())
.get('/performance/trend')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
});
describe('GET /performance/endpoints', () => {
it('should return endpoint performance list', async () => {
const endpointData = [
{
endpoint: '/api/v1/users',
method: 'GET',
avgResponseTime: '45.00',
p95ResponseTime: '120.00',
requestCount: '5000',
errorCount: '50',
},
];
metricQb.getRawMany.mockResolvedValue(endpointData);
const response = await request(app.getHttpServer())
.get('/performance/endpoints')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
if (response.body.length > 0) {
expect(response.body[0]).toHaveProperty('endpoint');
expect(response.body[0]).toHaveProperty('method');
expect(response.body[0]).toHaveProperty('avgResponseTime');
expect(response.body[0]).toHaveProperty('errorRate');
}
});
it('should accept limit and sortBy parameters', async () => {
await request(app.getHttpServer())
.get('/performance/endpoints?limit=10&sortBy=error_rate')
.expect(200);
});
});
});
describe('Realtime Endpoints', () => {
describe('GET /realtime/metrics', () => {
it('should return realtime metrics snapshot', async () => {
const response = await request(app.getHttpServer())
.get('/realtime/metrics')
.expect(200);
expect(response.body).toHaveProperty('activeUsers');
expect(response.body).toHaveProperty('activeCreators');
expect(response.body).toHaveProperty('liveTransactions');
expect(response.body).toHaveProperty('revenuePerMinute');
expect(response.body).toHaveProperty('systemLoad');
expect(response.body).toHaveProperty('responseTime');
expect(response.body).toHaveProperty('requestsPerSecond');
expect(response.body).toHaveProperty('errorRate');
expect(response.body).toHaveProperty('timestamp');
});
});
describe('GET /realtime/activity', () => {
it('should return activity feed', async () => {
const response = await request(app.getHttpServer())
.get('/realtime/activity')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
it('should accept limit and type parameters', async () => {
await request(app.getHttpServer())
.get('/realtime/activity?limit=20&type=transaction')
.expect(200);
});
});
describe('GET /realtime/active-users', () => {
it('should return active users time series', async () => {
engagementQb.getRawMany.mockResolvedValue([
{ minute: '2026-01-15T12:00:00Z', count: '50' },
]);
const response = await request(app.getHttpServer())
.get('/realtime/active-users')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
it('should accept minutes parameter', async () => {
await request(app.getHttpServer())
.get('/realtime/active-users?minutes=30')
.expect(200);
});
});
});
});