/** * 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', 'test', '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', '...' ); // 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', 'test', 'TRANSMITTED', ] ); // Record ACK await DeliveryManager.recordACK( testMessageId, testPaymentId, testUETR, testMsgId, '' + testUETR + '' ); 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', 'test', 'TRANSMITTED', ] ); // Record ACK with MsgId only await DeliveryManager.recordACK( testMessageId, testPaymentId, testUETR, testMsgId, '' + testMsgId + '' ); 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', 'test', 'TRANSMITTED', ] ); // Record ACK twice await DeliveryManager.recordACK( testMessageId, testPaymentId, testUETR, testMsgId, '' + testUETR + '' ); // Second ACK should be idempotent (no error) await expect( DeliveryManager.recordACK( testMessageId, testPaymentId, testUETR, testMsgId, '' + testUETR + '' ) ).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', 'test', '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, '' + testUETR + '' ); const acked = await query( 'SELECT status FROM iso_messages WHERE id = $1', [testMessageId] ); expect(['ACK_RECEIVED', 'TRANSMITTED']).toContain(acked.rows[0]?.status); }); }); });