319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
import request from 'supertest';
|
|
import { App } from 'supertest/types';
|
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
import { HealthController } from '../src/api/controllers/health.controller';
|
|
import { RewardController } from '../src/api/controllers/reward.controller';
|
|
import { SettlementController } from '../src/api/controllers/settlement.controller';
|
|
import { RewardApplicationService } from '../src/application/services/reward-application.service';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { JwtModule } from '@nestjs/jwt';
|
|
import { PassportModule } from '@nestjs/passport';
|
|
import { JwtStrategy } from '../src/shared/strategies/jwt.strategy';
|
|
import { RewardStatus } from '../src/domain/value-objects/reward-status.enum';
|
|
import { RightType } from '../src/domain/value-objects/right-type.enum';
|
|
|
|
describe('Reward Service (e2e)', () => {
|
|
let app: INestApplication<App>;
|
|
let jwtService: JwtService;
|
|
let mockRewardService: any;
|
|
|
|
const TEST_JWT_SECRET = 'test-secret-key-for-testing';
|
|
|
|
const createTestToken = (userId: string = '100') => {
|
|
return jwtService.sign({
|
|
sub: userId,
|
|
username: 'testuser',
|
|
roles: ['user'],
|
|
});
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
mockRewardService = {
|
|
getRewardSummary: jest.fn().mockResolvedValue({
|
|
pendingUsdt: 1000,
|
|
pendingHashpower: 0.5,
|
|
pendingExpireAt: new Date(Date.now() + 12 * 60 * 60 * 1000),
|
|
settleableUsdt: 500,
|
|
settleableHashpower: 0.2,
|
|
settledTotalUsdt: 2000,
|
|
settledTotalHashpower: 1.0,
|
|
expiredTotalUsdt: 100,
|
|
expiredTotalHashpower: 0.1,
|
|
}),
|
|
getRewardDetails: jest.fn().mockResolvedValue({
|
|
data: [
|
|
{
|
|
id: '1',
|
|
rightType: RightType.SHARE_RIGHT,
|
|
usdtAmount: 500,
|
|
hashpowerAmount: 0,
|
|
rewardStatus: RewardStatus.PENDING,
|
|
createdAt: new Date(),
|
|
expireAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
remainingTimeMs: 86400000,
|
|
claimedAt: null,
|
|
settledAt: null,
|
|
expiredAt: null,
|
|
memo: 'Test reward',
|
|
},
|
|
],
|
|
pagination: {
|
|
page: 1,
|
|
pageSize: 20,
|
|
total: 1,
|
|
},
|
|
}),
|
|
getPendingRewards: jest.fn().mockResolvedValue([
|
|
{
|
|
id: '1',
|
|
rightType: RightType.SHARE_RIGHT,
|
|
usdtAmount: 500,
|
|
hashpowerAmount: 0,
|
|
createdAt: new Date(),
|
|
expireAt: new Date(Date.now() + 12 * 60 * 60 * 1000),
|
|
remainingTimeMs: 43200000,
|
|
memo: 'Pending reward',
|
|
},
|
|
]),
|
|
settleRewards: jest.fn().mockResolvedValue({
|
|
success: true,
|
|
settledUsdtAmount: 500,
|
|
receivedAmount: 0.25,
|
|
settleCurrency: 'BNB',
|
|
txHash: '0x123abc',
|
|
}),
|
|
};
|
|
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [
|
|
ConfigModule.forRoot({
|
|
isGlobal: true,
|
|
load: [() => ({ JWT_SECRET: TEST_JWT_SECRET })],
|
|
}),
|
|
PassportModule,
|
|
JwtModule.register({
|
|
secret: TEST_JWT_SECRET,
|
|
signOptions: { expiresIn: '1h' },
|
|
}),
|
|
],
|
|
controllers: [HealthController, RewardController, SettlementController],
|
|
providers: [
|
|
{
|
|
provide: RewardApplicationService,
|
|
useValue: mockRewardService,
|
|
},
|
|
{
|
|
provide: JwtStrategy,
|
|
useFactory: (configService: ConfigService) => {
|
|
return new JwtStrategy({
|
|
get: (key: string) => key === 'JWT_SECRET' ? TEST_JWT_SECRET : undefined,
|
|
} as ConfigService);
|
|
},
|
|
inject: [ConfigService],
|
|
},
|
|
],
|
|
}).compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
|
await app.init();
|
|
|
|
jwtService = moduleFixture.get<JwtService>(JwtService);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('Health Check', () => {
|
|
it('/health (GET) should return healthy status', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/health')
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.status).toBe('ok');
|
|
expect(res.body.service).toBe('reward-service');
|
|
expect(res.body.timestamp).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Rewards API', () => {
|
|
describe('GET /rewards/summary', () => {
|
|
it('should return 401 without auth token', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/summary')
|
|
.expect(401);
|
|
});
|
|
|
|
it('should return reward summary with valid token', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/summary')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.pendingUsdt).toBe(1000);
|
|
expect(res.body.settleableUsdt).toBe(500);
|
|
expect(res.body.settledTotalUsdt).toBe(2000);
|
|
expect(res.body.expiredTotalUsdt).toBe(100);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GET /rewards/details', () => {
|
|
it('should return 401 without auth token', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/details')
|
|
.expect(401);
|
|
});
|
|
|
|
it('should return paginated reward details with valid token', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/details')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body.data).toHaveLength(1);
|
|
expect(res.body.data[0].usdtAmount).toBe(500);
|
|
expect(res.body.pagination.total).toBe(1);
|
|
});
|
|
});
|
|
|
|
it('should accept filter parameters', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/details')
|
|
.query({ status: RewardStatus.PENDING, page: 1, pageSize: 10 })
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
});
|
|
});
|
|
|
|
describe('GET /rewards/pending', () => {
|
|
it('should return 401 without auth token', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/pending')
|
|
.expect(401);
|
|
});
|
|
|
|
it('should return pending rewards with countdown', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.get('/rewards/pending')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200)
|
|
.expect((res) => {
|
|
expect(res.body).toHaveLength(1);
|
|
expect(res.body[0].usdtAmount).toBe(500);
|
|
expect(res.body[0].remainingTimeMs).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Settlement API', () => {
|
|
describe('POST /rewards/settle', () => {
|
|
it('should return 401 without auth token', () => {
|
|
return request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.send({ settleCurrency: 'BNB' })
|
|
.expect(401);
|
|
});
|
|
|
|
it('should settle rewards successfully with valid token', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ settleCurrency: 'BNB' })
|
|
.expect(201)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.body.settledUsdtAmount).toBe(500);
|
|
expect(res.body.receivedAmount).toBe(0.25);
|
|
expect(res.body.settleCurrency).toBe('BNB');
|
|
expect(res.body.txHash).toBe('0x123abc');
|
|
});
|
|
});
|
|
|
|
it('should validate settleCurrency parameter', () => {
|
|
const token = createTestToken();
|
|
return request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ settleCurrency: '' })
|
|
.expect(400);
|
|
});
|
|
|
|
it('should accept different settlement currencies', async () => {
|
|
const token = createTestToken();
|
|
const currencies = ['BNB', 'OG', 'USDT', 'DST'];
|
|
|
|
for (const currency of currencies) {
|
|
mockRewardService.settleRewards.mockResolvedValueOnce({
|
|
success: true,
|
|
settledUsdtAmount: 500,
|
|
receivedAmount: currency === 'USDT' ? 500 : 0.25,
|
|
settleCurrency: currency,
|
|
txHash: '0x123',
|
|
});
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ settleCurrency: currency })
|
|
.expect(201);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Settlement failure scenarios', () => {
|
|
it('should handle no settleable rewards', () => {
|
|
const token = createTestToken();
|
|
mockRewardService.settleRewards.mockResolvedValueOnce({
|
|
success: false,
|
|
settledUsdtAmount: 0,
|
|
receivedAmount: 0,
|
|
settleCurrency: 'BNB',
|
|
error: '没有可结算的收益',
|
|
});
|
|
|
|
return request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ settleCurrency: 'BNB' })
|
|
.expect(201)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(false);
|
|
expect(res.body.error).toBe('没有可结算的收益');
|
|
});
|
|
});
|
|
|
|
it('should handle wallet service failure', () => {
|
|
const token = createTestToken();
|
|
mockRewardService.settleRewards.mockResolvedValueOnce({
|
|
success: false,
|
|
settledUsdtAmount: 500,
|
|
receivedAmount: 0,
|
|
settleCurrency: 'BNB',
|
|
error: 'Insufficient liquidity',
|
|
});
|
|
|
|
return request(app.getHttpServer())
|
|
.post('/rewards/settle')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ settleCurrency: 'BNB' })
|
|
.expect(201)
|
|
.expect((res) => {
|
|
expect(res.body.success).toBe(false);
|
|
expect(res.body.error).toBe('Insufficient liquidity');
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|