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