370 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|