mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 22:00:50 +08:00
connection.rpc.handle looks up webServer on a Cordis fiber that does not inject it, so channel routes never registered after host restart. Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.7 KiB
JavaScript
133 lines
3.7 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { EventEmitter } from 'node:events';
|
|
import test from 'node:test';
|
|
|
|
import { installConnectionRpcChannel } from '../plugin-src/host/connection-rpc-mount.mjs';
|
|
|
|
function mockRequest({ method = 'POST', url, headers = {}, body }) {
|
|
const req = new EventEmitter();
|
|
req.method = method;
|
|
req.url = url;
|
|
req.headers = { 'content-type': 'application/json', ...headers };
|
|
queueMicrotask(() => {
|
|
if (body !== undefined) req.emit('data', Buffer.from(body));
|
|
req.emit('end');
|
|
});
|
|
req[Symbol.asyncIterator] = async function* () {
|
|
if (body !== undefined) yield Buffer.from(body);
|
|
};
|
|
return req;
|
|
}
|
|
|
|
function mockResponse() {
|
|
const res = new EventEmitter();
|
|
res.statusCode = undefined;
|
|
res.headers = undefined;
|
|
res.body = undefined;
|
|
res.writableEnded = false;
|
|
res.destroyed = false;
|
|
res.writeHead = (status, headers) => {
|
|
res.statusCode = status;
|
|
res.headers = headers;
|
|
};
|
|
res.end = (body) => {
|
|
res.body = body;
|
|
res.writableEnded = true;
|
|
res.emit('finish');
|
|
};
|
|
res.off = res.removeListener.bind(res);
|
|
return res;
|
|
}
|
|
|
|
function createCtx({ rejection } = {}) {
|
|
const routes = [];
|
|
const effects = [];
|
|
return {
|
|
routes,
|
|
effects,
|
|
connection: {
|
|
requestRejection() {
|
|
return rejection;
|
|
},
|
|
},
|
|
webServer: {
|
|
register(route) {
|
|
routes.push(route);
|
|
return () => {
|
|
const index = routes.indexOf(route);
|
|
if (index >= 0) routes.splice(index, 1);
|
|
};
|
|
},
|
|
},
|
|
effect(factory, label) {
|
|
const dispose = factory();
|
|
effects.push({ label, dispose });
|
|
return async () => {
|
|
await dispose?.();
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
test('installConnectionRpcChannel mounts a POST JSON RPC channel on webServer', async () => {
|
|
const ctx = createCtx();
|
|
const calls = [];
|
|
installConnectionRpcChannel(ctx, '/weixin', async (endpoint, payload) => {
|
|
calls.push([endpoint, payload]);
|
|
return { ok: true, value: { endpoint, payload } };
|
|
});
|
|
|
|
assert.equal(ctx.routes.length, 1);
|
|
assert.equal(ctx.routes[0].kind, 'prefix');
|
|
assert.equal(ctx.routes[0].path, '/weixin');
|
|
assert.equal(ctx.effects[0].label, 'dsh-im: /weixin rpc channel');
|
|
|
|
const req = mockRequest({
|
|
url: '/weixin/connection.status',
|
|
body: JSON.stringify({
|
|
type: 'client-request',
|
|
rpcId: 'rpc-1',
|
|
method: 'connection.status',
|
|
payload: { botId: 'b1' },
|
|
}),
|
|
});
|
|
const res = mockResponse();
|
|
await ctx.routes[0].handler(req, res);
|
|
|
|
assert.equal(res.statusCode, 200);
|
|
assert.deepEqual(JSON.parse(res.body), {
|
|
type: 'server-response',
|
|
rpcId: 'rpc-1',
|
|
result: { ok: true, value: { endpoint: 'connection.status', payload: { botId: 'b1' } } },
|
|
});
|
|
assert.deepEqual(calls, [['connection.status', { botId: 'b1' }]]);
|
|
});
|
|
|
|
test('installConnectionRpcChannel rejects unauthenticated requests via connection.requestRejection', async () => {
|
|
const ctx = createCtx({ rejection: 401 });
|
|
installConnectionRpcChannel(ctx, '/weixin', async () => ({ ok: true, value: {} }));
|
|
|
|
const req = mockRequest({
|
|
url: '/weixin/connection.status',
|
|
body: '{}',
|
|
});
|
|
const res = mockResponse();
|
|
await ctx.routes[0].handler(req, res);
|
|
|
|
assert.equal(res.statusCode, 401);
|
|
assert.equal(res.body, 'unauthorized');
|
|
});
|
|
|
|
test('installConnectionRpcChannel requires webServer and connection.requestRejection', () => {
|
|
assert.throws(
|
|
() => installConnectionRpcChannel({ effect() {} }, '/weixin', async () => ({})),
|
|
/webServer is required/,
|
|
);
|
|
assert.throws(
|
|
() => installConnectionRpcChannel({
|
|
effect() {},
|
|
webServer: { register() {} },
|
|
}, '/weixin', async () => ({})),
|
|
/requestRejection is required/,
|
|
);
|
|
});
|