528 lines
16 KiB
TypeScript
528 lines
16 KiB
TypeScript
|
|
/**
|
||
|
|
* Comprehensive Federation Scenarios
|
||
|
|
*
|
||
|
|
* Tests ALL edge cases and formats:
|
||
|
|
* - Both wire profiles (matrix3 and mxenv1)
|
||
|
|
* - Timeout handling
|
||
|
|
* - Error responses
|
||
|
|
* - Edge cases
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import {
|
||
|
|
FederationPeer, ExactInstanceResolver,
|
||
|
|
WIRE_PROFILES,
|
||
|
|
InMemoryFilterBroker,
|
||
|
|
getLegacyIngressTopic,
|
||
|
|
getMxIngressTopic,
|
||
|
|
createLegacyRequest,
|
||
|
|
serializeLegacyEnvelope,
|
||
|
|
createMxEnvV1,
|
||
|
|
serializeMxEnvV1,
|
||
|
|
} from '@open-matrix/federation';
|
||
|
|
|
||
|
|
describe('Comprehensive Federation Scenarios', () => {
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// MXENV/1 Format Tests (New Envelope)
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
describe('MXENV/1 Wire Format', () => {
|
||
|
|
|
||
|
|
it('cross-realm request/reply with mxenv1 format', async () => {
|
||
|
|
const REALM_A = 'mxenv-realm-a';
|
||
|
|
const REALM_B = 'mxenv-realm-b';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBusA = new InMemoryFilterBroker();
|
||
|
|
const localBusB = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gatewayA = new FederationPeer({
|
||
|
|
localRealm: REALM_A,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
backbone,
|
||
|
|
localBus: localBusA,
|
||
|
|
internalTimeoutMs: 5000,
|
||
|
|
});
|
||
|
|
|
||
|
|
const gatewayB = new FederationPeer({
|
||
|
|
localRealm: REALM_B,
|
||
|
|
profile: WIRE_PROFILES.mxenv1,
|
||
|
|
backbone,
|
||
|
|
localBus: localBusB,
|
||
|
|
internalTimeoutMs: 5000,
|
||
|
|
});
|
||
|
|
|
||
|
|
gatewayA.start();
|
||
|
|
gatewayB.start();
|
||
|
|
|
||
|
|
// Handler on Realm B
|
||
|
|
localBusB.subscribe(`service.accepts.greet`, (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload) as {
|
||
|
|
replyTo: string;
|
||
|
|
correlationId: string;
|
||
|
|
params: { name: string };
|
||
|
|
};
|
||
|
|
localBusB.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { greeting: `Hello, ${req.params.name}!` },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for reply at Realm A
|
||
|
|
let receivedReply: unknown = null;
|
||
|
|
backbone.subscribe(getMxIngressTopic(REALM_A), (topic, payload) => {
|
||
|
|
const parsed = JSON.parse(payload);
|
||
|
|
if (parsed.msgType === 'Result' || parsed.msgType === 'Nack') {
|
||
|
|
receivedReply = parsed;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 100));
|
||
|
|
|
||
|
|
// Send MXENV/1 request from A to B
|
||
|
|
const request = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: REALM_B,
|
||
|
|
toPath: 'service.accepts.greet',
|
||
|
|
fromRealm: REALM_A,
|
||
|
|
fromPath: 'client',
|
||
|
|
replyToRealm: REALM_A,
|
||
|
|
replyToPath: '_inbox',
|
||
|
|
correlationId: 'mxenv-cross-123',
|
||
|
|
body: { name: 'World' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getMxIngressTopic(REALM_B), serializeMxEnvV1(request));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 500));
|
||
|
|
|
||
|
|
assert.ok(receivedReply, 'Should receive MXENV/1 reply');
|
||
|
|
assert.strictEqual((receivedReply as { msgType: string }).msgType, 'Result');
|
||
|
|
assert.deepStrictEqual(
|
||
|
|
(receivedReply as { body: { greeting: string } }).body,
|
||
|
|
{ greeting: 'Hello, World!' }
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log('✓ MXENV/1 cross-realm: Hello, World!');
|
||
|
|
|
||
|
|
gatewayA.stop();
|
||
|
|
gatewayB.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('dual-profile gateway accepts both matrix3 and mxenv1', async () => {
|
||
|
|
const REALM = 'dual-profile-realm';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
// Create gateway that subscribes to BOTH mailbox formats
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.dualRail, // Accepts both formats
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
internalTimeoutMs: 5000,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
let requestCount = 0;
|
||
|
|
localBus.subscribe(`handler.accepts.ping`, (topic, payload) => {
|
||
|
|
requestCount++;
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { pong: requestCount },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 100));
|
||
|
|
|
||
|
|
// Send legacy (matrix3) request
|
||
|
|
const legacyReq = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'handler.accepts.ping',
|
||
|
|
fromRealm: 'legacy-client',
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'legacy-1',
|
||
|
|
body: {},
|
||
|
|
});
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(legacyReq));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 200));
|
||
|
|
|
||
|
|
// Send mxenv1 request
|
||
|
|
const mxReq = createMxEnvV1({
|
||
|
|
msgType: 'Cmd',
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'handler.accepts.ping',
|
||
|
|
fromRealm: 'mxenv-client',
|
||
|
|
fromPath: 'c',
|
||
|
|
replyToRealm: 'mxenv-client',
|
||
|
|
replyToPath: '_inbox',
|
||
|
|
correlationId: 'mxenv-1',
|
||
|
|
body: {},
|
||
|
|
});
|
||
|
|
backbone.publish(getMxIngressTopic(REALM), serializeMxEnvV1(mxReq));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 200));
|
||
|
|
|
||
|
|
assert.strictEqual(requestCount, 2, 'Should receive both legacy and mxenv1 requests');
|
||
|
|
|
||
|
|
console.log('✓ Dual-profile gateway accepts both formats');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Timeout Handling
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
describe('Timeout Handling', () => {
|
||
|
|
|
||
|
|
it('returns timeout error when handler does not reply', async () => {
|
||
|
|
const REALM = 'timeout-realm';
|
||
|
|
const CLIENT_REALM = 'timeout-client';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
internalTimeoutMs: 200, // Very short timeout for testing
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
// Handler that NEVER replies
|
||
|
|
localBus.subscribe(`slow.accepts.cmd`, () => {
|
||
|
|
// Do nothing - simulate hung handler
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen for error reply
|
||
|
|
let errorReply: unknown = null;
|
||
|
|
backbone.subscribe(getLegacyIngressTopic(CLIENT_REALM), (topic, payload) => {
|
||
|
|
errorReply = JSON.parse(payload);
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 50));
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'slow.accepts.cmd',
|
||
|
|
fromRealm: CLIENT_REALM,
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'timeout-test',
|
||
|
|
body: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
// Wait for timeout (200ms) + buffer
|
||
|
|
await new Promise(r => setTimeout(r, 400));
|
||
|
|
|
||
|
|
assert.ok(errorReply, 'Should receive timeout error reply');
|
||
|
|
assert.strictEqual((errorReply as { ok: boolean }).ok, false);
|
||
|
|
assert.ok(
|
||
|
|
(errorReply as { error?: { code?: string } }).error?.code?.includes('TIMEOUT'),
|
||
|
|
'Error code should indicate timeout'
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log('✓ Timeout handling works correctly');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Error Response Handling
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
describe('Error Responses', () => {
|
||
|
|
|
||
|
|
it('forwards handler error back to caller', async () => {
|
||
|
|
const REALM = 'error-realm';
|
||
|
|
const CLIENT_REALM = 'error-client';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
// Handler that returns an error
|
||
|
|
localBus.subscribe(`failing.accepts.cmd`, (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: false,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
error: {
|
||
|
|
code: 'APP.VALIDATION_ERROR',
|
||
|
|
message: 'Invalid input provided',
|
||
|
|
details: { field: 'email', reason: 'malformed' },
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
let errorReply: unknown = null;
|
||
|
|
backbone.subscribe(getLegacyIngressTopic(CLIENT_REALM), (topic, payload) => {
|
||
|
|
errorReply = JSON.parse(payload);
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 50));
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'failing.accepts.cmd',
|
||
|
|
fromRealm: CLIENT_REALM,
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'error-test',
|
||
|
|
body: { email: 'not-an-email' },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 300));
|
||
|
|
|
||
|
|
assert.ok(errorReply, 'Should receive error reply');
|
||
|
|
assert.strictEqual((errorReply as { ok: boolean }).ok, false);
|
||
|
|
assert.strictEqual(
|
||
|
|
(errorReply as { error?: { code?: string } }).error?.code,
|
||
|
|
'APP.VALIDATION_ERROR'
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log('✓ Error responses forwarded correctly');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ===========================================================================
|
||
|
|
// Edge Cases
|
||
|
|
// ===========================================================================
|
||
|
|
|
||
|
|
describe('Edge Cases', () => {
|
||
|
|
|
||
|
|
it('handles empty body', async () => {
|
||
|
|
const REALM = 'empty-body-realm';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
let receivedParams: unknown = 'NOT_SET';
|
||
|
|
localBus.subscribe(`empty.accepts.cmd`, (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string; params: unknown };
|
||
|
|
receivedParams = req.params;
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 50));
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'empty.accepts.cmd',
|
||
|
|
fromRealm: 'client',
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'empty-body',
|
||
|
|
body: null, // Empty/null body
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 200));
|
||
|
|
|
||
|
|
assert.strictEqual(receivedParams, null, 'Should receive null params');
|
||
|
|
|
||
|
|
console.log('✓ Empty body handled correctly');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles large payload', async () => {
|
||
|
|
const REALM = 'large-payload-realm';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
let receivedSize = 0;
|
||
|
|
localBus.subscribe(`large.accepts.cmd`, (topic, payload) => {
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string; params: { data: string } };
|
||
|
|
receivedSize = req.params.data.length;
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { receivedBytes: receivedSize },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 50));
|
||
|
|
|
||
|
|
// Create a 100KB payload
|
||
|
|
const largeData = 'x'.repeat(100 * 1024);
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'large.accepts.cmd',
|
||
|
|
fromRealm: 'client',
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'large-payload',
|
||
|
|
body: { data: largeData },
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 300));
|
||
|
|
|
||
|
|
assert.strictEqual(receivedSize, 100 * 1024, 'Should receive full 100KB payload');
|
||
|
|
|
||
|
|
console.log('✓ Large payload (100KB) handled correctly');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles special characters in realm name', async () => {
|
||
|
|
const REALM = 'realm-with.dots_and-dashes';
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
let receivedRequest = false;
|
||
|
|
localBus.subscribe(`special.accepts.cmd`, (topic, payload) => {
|
||
|
|
receivedRequest = true;
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({ ok: true, correlationId: req.correlationId }));
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 50));
|
||
|
|
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: REALM,
|
||
|
|
toPath: 'special.accepts.cmd',
|
||
|
|
fromRealm: 'client',
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: 'special-realm',
|
||
|
|
body: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(getLegacyIngressTopic(REALM), serializeLegacyEnvelope(request));
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 200));
|
||
|
|
|
||
|
|
assert.ok(receivedRequest, 'Should handle realm with special characters');
|
||
|
|
|
||
|
|
console.log('✓ Special characters in realm name handled');
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles concurrent requests from multiple realms', async () => {
|
||
|
|
const TARGET_REALM = 'concurrent-target';
|
||
|
|
const NUM_CLIENTS = 5;
|
||
|
|
|
||
|
|
const backbone = new InMemoryFilterBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const gateway = new FederationPeer({
|
||
|
|
localRealm: TARGET_REALM,
|
||
|
|
profile: WIRE_PROFILES.matrix3,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
internalTimeoutMs: 5000,
|
||
|
|
});
|
||
|
|
|
||
|
|
gateway.start();
|
||
|
|
|
||
|
|
let requestCount = 0;
|
||
|
|
localBus.subscribe(`counter.accepts.increment`, (topic, payload) => {
|
||
|
|
requestCount++;
|
||
|
|
const req = JSON.parse(payload) as { replyTo: string; correlationId: string };
|
||
|
|
localBus.publish(req.replyTo, JSON.stringify({
|
||
|
|
ok: true,
|
||
|
|
correlationId: req.correlationId,
|
||
|
|
result: { count: requestCount },
|
||
|
|
}));
|
||
|
|
});
|
||
|
|
|
||
|
|
const replies: Map<string, unknown> = new Map();
|
||
|
|
|
||
|
|
// Subscribe to all client mailboxes
|
||
|
|
for (let i = 0; i < NUM_CLIENTS; i++) {
|
||
|
|
backbone.subscribe(getLegacyIngressTopic(`client-${i}`), (topic, payload) => {
|
||
|
|
const parsed = JSON.parse(payload);
|
||
|
|
if (parsed.correlationId) {
|
||
|
|
replies.set(parsed.correlationId, parsed);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 100));
|
||
|
|
|
||
|
|
// Send concurrent requests from multiple clients
|
||
|
|
for (let i = 0; i < NUM_CLIENTS; i++) {
|
||
|
|
const request = createLegacyRequest({
|
||
|
|
toRealm: TARGET_REALM,
|
||
|
|
toPath: 'counter.accepts.increment',
|
||
|
|
fromRealm: `client-${i}`,
|
||
|
|
replyTo: '_inbox',
|
||
|
|
correlationId: `concurrent-${i}`,
|
||
|
|
body: { clientId: i },
|
||
|
|
});
|
||
|
|
backbone.publish(getLegacyIngressTopic(TARGET_REALM), serializeLegacyEnvelope(request));
|
||
|
|
}
|
||
|
|
|
||
|
|
await new Promise(r => setTimeout(r, 1000));
|
||
|
|
|
||
|
|
assert.strictEqual(requestCount, NUM_CLIENTS, `Should process all ${NUM_CLIENTS} requests`);
|
||
|
|
assert.strictEqual(replies.size, NUM_CLIENTS, `Should receive all ${NUM_CLIENTS} replies`);
|
||
|
|
|
||
|
|
console.log(`✓ Concurrent requests from ${NUM_CLIENTS} realms handled correctly`);
|
||
|
|
|
||
|
|
gateway.stop();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|