@ihazz/bitrix24 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +236 -42
- package/package.json +1 -1
- package/src/access-control.ts +31 -8
- package/src/api.ts +76 -7
- package/src/channel.ts +1025 -137
- package/src/commands.ts +7 -7
- package/src/config-schema.ts +24 -1
- package/src/config.ts +4 -2
- package/src/group-access.ts +279 -0
- package/src/history-cache.ts +122 -0
- package/src/i18n.ts +140 -50
- package/src/inbound-handler.ts +91 -8
- package/src/polling-service.ts +4 -0
- package/src/send-service.ts +14 -5
- package/src/types.ts +67 -3
- package/tests/access-control.test.ts +43 -0
- package/tests/api.test.ts +131 -0
- package/tests/channel-flow.test.ts +1692 -0
- package/tests/channel.test.ts +88 -2
- package/tests/config.test.ts +120 -0
- package/tests/group-access.test.ts +340 -0
- package/tests/history-cache.test.ts +117 -0
- package/tests/i18n.test.ts +55 -12
- package/tests/inbound-handler.test.ts +388 -3
- package/tests/polling-service.test.ts +38 -0
- package/tests/send-service.test.ts +17 -0
package/tests/channel.test.ts
CHANGED
|
@@ -4,12 +4,14 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
4
4
|
import {
|
|
5
5
|
__setGatewayStateForTests,
|
|
6
6
|
buildBotCodeCandidates,
|
|
7
|
+
buildConversationSessionKey,
|
|
7
8
|
canCoalesceDirectMessage,
|
|
8
9
|
convertButtonsToKeyboard,
|
|
9
10
|
extractKeyboardFromPayload,
|
|
10
11
|
handleWebhookRequest,
|
|
11
12
|
mergeBufferedDirectMessages,
|
|
12
13
|
mergeForwardedMessageContext,
|
|
14
|
+
resolveConversationRef,
|
|
13
15
|
resolveDirectMessageCoalesceDelay,
|
|
14
16
|
shouldSkipJoinChatWelcome,
|
|
15
17
|
bitrix24Plugin,
|
|
@@ -72,6 +74,19 @@ describe('convertButtonsToKeyboard', () => {
|
|
|
72
74
|
}
|
|
73
75
|
});
|
|
74
76
|
|
|
77
|
+
it('converts a registered command without leading slash', () => {
|
|
78
|
+
const kb = convertButtonsToKeyboard([
|
|
79
|
+
[{ text: 'Help', callback_data: 'help' }],
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const btn = kb[0];
|
|
83
|
+
expect(isButton(btn)).toBe(true);
|
|
84
|
+
if (isButton(btn)) {
|
|
85
|
+
expect(btn.COMMAND).toBe('help');
|
|
86
|
+
expect(btn.COMMAND_PARAMS).toBeUndefined();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
75
90
|
it('converts slash command with params', () => {
|
|
76
91
|
const kb = convertButtonsToKeyboard([
|
|
77
92
|
[{ text: 'Set Model', callback_data: '/model gpt-4o' }],
|
|
@@ -85,6 +100,18 @@ describe('convertButtonsToKeyboard', () => {
|
|
|
85
100
|
}
|
|
86
101
|
});
|
|
87
102
|
|
|
103
|
+
it('converts a registered command with params without leading slash', () => {
|
|
104
|
+
const kb = convertButtonsToKeyboard([
|
|
105
|
+
[{ text: 'Set Model', callback_data: 'model gpt-4o' }],
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const btn = kb[0];
|
|
109
|
+
if (isButton(btn)) {
|
|
110
|
+
expect(btn.COMMAND).toBe('model');
|
|
111
|
+
expect(btn.COMMAND_PARAMS).toBe('gpt-4o');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
88
115
|
it('converts slash command with multiple params', () => {
|
|
89
116
|
const kb = convertButtonsToKeyboard([
|
|
90
117
|
[{ text: 'Think', callback_data: '/think high extra' }],
|
|
@@ -359,14 +386,16 @@ describe('direct message coalescing helpers', () => {
|
|
|
359
386
|
...baseMsgCtx,
|
|
360
387
|
messageId: '103',
|
|
361
388
|
text: 'Forwarded body',
|
|
389
|
+
replyToMessageId: '77',
|
|
362
390
|
media: [{ id: '1', name: 'photo.jpg', extension: '', size: 42, type: 'file' }],
|
|
363
391
|
isForwarded: true,
|
|
364
392
|
},
|
|
365
393
|
);
|
|
366
394
|
|
|
367
|
-
expect(merged.text).toBe('
|
|
395
|
+
expect(merged.text).toBe('[User question about the forwarded message]\n\nhello\n\n[/User question]\n\nForwarded body');
|
|
368
396
|
expect(merged.messageId).toBe('103');
|
|
369
397
|
expect(merged.media).toHaveLength(1);
|
|
398
|
+
expect(merged.replyToMessageId).toBeUndefined();
|
|
370
399
|
expect(merged.isForwarded).toBe(false);
|
|
371
400
|
});
|
|
372
401
|
|
|
@@ -393,6 +422,45 @@ describe('direct message coalescing helpers', () => {
|
|
|
393
422
|
});
|
|
394
423
|
});
|
|
395
424
|
|
|
425
|
+
describe('conversation helpers', () => {
|
|
426
|
+
it('uses direct dialog id as the stable conversation identity', () => {
|
|
427
|
+
const conversation = resolveConversationRef({
|
|
428
|
+
accountId: 'default',
|
|
429
|
+
dialogId: '2386',
|
|
430
|
+
isDirect: true,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(conversation.dialogId).toBe('2386');
|
|
434
|
+
expect(conversation.address).toBe('bitrix24:2386');
|
|
435
|
+
expect(conversation.historyKey).toBe('default:2386');
|
|
436
|
+
expect(conversation.peer).toEqual({ kind: 'direct', id: '2386' });
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('uses group dialog id as the stable conversation identity', () => {
|
|
440
|
+
const conversation = resolveConversationRef({
|
|
441
|
+
accountId: 'default',
|
|
442
|
+
dialogId: 'chat520',
|
|
443
|
+
isDirect: false,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(conversation.dialogId).toBe('chat520');
|
|
447
|
+
expect(conversation.address).toBe('bitrix24:chat520');
|
|
448
|
+
expect(conversation.historyKey).toBe('default:chat520');
|
|
449
|
+
expect(conversation.peer).toEqual({ kind: 'group', id: 'chat520' });
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('builds session keys from the stable conversation address', () => {
|
|
453
|
+
const conversation = resolveConversationRef({
|
|
454
|
+
accountId: 'default',
|
|
455
|
+
dialogId: '2386',
|
|
456
|
+
isDirect: true,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(buildConversationSessionKey('route:session', conversation))
|
|
460
|
+
.toBe('route:session:bitrix24:2386');
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
396
464
|
describe('join chat helpers', () => {
|
|
397
465
|
it('does not skip unsupported group chats even in webhookUser mode', () => {
|
|
398
466
|
expect(shouldSkipJoinChatWelcome({
|
|
@@ -554,7 +622,7 @@ describe('bitrix24Plugin', () => {
|
|
|
554
622
|
});
|
|
555
623
|
|
|
556
624
|
it('declares capabilities', () => {
|
|
557
|
-
expect(bitrix24Plugin.capabilities.chatTypes).toEqual(['direct']);
|
|
625
|
+
expect(bitrix24Plugin.capabilities.chatTypes).toEqual(['direct', 'group']);
|
|
558
626
|
expect(bitrix24Plugin.capabilities.media).toBe(true);
|
|
559
627
|
expect(bitrix24Plugin.capabilities.reactions).toBe(true);
|
|
560
628
|
expect(bitrix24Plugin.capabilities.threads).toBe(false);
|
|
@@ -574,6 +642,24 @@ describe('bitrix24Plugin', () => {
|
|
|
574
642
|
expect(result.approveHint).toContain('openclaw pairing approve bitrix24');
|
|
575
643
|
});
|
|
576
644
|
|
|
645
|
+
it('resolveDmPolicy returns allowlist policy when configured', () => {
|
|
646
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({
|
|
647
|
+
account: { config: { dmPolicy: 'allowlist', allowFrom: ['bitrix24:42'] } },
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
expect(result.policy).toBe('allowlist');
|
|
651
|
+
expect(result.allowFrom).toEqual(['42']);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('resolveDmPolicy returns open policy when configured', () => {
|
|
655
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({
|
|
656
|
+
account: { config: { dmPolicy: 'open' } },
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(result.policy).toBe('open');
|
|
660
|
+
expect(result.allowFrom).toEqual([]);
|
|
661
|
+
});
|
|
662
|
+
|
|
577
663
|
it('resolveDmPolicy defaults to webhookUser', () => {
|
|
578
664
|
const result = bitrix24Plugin.security.resolveDmPolicy({});
|
|
579
665
|
expect(result.policy).toBe('webhookUser');
|
package/tests/config.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { getConfig, isConfigured, listAccountIds, resolveAccount } from '../src/config.js';
|
|
3
|
+
import { Bitrix24ConfigSchema } from '../src/config-schema.js';
|
|
3
4
|
|
|
4
5
|
const mockCfg = {
|
|
5
6
|
channels: {
|
|
@@ -24,6 +25,7 @@ describe('getConfig', () => {
|
|
|
24
25
|
const cfg = getConfig(mockCfg);
|
|
25
26
|
expect(cfg.webhookUrl).toBe('https://test.bitrix24.com/rest/1/abc123/');
|
|
26
27
|
expect(cfg.botName).toBe('TestBot');
|
|
28
|
+
expect(cfg.dmPolicy).toBe('pairing');
|
|
27
29
|
expect(cfg.allowFrom).toEqual(['bitrix24:1']);
|
|
28
30
|
});
|
|
29
31
|
|
|
@@ -43,6 +45,41 @@ describe('getConfig', () => {
|
|
|
43
45
|
const cfg = getConfig(mockCfg, 'nonexistent');
|
|
44
46
|
expect(cfg.webhookUrl).toBe('https://test.bitrix24.com/rest/1/abc123/');
|
|
45
47
|
});
|
|
48
|
+
|
|
49
|
+
it('keeps group-related config fields intact', () => {
|
|
50
|
+
const cfg = getConfig({
|
|
51
|
+
channels: {
|
|
52
|
+
bitrix24: {
|
|
53
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
54
|
+
groupPolicy: 'webhookUser',
|
|
55
|
+
groupAllowFrom: ['chat208', '615'],
|
|
56
|
+
requireMention: true,
|
|
57
|
+
historyLimit: 100,
|
|
58
|
+
groups: {
|
|
59
|
+
'*': { requireMention: true },
|
|
60
|
+
chat208: {
|
|
61
|
+
groupPolicy: 'open',
|
|
62
|
+
requireMention: false,
|
|
63
|
+
watch: [{ userId: '77', topics: ['секрет'] }],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(cfg.groupPolicy).toBe('webhookUser');
|
|
71
|
+
expect(cfg.groupAllowFrom).toEqual(['chat208', '615']);
|
|
72
|
+
expect(cfg.requireMention).toBe(true);
|
|
73
|
+
expect(cfg.historyLimit).toBe(100);
|
|
74
|
+
expect(cfg.groups).toEqual({
|
|
75
|
+
'*': { requireMention: true },
|
|
76
|
+
chat208: {
|
|
77
|
+
groupPolicy: 'open',
|
|
78
|
+
requireMention: false,
|
|
79
|
+
watch: [{ userId: '77', topics: ['секрет'] }],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
});
|
|
46
83
|
});
|
|
47
84
|
|
|
48
85
|
describe('isConfigured', () => {
|
|
@@ -87,4 +124,87 @@ describe('resolveAccount', () => {
|
|
|
87
124
|
const acct = resolveAccount({}, 'missing');
|
|
88
125
|
expect(acct.configured).toBe(false);
|
|
89
126
|
});
|
|
127
|
+
|
|
128
|
+
it('accepts new group policy fields in account config', () => {
|
|
129
|
+
const acct = resolveAccount({
|
|
130
|
+
channels: {
|
|
131
|
+
bitrix24: {
|
|
132
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
133
|
+
dmPolicy: 'allowlist',
|
|
134
|
+
groupPolicy: 'pairing',
|
|
135
|
+
groupAllowFrom: ['chat208'],
|
|
136
|
+
requireMention: false,
|
|
137
|
+
historyLimit: 50,
|
|
138
|
+
groups: {
|
|
139
|
+
'208': { groupPolicy: 'open' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(acct.configured).toBe(true);
|
|
146
|
+
expect(acct.config.groupPolicy).toBe('pairing');
|
|
147
|
+
expect(acct.config.groupAllowFrom).toEqual(['chat208']);
|
|
148
|
+
expect(acct.config.requireMention).toBe(false);
|
|
149
|
+
expect(acct.config.historyLimit).toBe(50);
|
|
150
|
+
expect(acct.config.groups).toEqual({
|
|
151
|
+
'208': { groupPolicy: 'open', requireMention: true },
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('accepts agent mode watch rules in account config', () => {
|
|
156
|
+
const acct = resolveAccount({
|
|
157
|
+
channels: {
|
|
158
|
+
bitrix24: {
|
|
159
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
160
|
+
agentMode: true,
|
|
161
|
+
agentWatch: {
|
|
162
|
+
'*': [
|
|
163
|
+
{ userId: '*', topics: ['авария'], mode: 'notifyOwnerDm' },
|
|
164
|
+
],
|
|
165
|
+
'77': [
|
|
166
|
+
{ userId: '*', topics: ['срочно'] },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(acct.configured).toBe(true);
|
|
174
|
+
expect(acct.config.agentMode).toBe(true);
|
|
175
|
+
expect(acct.config.agentWatch).toEqual({
|
|
176
|
+
'*': [
|
|
177
|
+
{ userId: '*', topics: ['авария'], mode: 'notifyOwnerDm' },
|
|
178
|
+
],
|
|
179
|
+
'77': [
|
|
180
|
+
{ userId: '*', topics: ['срочно'] },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('rejects invalid group policy values', () => {
|
|
186
|
+
expect(() => resolveAccount({
|
|
187
|
+
channels: {
|
|
188
|
+
bitrix24: {
|
|
189
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
190
|
+
groupPolicy: 'invalid',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
} as unknown as Record<string, unknown>)).toThrow('Invalid config');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('Bitrix24ConfigSchema', () => {
|
|
198
|
+
it('applies defaults for new access and history options', () => {
|
|
199
|
+
const parsed = Bitrix24ConfigSchema.parse({
|
|
200
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/abc123/',
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(parsed.dmPolicy).toBe('webhookUser');
|
|
204
|
+
expect(parsed.groupPolicy).toBe('webhookUser');
|
|
205
|
+
expect(parsed.agentMode).toBe(false);
|
|
206
|
+
expect(parsed.requireMention).toBe(true);
|
|
207
|
+
expect(parsed.historyLimit).toBe(100);
|
|
208
|
+
expect(parsed.showTyping).toBe(true);
|
|
209
|
+
});
|
|
90
210
|
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildGroupMatchKeys,
|
|
4
|
+
checkGroupAccessPassive,
|
|
5
|
+
checkGroupAccessWithPairing,
|
|
6
|
+
normalizeGroupAllowList,
|
|
7
|
+
normalizeGroupEntry,
|
|
8
|
+
resolveGroupAccess,
|
|
9
|
+
} from '../src/group-access.js';
|
|
10
|
+
import type { PluginRuntime, ChannelPairingAdapter } from '../src/runtime.js';
|
|
11
|
+
|
|
12
|
+
function makeMockRuntime(storeAllowFrom: string[] = []): PluginRuntime {
|
|
13
|
+
return {
|
|
14
|
+
config: { loadConfig: () => ({}) },
|
|
15
|
+
channel: {
|
|
16
|
+
routing: { resolveAgentRoute: vi.fn() },
|
|
17
|
+
reply: {
|
|
18
|
+
finalizeInboundContext: vi.fn(),
|
|
19
|
+
dispatchReplyWithBufferedBlockDispatcher: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
session: { recordInboundSession: vi.fn() },
|
|
22
|
+
pairing: {
|
|
23
|
+
readAllowFromStore: vi.fn().mockResolvedValue(storeAllowFrom),
|
|
24
|
+
upsertPairingRequest: vi.fn().mockResolvedValue({ code: 'ABCD1234', created: true }),
|
|
25
|
+
buildPairingReply: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
logging: { getChildLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }) },
|
|
29
|
+
} as unknown as PluginRuntime;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const mockAdapter: ChannelPairingAdapter = {
|
|
33
|
+
idLabel: 'bitrix24UserId',
|
|
34
|
+
normalizeAllowEntry: (entry) => entry,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('group-access helpers', () => {
|
|
38
|
+
it('normalizes numeric and dialog group ids', () => {
|
|
39
|
+
expect(normalizeGroupEntry(' 208 ')).toBe('208');
|
|
40
|
+
expect(normalizeGroupEntry(' Chat208 ')).toBe('chat208');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('normalizes and deduplicates group allowlist entries', () => {
|
|
44
|
+
expect(normalizeGroupAllowList(['208', 'chat208', '208'])).toEqual(['208', 'chat208']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('builds group match keys from dialogId and chatId', () => {
|
|
48
|
+
expect(buildGroupMatchKeys({ dialogId: 'chat208', chatId: '208' })).toEqual(['chat208', '208']);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('resolveGroupAccess', () => {
|
|
53
|
+
it('defaults to webhookUser and mention-required groups', () => {
|
|
54
|
+
expect(resolveGroupAccess({
|
|
55
|
+
config: {},
|
|
56
|
+
dialogId: 'chat208',
|
|
57
|
+
chatId: '208',
|
|
58
|
+
})).toMatchObject({
|
|
59
|
+
groupPolicy: 'webhookUser',
|
|
60
|
+
requireMention: true,
|
|
61
|
+
groupAllowed: true,
|
|
62
|
+
senderAllowFrom: [],
|
|
63
|
+
watch: [],
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('matches explicit group config before wildcard', () => {
|
|
68
|
+
const result = resolveGroupAccess({
|
|
69
|
+
config: {
|
|
70
|
+
groupPolicy: 'webhookUser',
|
|
71
|
+
groups: {
|
|
72
|
+
'*': { requireMention: true },
|
|
73
|
+
chat208: { groupPolicy: 'open', requireMention: false, allowFrom: ['42'] },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
dialogId: 'chat208',
|
|
77
|
+
chatId: '208',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.groupPolicy).toBe('open');
|
|
81
|
+
expect(result.requireMention).toBe(false);
|
|
82
|
+
expect(result.senderAllowFrom).toEqual(['42']);
|
|
83
|
+
expect(result.matchedGroupKey).toBe('chat208');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resolves watch rules from matched group config', () => {
|
|
87
|
+
const result = resolveGroupAccess({
|
|
88
|
+
config: {
|
|
89
|
+
groups: {
|
|
90
|
+
chat208: {
|
|
91
|
+
watch: [
|
|
92
|
+
{ userId: 'bitrix24:77', topics: ['секрет', 'важно'], mode: 'notifyOwnerDm' },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
dialogId: 'chat208',
|
|
98
|
+
chatId: '208',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result.watch).toEqual([
|
|
102
|
+
{ userId: '77', topics: ['секрет', 'важно'], mode: 'notifyOwnerDm' },
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('preserves wildcard watch sender ids for any-user matching', () => {
|
|
107
|
+
const result = resolveGroupAccess({
|
|
108
|
+
config: {
|
|
109
|
+
groups: {
|
|
110
|
+
chat208: {
|
|
111
|
+
watch: [
|
|
112
|
+
{ userId: '*', topics: ['авария'] },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
dialogId: 'chat208',
|
|
118
|
+
chatId: '208',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(result.watch).toEqual([
|
|
122
|
+
{ userId: '*', topics: ['авария'], mode: 'reply' },
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('merges explicit group watch rules before wildcard watch rules', () => {
|
|
127
|
+
const result = resolveGroupAccess({
|
|
128
|
+
config: {
|
|
129
|
+
groups: {
|
|
130
|
+
'*': {
|
|
131
|
+
watch: [
|
|
132
|
+
{ userId: '*', topics: ['авария'], mode: 'notifyOwnerDm' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
chat208: {
|
|
136
|
+
watch: [
|
|
137
|
+
{ userId: '77', topics: ['секрет'] },
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
dialogId: 'chat208',
|
|
143
|
+
chatId: '208',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(result.watch).toEqual([
|
|
147
|
+
{ userId: '77', topics: ['секрет'], mode: 'reply' },
|
|
148
|
+
{ userId: '*', topics: ['авария'], mode: 'notifyOwnerDm' },
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('matches numeric chat id after dialog id lookup misses', () => {
|
|
153
|
+
const result = resolveGroupAccess({
|
|
154
|
+
config: {
|
|
155
|
+
groups: {
|
|
156
|
+
'208': { groupPolicy: 'open', requireMention: false },
|
|
157
|
+
'*': { groupPolicy: 'webhookUser', requireMention: true },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
dialogId: 'chat208',
|
|
161
|
+
chatId: '208',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.groupPolicy).toBe('open');
|
|
165
|
+
expect(result.requireMention).toBe(false);
|
|
166
|
+
expect(result.matchedGroupKey).toBe('208');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('falls back to wildcard group config when no exact match exists', () => {
|
|
170
|
+
const result = resolveGroupAccess({
|
|
171
|
+
config: {
|
|
172
|
+
groups: {
|
|
173
|
+
'*': { groupPolicy: 'allowlist', requireMention: false, allowFrom: ['42'] },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
dialogId: 'chat615',
|
|
177
|
+
chatId: '615',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.groupPolicy).toBe('allowlist');
|
|
181
|
+
expect(result.requireMention).toBe(false);
|
|
182
|
+
expect(result.senderAllowFrom).toEqual(['42']);
|
|
183
|
+
expect(result.matchedGroupKey).toBe('*');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('treats explicit group override as allowed even when top-level group allowlist is present', () => {
|
|
187
|
+
const result = resolveGroupAccess({
|
|
188
|
+
config: {
|
|
189
|
+
groupAllowFrom: ['chat300'],
|
|
190
|
+
groups: {
|
|
191
|
+
chat208: { groupPolicy: 'open' },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
dialogId: 'chat208',
|
|
195
|
+
chatId: '208',
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.groupAllowed).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('checkGroupAccessWithPairing', () => {
|
|
203
|
+
it('reports pairing passively without creating a request', async () => {
|
|
204
|
+
const runtime = makeMockRuntime();
|
|
205
|
+
const result = await checkGroupAccessPassive({
|
|
206
|
+
senderId: '99',
|
|
207
|
+
dialogId: 'chat208',
|
|
208
|
+
chatId: '208',
|
|
209
|
+
config: { groupPolicy: 'pairing' },
|
|
210
|
+
runtime,
|
|
211
|
+
accountId: 'default',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(result).toBe('pairing');
|
|
215
|
+
expect(runtime.channel.pairing.upsertPairingRequest).not.toHaveBeenCalled();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('allows any sender in open mode', async () => {
|
|
219
|
+
const runtime = makeMockRuntime();
|
|
220
|
+
const result = await checkGroupAccessWithPairing({
|
|
221
|
+
senderId: '77',
|
|
222
|
+
dialogId: 'chat208',
|
|
223
|
+
chatId: '208',
|
|
224
|
+
config: { groupPolicy: 'open' },
|
|
225
|
+
runtime,
|
|
226
|
+
accountId: 'default',
|
|
227
|
+
pairingAdapter: mockAdapter,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(result).toBe('allow');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('allows only webhook owner in webhookUser mode', async () => {
|
|
234
|
+
const runtime = makeMockRuntime();
|
|
235
|
+
const result = await checkGroupAccessWithPairing({
|
|
236
|
+
senderId: '42',
|
|
237
|
+
dialogId: 'chat208',
|
|
238
|
+
chatId: '208',
|
|
239
|
+
config: {
|
|
240
|
+
groupPolicy: 'webhookUser',
|
|
241
|
+
webhookUrl: 'https://test.bitrix24.com/rest/42/token/',
|
|
242
|
+
},
|
|
243
|
+
runtime,
|
|
244
|
+
accountId: 'default',
|
|
245
|
+
pairingAdapter: mockAdapter,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result).toBe('allow');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('uses merged top-level and per-group allowFrom in allowlist mode', async () => {
|
|
252
|
+
const runtime = makeMockRuntime();
|
|
253
|
+
const allowed = await checkGroupAccessWithPairing({
|
|
254
|
+
senderId: '42',
|
|
255
|
+
dialogId: 'chat208',
|
|
256
|
+
chatId: '208',
|
|
257
|
+
config: {
|
|
258
|
+
groupPolicy: 'allowlist',
|
|
259
|
+
allowFrom: ['7'],
|
|
260
|
+
groups: {
|
|
261
|
+
chat208: { allowFrom: ['42'] },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
runtime,
|
|
265
|
+
accountId: 'default',
|
|
266
|
+
pairingAdapter: mockAdapter,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const denied = await checkGroupAccessWithPairing({
|
|
270
|
+
senderId: '99',
|
|
271
|
+
dialogId: 'chat208',
|
|
272
|
+
chatId: '208',
|
|
273
|
+
config: {
|
|
274
|
+
groupPolicy: 'allowlist',
|
|
275
|
+
allowFrom: ['7'],
|
|
276
|
+
groups: {
|
|
277
|
+
chat208: { allowFrom: ['42'] },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
runtime,
|
|
281
|
+
accountId: 'default',
|
|
282
|
+
pairingAdapter: mockAdapter,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(allowed).toBe('allow');
|
|
286
|
+
expect(denied).toBe('deny');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('creates pairing request for unknown sender in group pairing mode', async () => {
|
|
290
|
+
const runtime = makeMockRuntime();
|
|
291
|
+
const result = await checkGroupAccessWithPairing({
|
|
292
|
+
senderId: '99',
|
|
293
|
+
dialogId: 'chat208',
|
|
294
|
+
chatId: '208',
|
|
295
|
+
config: { groupPolicy: 'pairing' },
|
|
296
|
+
runtime,
|
|
297
|
+
accountId: 'default',
|
|
298
|
+
pairingAdapter: mockAdapter,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(result).toBe('pairing');
|
|
302
|
+
expect(runtime.channel.pairing.upsertPairingRequest).toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('denies groups outside configured group allowlist', async () => {
|
|
306
|
+
const runtime = makeMockRuntime();
|
|
307
|
+
const result = await checkGroupAccessWithPairing({
|
|
308
|
+
senderId: '42',
|
|
309
|
+
dialogId: 'chat208',
|
|
310
|
+
chatId: '208',
|
|
311
|
+
config: {
|
|
312
|
+
groupPolicy: 'open',
|
|
313
|
+
groupAllowFrom: ['chat300'],
|
|
314
|
+
},
|
|
315
|
+
runtime,
|
|
316
|
+
accountId: 'default',
|
|
317
|
+
pairingAdapter: mockAdapter,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(result).toBe('deny');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('allows groups listed by numeric chat id in top-level group allowlist', async () => {
|
|
324
|
+
const runtime = makeMockRuntime();
|
|
325
|
+
const result = await checkGroupAccessWithPairing({
|
|
326
|
+
senderId: '42',
|
|
327
|
+
dialogId: 'chat208',
|
|
328
|
+
chatId: '208',
|
|
329
|
+
config: {
|
|
330
|
+
groupPolicy: 'open',
|
|
331
|
+
groupAllowFrom: ['208'],
|
|
332
|
+
},
|
|
333
|
+
runtime,
|
|
334
|
+
accountId: 'default',
|
|
335
|
+
pairingAdapter: mockAdapter,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(result).toBe('allow');
|
|
339
|
+
});
|
|
340
|
+
});
|