@ihazz/bitrix24 0.1.5 → 0.1.6
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/package.json +1 -1
- package/src/access-control.ts +61 -4
- package/src/api.ts +80 -0
- package/src/channel.ts +134 -25
- package/src/commands.ts +1 -1
- package/src/config-schema.ts +1 -1
- package/src/inbound-handler.ts +1 -9
- package/src/media-service.ts +186 -0
- package/src/runtime.ts +23 -0
- package/src/types.ts +1 -0
- package/tests/access-control.test.ts +178 -6
- package/tests/channel.test.ts +538 -0
- package/tests/inbound-handler.test.ts +4 -2
- package/tests/media-service.test.ts +224 -0
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Readable, PassThrough } from 'node:stream';
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import {
|
|
5
|
+
convertButtonsToKeyboard,
|
|
6
|
+
extractKeyboardFromPayload,
|
|
7
|
+
handleWebhookRequest,
|
|
8
|
+
bitrix24Plugin,
|
|
9
|
+
} from '../src/channel.js';
|
|
10
|
+
import type { KeyboardButton, KeyboardNewline, B24Keyboard } from '../src/types.js';
|
|
11
|
+
|
|
12
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const silentLogger = {
|
|
15
|
+
info: vi.fn(),
|
|
16
|
+
warn: vi.fn(),
|
|
17
|
+
error: vi.fn(),
|
|
18
|
+
debug: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isButton(item: KeyboardButton | KeyboardNewline): item is KeyboardButton {
|
|
22
|
+
return !('TYPE' in item);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isNewline(item: KeyboardButton | KeyboardNewline): item is KeyboardNewline {
|
|
26
|
+
return 'TYPE' in item && item.TYPE === 'NEWLINE';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── convertButtonsToKeyboard ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
describe('convertButtonsToKeyboard', () => {
|
|
32
|
+
it('converts a single button with slash command', () => {
|
|
33
|
+
const kb = convertButtonsToKeyboard([
|
|
34
|
+
[{ text: 'Help', callback_data: '/help' }],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
expect(kb).toHaveLength(1);
|
|
38
|
+
const btn = kb[0];
|
|
39
|
+
expect(isButton(btn)).toBe(true);
|
|
40
|
+
if (isButton(btn)) {
|
|
41
|
+
expect(btn.TEXT).toBe('Help');
|
|
42
|
+
expect(btn.COMMAND).toBe('help');
|
|
43
|
+
expect(btn.COMMAND_PARAMS).toBeUndefined();
|
|
44
|
+
expect(btn.DISPLAY).toBe('LINE');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('converts slash command with params', () => {
|
|
49
|
+
const kb = convertButtonsToKeyboard([
|
|
50
|
+
[{ text: 'Set Model', callback_data: '/model gpt-4o' }],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
expect(kb).toHaveLength(1);
|
|
54
|
+
const btn = kb[0];
|
|
55
|
+
if (isButton(btn)) {
|
|
56
|
+
expect(btn.COMMAND).toBe('model');
|
|
57
|
+
expect(btn.COMMAND_PARAMS).toBe('gpt-4o');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('converts slash command with multiple params', () => {
|
|
62
|
+
const kb = convertButtonsToKeyboard([
|
|
63
|
+
[{ text: 'Think', callback_data: '/think high extra' }],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const btn = kb[0];
|
|
67
|
+
if (isButton(btn)) {
|
|
68
|
+
expect(btn.COMMAND).toBe('think');
|
|
69
|
+
expect(btn.COMMAND_PARAMS).toBe('high extra');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('converts non-slash callback_data to ACTION PUT', () => {
|
|
74
|
+
const kb = convertButtonsToKeyboard([
|
|
75
|
+
[{ text: 'Click me', callback_data: 'some_value' }],
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const btn = kb[0];
|
|
79
|
+
if (isButton(btn)) {
|
|
80
|
+
expect(btn.COMMAND).toBeUndefined();
|
|
81
|
+
expect(btn.ACTION).toBe('PUT');
|
|
82
|
+
expect(btn.ACTION_VALUE).toBe('some_value');
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('handles button without callback_data', () => {
|
|
87
|
+
const kb = convertButtonsToKeyboard([
|
|
88
|
+
[{ text: 'Plain' }],
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const btn = kb[0];
|
|
92
|
+
if (isButton(btn)) {
|
|
93
|
+
expect(btn.TEXT).toBe('Plain');
|
|
94
|
+
expect(btn.COMMAND).toBeUndefined();
|
|
95
|
+
expect(btn.ACTION).toBeUndefined();
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('maps primary style to BG_COLOR_TOKEN', () => {
|
|
100
|
+
const kb = convertButtonsToKeyboard([
|
|
101
|
+
[{ text: 'Go', callback_data: '/start', style: 'primary' }],
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const btn = kb[0];
|
|
105
|
+
if (isButton(btn)) {
|
|
106
|
+
expect(btn.BG_COLOR_TOKEN).toBe('primary');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('maps attention style to alert BG_COLOR_TOKEN', () => {
|
|
111
|
+
const kb = convertButtonsToKeyboard([
|
|
112
|
+
[{ text: 'Stop', callback_data: '/stop', style: 'attention' }],
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const btn = kb[0];
|
|
116
|
+
if (isButton(btn)) {
|
|
117
|
+
expect(btn.BG_COLOR_TOKEN).toBe('alert');
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('maps danger style to alert BG_COLOR_TOKEN', () => {
|
|
122
|
+
const kb = convertButtonsToKeyboard([
|
|
123
|
+
[{ text: 'Delete', callback_data: '/reset', style: 'danger' }],
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const btn = kb[0];
|
|
127
|
+
if (isButton(btn)) {
|
|
128
|
+
expect(btn.BG_COLOR_TOKEN).toBe('alert');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('does not set BG_COLOR_TOKEN for unknown style', () => {
|
|
133
|
+
const kb = convertButtonsToKeyboard([
|
|
134
|
+
[{ text: 'Btn', callback_data: '/ok', style: 'custom' }],
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const btn = kb[0];
|
|
138
|
+
if (isButton(btn)) {
|
|
139
|
+
expect(btn.BG_COLOR_TOKEN).toBeUndefined();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('adds NEWLINE between rows', () => {
|
|
144
|
+
const kb = convertButtonsToKeyboard([
|
|
145
|
+
[{ text: 'A', callback_data: '/a' }],
|
|
146
|
+
[{ text: 'B', callback_data: '/b' }],
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
// [btn, NEWLINE, btn] = 3 items
|
|
150
|
+
expect(kb).toHaveLength(3);
|
|
151
|
+
expect(isButton(kb[0])).toBe(true);
|
|
152
|
+
expect(isNewline(kb[1])).toBe(true);
|
|
153
|
+
expect(isButton(kb[2])).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('handles multiple buttons per row', () => {
|
|
157
|
+
const kb = convertButtonsToKeyboard([
|
|
158
|
+
[
|
|
159
|
+
{ text: 'Yes', callback_data: '/yes' },
|
|
160
|
+
{ text: 'No', callback_data: '/no' },
|
|
161
|
+
],
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
// Single row with 2 buttons, no NEWLINE
|
|
165
|
+
expect(kb).toHaveLength(2);
|
|
166
|
+
expect(isButton(kb[0])).toBe(true);
|
|
167
|
+
expect(isButton(kb[1])).toBe(true);
|
|
168
|
+
if (isButton(kb[0])) expect(kb[0].COMMAND).toBe('yes');
|
|
169
|
+
if (isButton(kb[1])) expect(kb[1].COMMAND).toBe('no');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('handles multiple rows with multiple buttons', () => {
|
|
173
|
+
const kb = convertButtonsToKeyboard([
|
|
174
|
+
[{ text: 'A' }, { text: 'B' }],
|
|
175
|
+
[{ text: 'C' }],
|
|
176
|
+
[{ text: 'D' }, { text: 'E' }, { text: 'F' }],
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// Row1(2) + NL + Row2(1) + NL + Row3(3) = 2+1+1+1+3 = 8
|
|
180
|
+
expect(kb).toHaveLength(8);
|
|
181
|
+
expect(isNewline(kb[2])).toBe(true);
|
|
182
|
+
expect(isNewline(kb[4])).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('handles empty rows array', () => {
|
|
186
|
+
const kb = convertButtonsToKeyboard([]);
|
|
187
|
+
expect(kb).toHaveLength(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('handles empty row inside rows', () => {
|
|
191
|
+
const kb = convertButtonsToKeyboard([[]]);
|
|
192
|
+
expect(kb).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ─── extractKeyboardFromPayload ──────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
describe('extractKeyboardFromPayload', () => {
|
|
199
|
+
it('returns undefined when no channelData', () => {
|
|
200
|
+
expect(extractKeyboardFromPayload({})).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns undefined when channelData is empty', () => {
|
|
204
|
+
expect(extractKeyboardFromPayload({ channelData: {} })).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('returns undefined when channelData buttons is empty array', () => {
|
|
208
|
+
const result = extractKeyboardFromPayload({
|
|
209
|
+
channelData: { telegram: { buttons: [] } },
|
|
210
|
+
});
|
|
211
|
+
expect(result).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('extracts direct B24 keyboard from channelData.bitrix24', () => {
|
|
215
|
+
const directKeyboard: B24Keyboard = [
|
|
216
|
+
{ TEXT: 'Direct', COMMAND: 'test' },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const result = extractKeyboardFromPayload({
|
|
220
|
+
channelData: { bitrix24: { keyboard: directKeyboard } },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(result).toEqual(directKeyboard);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('prefers B24 keyboard over generic channel buttons', () => {
|
|
227
|
+
const directKeyboard: B24Keyboard = [{ TEXT: 'B24' }];
|
|
228
|
+
|
|
229
|
+
const result = extractKeyboardFromPayload({
|
|
230
|
+
channelData: {
|
|
231
|
+
bitrix24: { keyboard: directKeyboard },
|
|
232
|
+
telegram: { buttons: [[{ text: 'TG', callback_data: '/tg' }]] },
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(result).toEqual(directKeyboard);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('converts channel buttons to B24 keyboard', () => {
|
|
240
|
+
const result = extractKeyboardFromPayload({
|
|
241
|
+
channelData: {
|
|
242
|
+
telegram: {
|
|
243
|
+
buttons: [[{ text: 'Help', callback_data: '/help' }]],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(result).toBeDefined();
|
|
249
|
+
expect(result).toHaveLength(1);
|
|
250
|
+
if (result && isButton(result[0])) {
|
|
251
|
+
expect(result[0].TEXT).toBe('Help');
|
|
252
|
+
expect(result[0].COMMAND).toBe('help');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('converts multi-row channel buttons', () => {
|
|
257
|
+
const result = extractKeyboardFromPayload({
|
|
258
|
+
channelData: {
|
|
259
|
+
telegram: {
|
|
260
|
+
buttons: [
|
|
261
|
+
[{ text: 'A', callback_data: '/a' }],
|
|
262
|
+
[{ text: 'B', callback_data: '/b' }],
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// [btn, NEWLINE, btn]
|
|
269
|
+
expect(result).toHaveLength(3);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('returns undefined when bitrix24.keyboard is empty', () => {
|
|
273
|
+
const result = extractKeyboardFromPayload({
|
|
274
|
+
channelData: { bitrix24: { keyboard: [] } },
|
|
275
|
+
});
|
|
276
|
+
expect(result).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('ignores unrelated channelData keys', () => {
|
|
280
|
+
const result = extractKeyboardFromPayload({
|
|
281
|
+
channelData: { discord: { something: true } },
|
|
282
|
+
});
|
|
283
|
+
expect(result).toBeUndefined();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ─── handleWebhookRequest ────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
describe('handleWebhookRequest', () => {
|
|
290
|
+
function mockReq(method: string, body?: string): IncomingMessage {
|
|
291
|
+
const stream = new PassThrough();
|
|
292
|
+
if (body) {
|
|
293
|
+
stream.end(body);
|
|
294
|
+
} else {
|
|
295
|
+
stream.end();
|
|
296
|
+
}
|
|
297
|
+
(stream as unknown as Record<string, unknown>).method = method;
|
|
298
|
+
return stream as unknown as IncomingMessage;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mockRes(): ServerResponse & { _statusCode: number; _body: string; _headers: Record<string, string> } {
|
|
302
|
+
const res = {
|
|
303
|
+
_statusCode: 200,
|
|
304
|
+
_body: '',
|
|
305
|
+
_headers: {} as Record<string, string>,
|
|
306
|
+
get statusCode() { return this._statusCode; },
|
|
307
|
+
set statusCode(v: number) { this._statusCode = v; },
|
|
308
|
+
setHeader(key: string, val: string) { this._headers[key] = val; },
|
|
309
|
+
end(body?: string) { this._body = body ?? ''; },
|
|
310
|
+
};
|
|
311
|
+
return res as unknown as ServerResponse & { _statusCode: number; _body: string; _headers: Record<string, string> };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
it('rejects non-POST with 405', async () => {
|
|
315
|
+
const req = mockReq('GET');
|
|
316
|
+
const res = mockRes();
|
|
317
|
+
|
|
318
|
+
await handleWebhookRequest(req, res);
|
|
319
|
+
|
|
320
|
+
expect(res._statusCode).toBe(405);
|
|
321
|
+
expect(res._body).toBe('Method Not Allowed');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns 503 when gateway not started', async () => {
|
|
325
|
+
const req = mockReq('POST', 'event=test');
|
|
326
|
+
const res = mockRes();
|
|
327
|
+
|
|
328
|
+
await handleWebhookRequest(req, res);
|
|
329
|
+
|
|
330
|
+
expect(res._statusCode).toBe(503);
|
|
331
|
+
expect(res._body).toBe('Channel not started');
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ─── bitrix24Plugin structure ────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
describe('bitrix24Plugin', () => {
|
|
338
|
+
describe('metadata', () => {
|
|
339
|
+
it('has correct id', () => {
|
|
340
|
+
expect(bitrix24Plugin.id).toBe('bitrix24');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('has correct meta', () => {
|
|
344
|
+
expect(bitrix24Plugin.meta.id).toBe('bitrix24');
|
|
345
|
+
expect(bitrix24Plugin.meta.label).toBe('Bitrix24');
|
|
346
|
+
expect(bitrix24Plugin.meta.aliases).toEqual(['b24', 'bx24']);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('declares capabilities', () => {
|
|
350
|
+
expect(bitrix24Plugin.capabilities.chatTypes).toEqual(['direct', 'group']);
|
|
351
|
+
expect(bitrix24Plugin.capabilities.media).toBe(true);
|
|
352
|
+
expect(bitrix24Plugin.capabilities.reactions).toBe(false);
|
|
353
|
+
expect(bitrix24Plugin.capabilities.threads).toBe(false);
|
|
354
|
+
expect(bitrix24Plugin.capabilities.nativeCommands).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe('security', () => {
|
|
359
|
+
it('resolveDmPolicy returns policy object with configured policy', () => {
|
|
360
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({
|
|
361
|
+
account: { config: { dmPolicy: 'allowlist', allowFrom: ['1'] } },
|
|
362
|
+
});
|
|
363
|
+
expect(result.policy).toBe('allowlist');
|
|
364
|
+
expect(result.allowFrom).toEqual(['1']);
|
|
365
|
+
expect(result.policyPath).toBe('channels.bitrix24.dmPolicy');
|
|
366
|
+
expect(result.allowFromPath).toBe('channels.bitrix24.');
|
|
367
|
+
expect(result.approveHint).toContain('openclaw pairing approve bitrix24');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('resolveDmPolicy defaults to pairing', () => {
|
|
371
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({});
|
|
372
|
+
expect(result.policy).toBe('pairing');
|
|
373
|
+
expect(result.allowFrom).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('resolveDmPolicy normalizeEntry strips prefixes', () => {
|
|
377
|
+
const result = bitrix24Plugin.security.resolveDmPolicy({});
|
|
378
|
+
expect(result.normalizeEntry('bitrix24:42')).toBe('42');
|
|
379
|
+
expect(result.normalizeEntry('b24:42')).toBe('42');
|
|
380
|
+
expect(result.normalizeEntry('BX24:7')).toBe('7');
|
|
381
|
+
expect(result.normalizeEntry('42')).toBe('42');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('normalizeAllowFrom strips bitrix24: prefix', () => {
|
|
385
|
+
expect(bitrix24Plugin.security.normalizeAllowFrom('bitrix24:42')).toBe('42');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('normalizeAllowFrom strips b24: prefix', () => {
|
|
389
|
+
expect(bitrix24Plugin.security.normalizeAllowFrom('b24:42')).toBe('42');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('normalizeAllowFrom strips bx24: prefix', () => {
|
|
393
|
+
expect(bitrix24Plugin.security.normalizeAllowFrom('bx24:42')).toBe('42');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('normalizeAllowFrom keeps plain id', () => {
|
|
397
|
+
expect(bitrix24Plugin.security.normalizeAllowFrom('42')).toBe('42');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('pairing adapter', () => {
|
|
402
|
+
it('has correct idLabel', () => {
|
|
403
|
+
expect(bitrix24Plugin.pairing.idLabel).toBe('bitrix24UserId');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('normalizeAllowEntry strips prefixes', () => {
|
|
407
|
+
expect(bitrix24Plugin.pairing.normalizeAllowEntry!('bitrix24:42')).toBe('42');
|
|
408
|
+
expect(bitrix24Plugin.pairing.normalizeAllowEntry!('b24:42')).toBe('42');
|
|
409
|
+
expect(bitrix24Plugin.pairing.normalizeAllowEntry!('BX24:7')).toBe('7');
|
|
410
|
+
expect(bitrix24Plugin.pairing.normalizeAllowEntry!('42')).toBe('42');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('has notifyApproval function', () => {
|
|
414
|
+
expect(typeof bitrix24Plugin.pairing.notifyApproval).toBe('function');
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe('outbound', () => {
|
|
419
|
+
it('has direct delivery mode', () => {
|
|
420
|
+
expect(bitrix24Plugin.outbound.deliveryMode).toBe('direct');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('sendText returns error when gateway not started', async () => {
|
|
424
|
+
const result = await bitrix24Plugin.outbound.sendText({
|
|
425
|
+
text: 'Hello',
|
|
426
|
+
context: {
|
|
427
|
+
channel: 'bitrix24',
|
|
428
|
+
senderId: '1',
|
|
429
|
+
senderName: 'Test',
|
|
430
|
+
chatId: '1',
|
|
431
|
+
chatInternalId: '100',
|
|
432
|
+
messageId: '1',
|
|
433
|
+
text: 'Hello',
|
|
434
|
+
isDm: true,
|
|
435
|
+
isGroup: false,
|
|
436
|
+
media: [],
|
|
437
|
+
raw: {} as any,
|
|
438
|
+
botToken: 'token',
|
|
439
|
+
userToken: 'utoken',
|
|
440
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
441
|
+
botId: 1,
|
|
442
|
+
memberId: 'mem1',
|
|
443
|
+
},
|
|
444
|
+
account: { config: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } },
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(result.ok).toBe(false);
|
|
448
|
+
expect(result.error).toBe('Gateway not started');
|
|
449
|
+
expect(result.channel).toBe('bitrix24');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('sendPayload returns error when gateway not started', async () => {
|
|
453
|
+
const result = await bitrix24Plugin.outbound.sendPayload({
|
|
454
|
+
text: 'Hello',
|
|
455
|
+
channelData: { telegram: { buttons: [[{ text: 'OK' }]] } },
|
|
456
|
+
context: {
|
|
457
|
+
channel: 'bitrix24',
|
|
458
|
+
senderId: '1',
|
|
459
|
+
senderName: 'Test',
|
|
460
|
+
chatId: '1',
|
|
461
|
+
chatInternalId: '100',
|
|
462
|
+
messageId: '1',
|
|
463
|
+
text: 'Hello',
|
|
464
|
+
isDm: true,
|
|
465
|
+
isGroup: false,
|
|
466
|
+
media: [],
|
|
467
|
+
raw: {} as any,
|
|
468
|
+
botToken: 'token',
|
|
469
|
+
userToken: 'utoken',
|
|
470
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
471
|
+
botId: 1,
|
|
472
|
+
memberId: 'mem1',
|
|
473
|
+
},
|
|
474
|
+
account: { config: { webhookUrl: 'https://test.bitrix24.com/rest/1/token/' } },
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expect(result.ok).toBe(false);
|
|
478
|
+
expect(result.error).toBe('Gateway not started');
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('config', () => {
|
|
483
|
+
it('listAccountIds returns empty for empty config', () => {
|
|
484
|
+
expect(bitrix24Plugin.config.listAccountIds({})).toEqual([]);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('listAccountIds returns default when webhookUrl present', () => {
|
|
488
|
+
const cfg = {
|
|
489
|
+
channels: {
|
|
490
|
+
bitrix24: {
|
|
491
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
expect(bitrix24Plugin.config.listAccountIds(cfg)).toEqual(['default']);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('resolveAccount returns config', () => {
|
|
499
|
+
const cfg = {
|
|
500
|
+
channels: {
|
|
501
|
+
bitrix24: {
|
|
502
|
+
webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
|
|
503
|
+
botName: 'TestBot',
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
const account = bitrix24Plugin.config.resolveAccount(cfg);
|
|
508
|
+
expect(account.accountId).toBe('default');
|
|
509
|
+
expect(account.config.webhookUrl).toBe('https://test.bitrix24.com/rest/1/token/');
|
|
510
|
+
expect(account.config.botName).toBe('TestBot');
|
|
511
|
+
expect(account.configured).toBe(true);
|
|
512
|
+
expect(account.enabled).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ─── gateway.startAccount ────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
describe('gateway.startAccount', () => {
|
|
520
|
+
it('skips when webhookUrl not configured', async () => {
|
|
521
|
+
const logger = { ...silentLogger };
|
|
522
|
+
|
|
523
|
+
const result = await bitrix24Plugin.gateway.startAccount({
|
|
524
|
+
cfg: { channels: { bitrix24: {} } },
|
|
525
|
+
accountId: 'test',
|
|
526
|
+
account: { config: {} },
|
|
527
|
+
runtime: {},
|
|
528
|
+
abortSignal: new AbortController().signal,
|
|
529
|
+
log: logger,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Should return undefined (early return, no promise)
|
|
533
|
+
expect(result).toBeUndefined();
|
|
534
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
535
|
+
expect.stringContaining('no webhookUrl configured'),
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
@@ -60,7 +60,7 @@ describe('InboundHandler', () => {
|
|
|
60
60
|
expect(onMessage).toHaveBeenCalledOnce();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it('
|
|
63
|
+
it('dispatches all messages (access control handled externally)', async () => {
|
|
64
64
|
const onMessage = vi.fn();
|
|
65
65
|
handler = new InboundHandler({
|
|
66
66
|
config: { dmPolicy: 'allowlist', allowFrom: ['99'] },
|
|
@@ -69,7 +69,8 @@ describe('InboundHandler', () => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
await handler.handleWebhook(textFixture);
|
|
72
|
-
|
|
72
|
+
// Access control is now handled in channel.ts onMessage callback, not in InboundHandler
|
|
73
|
+
expect(onMessage).toHaveBeenCalledOnce();
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
it('dispatches join chat events', async () => {
|
|
@@ -156,6 +157,7 @@ describe('normalizeMessageEvent', () => {
|
|
|
156
157
|
extension: 'txt',
|
|
157
158
|
size: 101,
|
|
158
159
|
type: 'file',
|
|
160
|
+
urlDownload: 'https://test.bitrix24.com/disk/downloadFile/94611/',
|
|
159
161
|
});
|
|
160
162
|
});
|
|
161
163
|
});
|