225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
import request from 'supertest';
|
|
import app from '@/app';
|
|
import { TestHelpers } from '../utils/test-helpers';
|
|
import { PaymentType, Currency, PaymentStatus } from '@/models/payment';
|
|
import { PaymentRequest } from '@/gateway/validation/payment-validation';
|
|
|
|
describe('E2E Payment Workflow', () => {
|
|
let makerToken: string;
|
|
let checkerToken: string;
|
|
let makerOperator: any;
|
|
let checkerOperator: any;
|
|
|
|
beforeAll(async () => {
|
|
// Clean database first (non-blocking)
|
|
TestHelpers.cleanDatabase().catch(() => {}); // Fire and forget
|
|
|
|
// Create test operators with timeout protection
|
|
const operatorPromises = [
|
|
TestHelpers.createTestOperator('E2E_MAKER', 'MAKER' as any, 'Test123!@#'),
|
|
TestHelpers.createTestOperator('E2E_CHECKER', 'CHECKER' as any, 'Test123!@#')
|
|
];
|
|
|
|
[makerOperator, checkerOperator] = await Promise.all(operatorPromises);
|
|
|
|
// Generate tokens
|
|
makerToken = TestHelpers.generateTestToken(
|
|
makerOperator.operatorId,
|
|
makerOperator.id,
|
|
makerOperator.role
|
|
);
|
|
checkerToken = TestHelpers.generateTestToken(
|
|
checkerOperator.operatorId,
|
|
checkerOperator.id,
|
|
checkerOperator.role
|
|
);
|
|
}, 90000);
|
|
|
|
afterAll(async () => {
|
|
// Fast cleanup with timeout protection
|
|
await Promise.race([
|
|
TestHelpers.cleanDatabase(),
|
|
new Promise(resolve => setTimeout(resolve, 5000))
|
|
]);
|
|
}, 30000);
|
|
|
|
beforeEach(async () => {
|
|
// Skip cleanup in beforeEach to speed up tests
|
|
// Tests should clean up their own data
|
|
});
|
|
|
|
describe('Complete Payment Flow', () => {
|
|
it('should complete full payment workflow: initiate → approve → process', async () => {
|
|
// Step 1: Maker initiates payment
|
|
const paymentRequest: PaymentRequest = {
|
|
type: PaymentType.CUSTOMER_CREDIT_TRANSFER,
|
|
amount: 1000.50,
|
|
currency: Currency.USD,
|
|
senderAccount: 'ACC001',
|
|
senderBIC: 'TESTBIC1',
|
|
receiverAccount: 'ACC002',
|
|
receiverBIC: 'TESTBIC2',
|
|
beneficiaryName: 'E2E Test Beneficiary',
|
|
purpose: 'E2E test payment',
|
|
};
|
|
|
|
const initiateResponse = await request(app)
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send(paymentRequest)
|
|
.expect(201);
|
|
|
|
expect(initiateResponse.body.paymentId).toBeDefined();
|
|
expect(initiateResponse.body.status).toBe(PaymentStatus.PENDING_APPROVAL);
|
|
|
|
const paymentId = initiateResponse.body.paymentId;
|
|
|
|
// Step 2: Checker approves payment
|
|
const approveResponse = await request(app)
|
|
.post(`/api/v1/payments/${paymentId}/approve`)
|
|
.set('Authorization', `Bearer ${checkerToken}`)
|
|
.expect(200);
|
|
|
|
expect(approveResponse.body.message).toContain('approved');
|
|
|
|
// Step 3: Verify payment status updated
|
|
// Note: Processing happens asynchronously, so we check status
|
|
const statusResponse = await request(app)
|
|
.get(`/api/v1/payments/${paymentId}`)
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.expect(200);
|
|
|
|
expect(statusResponse.body.paymentId).toBe(paymentId);
|
|
expect(statusResponse.body.status).toBeDefined();
|
|
});
|
|
|
|
it('should reject payment when checker rejects', async () => {
|
|
const paymentRequest: PaymentRequest = {
|
|
type: PaymentType.CUSTOMER_CREDIT_TRANSFER,
|
|
amount: 2000,
|
|
currency: Currency.USD,
|
|
senderAccount: 'ACC003',
|
|
senderBIC: 'TESTBIC3',
|
|
receiverAccount: 'ACC004',
|
|
receiverBIC: 'TESTBIC4',
|
|
beneficiaryName: 'Test Beneficiary Reject',
|
|
};
|
|
|
|
const initiateResponse = await request(app)
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send(paymentRequest)
|
|
.expect(201);
|
|
|
|
const paymentId = initiateResponse.body.paymentId;
|
|
|
|
// Checker rejects payment
|
|
const rejectResponse = await request(app)
|
|
.post(`/api/v1/payments/${paymentId}/reject`)
|
|
.set('Authorization', `Bearer ${checkerToken}`)
|
|
.send({ reason: 'E2E test rejection' })
|
|
.expect(200);
|
|
|
|
expect(rejectResponse.body.message).toContain('rejected');
|
|
|
|
// Verify status
|
|
const statusResponse = await request(app)
|
|
.get(`/api/v1/payments/${paymentId}`)
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.expect(200);
|
|
|
|
expect(['REJECTED', 'CANCELLED']).toContain(statusResponse.body.status);
|
|
});
|
|
|
|
it('should enforce dual control - maker cannot approve', async () => {
|
|
const paymentRequest: PaymentRequest = {
|
|
type: PaymentType.CUSTOMER_CREDIT_TRANSFER,
|
|
amount: 3000,
|
|
currency: Currency.EUR,
|
|
senderAccount: 'ACC005',
|
|
senderBIC: 'TESTBIC5',
|
|
receiverAccount: 'ACC006',
|
|
receiverBIC: 'TESTBIC6',
|
|
beneficiaryName: 'Test Dual Control',
|
|
};
|
|
|
|
const initiateResponse = await request(app)
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send(paymentRequest)
|
|
.expect(201);
|
|
|
|
const paymentId = initiateResponse.body.paymentId;
|
|
|
|
// Maker tries to approve - should fail
|
|
await request(app)
|
|
.post(`/api/v1/payments/${paymentId}/approve`)
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.expect(403); // Forbidden
|
|
});
|
|
|
|
it('should allow maker to cancel before approval', async () => {
|
|
const paymentRequest: PaymentRequest = {
|
|
type: PaymentType.CUSTOMER_CREDIT_TRANSFER,
|
|
amount: 4000,
|
|
currency: Currency.GBP,
|
|
senderAccount: 'ACC007',
|
|
senderBIC: 'TESTBIC7',
|
|
receiverAccount: 'ACC008',
|
|
receiverBIC: 'TESTBIC8',
|
|
beneficiaryName: 'Test Cancellation',
|
|
};
|
|
|
|
const initiateResponse = await request(app)
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send(paymentRequest)
|
|
.expect(201);
|
|
|
|
const paymentId = initiateResponse.body.paymentId;
|
|
|
|
// Maker cancels payment
|
|
const cancelResponse = await request(app)
|
|
.post(`/api/v1/payments/${paymentId}/cancel`)
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send({ reason: 'E2E test cancellation' })
|
|
.expect(200);
|
|
|
|
expect(cancelResponse.body.message).toContain('cancelled');
|
|
});
|
|
});
|
|
|
|
describe('Payment Listing', () => {
|
|
it('should list payments with pagination', async () => {
|
|
// Create multiple payments
|
|
for (let i = 0; i < 3; i++) {
|
|
const paymentRequest: PaymentRequest = {
|
|
type: PaymentType.CUSTOMER_CREDIT_TRANSFER,
|
|
amount: 1000 + i * 100,
|
|
currency: Currency.USD,
|
|
senderAccount: `ACC${i}`,
|
|
senderBIC: `TESTBIC${i}`,
|
|
receiverAccount: `ACCR${i}`,
|
|
receiverBIC: `TESTBICR${i}`,
|
|
beneficiaryName: `Beneficiary ${i}`,
|
|
};
|
|
|
|
await request(app)
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.send(paymentRequest)
|
|
.expect(201);
|
|
}
|
|
|
|
const listResponse = await request(app)
|
|
.get('/api/v1/payments?limit=2&offset=0')
|
|
.set('Authorization', `Bearer ${makerToken}`)
|
|
.expect(200);
|
|
|
|
expect(listResponse.body.payments).toBeDefined();
|
|
expect(listResponse.body.payments.length).toBeLessThanOrEqual(2);
|
|
});
|
|
});
|
|
});
|
|
|