rwadurian/backend/services/referral-service/test/e2e/app.e2e-spec.ts

370 lines
12 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
import { PrismaService } from '../../src/infrastructure/database/prisma.service';
import { KafkaService } from '../../src/infrastructure/messaging/kafka.service';
import { RedisService } from '../../src/infrastructure/cache/redis.service';
import * as jwt from 'jsonwebtoken';
// Mock services to avoid real connections in E2E tests
class MockPrismaService {
async onModuleInit() {}
async onModuleDestroy() {}
$connect = jest.fn();
$disconnect = jest.fn();
referralRelationship = {
upsert: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
count: jest.fn(),
};
teamStatistics = {
upsert: jest.fn(),
create: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
count: jest.fn(),
update: jest.fn(),
};
directReferral = {
upsert: jest.fn(),
findMany: jest.fn(),
};
$transaction = jest.fn((fn) => fn(this));
}
class MockKafkaService {
async onModuleInit() {}
async onModuleDestroy() {}
publish = jest.fn();
publishBatch = jest.fn();
subscribe = jest.fn();
}
class MockRedisService {
async onModuleInit() {}
async onModuleDestroy() {}
get = jest.fn().mockResolvedValue(null);
set = jest.fn();
del = jest.fn();
zadd = jest.fn();
zrevrank = jest.fn().mockResolvedValue(null);
zrevrange = jest.fn().mockResolvedValue([]);
zrevrangeWithScores = jest.fn().mockResolvedValue([]);
zincrby = jest.fn();
}
describe('Referral Service (E2E)', () => {
let app: INestApplication;
let mockPrisma: MockPrismaService;
const JWT_SECRET = 'test-jwt-secret-for-e2e-tests';
const generateToken = (userId: bigint) => {
return jwt.sign(
{ sub: `user-${userId}`, userId: userId.toString(), type: 'access' },
JWT_SECRET,
{ expiresIn: '1h' },
);
};
beforeAll(async () => {
mockPrisma = new MockPrismaService();
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(PrismaService)
.useValue(mockPrisma)
.overrideProvider(KafkaService)
.useClass(MockKafkaService)
.overrideProvider(RedisService)
.useClass(MockRedisService)
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
);
app.setGlobalPrefix('api/v1');
await app.init();
});
afterAll(async () => {
await app.close();
});
beforeEach(() => {
jest.clearAllMocks();
});
describe('Health Endpoints', () => {
it('GET /api/v1/health - should return health status', () => {
return request(app.getHttpServer())
.get('/api/v1/health')
.expect(200)
.expect((res) => {
expect(res.body.status).toBe('ok');
expect(res.body.service).toBe('referral-service');
});
});
it('GET /api/v1/health/ready - should return ready status', () => {
return request(app.getHttpServer())
.get('/api/v1/health/ready')
.expect(200)
.expect((res) => {
expect(res.body.status).toBe('ready');
});
});
it('GET /api/v1/health/live - should return alive status', () => {
return request(app.getHttpServer())
.get('/api/v1/health/live')
.expect(200)
.expect((res) => {
expect(res.body.status).toBe('alive');
});
});
});
describe('Referral Endpoints', () => {
describe('GET /api/v1/referral/validate/:code', () => {
it('should return valid=true for existing referral code', async () => {
mockPrisma.referralRelationship.findUnique.mockResolvedValueOnce({
id: 1n,
userId: 100n,
myReferralCode: 'RWATEST123',
referrerId: null,
ancestorPath: [],
createdAt: new Date(),
updatedAt: new Date(),
});
return request(app.getHttpServer())
.get('/api/v1/referral/validate/RWATEST123')
.expect(200)
.expect((res) => {
expect(res.body.valid).toBe(true);
expect(res.body.referrerId).toBe('100');
});
});
it('should return valid=false for non-existing referral code', async () => {
mockPrisma.referralRelationship.findUnique.mockResolvedValueOnce(null);
return request(app.getHttpServer())
.get('/api/v1/referral/validate/NONEXISTENT')
.expect(200)
.expect((res) => {
expect(res.body.valid).toBe(false);
});
});
});
describe('POST /api/v1/referral/validate', () => {
it('should validate referral code via POST', async () => {
mockPrisma.referralRelationship.findUnique.mockResolvedValueOnce({
id: 1n,
userId: 100n,
myReferralCode: 'RWATEST123',
referrerId: null,
ancestorPath: [],
createdAt: new Date(),
updatedAt: new Date(),
});
return request(app.getHttpServer())
.post('/api/v1/referral/validate')
.send({ code: 'RWATEST123' })
.expect(200)
.expect((res) => {
expect(res.body.valid).toBe(true);
});
});
it('should reject invalid code format', () => {
return request(app.getHttpServer())
.post('/api/v1/referral/validate')
.send({ code: 'abc' }) // Too short
.expect(400);
});
});
describe('GET /api/v1/referral/me (Protected)', () => {
it('should return 401 without token', () => {
return request(app.getHttpServer()).get('/api/v1/referral/me').expect(401);
});
it('should return user referral info with valid token', async () => {
const userId = 100n;
const token = generateToken(userId);
mockPrisma.referralRelationship.findUnique.mockResolvedValueOnce({
id: 1n,
userId,
myReferralCode: 'RWATEST123',
referrerId: null,
ancestorPath: [],
createdAt: new Date(),
updatedAt: new Date(),
});
mockPrisma.teamStatistics.findUnique.mockResolvedValueOnce({
id: 1n,
userId,
directReferralCount: 5,
totalTeamCount: 100,
selfPlantingCount: 10,
totalTeamPlantingCount: 90,
effectivePlantingCountForRanking: 60,
maxSingleTeamPlantingCount: 40,
provinceCityDistribution: {},
lastCalcAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
});
mockPrisma.directReferral.findMany.mockResolvedValueOnce([]);
return request(app.getHttpServer())
.get('/api/v1/referral/me')
.set('Authorization', `Bearer ${token}`)
.expect(200)
.expect((res) => {
expect(res.body.userId).toBe('100');
expect(res.body.referralCode).toBe('RWATEST123');
expect(res.body.directReferralCount).toBe(5);
});
});
});
});
describe('Leaderboard Endpoints', () => {
describe('GET /api/v1/leaderboard', () => {
it('should return leaderboard entries', async () => {
mockPrisma.teamStatistics.findMany.mockResolvedValueOnce([
{
userId: 100n,
effectivePlantingCountForRanking: 100,
totalTeamPlantingCount: 150,
directReferralCount: 10,
},
{
userId: 101n,
effectivePlantingCountForRanking: 80,
totalTeamPlantingCount: 100,
directReferralCount: 5,
},
]);
return request(app.getHttpServer())
.get('/api/v1/leaderboard')
.expect(200)
.expect((res) => {
expect(res.body.entries).toBeDefined();
expect(res.body.entries.length).toBe(2);
expect(res.body.entries[0].rank).toBe(1);
expect(res.body.entries[0].score).toBe(100);
});
});
it('should support pagination', async () => {
mockPrisma.teamStatistics.findMany.mockResolvedValueOnce([]);
return request(app.getHttpServer())
.get('/api/v1/leaderboard?limit=10&offset=20')
.expect(200)
.expect((res) => {
expect(res.body.entries).toBeDefined();
});
});
});
describe('GET /api/v1/leaderboard/me (Protected)', () => {
it('should return user rank with valid token', async () => {
const userId = 100n;
const token = generateToken(userId);
mockPrisma.teamStatistics.findUnique.mockResolvedValueOnce({
effectivePlantingCountForRanking: 50,
});
mockPrisma.teamStatistics.count.mockResolvedValueOnce(5);
mockPrisma.teamStatistics.findMany.mockResolvedValueOnce([
{ userId: 100n, effectivePlantingCountForRanking: 50 },
]);
return request(app.getHttpServer())
.get('/api/v1/leaderboard/me')
.set('Authorization', `Bearer ${token}`)
.expect(200)
.expect((res) => {
expect(res.body.userId).toBe('100');
expect(res.body.rank).toBeDefined();
});
});
});
});
describe('Team Statistics Endpoints', () => {
describe('GET /api/v1/team-statistics/me/distribution (Protected)', () => {
it('should return province/city distribution', async () => {
const userId = 100n;
const token = generateToken(userId);
mockPrisma.teamStatistics.findUnique.mockResolvedValueOnce({
id: 1n,
userId,
directReferralCount: 5,
totalTeamCount: 100,
selfPlantingCount: 10,
totalTeamPlantingCount: 90,
effectivePlantingCountForRanking: 60,
maxSingleTeamPlantingCount: 40,
provinceCityDistribution: {
'110000': { '110100': 50, '110200': 30 },
'120000': { '120100': 20 },
},
lastCalcAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
});
mockPrisma.directReferral.findMany.mockResolvedValueOnce([]);
return request(app.getHttpServer())
.get('/api/v1/team-statistics/me/distribution')
.set('Authorization', `Bearer ${token}`)
.expect(200)
.expect((res) => {
expect(res.body.provinces).toBeDefined();
expect(res.body.totalCount).toBeDefined();
});
});
});
});
describe('Error Handling', () => {
it('should return 404 for non-existent routes', () => {
return request(app.getHttpServer()).get('/api/v1/nonexistent').expect(404);
});
it('should return 401 for protected routes without auth', () => {
return request(app.getHttpServer()).get('/api/v1/referral/me').expect(401);
});
it('should return 401 for invalid token', () => {
return request(app.getHttpServer())
.get('/api/v1/referral/me')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});