@ihazz/bitrix24 0.1.4 → 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.
@@ -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
+ });
@@ -78,18 +78,16 @@ describe('splitMessage', () => {
78
78
  });
79
79
 
80
80
  describe('buildKeyboard', () => {
81
- it('builds a single row keyboard', () => {
81
+ it('builds a single row keyboard (flat array)', () => {
82
82
  const kb = buildKeyboard([
83
83
  [{ text: 'Yes', command: 'answer', commandParams: 'yes' }],
84
84
  ]);
85
+ // Flat array: 1 button, no NEWLINE
85
86
  expect(kb).toHaveLength(1);
86
- expect(kb[0]).toHaveLength(1);
87
- expect(kb[0][0].TEXT).toBe('Yes');
88
- expect(kb[0][0].COMMAND).toBe('answer');
89
- expect(kb[0][0].COMMAND_PARAMS).toBe('yes');
87
+ expect(kb[0]).toMatchObject({ TEXT: 'Yes', COMMAND: 'answer', COMMAND_PARAMS: 'yes' });
90
88
  });
91
89
 
92
- it('builds multi-row keyboard', () => {
90
+ it('builds multi-row keyboard with NEWLINE separators', () => {
93
91
  const kb = buildKeyboard([
94
92
  [
95
93
  { text: 'Yes', command: 'answer', commandParams: 'yes' },
@@ -97,27 +95,26 @@ describe('buildKeyboard', () => {
97
95
  ],
98
96
  [{ text: 'More info', link: 'https://example.com', fullWidth: true }],
99
97
  ]);
100
- expect(kb).toHaveLength(2);
101
- expect(kb[0]).toHaveLength(2);
102
- expect(kb[1]).toHaveLength(1);
103
- expect(kb[1][0].LINK).toBe('https://example.com');
104
- expect(kb[1][0].DISPLAY).toBe('LINE');
98
+ // Flat: [btn, btn, NEWLINE, btn] = 4 items
99
+ expect(kb).toHaveLength(4);
100
+ expect(kb[0]).toMatchObject({ TEXT: 'Yes' });
101
+ expect(kb[1]).toMatchObject({ TEXT: 'No' });
102
+ expect(kb[2]).toMatchObject({ TYPE: 'NEWLINE' });
103
+ expect(kb[3]).toMatchObject({ TEXT: 'More info', LINK: 'https://example.com', DISPLAY: 'LINE' });
105
104
  });
106
105
 
107
106
  it('applies default colors', () => {
108
107
  const kb = buildKeyboard([[{ text: 'Click' }]]);
109
- expect(kb[0][0].BG_COLOR).toBe('#29619b');
110
- expect(kb[0][0].TEXT_COLOR).toBe('#fff');
108
+ expect(kb[0]).toMatchObject({ BG_COLOR: '#29619b', TEXT_COLOR: '#fff' });
111
109
  });
112
110
 
113
111
  it('applies custom colors', () => {
114
112
  const kb = buildKeyboard([[{ text: 'Click', bgColor: '#333', textColor: '#eee' }]]);
115
- expect(kb[0][0].BG_COLOR).toBe('#333');
116
- expect(kb[0][0].TEXT_COLOR).toBe('#eee');
113
+ expect(kb[0]).toMatchObject({ BG_COLOR: '#333', TEXT_COLOR: '#eee' });
117
114
  });
118
115
 
119
116
  it('sets BLOCK=Y when disableAfterClick is true', () => {
120
117
  const kb = buildKeyboard([[{ text: 'Once', command: 'once', disableAfterClick: true }]]);
121
- expect(kb[0][0].BLOCK).toBe('Y');
118
+ expect(kb[0]).toMatchObject({ BLOCK: 'Y' });
122
119
  });
123
120
  });