Initial commit: add .gitignore and README
This commit is contained in:
343
tests/integration/transport/idempotency.test.ts
Normal file
343
tests/integration/transport/idempotency.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Idempotency Test Suite
|
||||
* Tests UETR and MsgId handling for exactly-once delivery
|
||||
*/
|
||||
|
||||
import { DeliveryManager } from '@/transport/delivery/delivery-manager';
|
||||
import { query } from '@/database/connection';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
describe('Idempotency Tests', () => {
|
||||
const testUETR = '03BD66B4-6C81-48DB-B3D8-F5E5E0DC809A';
|
||||
const testMsgId = 'DFCUUGKA20251231201119366023';
|
||||
const testPaymentId = uuidv4();
|
||||
const testMessageId = uuidv4();
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up test data
|
||||
await query('DELETE FROM delivery_status WHERE uetr = $1 OR msg_id = $2', [
|
||||
testUETR,
|
||||
testMsgId,
|
||||
]);
|
||||
await query('DELETE FROM iso_messages WHERE uetr = $1 OR msg_id = $2', [
|
||||
testUETR,
|
||||
testMsgId,
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up test data
|
||||
await query('DELETE FROM delivery_status WHERE uetr = $1 OR msg_id = $2', [
|
||||
testUETR,
|
||||
testMsgId,
|
||||
]);
|
||||
await query('DELETE FROM iso_messages WHERE uetr = $1 OR msg_id = $2', [
|
||||
testUETR,
|
||||
testMsgId,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('UETR Handling', () => {
|
||||
it('should generate unique UETR for each message', () => {
|
||||
const uetr1 = uuidv4();
|
||||
const uetr2 = uuidv4();
|
||||
|
||||
expect(uetr1).not.toBe(uetr2);
|
||||
expect(uetr1.length).toBe(36); // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
expect(uetr2.length).toBe(36);
|
||||
});
|
||||
|
||||
it('should validate UETR format', () => {
|
||||
const validUETR = '03BD66B4-6C81-48DB-B3D8-F5E5E0DC809A';
|
||||
const invalidUETR = 'not-a-valid-uuid';
|
||||
|
||||
// UETR should be UUID format
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
expect(uuidRegex.test(validUETR)).toBe(true);
|
||||
expect(uuidRegex.test(invalidUETR)).toBe(false);
|
||||
});
|
||||
|
||||
it('should prevent duplicate transmission by UETR', async () => {
|
||||
// Record first transmission
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
// Check if already transmitted
|
||||
const isTransmitted = await DeliveryManager.isTransmitted(testMessageId);
|
||||
expect(isTransmitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow different messages with different UETRs', async () => {
|
||||
const uetr1 = uuidv4();
|
||||
const uetr2 = uuidv4();
|
||||
const msgId1 = uuidv4();
|
||||
const msgId2 = uuidv4();
|
||||
|
||||
await DeliveryManager.recordTransmission(msgId1, testPaymentId, uetr1, 'session-1');
|
||||
await DeliveryManager.recordTransmission(msgId2, testPaymentId, uetr2, 'session-1');
|
||||
|
||||
const transmitted1 = await DeliveryManager.isTransmitted(msgId1);
|
||||
const transmitted2 = await DeliveryManager.isTransmitted(msgId2);
|
||||
|
||||
expect(transmitted1).toBe(true);
|
||||
expect(transmitted2).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MsgId Handling', () => {
|
||||
it('should generate unique MsgId for each message', () => {
|
||||
const msgId1 = `DFCUUGKA${Date.now()}${Math.random().toString().slice(2, 8)}`;
|
||||
const msgId2 = `DFCUUGKA${Date.now() + 1}${Math.random().toString().slice(2, 8)}`;
|
||||
|
||||
expect(msgId1).not.toBe(msgId2);
|
||||
});
|
||||
|
||||
it('should validate MsgId format', () => {
|
||||
const validMsgId = 'DFCUUGKA20251231201119366023';
|
||||
const invalidMsgId = '';
|
||||
|
||||
expect(validMsgId.length).toBeGreaterThan(0);
|
||||
expect(invalidMsgId.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should prevent duplicate transmission by MsgId', async () => {
|
||||
// Create message record first
|
||||
await query(
|
||||
`INSERT INTO iso_messages (id, payment_id, msg_id, uetr, message_type, xml_content, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testMsgId,
|
||||
testUETR,
|
||||
'pacs.008',
|
||||
'<Document>test</Document>',
|
||||
'PENDING',
|
||||
]
|
||||
);
|
||||
|
||||
// Record transmission
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
// Check if already transmitted
|
||||
const isTransmitted = await DeliveryManager.isTransmitted(testMessageId);
|
||||
expect(isTransmitted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Exactly-Once Delivery', () => {
|
||||
it('should track message transmission state', async () => {
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
const isTransmitted = await DeliveryManager.isTransmitted(testMessageId);
|
||||
expect(isTransmitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle retry attempts for same message', async () => {
|
||||
// First transmission attempt
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
// Second attempt should be blocked
|
||||
const isTransmitted = await DeliveryManager.isTransmitted(testMessageId);
|
||||
expect(isTransmitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow retransmission after NACK', async () => {
|
||||
// Record transmission
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
// Record NACK
|
||||
await DeliveryManager.recordNACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'Temporary error',
|
||||
'<Nack>...</Nack>'
|
||||
);
|
||||
|
||||
// After NACK, system should allow retry with new message ID
|
||||
// (This depends on business logic - some systems allow retry, others don't)
|
||||
const nackResult = await query(
|
||||
'SELECT nack_reason FROM delivery_status WHERE message_id = $1',
|
||||
[testMessageId]
|
||||
);
|
||||
expect(nackResult.rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ACK/NACK with Idempotency', () => {
|
||||
it('should match ACK to message by UETR', async () => {
|
||||
// Create message with UETR
|
||||
await query(
|
||||
`INSERT INTO iso_messages (id, payment_id, msg_id, uetr, message_type, xml_content, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testMsgId,
|
||||
testUETR,
|
||||
'pacs.008',
|
||||
'<Document>test</Document>',
|
||||
'TRANSMITTED',
|
||||
]
|
||||
);
|
||||
|
||||
// Record ACK
|
||||
await DeliveryManager.recordACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'<Ack><UETR>' + testUETR + '</UETR></Ack>'
|
||||
);
|
||||
|
||||
const ackResult = await query(
|
||||
'SELECT ack_received FROM delivery_status WHERE message_id = $1',
|
||||
[testMessageId]
|
||||
);
|
||||
expect(ackResult.rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should match ACK to message by MsgId', async () => {
|
||||
// Create message with MsgId
|
||||
await query(
|
||||
`INSERT INTO iso_messages (id, payment_id, msg_id, uetr, message_type, xml_content, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testMsgId,
|
||||
testUETR,
|
||||
'pacs.008',
|
||||
'<Document>test</Document>',
|
||||
'TRANSMITTED',
|
||||
]
|
||||
);
|
||||
|
||||
// Record ACK with MsgId only
|
||||
await DeliveryManager.recordACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'<Ack><MsgId>' + testMsgId + '</MsgId></Ack>'
|
||||
);
|
||||
|
||||
const ackResult = await query(
|
||||
'SELECT ack_received FROM delivery_status WHERE message_id = $1',
|
||||
[testMessageId]
|
||||
);
|
||||
expect(ackResult.rows.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle duplicate ACK gracefully', async () => {
|
||||
// Create message
|
||||
await query(
|
||||
`INSERT INTO iso_messages (id, payment_id, msg_id, uetr, message_type, xml_content, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testMsgId,
|
||||
testUETR,
|
||||
'pacs.008',
|
||||
'<Document>test</Document>',
|
||||
'TRANSMITTED',
|
||||
]
|
||||
);
|
||||
|
||||
// Record ACK twice
|
||||
await DeliveryManager.recordACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'<Ack><UETR>' + testUETR + '</UETR></Ack>'
|
||||
);
|
||||
|
||||
// Second ACK should be idempotent (no error)
|
||||
await expect(
|
||||
DeliveryManager.recordACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'<Ack><UETR>' + testUETR + '</UETR></Ack>'
|
||||
)
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message State Transitions', () => {
|
||||
it('should track PENDING -> TRANSMITTED -> ACK_RECEIVED', async () => {
|
||||
// Create message in PENDING state
|
||||
await query(
|
||||
`INSERT INTO iso_messages (id, payment_id, msg_id, uetr, message_type, xml_content, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testMsgId,
|
||||
testUETR,
|
||||
'pacs.008',
|
||||
'<Document>test</Document>',
|
||||
'PENDING',
|
||||
]
|
||||
);
|
||||
|
||||
// Transmit
|
||||
await DeliveryManager.recordTransmission(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
'session-1'
|
||||
);
|
||||
|
||||
const transmitted = await query(
|
||||
'SELECT status FROM iso_messages WHERE id = $1',
|
||||
[testMessageId]
|
||||
);
|
||||
expect(['TRANSMITTED', 'PENDING']).toContain(transmitted.rows[0]?.status);
|
||||
|
||||
// Receive ACK
|
||||
await DeliveryManager.recordACK(
|
||||
testMessageId,
|
||||
testPaymentId,
|
||||
testUETR,
|
||||
testMsgId,
|
||||
'<Ack><UETR>' + testUETR + '</UETR></Ack>'
|
||||
);
|
||||
|
||||
const acked = await query(
|
||||
'SELECT status FROM iso_messages WHERE id = $1',
|
||||
[testMessageId]
|
||||
);
|
||||
expect(['ACK_RECEIVED', 'TRANSMITTED']).toContain(acked.rows[0]?.status);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user