mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-14 16:50:47 +08:00
Internalize all IM channel implementations
This commit is contained in:
parent
8996476693
commit
bd469f58f8
95 changed files with 25012 additions and 100 deletions
127
test/channels/dingtalk/config-store.test.mjs
Normal file
127
test/channels/dingtalk/config-store.test.mjs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
DingtalkConfigStore,
|
||||
deriveDingtalkBotIdentity,
|
||||
deriveDingtalkSenderKey,
|
||||
} from '../../../src/channels/dingtalk/config-store.mjs';
|
||||
|
||||
async function temporaryConfig(t) {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'dsh-dingtalk-config-'));
|
||||
t.after(() => rm(directory, { recursive: true, force: true }));
|
||||
return join(directory, 'nested', 'bots.json');
|
||||
}
|
||||
|
||||
test('bot identity is deterministic while sender keys are random opaque values', () => {
|
||||
const clientId = 'ding-client-one';
|
||||
const clientDigest = createHash('sha256').update(clientId).digest('hex').slice(0, 24);
|
||||
assert.deepEqual(deriveDingtalkBotIdentity(clientId), {
|
||||
botId: `dt_${clientDigest}`,
|
||||
secretRef: `DSH_DINGTALK_BOT_SECRET_${clientDigest.toUpperCase()}`,
|
||||
});
|
||||
|
||||
const firstSenderKey = deriveDingtalkSenderKey();
|
||||
const secondSenderKey = deriveDingtalkSenderKey();
|
||||
assert.match(firstSenderKey, /^dt_sender_[a-f0-9]{32}$/);
|
||||
assert.notEqual(firstSenderKey, secondSenderKey);
|
||||
});
|
||||
|
||||
test('config persists only clientId, derived secretRef, and approvedSenders with mode 0600', async (t) => {
|
||||
const path = await temporaryConfig(t);
|
||||
const store = await new DingtalkConfigStore(path).load();
|
||||
const identity = deriveDingtalkBotIdentity('ding-client-one');
|
||||
await store.save({
|
||||
...identity,
|
||||
clientId: 'ding-client-one',
|
||||
approvedSenders: [{
|
||||
senderKey: 'dt_sender_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
staffId: 'staff-private-one',
|
||||
displayName: '测试用户',
|
||||
approvedAt: '2026-08-15T00:00:00.000Z',
|
||||
}],
|
||||
ignoredMetadata: 'not-persisted',
|
||||
});
|
||||
|
||||
const document = JSON.parse(await readFile(path, 'utf8'));
|
||||
assert.deepEqual(Object.keys(document.bots[0]).sort(), [
|
||||
'approvedSenders',
|
||||
'clientId',
|
||||
'secretRef',
|
||||
]);
|
||||
assert.equal('botId' in document.bots[0], false);
|
||||
assert.equal('clientSecret' in document.bots[0], false);
|
||||
assert.equal((await stat(path)).mode & 0o777, 0o600);
|
||||
|
||||
const reloaded = await new DingtalkConfigStore(path).load();
|
||||
assert.deepEqual(reloaded.get(identity.botId), {
|
||||
botId: identity.botId,
|
||||
clientId: 'ding-client-one',
|
||||
secretRef: identity.secretRef,
|
||||
approvedSenders: [{
|
||||
senderKey: 'dt_sender_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
staffId: 'staff-private-one',
|
||||
displayName: '测试用户',
|
||||
approvedAt: '2026-08-15T00:00:00.000Z',
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
test('config rejects a persisted secret or a non-derived secret reference', async (t) => {
|
||||
const path = await temporaryConfig(t);
|
||||
const identity = deriveDingtalkBotIdentity('ding-client-one');
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, JSON.stringify({
|
||||
version: 1,
|
||||
bots: [{
|
||||
clientId: 'ding-client-one',
|
||||
secretRef: identity.secretRef,
|
||||
clientSecret: 'must-not-be-here',
|
||||
approvedSenders: [],
|
||||
}],
|
||||
}));
|
||||
await assert.rejects(
|
||||
new DingtalkConfigStore(path).load(),
|
||||
/invalid bot data/,
|
||||
);
|
||||
|
||||
const store = await new DingtalkConfigStore(`${path}.other`).load();
|
||||
await assert.rejects(
|
||||
store.save({
|
||||
...identity,
|
||||
clientId: 'ding-client-one',
|
||||
clientSecret: 'must-not-be-here',
|
||||
approvedSenders: [],
|
||||
}),
|
||||
/invalid dsh-dingtalk bot data/,
|
||||
);
|
||||
await assert.rejects(
|
||||
store.save({
|
||||
botId: identity.botId,
|
||||
clientId: 'ding-client-one',
|
||||
secretRef: 'DSH_DINGTALK_BOT_SECRET_000000000000000000000000',
|
||||
approvedSenders: [],
|
||||
}),
|
||||
/invalid dsh-dingtalk bot data/,
|
||||
);
|
||||
});
|
||||
|
||||
test('queued concurrent saves retain every bot and remove by derived bot ID', async (t) => {
|
||||
const path = await temporaryConfig(t);
|
||||
const store = await new DingtalkConfigStore(path).load();
|
||||
const first = deriveDingtalkBotIdentity('ding-client-one');
|
||||
const second = deriveDingtalkBotIdentity('ding-client-two');
|
||||
|
||||
await Promise.all([
|
||||
store.save({ ...first, clientId: 'ding-client-one', approvedSenders: [] }),
|
||||
store.save({ ...second, clientId: 'ding-client-two', approvedSenders: [] }),
|
||||
]);
|
||||
assert.equal(store.list().length, 2);
|
||||
assert.equal(store.getByClientId('ding-client-two').botId, second.botId);
|
||||
assert.equal((await store.remove(first.botId)).clientId, 'ding-client-one');
|
||||
assert.deepEqual(store.list().map((bot) => bot.botId), [second.botId]);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue