rwadurian/backend/services/wallet-service/test/app.e2e-spec.ts

357 lines
12 KiB
TypeScript

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