Initial commit: add .gitignore and README
This commit is contained in:
310
tests/e2e/exports/export-workflow.test.ts
Normal file
310
tests/e2e/exports/export-workflow.test.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* E2E Tests for Export Workflow
|
||||
*
|
||||
* Complete end-to-end tests for export functionality from API to file download
|
||||
*/
|
||||
|
||||
import request from 'supertest';
|
||||
import app from '@/app';
|
||||
import { TestHelpers } from '../../utils/test-helpers';
|
||||
import { PaymentRepository } from '@/repositories/payment-repository';
|
||||
import { MessageRepository } from '@/repositories/message-repository';
|
||||
import { PaymentStatus } from '@/models/payment';
|
||||
import { MessageType, MessageStatus } from '@/models/message';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { query } from '@/database/connection';
|
||||
|
||||
describe('Export Workflow E2E', () => {
|
||||
let authToken: string;
|
||||
let paymentRepository: PaymentRepository;
|
||||
let messageRepository: MessageRepository;
|
||||
let testPaymentIds: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
paymentRepository = new PaymentRepository();
|
||||
messageRepository = new MessageRepository();
|
||||
// Clean database (non-blocking)
|
||||
TestHelpers.cleanDatabase().catch(() => {}); // Fire and forget
|
||||
}, 30000);
|
||||
|
||||
beforeEach(async () => {
|
||||
// Skip cleanup in beforeEach to speed up tests - use timeout protection if needed
|
||||
await Promise.race([
|
||||
TestHelpers.cleanDatabase().catch(() => {}), // Ignore errors
|
||||
new Promise(resolve => setTimeout(resolve, 2000)) // Max 2 seconds
|
||||
]);
|
||||
testPaymentIds = [];
|
||||
|
||||
// Create test operator with CHECKER role
|
||||
const operator = await TestHelpers.createTestOperator('TEST_E2E_EXPORT', 'CHECKER' as any);
|
||||
authToken = TestHelpers.generateTestToken(operator.operatorId, operator.id, 'CHECKER' as any);
|
||||
|
||||
// Create multiple test payments with messages for comprehensive testing
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const paymentRequest = TestHelpers.createTestPaymentRequest();
|
||||
paymentRequest.amount = 1000 + i * 100;
|
||||
const paymentId = await paymentRepository.create(
|
||||
paymentRequest,
|
||||
operator.id,
|
||||
`TEST-E2E-${Date.now()}-${i}`
|
||||
);
|
||||
|
||||
const uetr = uuidv4();
|
||||
const internalTxnId = `TXN-E2E-${i}`;
|
||||
|
||||
await paymentRepository.update(paymentId, {
|
||||
internalTransactionId: internalTxnId,
|
||||
uetr,
|
||||
status: PaymentStatus.LEDGER_POSTED,
|
||||
});
|
||||
|
||||
// Create ledger posting
|
||||
await query(
|
||||
`INSERT INTO ledger_postings (
|
||||
internal_transaction_id, payment_id, account_number, transaction_type,
|
||||
amount, currency, status, posting_timestamp, reference
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
internalTxnId,
|
||||
paymentId,
|
||||
paymentRequest.senderAccount,
|
||||
'DEBIT',
|
||||
paymentRequest.amount,
|
||||
paymentRequest.currency,
|
||||
'POSTED',
|
||||
new Date(),
|
||||
paymentId,
|
||||
]
|
||||
);
|
||||
|
||||
// Create ISO message
|
||||
const messageId = uuidv4();
|
||||
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10">
|
||||
<FIToFICstmrCdtTrf>
|
||||
<GrpHdr>
|
||||
<MsgId>MSG-E2E-${i}</MsgId>
|
||||
<CreDtTm>${new Date().toISOString()}</CreDtTm>
|
||||
</GrpHdr>
|
||||
<CdtTrfTxInf>
|
||||
<PmtId>
|
||||
<EndToEndId>E2E-${i}</EndToEndId>
|
||||
<TxId>TX-${i}</TxId>
|
||||
<UETR>${uetr}</UETR>
|
||||
</PmtId>
|
||||
<IntrBkSttlmAmt Ccy="${paymentRequest.currency}">${paymentRequest.amount.toFixed(2)}</IntrBkSttlmAmt>
|
||||
</CdtTrfTxInf>
|
||||
</FIToFICstmrCdtTrf>
|
||||
</Document>`;
|
||||
|
||||
await messageRepository.create({
|
||||
id: messageId,
|
||||
messageId: messageId,
|
||||
paymentId,
|
||||
messageType: MessageType.PACS_008,
|
||||
uetr,
|
||||
msgId: `MSG-E2E-${i}`,
|
||||
xmlContent,
|
||||
xmlHash: 'test-hash',
|
||||
status: MessageStatus.VALIDATED,
|
||||
});
|
||||
|
||||
testPaymentIds.push(paymentId);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Fast cleanup with timeout protection
|
||||
await Promise.race([
|
||||
TestHelpers.cleanDatabase(),
|
||||
new Promise(resolve => setTimeout(resolve, 5000))
|
||||
]);
|
||||
}, 30000);
|
||||
|
||||
describe('Complete Export Workflow', () => {
|
||||
it('should complete full export workflow: API request → file generation → download', async () => {
|
||||
// Step 1: Request export via API
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({
|
||||
format: 'raw-iso',
|
||||
scope: 'messages',
|
||||
batch: 'true',
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Step 2: Verify response headers
|
||||
expect(response.headers['content-type']).toContain('application/xml');
|
||||
expect(response.headers['content-disposition']).toContain('attachment');
|
||||
expect(response.headers['x-export-id']).toBeDefined();
|
||||
expect(response.headers['x-record-count']).toBeDefined();
|
||||
|
||||
// Step 3: Verify file content
|
||||
expect(response.text).toContain('urn:iso:std:iso:20022');
|
||||
expect(response.text).toContain('FIToFICstmrCdtTrf');
|
||||
|
||||
// Step 4: Verify export history was recorded
|
||||
const exportId = response.headers['x-export-id'];
|
||||
const historyResult = await query(
|
||||
'SELECT * FROM export_history WHERE id = $1',
|
||||
[exportId]
|
||||
);
|
||||
|
||||
expect(historyResult.rows.length).toBe(1);
|
||||
expect(historyResult.rows[0].format).toBe('raw-iso');
|
||||
expect(historyResult.rows[0].scope).toBe('messages');
|
||||
expect(historyResult.rows[0].record_count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should export and verify identity correlation in full scope', async () => {
|
||||
// Step 1: Export with full scope
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({
|
||||
format: 'json',
|
||||
scope: 'full',
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
// Step 2: Parse JSON response
|
||||
const data = JSON.parse(response.text);
|
||||
|
||||
// Step 3: Verify correlation metadata exists
|
||||
expect(data.metadata).toBeDefined();
|
||||
expect(data.metadata.correlation).toBeDefined();
|
||||
expect(Array.isArray(data.metadata.correlation)).toBe(true);
|
||||
|
||||
// Step 4: Verify each correlation has required IDs
|
||||
if (data.metadata.correlation.length > 0) {
|
||||
const correlation = data.metadata.correlation[0];
|
||||
expect(correlation.paymentId).toBeDefined();
|
||||
expect(correlation.ledgerJournalIds).toBeDefined();
|
||||
expect(Array.isArray(correlation.ledgerJournalIds)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle export with date range filtering', async () => {
|
||||
const startDate = new Date();
|
||||
startDate.setDate(startDate.getDate() - 1);
|
||||
const endDate = new Date();
|
||||
endDate.setDate(endDate.getDate() + 1);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({
|
||||
format: 'raw-iso',
|
||||
scope: 'messages',
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.text).toContain('urn:iso:std:iso:20022');
|
||||
});
|
||||
|
||||
it('should export ledger with message correlation', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/ledger')
|
||||
.query({
|
||||
includeMessages: 'true',
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const data = JSON.parse(response.text);
|
||||
expect(data.postings).toBeDefined();
|
||||
expect(data.postings.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify correlation data exists
|
||||
const posting = data.postings[0];
|
||||
expect(posting.correlation).toBeDefined();
|
||||
expect(posting.message).toBeDefined();
|
||||
});
|
||||
|
||||
it('should retrieve identity map via API', async () => {
|
||||
const paymentId = testPaymentIds[0];
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/identity-map')
|
||||
.query({ paymentId })
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
const identityMap = response.body;
|
||||
expect(identityMap.paymentId).toBe(paymentId);
|
||||
expect(identityMap.uetr).toBeDefined();
|
||||
expect(identityMap.ledgerJournalIds).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Workflow', () => {
|
||||
it('should handle invalid date range gracefully', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({
|
||||
format: 'raw-iso',
|
||||
scope: 'messages',
|
||||
startDate: '2024-01-31',
|
||||
endDate: '2024-01-01', // Invalid: end before start
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle missing authentication', async () => {
|
||||
await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({ format: 'raw-iso', scope: 'messages' })
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('should handle insufficient permissions', async () => {
|
||||
const makerOperator = await TestHelpers.createTestOperator('TEST_MAKER_E2E', 'MAKER' as any);
|
||||
const makerToken = TestHelpers.generateTestToken(
|
||||
makerOperator.operatorId,
|
||||
makerOperator.id,
|
||||
'MAKER' as any
|
||||
);
|
||||
|
||||
await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({ format: 'raw-iso', scope: 'messages' })
|
||||
.set('Authorization', `Bearer ${makerToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-Format Export Workflow', () => {
|
||||
it('should export same data in different formats', async () => {
|
||||
const formats = ['raw-iso', 'xmlv2', 'json'];
|
||||
|
||||
for (const format of formats) {
|
||||
const response = await request(app)
|
||||
.get('/api/v1/exports/messages')
|
||||
.query({
|
||||
format,
|
||||
scope: 'messages',
|
||||
})
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.headers['x-export-id']).toBeDefined();
|
||||
expect(response.headers['x-record-count']).toBeDefined();
|
||||
|
||||
// Verify format-specific content
|
||||
if (format === 'raw-iso' || format === 'xmlv2') {
|
||||
expect(response.text).toContain('<?xml');
|
||||
} else if (format === 'json') {
|
||||
const data = JSON.parse(response.text);
|
||||
expect(data).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user