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