344 lines
9.7 KiB
TypeScript
344 lines
9.7 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|