mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-16 15:10:45 +08:00
Add manual IM bot credential access
This commit is contained in:
parent
d5d2c18ce0
commit
7123fce47a
42 changed files with 2458 additions and 889 deletions
|
|
@ -79,3 +79,25 @@ test('QQ bridge accepts only the scanner and requires an at-message event in gro
|
|||
assert.equal(asks, 0);
|
||||
assert.equal(bridge.status.messagesRejected, 1);
|
||||
});
|
||||
|
||||
test('QQ credential-bound bots accept senders within the platform visibility scope', async () => {
|
||||
let asks = 0;
|
||||
const bridge = new QqHarnessBridge({
|
||||
bot: { sendText: async () => {} },
|
||||
ownerUserOpenid: '*',
|
||||
harness: {
|
||||
sessionExists: async () => true,
|
||||
ask: async () => { asks += 1; return 'ok'; },
|
||||
},
|
||||
state: {
|
||||
hasSeen: () => false,
|
||||
markSeen: async () => {},
|
||||
sessionFor: () => 'session',
|
||||
setSession: async () => {},
|
||||
clearSession: async () => {},
|
||||
},
|
||||
});
|
||||
await bridge.accept(message({ messageId: 'visible', senderId: 'visible-user' }));
|
||||
assert.equal(asks, 1);
|
||||
assert.equal(bridge.status.messagesRejected, 0);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ test('QQ settings uses the shared compact channel toolbar', () => {
|
|||
}));
|
||||
assert.match(markup, /class="ddt-page dqq-page dim-channelPage"/);
|
||||
assert.match(markup, /class="ddt-button dim-scanButton"/);
|
||||
assert.match(markup, />扫码绑定QQ机器人</);
|
||||
assert.match(markup, /aria-label="扫码接入 QQ 机器人"/);
|
||||
assert.match(markup, /class="dim-actionIcon"[^]*扫码接入机器人/);
|
||||
assert.doesNotMatch(markup, /凭据仅保存在本机|role="switch"|type="checkbox"/);
|
||||
});
|
||||
|
||||
|
|
@ -36,5 +37,16 @@ test('QQ bot cards match the shared two-metric card treatment', () => {
|
|||
assert.equal((markup.match(/class="ddt-metric dim-botMetric"/g) ?? []).length, 2);
|
||||
assert.match(markup, />消息通道<[^]*>最近检查</);
|
||||
assert.match(markup, />检查连接<[^]*>移除接入</);
|
||||
assert.doesNotMatch(markup, /收到\s*\/\s*回复/);
|
||||
assert.doesNotMatch(markup, /收到\s*\/\s*回复|dim-cardSummary|QQ WebSocket 长连接运行正常/);
|
||||
|
||||
const offlineMarkup = renderToStaticMarkup(React.createElement(AccountCard, {
|
||||
account: {
|
||||
botId: 'qq_bot', connected: false, state: 'error',
|
||||
bot: { name: 'QQ机器人', appIdMasked: '123••••456' },
|
||||
health: { summary: '连接失败,请检查凭据', lastCheckedAt: Date.now() },
|
||||
error: null,
|
||||
},
|
||||
onReconnect() {}, onRequestRemove() {}, onConfirmRemove() {}, onCancelRemove() {},
|
||||
}));
|
||||
assert.match(offlineMarkup, /class="ddt-summary dim-cardSummary">连接失败,请检查凭据</);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,6 +54,42 @@ test('QQ QR success stores the secret off-config and starts a scanner-owned runt
|
|||
qr.resolve();
|
||||
});
|
||||
|
||||
test('QQ AppID and AppSecret binding stores the secret and accepts the platform visibility scope', async () => {
|
||||
const values = new Map();
|
||||
const configs = [];
|
||||
let runtimeArgs;
|
||||
const controller = new QqController({
|
||||
qrAuth: { start() { return () => {}; } },
|
||||
credentials: {
|
||||
resolve: async (ref) => values.has(ref) ? { value: values.get(ref) } : undefined,
|
||||
set: async (ref, value) => values.set(ref, value),
|
||||
unset: async (ref) => values.delete(ref),
|
||||
},
|
||||
configStore: {
|
||||
list: () => [...configs],
|
||||
get: (id) => configs.find((value) => value.botId === id) ?? null,
|
||||
getByAppId: (id) => configs.find((value) => value.appId === id) ?? null,
|
||||
save: async (value) => { configs.splice(0, configs.length, value); },
|
||||
remove: async () => null,
|
||||
},
|
||||
createRuntime: async (args) => {
|
||||
runtimeArgs = args;
|
||||
return {
|
||||
status: { ready: true, qqConnectionState: 'connected', harnessReachable: true },
|
||||
start: async () => {}, stop: async () => {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const status = await controller.bindCredentials({ appId: 'manual-app', appSecret: 'manual-secret' });
|
||||
assert.equal(status.totals.connected, 1);
|
||||
assert.equal(configs[0].ownerUserOpenid, '*');
|
||||
assert.equal(values.get(configs[0].secretRef), 'manual-secret');
|
||||
assert.equal(runtimeArgs.appSecret, 'manual-secret');
|
||||
assert.doesNotMatch(JSON.stringify(status), /manual-secret|ownerUserOpenid|secretRef/);
|
||||
await controller.close();
|
||||
});
|
||||
|
||||
test('QQ RPC turns the host-only QR URL into an image and strips credential fields', async () => {
|
||||
const handler = createQqRpcHandler({
|
||||
status: () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
|
||||
|
|
@ -63,6 +99,7 @@ test('QQ RPC turns the host-only QR URL into an image and strips credential fiel
|
|||
}),
|
||||
registrationStatus: () => null,
|
||||
cancelProvisioning: async () => ({}),
|
||||
bindCredentials: async () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
|
||||
reconnectBot: async () => ({}),
|
||||
deleteBot: async () => ({}),
|
||||
}, { encodeQr: async () => 'data:image/png;base64,YWJjZA==' });
|
||||
|
|
@ -71,3 +108,23 @@ test('QQ RPC turns the host-only QR URL into an image and strips credential fiel
|
|||
assert.equal(result.value.qrCodeDataUrl, 'data:image/png;base64,YWJjZA==');
|
||||
assert.doesNotMatch(JSON.stringify(result), /q\.qq\.com|never-public|ownerUserOpenid|appSecret/);
|
||||
});
|
||||
|
||||
test('QQ credential RPC validates the pair and never returns AppSecret', async () => {
|
||||
let received;
|
||||
const handler = createQqRpcHandler({
|
||||
status: () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
|
||||
startProvisioning: async () => ({}), registrationStatus: () => null,
|
||||
cancelProvisioning: async () => ({}), reconnectBot: async () => ({}), deleteBot: async () => ({}),
|
||||
bindCredentials: async (payload) => {
|
||||
received = payload;
|
||||
return { bots: [], totals: { configured: 0, connected: 0 }, appSecret: payload.appSecret };
|
||||
},
|
||||
});
|
||||
const result = await handler(QQ_ENDPOINTS.bindCredentials, {
|
||||
appId: 'manual-app', appSecret: 'manual-secret',
|
||||
});
|
||||
assert.equal(result.ok, true);
|
||||
assert.deepEqual(received, { appId: 'manual-app', appSecret: 'manual-secret' });
|
||||
assert.doesNotMatch(JSON.stringify(result), /manual-secret|appSecret/);
|
||||
assert.equal((await handler(QQ_ENDPOINTS.bindCredentials, { appId: 'manual-app' })).ok, false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue