mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 01:53:21 +08:00
165 lines
5.8 KiB
JavaScript
165 lines
5.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
WEIXIN_ENDPOINTS,
|
|
apply,
|
|
createWeixinRpcHandler,
|
|
} from '../../../plugin-src/host/channels/weixin/index.mjs';
|
|
|
|
function snapshot(overrides = {}) {
|
|
return {
|
|
schemaVersion: 1,
|
|
revision: 1,
|
|
state: 'disconnected',
|
|
bots: [],
|
|
totals: { configured: 0, connected: 0 },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function controllerFixture() {
|
|
const attempts = new Map();
|
|
const controller = {
|
|
status: () => snapshot(),
|
|
startProvisioning: async () => {
|
|
const value = {
|
|
attemptId: 'attempt-1',
|
|
status: 'pending',
|
|
verificationUrl: 'https://liteapp.weixin.qq.com/q/test',
|
|
expiresAt: Date.now() + 60_000,
|
|
pollIntervalMs: 1_000,
|
|
};
|
|
attempts.set(value.attemptId, value);
|
|
return value;
|
|
},
|
|
registrationStatus: (id) => attempts.get(id) ?? null,
|
|
submitVerification: async (id) => ({ ...attempts.get(id), status: 'scanned' }),
|
|
cancelProvisioning: async (id) => ({ ...attempts.get(id), status: 'cancelled' }),
|
|
reconnectBot: async () => snapshot(),
|
|
sendConnectionTest: async () => ({ sent: true }),
|
|
deleteBot: async () => snapshot(),
|
|
};
|
|
return controller;
|
|
}
|
|
|
|
test('Host plugin registers the Weixin RPC channel as loopback-only', async () => {
|
|
let registration;
|
|
const dispose = async () => {};
|
|
const ctx = {
|
|
connection: { rpc: { handle: (channel, handler, options) => {
|
|
registration = { channel, handler, options };
|
|
return dispose;
|
|
} } },
|
|
};
|
|
const returned = await apply(ctx, { controller: controllerFixture() });
|
|
assert.equal(returned, dispose);
|
|
assert.equal(registration.channel, '/weixin');
|
|
assert.deepEqual(registration.options, { authority: 'loopback' });
|
|
});
|
|
|
|
test('Host plugin opts the Weixin RPC channel into trusted Host authorities', async () => {
|
|
let registration;
|
|
const ctx = {
|
|
connection: { rpc: { handle: (channel, handler, options) => {
|
|
registration = { channel, handler, options };
|
|
return async () => {};
|
|
} } },
|
|
};
|
|
await apply(ctx, {
|
|
controller: controllerFixture(),
|
|
rpcAuthority: 'trusted-host',
|
|
});
|
|
assert.deepEqual(registration.options, { authority: 'trusted-host' });
|
|
});
|
|
|
|
test('RPC returns QR data and verification states without exposing secret-shaped fields', async () => {
|
|
const controller = controllerFixture();
|
|
const handler = createWeixinRpcHandler(controller, {
|
|
encodeQr: async (url) => `data:image/png;base64,${Buffer.from(url).toString('base64')}`,
|
|
});
|
|
const signal = new AbortController().signal;
|
|
|
|
const begun = await handler(WEIXIN_ENDPOINTS.beginProvisioning, {}, signal);
|
|
assert.equal(begun.ok, true);
|
|
assert.match(begun.value.qrCodeDataUrl, /^data:image\/png;base64,/);
|
|
const verified = await handler(WEIXIN_ENDPOINTS.submitVerification, {
|
|
attemptId: 'attempt-1', verifyCode: '123456',
|
|
}, signal);
|
|
assert.equal(verified.ok, true);
|
|
assert.equal(verified.value.status, 'scanned');
|
|
|
|
const secretAttempt = await handler(WEIXIN_ENDPOINTS.beginProvisioning, {
|
|
bot_token: 'must-never-cross-the-browser-boundary',
|
|
}, signal);
|
|
assert.equal(secretAttempt.ok, false);
|
|
assert.equal(secretAttempt.error.code, 'bad-request');
|
|
assert.doesNotMatch(JSON.stringify(secretAttempt), /must-never-cross/);
|
|
});
|
|
|
|
test('RPC requires explicit confirmation before removing a Weixin account', async () => {
|
|
const handler = createWeixinRpcHandler(controllerFixture());
|
|
const result = await handler(WEIXIN_ENDPOINTS.deleteBot, {
|
|
botId: 'wx_0123456789abcdef01234567',
|
|
confirm: false,
|
|
}, new AbortController().signal);
|
|
assert.equal(result.ok, false);
|
|
assert.equal(result.error.code, 'bad-request');
|
|
});
|
|
|
|
test('RPC sends a Weixin connection test only after reconnect reports the account connected', async () => {
|
|
const botId = 'wx_0123456789abcdef01234567';
|
|
const sent = [];
|
|
const connected = controllerFixture();
|
|
connected.reconnectBot = async () => snapshot({
|
|
state: 'connected',
|
|
bots: [{ botId, connected: true }],
|
|
totals: { configured: 1, connected: 1 },
|
|
});
|
|
connected.sendConnectionTest = async (id) => { sent.push(id); };
|
|
const success = await createWeixinRpcHandler(connected)(WEIXIN_ENDPOINTS.reconnectBot, {
|
|
botId, sendTest: true,
|
|
});
|
|
assert.equal(success.ok, true);
|
|
assert.deepEqual(success.value.testMessage, { sent: true });
|
|
assert.deepEqual(sent, [botId]);
|
|
|
|
connected.sendConnectionTest = async () => { throw new Error('send rejected'); };
|
|
const failedSend = await createWeixinRpcHandler(connected)(WEIXIN_ENDPOINTS.reconnectBot, {
|
|
botId, sendTest: true,
|
|
});
|
|
assert.equal(failedSend.ok, true);
|
|
assert.deepEqual(failedSend.value.testMessage, {
|
|
sent: false, code: 'test-message-failed',
|
|
});
|
|
|
|
let offlineSendCalled = false;
|
|
const offline = controllerFixture();
|
|
offline.reconnectBot = async () => snapshot({
|
|
state: 'offline',
|
|
bots: [{ botId, connected: false }],
|
|
totals: { configured: 1, connected: 0 },
|
|
});
|
|
offline.sendConnectionTest = async () => { offlineSendCalled = true; };
|
|
const unavailable = await createWeixinRpcHandler(offline)(WEIXIN_ENDPOINTS.reconnectBot, {
|
|
botId, sendTest: true,
|
|
});
|
|
assert.equal(unavailable.ok, true);
|
|
assert.deepEqual(unavailable.value.testMessage, {
|
|
sent: false, code: 'test-target-unavailable',
|
|
});
|
|
assert.equal(offlineSendCalled, false);
|
|
const missingMethod = controllerFixture();
|
|
missingMethod.reconnectBot = connected.reconnectBot;
|
|
delete missingMethod.sendConnectionTest;
|
|
const missing = await createWeixinRpcHandler(missingMethod)(WEIXIN_ENDPOINTS.reconnectBot, {
|
|
botId, sendTest: true,
|
|
});
|
|
assert.equal(missing.ok, true);
|
|
assert.deepEqual(missing.value.testMessage, {
|
|
sent: false, code: 'test-target-unavailable',
|
|
});
|
|
assert.equal((await createWeixinRpcHandler(connected)(WEIXIN_ENDPOINTS.reconnectBot, {
|
|
botId, sendTest: false,
|
|
})).ok, false);
|
|
});
|