Files
dbis_core-lite/tests/integration/transport/mock-receiver-server.ts
2026-02-09 21:51:45 -08:00

247 lines
6.9 KiB
TypeScript

/**
* Mock TLS Receiver Server
* Simulates receiver for testing without external dependencies
*/
import * as tls from 'tls';
import * as fs from 'fs';
import * as path from 'path';
import { LengthPrefixFramer } from '@/transport/framing/length-prefix';
export interface MockReceiverConfig {
port: number;
host?: string;
responseDelay?: number; // ms
ackResponse?: boolean; // true for ACK, false for NACK
simulateErrors?: boolean;
errorRate?: number; // 0-1, probability of error
}
export class MockReceiverServer {
private server: tls.Server | null = null;
private config: MockReceiverConfig;
private connections: Set<tls.TLSSocket> = new Set();
private messageCount = 0;
private ackCount = 0;
private nackCount = 0;
constructor(config: MockReceiverConfig) {
this.config = {
host: '0.0.0.0',
responseDelay: 0,
ackResponse: true,
simulateErrors: false,
errorRate: 0,
...config,
};
}
/**
* Start the mock server
*/
async start(): Promise<void> {
return new Promise((resolve, reject) => {
try {
// Create self-signed certificate for testing
const certPath = path.join(__dirname, '../../test-certs/server-cert.pem');
const keyPath = path.join(__dirname, '../../test-certs/server-key.pem');
// Create test certificates if they don't exist
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
this.createTestCertificates(certPath, keyPath);
}
const options: tls.TlsOptions = {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath),
rejectUnauthorized: false, // For testing only
};
this.server = tls.createServer(options, (socket) => {
this.connections.add(socket);
let buffer = Buffer.alloc(0);
socket.on('data', async (data) => {
buffer = Buffer.concat([buffer, data]);
// Try to unframe messages
while (buffer.length >= 4) {
// Create a proper Buffer to avoid ArrayBufferLike type issue
const bufferCopy = Buffer.from(buffer);
const { message, remaining } = LengthPrefixFramer.unframe(bufferCopy);
if (!message) {
// Need more data
break;
}
// Process message
await this.handleMessage(socket, message.toString('utf-8'));
// Create new Buffer from remaining to avoid type issues
buffer = Buffer.from(remaining);
}
});
socket.on('error', (error) => {
console.error('Mock server socket error:', error);
});
socket.on('close', () => {
this.connections.delete(socket);
});
});
this.server.listen(this.config.port, this.config.host, () => {
console.log(`Mock receiver server listening on ${this.config.host}:${this.config.port}`);
resolve();
});
this.server.on('error', (error) => {
reject(error);
});
} catch (error) {
reject(error);
}
});
}
/**
* Stop the mock server
*/
async stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
// Close all connections
for (const socket of this.connections) {
socket.destroy();
}
this.connections.clear();
this.server.close(() => {
this.server = null;
resolve();
});
} else {
resolve();
}
});
}
/**
* Handle incoming message
*/
private async handleMessage(socket: tls.TLSSocket, xmlContent: string): Promise<void> {
this.messageCount++;
// Simulate response delay
if (this.config.responseDelay && this.config.responseDelay > 0) {
await new Promise((resolve) => setTimeout(resolve, this.config.responseDelay));
}
// Simulate errors
if (this.config.simulateErrors && Math.random() < this.config.errorRate!) {
socket.destroy();
return;
}
// Generate response
const response = this.generateResponse(xmlContent);
const responseBuffer = Buffer.from(response, 'utf-8');
// Create new Buffer to avoid ArrayBufferLike type issue
const responseBufferCopy = Buffer.allocUnsafe(responseBuffer.length);
responseBuffer.copy(responseBufferCopy);
const framedResponse = LengthPrefixFramer.frame(responseBufferCopy);
socket.write(framedResponse);
}
/**
* Generate ACK/NACK response
*/
private generateResponse(xmlContent: string): string {
// Extract UETR and MsgId from incoming message
const uetrMatch = xmlContent.match(/<UETR>([^<]+)<\/UETR>/);
const msgIdMatch = xmlContent.match(/<MsgId>([^<]+)<\/MsgId>/);
const uetr = uetrMatch ? uetrMatch[1] : '00000000-0000-0000-0000-000000000000';
const msgId = msgIdMatch ? msgIdMatch[1] : 'TEST-MSG-ID';
if (this.config.ackResponse) {
this.ackCount++;
return `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10">
<Ack>
<UETR>${uetr}</UETR>
<MsgId>${msgId}</MsgId>
<Status>ACCEPTED</Status>
</Ack>
</Document>`;
} else {
this.nackCount++;
return `<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10">
<Nack>
<UETR>${uetr}</UETR>
<MsgId>${msgId}</MsgId>
<Reason>Test NACK response</Reason>
</Nack>
</Document>`;
}
}
/**
* Create test certificates (simplified - in production use proper certs)
*/
private createTestCertificates(certPath: string, keyPath: string): void {
const certDir = path.dirname(certPath);
if (!fs.existsSync(certDir)) {
fs.mkdirSync(certDir, { recursive: true });
}
// Note: In a real implementation, use openssl or a proper certificate generator
// This is a placeholder - actual certificates should be generated properly
const { execSync } = require('child_process');
try {
// Generate self-signed certificate for testing
execSync(
`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 365 -nodes -subj "/CN=test-receiver"`,
{ stdio: 'ignore' }
);
} catch (error) {
console.warn('Could not generate test certificates. Using placeholder.');
// Create placeholder files
fs.writeFileSync(certPath, 'PLACEHOLDER_CERT');
fs.writeFileSync(keyPath, 'PLACEHOLDER_KEY');
}
}
/**
* Get server statistics
*/
getStats() {
return {
messageCount: this.messageCount,
ackCount: this.ackCount,
nackCount: this.nackCount,
activeConnections: this.connections.size,
};
}
/**
* Reset statistics
*/
resetStats(): void {
this.messageCount = 0;
this.ackCount = 0;
this.nackCount = 0;
}
/**
* Configure response behavior
*/
configure(config: Partial<MockReceiverConfig>): void {
this.config = { ...this.config, ...config };
}
}