mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-08 23:13:17 +08:00
Localize WhatsApp access-grant approvals via t() and push Host language from the UI on load and locale change. Co-authored-by: Cursor <cursoragent@cursor.com>
285 lines
9 KiB
JavaScript
285 lines
9 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import {
|
||
approverPhonesForPending,
|
||
emptyAccessGrant,
|
||
enqueueAccessPending,
|
||
ensureOwnerGlobalAdmin,
|
||
evaluateAccessGrant,
|
||
findPendingByNotifyMessageId,
|
||
formatPendingNotifyBody,
|
||
migrateAccessPolicyToGrant,
|
||
normalizeAccessPhone,
|
||
parseApprovalIntent,
|
||
parsePendingIdFromNotifyText,
|
||
resolveAccessPending,
|
||
upsertAccessContact,
|
||
resolveAccessAgentPreset,
|
||
validateAccessGrant,
|
||
} from '../../../src/channels/shared/access-grant.mjs';
|
||
|
||
const GROUP_A = '120363111111111111@g.us';
|
||
const GROUP_B = '120363222222222222@g.us';
|
||
|
||
function grant(overrides = {}) {
|
||
return validateAccessGrant({
|
||
version: 1,
|
||
globalAdmins: ['8618111111111'],
|
||
directMembers: [],
|
||
groups: {},
|
||
pending: [],
|
||
contacts: [],
|
||
...overrides,
|
||
});
|
||
}
|
||
|
||
test('ACCESS_GRANT_GROUP_JID_PATTERN accepts legacy hyphen group JIDs', () => {
|
||
const legacy = '123456789-120363000000000000@g.us';
|
||
assert.doesNotThrow(() => validateAccessGrant({
|
||
...emptyAccessGrant(),
|
||
globalAdmins: ['8618111111111'],
|
||
groups: {
|
||
[legacy]: { admins: [], members: [] },
|
||
},
|
||
}));
|
||
});
|
||
|
||
test('normalizeAccessPhone accepts E.164, JIDs, and rejects LID servers', () => {
|
||
assert.equal(normalizeAccessPhone('+86 181-4238-7786'), '8618142387786');
|
||
assert.equal(normalizeAccessPhone('8618142387786@s.whatsapp.net'), '8618142387786');
|
||
assert.equal(normalizeAccessPhone('12345:12@c.us'), '12345');
|
||
assert.equal(normalizeAccessPhone('123456789012345@lid'), null);
|
||
assert.equal(normalizeAccessPhone('12'), null);
|
||
});
|
||
|
||
test('ensureOwnerGlobalAdmin seeds linked account and refuses empty admins on validate', () => {
|
||
const seeded = ensureOwnerGlobalAdmin(null, '8618111111111@s.whatsapp.net');
|
||
assert.deepEqual(seeded.globalAdmins, ['8618111111111']);
|
||
assert.throws(
|
||
() => validateAccessGrant({ ...emptyAccessGrant(), globalAdmins: [] }),
|
||
/全局管理员/,
|
||
);
|
||
});
|
||
|
||
test('migrateAccessPolicyToGrant copies allowlist phones into directMembers only', () => {
|
||
const migrated = migrateAccessPolicyToGrant({
|
||
direct: { allowlist: { users: [{ id: '8618222222222@s.whatsapp.net', canExecuteCommands: true }] } },
|
||
group: { allowlist: { users: [{ id: '+86 183-3333-3333', canExecuteCommands: false }] } },
|
||
}, null, '8618111111111');
|
||
assert.deepEqual(migrated.globalAdmins, ['8618111111111']);
|
||
assert.deepEqual(
|
||
migrated.directMembers.map((m) => m.phone).sort(),
|
||
['8618222222222', '8618333333333'],
|
||
);
|
||
assert.equal(Object.keys(migrated.groups).length, 0);
|
||
});
|
||
|
||
test('evaluateAccessGrant keeps DM and group scopes isolated', () => {
|
||
const doc = grant({
|
||
directMembers: [{ phone: '8618222222222', canExecuteCommands: true }],
|
||
groups: {
|
||
[GROUP_A]: {
|
||
admins: ['8618333333333'],
|
||
members: [{ phone: '8618444444444', canExecuteCommands: false }],
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'direct', phone: '8618111111111',
|
||
}).reason, 'global-admin');
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'direct', phone: '8618222222222',
|
||
}).allowed, true);
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'direct', phone: '8618333333333',
|
||
}).allowed, false);
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'group', phone: '8618444444444', groupJid: GROUP_A,
|
||
}).reason, 'group-member');
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'group', phone: '8618444444444', groupJid: GROUP_B,
|
||
}).allowed, false);
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'group', phone: '8618222222222', groupJid: GROUP_A,
|
||
}).allowed, false);
|
||
assert.equal(evaluateAccessGrant(doc, {
|
||
scene: 'group',
|
||
phone: '8618444444444',
|
||
groupJid: GROUP_A,
|
||
isCommand: true,
|
||
}).reason, 'command-not-allowed');
|
||
});
|
||
|
||
test('pending notify falls back to global admins when group has no admins', () => {
|
||
const doc = grant({
|
||
groups: {
|
||
[GROUP_A]: { admins: [], members: [] },
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
approverPhonesForPending(doc, { kind: 'group', groupJid: GROUP_A }),
|
||
['8618111111111'],
|
||
);
|
||
assert.deepEqual(
|
||
approverPhonesForPending(grant({
|
||
groups: { [GROUP_A]: { admins: ['8618555555555'], members: [] } },
|
||
}), { kind: 'group', groupJid: GROUP_A }),
|
||
['8618555555555'],
|
||
);
|
||
});
|
||
|
||
test('enqueue + approve places member into the matching grant bucket', () => {
|
||
let doc = grant();
|
||
const { grant: withPending, pending, created } = enqueueAccessPending(doc, {
|
||
kind: 'group',
|
||
groupJid: GROUP_A,
|
||
phone: '8618666666666',
|
||
pushName: 'Alice',
|
||
requestText: 'hello',
|
||
}, { idFactory: () => 'p_test_approve_1' });
|
||
assert.equal(created, true);
|
||
assert.equal(pending.id, 'p_test_approve_1');
|
||
doc = withPending;
|
||
|
||
const reused = enqueueAccessPending(doc, {
|
||
kind: 'group',
|
||
groupJid: GROUP_A,
|
||
phone: '8618666666666',
|
||
});
|
||
assert.equal(reused.created, false);
|
||
|
||
const { grant: approved } = resolveAccessPending(doc, {
|
||
pendingId: pending.id,
|
||
action: 'approve',
|
||
resolvedByPhone: '8618111111111',
|
||
});
|
||
assert.equal(approved.groups[GROUP_A].members[0].phone, '8618666666666');
|
||
assert.equal(approved.directMembers.length, 0);
|
||
assert.equal(approved.pending[0].status, 'approved');
|
||
});
|
||
|
||
test('contacts upsert merges lid and phone sightings', () => {
|
||
let doc = grant();
|
||
doc = upsertAccessContact(doc, {
|
||
lid: '999@lid',
|
||
pushName: 'Bob',
|
||
scene: 'group',
|
||
groupJid: GROUP_A,
|
||
});
|
||
doc = upsertAccessContact(doc, {
|
||
phone: '8618777777777',
|
||
lid: '999@lid',
|
||
pushName: 'Bobby',
|
||
scene: 'direct',
|
||
});
|
||
assert.equal(doc.contacts.length, 1);
|
||
assert.equal(doc.contacts[0].phone, '8618777777777');
|
||
assert.deepEqual(doc.contacts[0].lids, ['999@lid']);
|
||
assert.deepEqual(doc.contacts[0].scenes.slice().sort(), ['direct', 'group']);
|
||
});
|
||
|
||
test('quote approval helpers parse intent, pending id, and notify refs', () => {
|
||
assert.equal(parseApprovalIntent('同意 顺便说一声'), 'approve');
|
||
assert.equal(parseApprovalIntent('拒绝'), 'deny');
|
||
assert.equal(parseApprovalIntent('maybe later'), null);
|
||
const body = formatPendingNotifyBody({
|
||
id: 'p_abc123',
|
||
kind: 'direct',
|
||
phone: '8618888888888',
|
||
pushName: 'Carol',
|
||
requestText: 'hi',
|
||
});
|
||
assert.match(body, /请求编号:p_abc123/);
|
||
assert.equal(parsePendingIdFromNotifyText(body), 'p_abc123');
|
||
const enBody = formatPendingNotifyBody({
|
||
id: 'p_abc123',
|
||
kind: 'direct',
|
||
phone: '8618888888888',
|
||
pushName: 'Carol',
|
||
requestText: 'hi',
|
||
}, (text, params) => {
|
||
const map = {
|
||
'[dsh-im-ops] 访问申请': '[dsh-im-ops] Access request',
|
||
'私聊': 'Direct chat',
|
||
'场景:{scene}': 'Scene: {scene}',
|
||
'申请人:{who}': 'Requester: {who}',
|
||
'请求编号:{id}': 'Request: {id}',
|
||
'原文:{text}': 'Original message: {text}',
|
||
'请引用本条消息回复「同意」或「拒绝」。': 'Quote this message…',
|
||
'未知': 'unknown',
|
||
};
|
||
const translated = map[text] ?? text;
|
||
if (params == null) return translated;
|
||
return translated.replace(/\{(\w+)\}/g, (match, name) =>
|
||
Object.hasOwn(params, name) ? String(params[name]) : match);
|
||
});
|
||
assert.match(enBody, /Request: p_abc123/);
|
||
assert.equal(parsePendingIdFromNotifyText(enBody), 'p_abc123');
|
||
const withRef = {
|
||
...grant(),
|
||
pending: [{
|
||
id: 'p_abc123',
|
||
kind: 'direct',
|
||
phone: '8618888888888',
|
||
createdAt: new Date().toISOString(),
|
||
status: 'pending',
|
||
unresolved: false,
|
||
notifyRefs: [{ adminPhone: '8618111111111', providerMessageId: 'MSG1' }],
|
||
}],
|
||
};
|
||
assert.equal(findPendingByNotifyMessageId(withRef, 'MSG1')?.id, 'p_abc123');
|
||
});
|
||
|
||
|
||
test('resolveAccessAgentPreset prefers direct-member and group overrides over global', () => {
|
||
const doc = grant({
|
||
directMembers: [
|
||
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
|
||
{ phone: '8618333333333', canExecuteCommands: true },
|
||
],
|
||
groups: {
|
||
[GROUP_A]: {
|
||
title: 'A',
|
||
agentPreset: 'ops-group',
|
||
admins: [],
|
||
members: [{ phone: '8618444444444', canExecuteCommands: true }],
|
||
},
|
||
},
|
||
});
|
||
assert.equal(resolveAccessAgentPreset(doc, {
|
||
kind: 'direct',
|
||
phone: '8618222222222',
|
||
}), 'ops-user');
|
||
assert.equal(resolveAccessAgentPreset(doc, {
|
||
kind: 'direct',
|
||
phone: '8618333333333',
|
||
}), null);
|
||
assert.equal(resolveAccessAgentPreset(doc, {
|
||
kind: 'group',
|
||
groupJid: GROUP_A,
|
||
}), 'ops-group');
|
||
assert.equal(resolveAccessAgentPreset(doc, {
|
||
kind: 'group',
|
||
groupJid: GROUP_B,
|
||
}), null);
|
||
});
|
||
|
||
test('validateAccessGrant keeps optional agentPreset on members and groups', () => {
|
||
const doc = grant({
|
||
directMembers: [{ phone: '8618222222222', canExecuteCommands: false, agentPreset: 'desk' }],
|
||
groups: {
|
||
[GROUP_A]: {
|
||
agentPreset: 'night',
|
||
admins: ['8618111111111'],
|
||
members: [],
|
||
},
|
||
},
|
||
});
|
||
assert.equal(doc.directMembers[0].agentPreset, 'desk');
|
||
assert.equal(doc.groups[GROUP_A].agentPreset, 'night');
|
||
assert.throws(() => grant({
|
||
directMembers: [{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'BAD Preset' }],
|
||
}), /Agent Preset/);
|
||
});
|