@ihazz/bitrix24 0.1.5 → 0.2.0
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 +92 -0
- package/src/channel.ts +178 -28
- 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 +195 -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,224 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { MediaService } from '../src/media-service.js';
|
|
4
|
+
import { Bitrix24Api } from '../src/api.js';
|
|
5
|
+
|
|
6
|
+
const silentLogger = {
|
|
7
|
+
info: vi.fn(),
|
|
8
|
+
warn: vi.fn(),
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
debug: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Mock the API
|
|
14
|
+
function mockApi(): Bitrix24Api {
|
|
15
|
+
return {
|
|
16
|
+
getFileInfo: vi.fn().mockResolvedValue({
|
|
17
|
+
DOWNLOAD_URL: 'https://example.com/download/test.txt?token=abc',
|
|
18
|
+
}),
|
|
19
|
+
getChatFolder: vi.fn().mockResolvedValue(42),
|
|
20
|
+
uploadFile: vi.fn().mockResolvedValue(100),
|
|
21
|
+
commitFileToChat: vi.fn().mockResolvedValue(true),
|
|
22
|
+
} as unknown as Bitrix24Api;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('MediaService', () => {
|
|
26
|
+
let service: MediaService;
|
|
27
|
+
let api: Bitrix24Api;
|
|
28
|
+
const savedPaths: string[] = [];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
api = mockApi();
|
|
32
|
+
service = new MediaService(api, silentLogger);
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
// Clean up any files created during tests
|
|
38
|
+
for (const p of savedPaths) {
|
|
39
|
+
try { unlinkSync(p); } catch { /* ignore */ }
|
|
40
|
+
}
|
|
41
|
+
savedPaths.length = 0;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('downloadMedia', () => {
|
|
45
|
+
it('downloads and saves a file', async () => {
|
|
46
|
+
const mockContent = Buffer.from('hello world');
|
|
47
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
48
|
+
new Response(mockContent, { status: 200 }),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const result = await service.downloadMedia({
|
|
52
|
+
fileId: '123',
|
|
53
|
+
fileName: 'test.txt',
|
|
54
|
+
extension: 'txt',
|
|
55
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
56
|
+
userToken: 'user_token',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result).not.toBeNull();
|
|
60
|
+
expect(result!.contentType).toBe('text/plain');
|
|
61
|
+
expect(result!.name).toBe('test.txt');
|
|
62
|
+
expect(result!.path).toContain('test.txt');
|
|
63
|
+
expect(existsSync(result!.path)).toBe(true);
|
|
64
|
+
savedPaths.push(result!.path);
|
|
65
|
+
|
|
66
|
+
expect(api.getFileInfo).toHaveBeenCalledWith(
|
|
67
|
+
'https://test.bitrix24.com/rest/',
|
|
68
|
+
'user_token',
|
|
69
|
+
123,
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns correct MIME for images', async () => {
|
|
74
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
75
|
+
new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), { status: 200 }),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const result = await service.downloadMedia({
|
|
79
|
+
fileId: '456',
|
|
80
|
+
fileName: 'photo.jpg',
|
|
81
|
+
extension: 'jpg',
|
|
82
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
83
|
+
userToken: 'token',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).not.toBeNull();
|
|
87
|
+
expect(result!.contentType).toBe('image/jpeg');
|
|
88
|
+
if (result) savedPaths.push(result.path);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('returns null when API fails', async () => {
|
|
92
|
+
(api.getFileInfo as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
|
|
93
|
+
new Error('API error'),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await service.downloadMedia({
|
|
97
|
+
fileId: '789',
|
|
98
|
+
fileName: 'fail.txt',
|
|
99
|
+
extension: 'txt',
|
|
100
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
101
|
+
userToken: 'token',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result).toBeNull();
|
|
105
|
+
expect(silentLogger.error).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('returns null when no DOWNLOAD_URL', async () => {
|
|
109
|
+
(api.getFileInfo as ReturnType<typeof vi.fn>).mockResolvedValueOnce({});
|
|
110
|
+
|
|
111
|
+
const result = await service.downloadMedia({
|
|
112
|
+
fileId: '789',
|
|
113
|
+
fileName: 'nourl.txt',
|
|
114
|
+
extension: 'txt',
|
|
115
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
116
|
+
userToken: 'token',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(result).toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns null when download fails', async () => {
|
|
123
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
124
|
+
new Response(null, { status: 404 }),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const result = await service.downloadMedia({
|
|
128
|
+
fileId: '789',
|
|
129
|
+
fileName: 'missing.txt',
|
|
130
|
+
extension: 'txt',
|
|
131
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
132
|
+
userToken: 'token',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('handles unknown extensions with octet-stream', async () => {
|
|
139
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
140
|
+
new Response(Buffer.from('data'), { status: 200 }),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const result = await service.downloadMedia({
|
|
144
|
+
fileId: '100',
|
|
145
|
+
fileName: 'file.xyz',
|
|
146
|
+
extension: 'xyz',
|
|
147
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
148
|
+
userToken: 'token',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result).not.toBeNull();
|
|
152
|
+
expect(result!.contentType).toBe('application/octet-stream');
|
|
153
|
+
if (result) savedPaths.push(result.path);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('uploadMediaToChat', () => {
|
|
158
|
+
it('uploads file through 3-step process', async () => {
|
|
159
|
+
// Create a temp file to upload
|
|
160
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
161
|
+
const { join } = await import('node:path');
|
|
162
|
+
const { tmpdir } = await import('node:os');
|
|
163
|
+
const testDir = join(tmpdir(), 'openclaw-b24-test');
|
|
164
|
+
mkdirSync(testDir, { recursive: true });
|
|
165
|
+
const testPath = join(testDir, 'upload-test.txt');
|
|
166
|
+
writeFileSync(testPath, 'upload content');
|
|
167
|
+
savedPaths.push(testPath);
|
|
168
|
+
|
|
169
|
+
const result = await service.uploadMediaToChat({
|
|
170
|
+
localPath: testPath,
|
|
171
|
+
fileName: 'upload-test.txt',
|
|
172
|
+
chatId: 40985,
|
|
173
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
174
|
+
botToken: 'bot_token',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result).toBe(true);
|
|
178
|
+
expect(api.getChatFolder).toHaveBeenCalledWith(
|
|
179
|
+
'https://test.bitrix24.com/rest/',
|
|
180
|
+
'bot_token',
|
|
181
|
+
40985,
|
|
182
|
+
);
|
|
183
|
+
expect(api.uploadFile).toHaveBeenCalledWith(
|
|
184
|
+
'https://test.bitrix24.com/rest/',
|
|
185
|
+
'bot_token',
|
|
186
|
+
42,
|
|
187
|
+
'upload-test.txt',
|
|
188
|
+
expect.any(Buffer),
|
|
189
|
+
);
|
|
190
|
+
expect(api.commitFileToChat).toHaveBeenCalledWith(
|
|
191
|
+
'https://test.bitrix24.com/rest/',
|
|
192
|
+
'bot_token',
|
|
193
|
+
40985,
|
|
194
|
+
100,
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns false when getChatFolder fails', async () => {
|
|
199
|
+
(api.getChatFolder as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
|
|
200
|
+
new Error('folder error'),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
204
|
+
const { join } = await import('node:path');
|
|
205
|
+
const { tmpdir } = await import('node:os');
|
|
206
|
+
const testDir = join(tmpdir(), 'openclaw-b24-test');
|
|
207
|
+
mkdirSync(testDir, { recursive: true });
|
|
208
|
+
const testPath = join(testDir, 'fail-upload.txt');
|
|
209
|
+
writeFileSync(testPath, 'content');
|
|
210
|
+
savedPaths.push(testPath);
|
|
211
|
+
|
|
212
|
+
const result = await service.uploadMediaToChat({
|
|
213
|
+
localPath: testPath,
|
|
214
|
+
fileName: 'fail-upload.txt',
|
|
215
|
+
chatId: 40985,
|
|
216
|
+
clientEndpoint: 'https://test.bitrix24.com/rest/',
|
|
217
|
+
botToken: 'bot_token',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result).toBe(false);
|
|
221
|
+
expect(silentLogger.error).toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|