Files
dbis_core-lite/tests/integration/transport/idempotency.test.ts
2026-02-09 21:51:45 -08:00

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);
});
});
});