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); }); }); });