mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 00:32:43 +08:00
feat: add unified IM access policies
This commit is contained in:
parent
075d2713c1
commit
840e5aa857
89 changed files with 5573 additions and 1676 deletions
|
|
@ -4,16 +4,12 @@ import test from 'node:test';
|
|||
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import TestRenderer from 'react-test-renderer';
|
||||
|
||||
import {
|
||||
TelegramAccessSettings,
|
||||
TelegramAccountCard,
|
||||
TelegramSettingsTab,
|
||||
} from '../../../plugin-src/client/channels/telegram/index.js';
|
||||
|
||||
const { act } = TestRenderer;
|
||||
|
||||
test('Telegram settings exposes a Bot Token action without a fake QR action', () => {
|
||||
const markup = renderToStaticMarkup(React.createElement(TelegramSettingsTab, {
|
||||
rpcCall: async () => ({ ok: true, value: { bots: [] } }),
|
||||
|
|
@ -44,125 +40,16 @@ test('Telegram account card matches the unified compact card layout', () => {
|
|||
assert.doesNotMatch(markup, /Bot API 长轮询|消息通道|dim-botMetric/);
|
||||
assert.match(markup, />检查连接</);
|
||||
assert.match(markup, />移除接入</);
|
||||
assert.match(markup, />访问设置</);
|
||||
assert.match(markup, /aria-label="Telegram 访问模式"/);
|
||||
assert.match(markup, />兼容模式(默认)</);
|
||||
assert.match(markup, /aria-label="更多机器人设置"/);
|
||||
assert.doesNotMatch(markup, /Telegram 访问模式|兼容模式(默认)|安全模式(私聊白名单)/);
|
||||
assert.doesNotMatch(markup, /dim-cardSummary/);
|
||||
});
|
||||
|
||||
test('Telegram access settings edits and saves one bot policy', async () => {
|
||||
const saved = [];
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = TestRenderer.create(React.createElement(TelegramAccessSettings, {
|
||||
account: {
|
||||
botId: 'telegram_test',
|
||||
accessPolicy: { accessMode: 'compatible', allowedUsers: ['111111111'] },
|
||||
},
|
||||
onSave: async (policy) => saved.push(policy),
|
||||
}));
|
||||
});
|
||||
|
||||
const select = renderer.root.findByProps({ 'aria-label': 'Telegram 访问模式' });
|
||||
let textarea = renderer.root.findByProps({
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
});
|
||||
assert.equal(textarea.props.disabled, true);
|
||||
await act(async () => {
|
||||
select.props.onChange({ target: { value: 'private-allowlist' } });
|
||||
});
|
||||
textarea = renderer.root.findByProps({
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
});
|
||||
assert.equal(textarea.props.disabled, false);
|
||||
await act(async () => {
|
||||
textarea.props.onChange({ target: { value: '6087707998\n1202499116\n6087707998' } });
|
||||
});
|
||||
await act(async () => {
|
||||
select.props.onChange({ target: { value: 'compatible' } });
|
||||
});
|
||||
textarea = renderer.root.findByProps({
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
});
|
||||
assert.equal(textarea.props.disabled, true);
|
||||
assert.equal(textarea.props.value, '6087707998\n1202499116\n6087707998');
|
||||
await act(async () => {
|
||||
select.props.onChange({ target: { value: 'private-allowlist' } });
|
||||
});
|
||||
textarea = renderer.root.findByProps({
|
||||
'aria-label': '允许私聊的 Telegram User ID',
|
||||
});
|
||||
assert.equal(textarea.props.disabled, false);
|
||||
assert.equal(textarea.props.value, '6087707998\n1202499116\n6087707998');
|
||||
assert.deepEqual(
|
||||
renderer.root.findByProps({ className: 'dtg-accessBadge' }).children,
|
||||
['已生效:兼容模式'],
|
||||
);
|
||||
await act(async () => {
|
||||
await renderer.root.findByType('form').props.onSubmit({ preventDefault() {} });
|
||||
});
|
||||
assert.deepEqual(saved, [{
|
||||
accessMode: 'private-allowlist',
|
||||
allowedUsers: ['6087707998', '1202499116'],
|
||||
}]);
|
||||
await act(async () => renderer.unmount());
|
||||
});
|
||||
|
||||
test('Telegram access settings keeps both mode descriptions in an accessible help tooltip', async () => {
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = TestRenderer.create(React.createElement(TelegramAccessSettings, {
|
||||
account: {
|
||||
botId: 'telegram_test',
|
||||
accessPolicy: { accessMode: 'compatible', allowedUsers: ['111111111'] },
|
||||
},
|
||||
onSave() {},
|
||||
}));
|
||||
});
|
||||
const helpButton = renderer.root.findByProps({
|
||||
'aria-label': '查看 Telegram 访问模式说明',
|
||||
});
|
||||
const tooltip = renderer.root.findByProps({ role: 'tooltip' });
|
||||
const heading = renderer.root.findByProps({ className: 'dtg-accessHeading' });
|
||||
assert.equal(helpButton.props.type, 'button');
|
||||
assert.ok(tooltip.props.id);
|
||||
assert.equal(helpButton.props['aria-describedby'], tooltip.props.id);
|
||||
assert.equal(heading.findAllByType('p').length, 0);
|
||||
|
||||
const markup = renderToStaticMarkup(React.createElement(TelegramAccessSettings, {
|
||||
account: {
|
||||
botId: 'telegram_test',
|
||||
accessPolicy: { accessMode: 'compatible', allowedUsers: ['111111111'] },
|
||||
},
|
||||
onSave() {},
|
||||
}));
|
||||
assert.match(markup, />兼容模式<\/strong>/);
|
||||
assert.match(markup, />安全模式<\/strong>/);
|
||||
assert.match(markup, /保持原有行为:私聊直接响应,群聊在被提及或回复时响应。/);
|
||||
assert.match(markup, /群聊全部忽略,私聊仅允许白名单用户。/);
|
||||
await act(async () => renderer.unmount());
|
||||
});
|
||||
|
||||
test('Telegram access mode help opens for pointer hover and keyboard focus', async () => {
|
||||
const styles = await readFile(
|
||||
new URL('../../../plugin-src/client/channels/telegram/styles.js', import.meta.url),
|
||||
test('Telegram cards shrink to a narrow English panel without horizontal scrolling', async () => {
|
||||
const sharedStyles = await readFile(
|
||||
new URL('../../../plugin-src/client/styles.js', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
assert.match(styles, /\.dtg-accessHeading \{[^}]*position: relative;/);
|
||||
assert.match(styles, /\.dtg-accessHelp \{[^}]*position: static;/);
|
||||
assert.match(styles, /\.dtg-accessTooltip \{[^}]*right: 0;[^}]*width: min\(300px, 100%\);[^}]*max-width: 100%;/);
|
||||
assert.match(styles, /\.dtg-accessHelpButton:focus-visible \{/);
|
||||
assert.match(styles, /\.dtg-accessHelp:hover \.dtg-accessTooltip, \.dtg-accessHelp:focus-within \.dtg-accessTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
|
||||
});
|
||||
|
||||
test('Telegram cards shrink to a narrow English panel without horizontal scrolling', async () => {
|
||||
const [sharedStyles, telegramStyles] = await Promise.all([
|
||||
readFile(new URL('../../../plugin-src/client/styles.js', import.meta.url), 'utf8'),
|
||||
readFile(
|
||||
new URL('../../../plugin-src/client/channels/telegram/styles.js', import.meta.url),
|
||||
'utf8',
|
||||
),
|
||||
]);
|
||||
|
||||
assert.match(sharedStyles, /\.dim-panel \.dim-botList \{[^}]*grid-template-columns: minmax\(0, 1fr\);/);
|
||||
assert.match(sharedStyles, /\.dim-panel \.dim-botCard \{[^}]*min-width: 0;[^}]*width: 100%;[^}]*max-width: 100%;[^}]*overflow: hidden;/);
|
||||
|
|
@ -170,19 +57,4 @@ test('Telegram cards shrink to a narrow English panel without horizontal scrolli
|
|||
assert.doesNotMatch(sharedStyles, /\.dim-panel \.dim-botCardTop \{ flex-direction: column;/);
|
||||
assert.match(sharedStyles, /\.dim-panel \.dim-workspacePath \{[^}]*overflow: hidden;[^}]*overflow-wrap: anywhere;[^}]*white-space: normal;/);
|
||||
assert.doesNotMatch(sharedStyles, /\.dim-panel \.dim-workspacePath \{[^}]*overflow-x: auto;/);
|
||||
assert.match(telegramStyles, /\.dtg-access \{[^}]*min-width: 0;[^}]*width: 100%;[^}]*max-width: 100%;/);
|
||||
assert.match(telegramStyles, /\.dtg-accessHeading \{[^}]*flex-wrap: wrap;/);
|
||||
assert.match(telegramStyles, /\.dtg-accessStatus \{[^}]*max-width: 100%;[^}]*flex-wrap: wrap;/);
|
||||
assert.match(telegramStyles, /\.dtg-accessField select, \.dtg-accessField textarea \{[^}]*min-width: 0;[^}]*max-width: 100%;/);
|
||||
});
|
||||
|
||||
test('Telegram access settings warns when safe mode has an empty allowlist', () => {
|
||||
const markup = renderToStaticMarkup(React.createElement(TelegramAccessSettings, {
|
||||
account: {
|
||||
botId: 'telegram_test',
|
||||
accessPolicy: { accessMode: 'private-allowlist', allowedUsers: [] },
|
||||
},
|
||||
onSave() {},
|
||||
}));
|
||||
assert.match(markup, /白名单为空;保存后该机器人会拒绝所有入站消息。/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -902,7 +902,7 @@ test('Telegram queued policy update cannot persist after controller close begins
|
|||
assert.equal(configStore.get(botId).allowedUsers, undefined);
|
||||
});
|
||||
|
||||
test('Telegram RPC accepts only token binding and strips credential internals', async () => {
|
||||
test('Telegram RPC accepts the unified access policy and strips credential internals', async () => {
|
||||
const calls = [];
|
||||
const connectionTests = [];
|
||||
const controller = {
|
||||
|
|
@ -925,7 +925,7 @@ test('Telegram RPC accepts only token binding and strips credential internals',
|
|||
}),
|
||||
sendConnectionTest: async (botId) => { connectionTests.push(botId); },
|
||||
deleteBot: async () => ({ bots: [], totals: { configured: 0, connected: 0 } }),
|
||||
setAccessPolicy: async (botId, policy) => {
|
||||
updateAccessPolicy: async (botId, policy) => {
|
||||
calls.push({ botId, policy });
|
||||
return {
|
||||
bots: [{ botId, accessPolicy: policy }],
|
||||
|
|
@ -973,28 +973,35 @@ test('Telegram RPC accepts only token binding and strips credential internals',
|
|||
code: 'test-target-unavailable',
|
||||
});
|
||||
|
||||
const unifiedPolicy = {
|
||||
direct: {
|
||||
mode: 'allowlist',
|
||||
open: { defaultCanExecuteCommands: false, commandPermissionOverrides: [] },
|
||||
allowlist: { users: [{ id: '6087707998', canExecuteCommands: true }] },
|
||||
},
|
||||
group: {
|
||||
mode: 'open',
|
||||
open: { defaultCanExecuteCommands: true, commandPermissionOverrides: [] },
|
||||
allowlist: { users: [] },
|
||||
},
|
||||
};
|
||||
const access = await handler(TELEGRAM_ENDPOINTS.setAccessPolicy, {
|
||||
botId: 'telegram_123',
|
||||
accessMode: TELEGRAM_ACCESS_MODES.privateAllowlist,
|
||||
allowedUsers: ['6087707998', '6087707998'],
|
||||
policy: unifiedPolicy,
|
||||
});
|
||||
assert.equal(access.ok, true);
|
||||
assert.deepEqual(calls.at(-1), {
|
||||
botId: 'telegram_123',
|
||||
policy: {
|
||||
accessMode: TELEGRAM_ACCESS_MODES.privateAllowlist,
|
||||
allowedUsers: ['6087707998'],
|
||||
},
|
||||
policy: unifiedPolicy,
|
||||
});
|
||||
assert.equal((await handler(TELEGRAM_ENDPOINTS.setAccessPolicy, {
|
||||
botId: 'telegram_123',
|
||||
accessMode: TELEGRAM_ACCESS_MODES.privateAllowlist,
|
||||
allowedUsers: ['@username'],
|
||||
allowedUsers: ['6087707998'],
|
||||
})).error.code, 'bad-request');
|
||||
assert.equal((await handler(TELEGRAM_ENDPOINTS.setAccessPolicy, {
|
||||
botId: 'telegram_123',
|
||||
accessMode: TELEGRAM_ACCESS_MODES.compatible,
|
||||
allowedUsers: [],
|
||||
policy: unifiedPolicy,
|
||||
extra: true,
|
||||
})).error.code, 'bad-request');
|
||||
});
|
||||
|
|
@ -1009,7 +1016,7 @@ test('shared token RPC never sends a connection test after reconnect is cancelle
|
|||
reconnectBot: async () => reconnect,
|
||||
sendConnectionTest: async () => { sendCalls += 1; },
|
||||
deleteBot: async () => ({ bots: [] }),
|
||||
setAccessPolicy: async () => ({ bots: [] }),
|
||||
updateAccessPolicy: async () => ({ bots: [] }),
|
||||
};
|
||||
const abort = new AbortController();
|
||||
const result = createTelegramRpcHandler(controller)(TELEGRAM_ENDPOINTS.reconnectBot, {
|
||||
|
|
@ -1446,7 +1453,7 @@ test('Telegram runtime still starts when the command menu setup fails', async ()
|
|||
}
|
||||
});
|
||||
|
||||
test('Telegram runtime enforces the selected bot private allowlist', async () => {
|
||||
test('Telegram runtime enforces the unified direct and group access policy', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'dsh-im-telegram-allowlist-runtime-'));
|
||||
const state = await new TelegramStateStore(join(directory, 'state.json')).load();
|
||||
const asked = [];
|
||||
|
|
@ -1508,8 +1515,10 @@ test('Telegram runtime enforces the selected bot private allowlist', async () =>
|
|||
botId: 'telegram_allowlist',
|
||||
platformId: '123456789',
|
||||
username: 'HarnessBot',
|
||||
// Kept deliberately contradictory: legacy fields are migration input,
|
||||
// not a second active Runtime gate after unified policy injection.
|
||||
accessMode: TELEGRAM_ACCESS_MODES.privateAllowlist,
|
||||
allowedUsers: ['7'],
|
||||
allowedUsers: ['999'],
|
||||
},
|
||||
token: TOKEN,
|
||||
harness: {
|
||||
|
|
@ -1521,6 +1530,20 @@ test('Telegram runtime enforces the selected bot private allowlist', async () =>
|
|||
},
|
||||
},
|
||||
state,
|
||||
accessPolicy: {
|
||||
getSettings: () => ({
|
||||
direct: {
|
||||
mode: 'allowlist',
|
||||
open: { defaultCanExecuteCommands: false, commandPermissionOverrides: [] },
|
||||
allowlist: { users: [{ id: '7', canExecuteCommands: true }] },
|
||||
},
|
||||
group: {
|
||||
mode: 'allowlist',
|
||||
open: { defaultCanExecuteCommands: false, commandPermissionOverrides: [] },
|
||||
allowlist: { users: [] },
|
||||
},
|
||||
}),
|
||||
},
|
||||
createApi: () => fakeApi,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue