mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 07:43:13 +08:00
168 lines
6.1 KiB
JavaScript
168 lines
6.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
DINGTALK_ENDPOINTS,
|
|
createDingtalkRpcHandler,
|
|
installDingtalkRpc,
|
|
} from '../../../plugin-src/host/channels/dingtalk/rpc.mjs';
|
|
|
|
function controller(overrides = {}) {
|
|
return {
|
|
status: async () => ({ bots: [] }),
|
|
startProvisioning: async () => ({
|
|
attemptId: 'attempt_1',
|
|
status: 'pending',
|
|
verificationUrl: 'https://open-dev.dingtalk.com/registration',
|
|
deviceCode: 'must-not-leak',
|
|
secretRef: 'must-not-leak',
|
|
}),
|
|
registrationStatus: async () => ({ attemptId: 'attempt_1', status: 'pending' }),
|
|
cancelProvisioning: async () => ({ attemptId: 'attempt_1', status: 'cancelled' }),
|
|
bindCredentials: async () => ({ bots: [] }),
|
|
reconnectBot: async () => ({ bots: [] }),
|
|
sendConnectionTest: async () => ({ sent: true }),
|
|
deleteBot: async () => ({ bots: [] }),
|
|
approveSender: async () => ({ bots: [] }),
|
|
revokeSender: async () => ({ bots: [] }),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test('RPC encodes QR on the Host and strips all credential material', async () => {
|
|
const handler = createDingtalkRpcHandler(controller(), {
|
|
encodeQr: async (url) => `data:image/png;base64,${Buffer.from(url).toString('base64')}`,
|
|
});
|
|
|
|
const result = await handler(DINGTALK_ENDPOINTS.beginProvisioning, { locale: 'zh-CN' });
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.match(result.value.qrCodeDataUrl, /^data:image\/png;base64,/);
|
|
assert.equal('verificationUrl' in result.value, false);
|
|
assert.equal('deviceCode' in result.value, false);
|
|
assert.equal('secretRef' in result.value, false);
|
|
});
|
|
|
|
test('status re-encodes an active QR without exposing its authorization URL', async () => {
|
|
const handler = createDingtalkRpcHandler(controller({
|
|
status: async () => ({
|
|
bots: [],
|
|
provisioning: {
|
|
attemptId: 'attempt_1',
|
|
status: 'pending',
|
|
verificationUrl: 'https://open-dev.dingtalk.com/registration',
|
|
},
|
|
}),
|
|
}), {
|
|
encodeQr: async () => 'data:image/png;base64,AAAA',
|
|
});
|
|
|
|
const result = await handler(DINGTALK_ENDPOINTS.status, {});
|
|
|
|
assert.equal(result.ok, true);
|
|
assert.equal(result.value.provisioning.qrCodeDataUrl, 'data:image/png;base64,AAAA');
|
|
assert.equal('verificationUrl' in result.value.provisioning, false);
|
|
});
|
|
|
|
test('RPC validates mutating requests before invoking the controller', async () => {
|
|
let calls = 0;
|
|
const handler = createDingtalkRpcHandler(controller({
|
|
approveSender: async () => { calls += 1; },
|
|
}));
|
|
|
|
const rejected = await handler(DINGTALK_ENDPOINTS.approveSender, {
|
|
botId: 'dt_abc', requestId: 'request_1', confirm: false,
|
|
});
|
|
assert.equal(rejected.ok, false);
|
|
assert.equal(rejected.error.code, 'bad-request');
|
|
assert.equal(calls, 0);
|
|
});
|
|
|
|
test('credential RPC accepts Client ID fields while keeping Client Secret host-only', async () => {
|
|
let received;
|
|
const handler = createDingtalkRpcHandler(controller({
|
|
bindCredentials: async (payload) => {
|
|
received = payload;
|
|
return { bots: [], clientSecret: payload.clientSecret };
|
|
},
|
|
}));
|
|
|
|
const result = await handler(DINGTALK_ENDPOINTS.bindCredentials, {
|
|
clientId: 'manual-client', clientSecret: 'manual-secret',
|
|
});
|
|
assert.equal(result.ok, true);
|
|
assert.deepEqual(received, { clientId: 'manual-client', clientSecret: 'manual-secret' });
|
|
assert.doesNotMatch(JSON.stringify(result), /manual-secret|clientSecret/);
|
|
assert.equal((await handler(DINGTALK_ENDPOINTS.bindCredentials, { clientId: 'manual-client' })).ok, false);
|
|
});
|
|
|
|
test('RPC is registered for loopback clients only', () => {
|
|
const registrations = [];
|
|
const dispose = () => {};
|
|
const ctx = {
|
|
connection: {
|
|
rpc: {
|
|
handle: (...args) => {
|
|
registrations.push(args);
|
|
return dispose;
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
assert.equal(installDingtalkRpc(ctx, controller()), dispose);
|
|
assert.equal(registrations[0][0], '/dingtalk');
|
|
assert.deepEqual(registrations[0][2], { authority: 'loopback' });
|
|
});
|
|
|
|
test('reconnect sends a DingTalk test message only for a connected bot and isolates send failures', async () => {
|
|
const sent = [];
|
|
const connectedSnapshot = {
|
|
bots: [{ botId: 'dt_abc', connected: true }],
|
|
totals: { configured: 1, connected: 1 },
|
|
};
|
|
const success = await createDingtalkRpcHandler(controller({
|
|
reconnectBot: async () => connectedSnapshot,
|
|
sendConnectionTest: async (botId) => { sent.push(botId); },
|
|
}))(DINGTALK_ENDPOINTS.reconnectBot, { botId: 'dt_abc', sendTest: true });
|
|
assert.equal(success.ok, true);
|
|
assert.deepEqual(success.value.testMessage, { sent: true });
|
|
assert.deepEqual(sent, ['dt_abc']);
|
|
|
|
const failedSend = await createDingtalkRpcHandler(controller({
|
|
reconnectBot: async () => connectedSnapshot,
|
|
sendConnectionTest: async () => { throw new Error('expired webhook'); },
|
|
}))(DINGTALK_ENDPOINTS.reconnectBot, { botId: 'dt_abc', sendTest: true });
|
|
assert.equal(failedSend.ok, true);
|
|
assert.deepEqual(failedSend.value.testMessage, {
|
|
sent: false, code: 'test-message-failed',
|
|
});
|
|
|
|
let offlineSendCalled = false;
|
|
const offline = await createDingtalkRpcHandler(controller({
|
|
reconnectBot: async () => ({
|
|
bots: [{ botId: 'dt_abc', connected: false }],
|
|
totals: { configured: 1, connected: 0 },
|
|
}),
|
|
sendConnectionTest: async () => { offlineSendCalled = true; },
|
|
}))(DINGTALK_ENDPOINTS.reconnectBot, { botId: 'dt_abc', sendTest: true });
|
|
assert.equal(offline.ok, true);
|
|
assert.deepEqual(offline.value.testMessage, {
|
|
sent: false, code: 'test-target-unavailable',
|
|
});
|
|
assert.equal(offlineSendCalled, false);
|
|
const withoutMethod = controller({ reconnectBot: async () => connectedSnapshot });
|
|
delete withoutMethod.sendConnectionTest;
|
|
const unavailableWithoutMethod = await createDingtalkRpcHandler(withoutMethod)(
|
|
DINGTALK_ENDPOINTS.reconnectBot,
|
|
{ botId: 'dt_abc', sendTest: true },
|
|
);
|
|
assert.equal(unavailableWithoutMethod.ok, true);
|
|
assert.deepEqual(unavailableWithoutMethod.value.testMessage, {
|
|
sent: false, code: 'test-target-unavailable',
|
|
});
|
|
assert.equal((await createDingtalkRpcHandler(controller())(
|
|
DINGTALK_ENDPOINTS.reconnectBot,
|
|
{ botId: 'dt_abc', sendTest: false },
|
|
)).ok, false);
|
|
});
|