123 lines
4.6 KiB
TypeScript
123 lines
4.6 KiB
TypeScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { InMemoryExactBroker, InMemoryFilterBroker } from '@open-matrix/federation/transport/inmem';
|
||
|
|
import { FederationPeer, ExactInstanceResolver } from '@open-matrix/federation/federation_peer';
|
||
|
|
import { getInternalTopic, getLegacyIngressTopic } from '@open-matrix/federation/topics';
|
||
|
|
import { createLegacyRequest, deserializeTaggedEnvelope, serializeLegacyEnvelope } from '@open-matrix/federation/envelope_codec';
|
||
|
|
import { parseMatrixUri } from '@open-matrix/federation/uri';
|
||
|
|
import { isRecord } from '../../src/util/guards';
|
||
|
|
import type { Transport } from '@open-matrix/federation/transport/base';
|
||
|
|
|
||
|
|
test('matrix3 compat: legacy request with ttlMs + flat replyTo is accepted and produces a reply', async () => {
|
||
|
|
const backbone: Transport = new InMemoryExactBroker();
|
||
|
|
const localBus = new InMemoryFilterBroker();
|
||
|
|
|
||
|
|
const serverRealm = 'matrix-3';
|
||
|
|
const clientRealm = 'flowpad-demo.tab-01';
|
||
|
|
|
||
|
|
// Start EdgeGateway (server-side)
|
||
|
|
const edge = new FederationPeer({
|
||
|
|
localRealm: serverRealm,
|
||
|
|
backbone,
|
||
|
|
localBus,
|
||
|
|
});
|
||
|
|
edge.start();
|
||
|
|
|
||
|
|
// Local handler (no helper wrappers) that replies with a NON-standard reply payload
|
||
|
|
// to prove lenient decoding on the EdgeGateway.
|
||
|
|
const toPath = 'project/daemon.project.accepts.scan';
|
||
|
|
const internalTopic = getInternalTopic(serverRealm, toPath);
|
||
|
|
localBus.subscribe(internalTopic, (_t, raw) => {
|
||
|
|
const parsed = (() => {
|
||
|
|
try {
|
||
|
|
return JSON.parse(raw) as unknown;
|
||
|
|
} catch {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
})();
|
||
|
|
if (!isRecord(parsed)) throw new Error('internal request must be a JSON object');
|
||
|
|
const replyTo = parsed.replyTo;
|
||
|
|
if (typeof replyTo !== 'string') throw new Error('replyTo must be a string');
|
||
|
|
|
||
|
|
// Reply with `{ result: ... }` only (no correlationId, no ok), to exercise lenient reply parsing.
|
||
|
|
localBus.publish(replyTo, JSON.stringify({ result: { hello: true } }));
|
||
|
|
});
|
||
|
|
|
||
|
|
// Client mailbox watcher
|
||
|
|
const clientMailbox = getLegacyIngressTopic(clientRealm);
|
||
|
|
const serverMailbox = getLegacyIngressTopic(serverRealm);
|
||
|
|
|
||
|
|
const correlationId = 'cid-ttlms-001';
|
||
|
|
const replyToPath = `_replies.emits.${correlationId}`;
|
||
|
|
|
||
|
|
const repPromise = new Promise<unknown>((resolve, reject) => {
|
||
|
|
const timer = setTimeout(() => reject(new Error('timeout waiting for reply')), 250);
|
||
|
|
const unsub = backbone.subscribe(clientMailbox, (_topic, payload) => {
|
||
|
|
clearTimeout(timer);
|
||
|
|
unsub();
|
||
|
|
const tagged = deserializeTaggedEnvelope(payload);
|
||
|
|
if (!tagged) throw new Error('expected an envelope');
|
||
|
|
if (tagged.profile !== 'matrix3') throw new Error('expected matrix3 envelope');
|
||
|
|
if (tagged.kind !== 'reply') throw new Error('expected matrix3 reply envelope');
|
||
|
|
resolve(tagged.envelope.body);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// Send raw legacy request with ttlMs (expiry semantics) and flat replyTo
|
||
|
|
const req = createLegacyRequest({
|
||
|
|
toRealm: serverRealm,
|
||
|
|
toPath,
|
||
|
|
fromRealm: clientRealm,
|
||
|
|
replyTo: replyToPath,
|
||
|
|
correlationId,
|
||
|
|
body: { depth: 1 },
|
||
|
|
ttlMs: 1234,
|
||
|
|
type: 'request',
|
||
|
|
});
|
||
|
|
|
||
|
|
backbone.publish(serverMailbox, serializeLegacyEnvelope(req));
|
||
|
|
|
||
|
|
const body = await repPromise;
|
||
|
|
assert.deepEqual(body, { hello: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('matrix3 compat: reply detection works for ok:true and ok:false', () => {
|
||
|
|
const okRep = JSON.stringify({
|
||
|
|
toRealm: 'a',
|
||
|
|
toPath: '_replies.emits.x',
|
||
|
|
fromRealm: 'b',
|
||
|
|
correlationId: 'x',
|
||
|
|
ok: true,
|
||
|
|
body: { v: 1 },
|
||
|
|
});
|
||
|
|
const badRep = JSON.stringify({
|
||
|
|
toRealm: 'a',
|
||
|
|
toPath: '_replies.emits.x',
|
||
|
|
fromRealm: 'b',
|
||
|
|
correlationId: 'x',
|
||
|
|
ok: false,
|
||
|
|
body: null,
|
||
|
|
error: { code: 'MX.FAIL', message: 'nope' },
|
||
|
|
});
|
||
|
|
|
||
|
|
const t1 = deserializeTaggedEnvelope(okRep);
|
||
|
|
if (!t1) throw new Error('expected non-null');
|
||
|
|
if (t1.profile !== 'matrix3') throw new Error('expected matrix3 profile');
|
||
|
|
if (t1.kind !== 'reply') throw new Error('expected reply');
|
||
|
|
|
||
|
|
const t2 = deserializeTaggedEnvelope(badRep);
|
||
|
|
if (!t2) throw new Error('expected non-null');
|
||
|
|
if (t2.profile !== 'matrix3') throw new Error('expected matrix3 profile');
|
||
|
|
if (t2.kind !== 'reply') throw new Error('expected reply');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('matrix3 compat: URI parsing tolerant (legacy vs /-/ delimiter) canonicalizes to same toPath', () => {
|
||
|
|
const a = parseMatrixUri('matrix-3/project/daemon.project.accepts.scan');
|
||
|
|
const b = parseMatrixUri('matrix-3/project/-/daemon/project/accepts/scan');
|
||
|
|
if (!a) throw new Error('expected non-null');
|
||
|
|
if (!b) throw new Error('expected non-null');
|
||
|
|
assert.equal(a.realm, 'matrix-3');
|
||
|
|
assert.equal(b.realm, 'matrix-3');
|
||
|
|
assert.equal(a.toPath, b.toPath);
|
||
|
|
});
|