import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import * as jwt from 'jsonwebtoken'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/infrastructure/persistence/prisma/prisma.service'; describe('Wallet Service (e2e) - Real Database', () => { let app: INestApplication; let prisma: PrismaService; let authToken: string; const testUserId = '99999'; // Unique test user ID const jwtSecret = process.env.JWT_SECRET || 'test-jwt-secret-key-for-e2e-testing'; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.setGlobalPrefix('api/v1'); // ValidationPipe is needed for request validation // DomainExceptionFilter and TransformInterceptor are already provided globally by AppModule app.useGlobalPipes( new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, }), ); prisma = app.get(PrismaService); await app.init(); // Generate test JWT token authToken = jwt.sign( { sub: testUserId, seq: 1001 }, jwtSecret, { expiresIn: '1h' }, ); // Clean up any existing test data await cleanupTestData(); }); afterAll(async () => { // Clean up test data await cleanupTestData(); await app.close(); }); async function cleanupTestData() { try { await prisma.ledgerEntry.deleteMany({ where: { userId: BigInt(testUserId) } }); await prisma.depositOrder.deleteMany({ where: { userId: BigInt(testUserId) } }); await prisma.settlementOrder.deleteMany({ where: { userId: BigInt(testUserId) } }); await prisma.walletAccount.deleteMany({ where: { userId: BigInt(testUserId) } }); } catch (e) { console.log('Cleanup error (may be expected):', e); } } describe('Health Check', () => { it('/api/v1/health (GET) - should return health status', () => { return request(app.getHttpServer()) .get('/api/v1/health') .expect(200) .expect(res => { expect(res.body.success).toBe(true); expect(res.body.data.status).toBe('ok'); expect(res.body.data.service).toBe('wallet-service'); }); }); }); describe('Authentication', () => { it('/api/v1/wallet/my-wallet (GET) - should reject without auth', () => { return request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .expect(401); }); it('/api/v1/wallet/my-wallet (GET) - should reject with invalid token', () => { return request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .set('Authorization', 'Bearer invalid_token') .expect(401); }); }); describe('Wallet Operations', () => { it('/api/v1/wallet/my-wallet (GET) - should return wallet info (creates wallet if not exists)', async () => { const res = await request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('walletId'); expect(res.body.data).toHaveProperty('userId', testUserId); expect(res.body.data).toHaveProperty('balances'); expect(res.body.data).toHaveProperty('hashpower'); expect(res.body.data).toHaveProperty('rewards'); expect(res.body.data).toHaveProperty('status'); expect(res.body.data.status).toBe('ACTIVE'); }); it('/api/v1/wallet/my-wallet (GET) - should have correct balance structure', async () => { const res = await request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .set('Authorization', `Bearer ${authToken}`) .expect(200); const { balances } = res.body.data; expect(balances).toHaveProperty('usdt'); expect(balances).toHaveProperty('dst'); expect(balances).toHaveProperty('bnb'); expect(balances).toHaveProperty('og'); expect(balances).toHaveProperty('rwad'); expect(balances.usdt).toHaveProperty('available'); expect(balances.usdt).toHaveProperty('frozen'); expect(typeof balances.usdt.available).toBe('number'); }); }); describe('Ledger Operations', () => { it('/api/v1/wallet/ledger/my-ledger (GET) - should return ledger entries', async () => { const res = await request(app.getHttpServer()) .get('/api/v1/wallet/ledger/my-ledger') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(res.body.success).toBe(true); expect(res.body.data).toHaveProperty('data'); expect(res.body.data).toHaveProperty('total'); expect(res.body.data).toHaveProperty('page'); expect(res.body.data).toHaveProperty('pageSize'); expect(res.body.data).toHaveProperty('totalPages'); expect(Array.isArray(res.body.data.data)).toBe(true); }); it('/api/v1/wallet/ledger/my-ledger (GET) - should support pagination', async () => { const res = await request(app.getHttpServer()) .get('/api/v1/wallet/ledger/my-ledger?page=1&pageSize=5') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(res.body.data.page).toBe(1); expect(res.body.data.pageSize).toBe(5); }); it('/api/v1/wallet/ledger/my-ledger (GET) - should reject invalid pagination', async () => { await request(app.getHttpServer()) .get('/api/v1/wallet/ledger/my-ledger?page=0') .set('Authorization', `Bearer ${authToken}`) .expect(400); }); }); describe('Deposit Operations (Internal API)', () => { const testTxHash = `test_tx_e2e_${Date.now()}`; it('/api/v1/wallet/deposit (POST) - should process deposit', async () => { const res = await request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .send({ userId: testUserId, amount: 100, chainType: 'KAVA', txHash: testTxHash, }) .expect(201); expect(res.body.success).toBe(true); expect(res.body.data.message).toBe('Deposit processed successfully'); // Verify balance was updated const walletRes = await request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(walletRes.body.data.balances.usdt.available).toBe(100); }); it('/api/v1/wallet/deposit (POST) - should reject duplicate transaction', async () => { const res = await request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .send({ userId: testUserId, amount: 50, chainType: 'KAVA', txHash: testTxHash, // Same txHash as before }) .expect(409); expect(res.body.success).toBe(false); expect(res.body.code).toBe('DUPLICATE_TRANSACTION'); }); it('/api/v1/wallet/deposit (POST) - should validate request body', async () => { await request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .send({ userId: testUserId, amount: -100, // Invalid negative amount chainType: 'KAVA', txHash: 'invalid_tx', }) .expect(400); }); it('/api/v1/wallet/deposit (POST) - should validate chain type', async () => { await request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .send({ userId: testUserId, amount: 100, chainType: 'INVALID_CHAIN', txHash: 'tx_invalid_chain', }) .expect(400); }); it('/api/v1/wallet/deposit (POST) - should process BSC deposit', async () => { const bscTxHash = `test_bsc_tx_${Date.now()}`; const res = await request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .send({ userId: testUserId, amount: 50, chainType: 'BSC', txHash: bscTxHash, }) .expect(201); expect(res.body.success).toBe(true); // Verify balance was updated (should be 100 + 50 = 150) const walletRes = await request(app.getHttpServer()) .get('/api/v1/wallet/my-wallet') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(walletRes.body.data.balances.usdt.available).toBe(150); }); }); describe('Ledger After Deposits', () => { it('should have ledger entries after deposits', async () => { const res = await request(app.getHttpServer()) .get('/api/v1/wallet/ledger/my-ledger') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(res.body.data.total).toBeGreaterThanOrEqual(2); // At least 2 deposits expect(res.body.data.data.length).toBeGreaterThanOrEqual(2); // Check ledger entry structure const entry = res.body.data.data[0]; expect(entry).toHaveProperty('id'); expect(entry).toHaveProperty('entryType'); expect(entry).toHaveProperty('amount'); expect(entry).toHaveProperty('assetType'); expect(entry).toHaveProperty('createdAt'); }); }); describe('Settlement Operations', () => { it('/api/v1/wallet/claim-rewards (POST) - should handle no pending rewards', async () => { const res = await request(app.getHttpServer()) .post('/api/v1/wallet/claim-rewards') .set('Authorization', `Bearer ${authToken}`) .expect(400); expect(res.body.success).toBe(false); }); it('/api/v1/wallet/settle (POST) - should validate settle currency', async () => { await request(app.getHttpServer()) .post('/api/v1/wallet/settle') .set('Authorization', `Bearer ${authToken}`) .send({ usdtAmount: 10, settleCurrency: 'INVALID', }) .expect(400); }); it('/api/v1/wallet/settle (POST) - should validate amount', async () => { await request(app.getHttpServer()) .post('/api/v1/wallet/settle') .set('Authorization', `Bearer ${authToken}`) .send({ usdtAmount: -10, settleCurrency: 'BNB', }) .expect(400); }); }); describe('Error Handling', () => { it('should return 404 for non-existent routes', () => { return request(app.getHttpServer()) .get('/api/v1/non-existent') .expect(404); }); it('should handle malformed JSON', () => { return request(app.getHttpServer()) .post('/api/v1/wallet/deposit') .set('Content-Type', 'application/json') .send('{ invalid json }') .expect(400); }); }); describe('Database Integrity', () => { it('should persist wallet data correctly', async () => { const wallet = await prisma.walletAccount.findFirst({ where: { userId: BigInt(testUserId) }, }); expect(wallet).not.toBeNull(); expect(wallet?.status).toBe('ACTIVE'); expect(Number(wallet?.usdtAvailable)).toBe(150); // 100 + 50 from deposits }); it('should persist ledger entries correctly', async () => { const entries = await prisma.ledgerEntry.findMany({ where: { userId: BigInt(testUserId) }, orderBy: { createdAt: 'desc' }, }); expect(entries.length).toBeGreaterThanOrEqual(2); // Check that deposits are recorded const depositEntries = entries.filter(e => e.entryType === 'DEPOSIT_KAVA' || e.entryType === 'DEPOSIT_BSC' ); expect(depositEntries.length).toBe(2); }); it('should persist deposit orders correctly', async () => { const deposits = await prisma.depositOrder.findMany({ where: { userId: BigInt(testUserId) }, }); expect(deposits.length).toBe(2); deposits.forEach(deposit => { expect(deposit.status).toBe('CONFIRMED'); }); }); }); });