| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- import test from 'node:test';
- import assert from 'node:assert/strict';
- import crypto from 'node:crypto';
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import { attachWecomEmployeeReadHandler } from '../server/identity/employee-read.mjs';
- import { buildEmployeeRead } from '../server/identity/build-employee-read.mjs';
- import { manage } from '../server/identity/manage.mjs';
- import { runWorkshop, createWorkshopCaller } from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
- import { parseArgs } from '../skills/ksf-workshop-runtime/scripts/ksf-secure.mjs';
- const hash = v => crypto.createHash('sha256').update(v).digest('hex');
- const canonical = v => v && typeof v === 'object' ? Array.isArray(v) ? '[' + v.map(canonical).join(',') + ']' : '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}' : JSON.stringify(v);
- const account = { platform: 'wecom', identifierType: 'wecom_aibot_sender', scopeId: 'private-bot', accountId: 'private-sender' };
- const participant = { key: 'participant-account:' + 'a'.repeat(64), name: '员工待绑定', identityBasis: 'channel_context', account };
- const agent = { key: 'agent-account:' + 'b'.repeat(64), name: '测试数字人', role: 'workshop', runtimePlatform: 'hermes', channel: 'wecom', account: { platform: 'wecom', accountId: account.scopeId } };
- const params = { sessionId: 'session1', participantProfileKey: participant.key, agentProfileKey: agent.key, requestId: 'read1' };
- function fixture({ configured = true, apiError = false } = {}) {
- const calls = [], writes = [];
- const session = { objectId: 'session1', workshopKey: 'test-workshop', meta: { ownerId: 'service1', attribution: {
- participantKey: 'participant:' + hash(canonical(['test-workshop', participant.key])), participantAccount: account, identityBasis: 'channel_context',
- agentKey: agent.key, agentName: agent.name, agentRuntimePlatform: agent.runtimePlatform, agentChannel: agent.channel, agentAccount: agent.account,
- } } };
- const fail = code => { throw Object.assign(Error(code), { code }); };
- const app = { handlers: {}, hash, canonical, fail, need: (actor, scope) => { if (!actor) fail('AUTH_REQUIRED'); if (!actor.scopes.some(s => s === scope || s === '*')) fail('FORBIDDEN'); }, operation: () => { writes.push('operation'); throw Error('must not write'); } };
- const config = { enabled: true, corpId: 'private-corp', secret: 'fixture-secret', bots: { 'private-bot': { mappingVerified: true, senderIdType: 'mapped', userIds: { 'private-sender': 'private-user' } } } };
- const fetchImpl = async url => {
- calls.push(url);
- const body = apiError ? { errcode: 48002, errmsg: 'fixture-secret must not appear in error' } : new URL(url).pathname.endsWith('/gettoken')
- ? { errcode: 0, access_token: 'fixture-access-token', expires_in: 7200 }
- : { errcode: 0, userid: 'private-user', status: 1, name: '测试员工甲', department: [], mobile: 'private-mobile' };
- return { ok: true, json: async () => body };
- };
- const store = { get: async (kind, id) => kind === 'KfSession' && id === 'session1' ? structuredClone(session) : null,
- update: () => { writes.push('update'); throw Error('must not write'); } };
- const settings = configured ? { wecomDirectory: config } : {};
- attachWecomEmployeeReadHandler(app, { store, settings, fetchImpl });
- return { app, session, settings, store, calls, writes, actor: { id: 'service1', scopes: ['data:read'] } };
- }
- test('employee-read从Skill到服务端实际查询姓名,沿用data:read且服务端零写入', async () => {
- const f = fixture(), calls = [];
- const result = await runWorkshop('employee-read', { sessionId: 'session1', participant: { profileKey: participant.key } }, {
- requestId: 'minimal-read', agentProfile: agent, participantProfile: participant,
- call: async (name, p) => { calls.push({ name, p }); return name === 'ksfGetRecord' ? { record: structuredClone(f.session) } : f.app.handlers[name](p, f.actor); },
- });
- assert.deepEqual(calls.map(c => c.name), ['ksfGetRecord', 'ksfReadWecomEmployee']);
- assert.equal(calls[1].p.requestId, 'minimal-read');
- assert.equal(result.employee.name, '测试员工甲'); assert.equal(result.employee.nameVerified, true);
- assert.equal(result.readOnly, true); assert.equal(result.databaseWrites, 0); assert.equal(result.employeeIdentity.persisted, false);
- assert.equal(f.calls.length, 2); assert.deepEqual(f.writes, []);
- for (const secret of ['fixture-secret', 'fixture-access-token', 'private-user', 'private-mobile', 'private-bot', 'private-sender']) assert.ok(!JSON.stringify(result).includes(secret));
- });
- test('匿名、其他服务、其他员工、其他Agent或无读取权限在通讯录请求前被拒绝', async () => {
- for (const [input, actor, code] of [
- [params, null, 'AUTH_REQUIRED'], [params, { id: 'service1', scopes: ['ingest:write'] }, 'FORBIDDEN'],
- [params, { id: 'another', scopes: ['data:read'] }, 'FORBIDDEN'],
- [{ ...params, participantProfileKey: 'participant-account:' + 'c'.repeat(64) }, null, 'IDENTITY_CONFLICT'],
- [{ ...params, agentProfileKey: 'agent-account:' + 'c'.repeat(64) }, null, 'IDENTITY_CONFLICT'],
- [{ ...params, apply: true }, null, 'VALIDATION_FAILED'],
- ]) {
- const f = fixture();
- const effectiveActor = actor || (code === 'AUTH_REQUIRED' ? null : f.actor);
- await assert.rejects(() => f.app.handlers.ksfReadWecomEmployee(input, effectiveActor), { code });
- assert.equal(f.calls.length, 0); assert.deepEqual(f.writes, []);
- }
- });
- test('缺配置、缺映射和无通讯录权限给出真实错误,不退回看板占位名', async () => {
- const absent = fixture({ configured: false });
- await assert.rejects(() => absent.app.handlers.ksfReadWecomEmployee(params, absent.actor), { code: 'WECOM_DIRECTORY_NOT_CONFIGURED' });
- assert.equal(absent.calls.length, 0);
- const mapping = fixture(); mapping.settings.wecomDirectory.bots['private-bot'].userIds = {};
- await assert.rejects(() => mapping.app.handlers.ksfReadWecomEmployee(params, mapping.actor), { code: 'WECOM_SENDER_MAPPING_REQUIRED' });
- assert.equal(mapping.calls.length, 0);
- const denied = fixture({ apiError: true });
- await assert.rejects(() => denied.app.handlers.ksfReadWecomEmployee(params, denied.actor), e => e.code === 'WECOM_DIRECTORY_PERMISSION_REQUIRED' && !e.message.includes('fixture-secret'));
- assert.deepEqual(denied.writes, []);
- });
- test('CLI白名单接受employee-read但仍要求Agent,错员工在远端姓名查询前停止', async () => {
- const input = path.resolve('input.json');
- assert.equal(parseArgs(['employee-read', '--input', input, '--request-id', 'r1', '--agent-profile', agent.key]).command, 'employee-read');
- assert.throws(() => parseArgs(['employee-read', '--input', input, '--request-id', 'r1']), { code: 'AGENT_PROFILE_REQUIRED' });
- const f = fixture(), calls = [];
- await assert.rejects(() => runWorkshop('employee-read', { sessionId: 'session1', participant: { profileKey: participant.key } }, {
- requestId: 'r1', agentProfile: agent, participantProfile: participant, call: async name => { calls.push(name); return { record: { ...f.session, meta: { attribution: { ...f.session.meta.attribution, participantKey: 'another-participant' } } } }; },
- }), { code: 'PARTICIPANT_SESSION_MISMATCH' });
- assert.deepEqual(calls, ['ksfGetRecord']);
- });
- test('受权网络调用保留远端函数及真实错误码且不自动重试', async () => {
- const calls = [];
- const caller = createWorkshopCaller({ token: 'fixture-token', fetchImpl: async (url, options) => {
- calls.push({ url, options });
- return { ok: true, json: async () => ({ result: { ok: false, errors: [{ code: 'WECOM_DIRECTORY_NOT_CONFIGURED', message: 'private-secret' }] } }) };
- } });
- await assert.rejects(() => caller('ksfReadWecomEmployee', params), e => e.code === 'WECOM_DIRECTORY_NOT_CONFIGURED' && e.remoteFunction === 'ksfReadWecomEmployee' && e.remoteResponseReceived === true && !e.message.includes('private-secret'));
- assert.equal(calls.length, 1); assert.equal(JSON.parse(calls[0].options.body).path, '/ksfReadWecomEmployee');
- });
- test('单函数补丁只新增读取函数,部署可重复、可回退且保留r5四函数', async t => {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-employee-read-'));
- t.after(() => fs.rmSync(dir, { recursive: true }));
- const base = { name: 'ksfGetPublicBoard', objectId: 'public1', path: '/ksfGetPublicBoard', ACL: {},
- code: 'function bootstrap(){attachWecomEmployeeHandlers(app,{store,settings});}\nfunction handler(request,response){return runDatabaseFunction("ksfGetPublicBoard",request,response,{});}' };
- const built = buildEmployeeRead(base);
- new Function(built.functions[0].code);
- for (const entry of built.functions) fs.writeFileSync(path.join(dir, entry.name + '.js'), entry.code);
- fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify({ ...built, functions: built.functions.map(({ code, ...meta }) => meta) }));
- const rows = [structuredClone(base), ...['ksfStartWorkshopSession', 'ksfSaveWorkshopTask', 'ksfSyncWecomEmployee'].map(name => ({ name, objectId: name, code: 'preserved', ACL: {} }))];
- const before = structuredClone(rows), writes = [];
- const request = async (method, route, body) => {
- if (route.includes('?')) return { results: structuredClone(rows.filter(r => r.name === JSON.parse(new URL('https://test' + route).searchParams.get('where')).name)) };
- const id = route.split('/').at(-1);
- if (method === 'GET') return structuredClone(rows.find(r => r.objectId === id));
- writes.push({ method, route, body });
- if (method === 'POST') { rows.push({ objectId: 'read1', ...body }); return { objectId: 'read1' }; }
- if (method === 'DELETE') { rows.splice(rows.findIndex(r => r.objectId === id), 1); return {}; }
- throw Error('must not edit existing functions');
- };
- const run = args => manage({ args, request, releaseDir: dir });
- const dry = await run(['deploy-employee-read']); assert.equal(dry.readOnly, true); assert.equal(writes.length, 0);
- const applied = await run(['deploy-employee-read', '--apply', '--receipt-dir', dir]);
- assert.equal(applied.functions, 1); assert.equal(writes.length, 1); assert.equal(rows.length, 5);
- assert.deepEqual(rows.slice(0, 4), before);
- assert.equal((await run(['deploy-employee-read', '--apply', '--receipt-dir', dir])).unchanged, true); assert.equal(writes.length, 1);
- await run(['rollback', '--receipt', applied.receipt, '--apply']); assert.deepEqual(rows, before);
- assert.throws(() => buildEmployeeRead({ ...base, code: base.code.replace('attachWecomEmployeeHandlers', 'changed') }), /UNEXPECTED_R5_BASE/);
- rows[0].code += '// concurrent edit';
- await assert.rejects(() => run(['deploy-employee-read']), { code: 'FUNCTION_BASELINE_MISMATCH' });
- });
|