@urugus/slack-cli 0.2.8 → 0.2.10
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/.claude/settings.local.json +6 -2
- package/.github/workflows/ci.yml +2 -2
- package/README.md +8 -0
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +16 -2
- package/dist/commands/send.js.map +1 -1
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +32 -16
- package/dist/commands/unread.js.map +1 -1
- package/dist/types/commands.d.ts +2 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +1 -5
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/schedule-utils.d.ts +3 -0
- package/dist/utils/schedule-utils.d.ts.map +1 -0
- package/dist/utils/schedule-utils.js +34 -0
- package/dist/utils/schedule-utils.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +2 -1
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +3 -0
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +2 -1
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +11 -0
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/validators.d.ts +4 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +31 -0
- package/dist/utils/validators.js.map +1 -1
- package/eslint.config.js +38 -0
- package/package.json +13 -15
- package/src/commands/send.ts +21 -3
- package/src/commands/unread.ts +52 -22
- package/src/types/commands.ts +2 -0
- package/src/utils/constants.ts +7 -0
- package/src/utils/errors.ts +1 -5
- package/src/utils/schedule-utils.ts +41 -0
- package/src/utils/slack-api-client.ts +10 -1
- package/src/utils/slack-operations/message-operations.ts +25 -1
- package/src/utils/validators.ts +38 -0
- package/tests/commands/send.test.ts +235 -44
- package/tests/index.test.ts +2 -2
- package/tests/utils/schedule-utils.test.ts +63 -0
- package/tests/utils/slack-api-client.test.ts +18 -1
- package/tests/utils/slack-operations/message-operations.test.ts +19 -1
- package/.eslintrc.json +0 -25
- package/src/utils/formatters/output-formatter.ts +0 -7
- package/tests/utils/slack-operations/channel-operations-refactored.test.ts +0 -179
|
@@ -18,13 +18,13 @@ describe('send command', () => {
|
|
|
18
18
|
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
vi.clearAllMocks();
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
mockConfigManager = new ProfileConfigManager();
|
|
23
23
|
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
mockSlackClient = new SlackApiClient('test-token');
|
|
26
26
|
vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
mockConsole = setupMockConsole();
|
|
29
29
|
program = createTestProgram();
|
|
30
30
|
program.addCommand(setupSendCommand());
|
|
@@ -38,30 +38,42 @@ describe('send command', () => {
|
|
|
38
38
|
it('should send a message to specified channel', async () => {
|
|
39
39
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
40
40
|
token: 'test-token',
|
|
41
|
-
updatedAt: new Date().toISOString()
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
42
|
});
|
|
43
43
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
44
44
|
ok: true,
|
|
45
|
-
ts: '1234567890.123456'
|
|
45
|
+
ts: '1234567890.123456',
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello, World!']);
|
|
49
49
|
|
|
50
50
|
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', 'Hello, World!', undefined);
|
|
51
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
51
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
52
|
+
expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
|
|
53
|
+
);
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
it('should use specified profile', async () => {
|
|
55
57
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
56
58
|
token: 'work-token',
|
|
57
|
-
updatedAt: new Date().toISOString()
|
|
59
|
+
updatedAt: new Date().toISOString(),
|
|
58
60
|
});
|
|
59
61
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
60
62
|
ok: true,
|
|
61
|
-
ts: '1234567890.123456'
|
|
63
|
+
ts: '1234567890.123456',
|
|
62
64
|
});
|
|
63
65
|
|
|
64
|
-
await program.parseAsync([
|
|
66
|
+
await program.parseAsync([
|
|
67
|
+
'node',
|
|
68
|
+
'slack-cli',
|
|
69
|
+
'send',
|
|
70
|
+
'-c',
|
|
71
|
+
'general',
|
|
72
|
+
'-m',
|
|
73
|
+
'Hello',
|
|
74
|
+
'--profile',
|
|
75
|
+
'work',
|
|
76
|
+
]);
|
|
65
77
|
|
|
66
78
|
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
67
79
|
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
@@ -74,11 +86,11 @@ describe('send command', () => {
|
|
|
74
86
|
vi.mocked(fs.readFile).mockResolvedValue(fileContent);
|
|
75
87
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
76
88
|
token: 'test-token',
|
|
77
|
-
updatedAt: new Date().toISOString()
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
78
90
|
});
|
|
79
91
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
80
92
|
ok: true,
|
|
81
|
-
ts: '1234567890.123456'
|
|
93
|
+
ts: '1234567890.123456',
|
|
82
94
|
});
|
|
83
95
|
|
|
84
96
|
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'message.txt']);
|
|
@@ -92,46 +104,80 @@ describe('send command', () => {
|
|
|
92
104
|
it('should send a reply to a thread with --thread option', async () => {
|
|
93
105
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
94
106
|
token: 'test-token',
|
|
95
|
-
updatedAt: new Date().toISOString()
|
|
107
|
+
updatedAt: new Date().toISOString(),
|
|
96
108
|
});
|
|
97
109
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
98
110
|
ok: true,
|
|
99
|
-
ts: '1234567890.123456'
|
|
111
|
+
ts: '1234567890.123456',
|
|
100
112
|
});
|
|
101
113
|
|
|
102
|
-
await program.parseAsync([
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
await program.parseAsync([
|
|
115
|
+
'node',
|
|
116
|
+
'slack-cli',
|
|
117
|
+
'send',
|
|
118
|
+
'-c',
|
|
119
|
+
'general',
|
|
120
|
+
'-m',
|
|
121
|
+
'Reply to thread',
|
|
122
|
+
'--thread',
|
|
123
|
+
'1719207629.000100',
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
|
|
127
|
+
'general',
|
|
128
|
+
'Reply to thread',
|
|
129
|
+
'1719207629.000100'
|
|
130
|
+
);
|
|
131
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
132
|
+
expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
|
|
133
|
+
);
|
|
106
134
|
});
|
|
107
135
|
|
|
108
136
|
it('should send a reply to a thread with -t option', async () => {
|
|
109
137
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
110
138
|
token: 'test-token',
|
|
111
|
-
updatedAt: new Date().toISOString()
|
|
139
|
+
updatedAt: new Date().toISOString(),
|
|
112
140
|
});
|
|
113
141
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
114
142
|
ok: true,
|
|
115
|
-
ts: '1234567890.123456'
|
|
143
|
+
ts: '1234567890.123456',
|
|
116
144
|
});
|
|
117
145
|
|
|
118
|
-
await program.parseAsync([
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
146
|
+
await program.parseAsync([
|
|
147
|
+
'node',
|
|
148
|
+
'slack-cli',
|
|
149
|
+
'send',
|
|
150
|
+
'-c',
|
|
151
|
+
'general',
|
|
152
|
+
'-m',
|
|
153
|
+
'Reply to thread',
|
|
154
|
+
'-t',
|
|
155
|
+
'1719207629.000100',
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
|
|
159
|
+
'general',
|
|
160
|
+
'Reply to thread',
|
|
161
|
+
'1719207629.000100'
|
|
162
|
+
);
|
|
163
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
164
|
+
expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
|
|
165
|
+
);
|
|
122
166
|
});
|
|
123
167
|
|
|
124
168
|
it('should validate thread timestamp format', async () => {
|
|
125
169
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
126
170
|
token: 'test-token',
|
|
127
|
-
updatedAt: new Date().toISOString()
|
|
171
|
+
updatedAt: new Date().toISOString(),
|
|
128
172
|
});
|
|
129
173
|
|
|
130
174
|
const sendCommand = setupSendCommand();
|
|
131
175
|
sendCommand.exitOverride();
|
|
132
176
|
|
|
133
177
|
await expect(
|
|
134
|
-
sendCommand.parseAsync(['-c', 'general', '-m', 'Reply', '-t', 'invalid-timestamp'], {
|
|
178
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Reply', '-t', 'invalid-timestamp'], {
|
|
179
|
+
from: 'user',
|
|
180
|
+
})
|
|
135
181
|
).rejects.toThrow(ERROR_MESSAGES.INVALID_THREAD_TIMESTAMP);
|
|
136
182
|
});
|
|
137
183
|
|
|
@@ -140,18 +186,107 @@ describe('send command', () => {
|
|
|
140
186
|
vi.mocked(fs.readFile).mockResolvedValue(fileContent);
|
|
141
187
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
142
188
|
token: 'test-token',
|
|
143
|
-
updatedAt: new Date().toISOString()
|
|
189
|
+
updatedAt: new Date().toISOString(),
|
|
144
190
|
});
|
|
145
191
|
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
146
192
|
ok: true,
|
|
147
|
-
ts: '1234567890.123456'
|
|
193
|
+
ts: '1234567890.123456',
|
|
148
194
|
});
|
|
149
195
|
|
|
150
|
-
await program.parseAsync([
|
|
196
|
+
await program.parseAsync([
|
|
197
|
+
'node',
|
|
198
|
+
'slack-cli',
|
|
199
|
+
'send',
|
|
200
|
+
'-c',
|
|
201
|
+
'general',
|
|
202
|
+
'-f',
|
|
203
|
+
'reply.txt',
|
|
204
|
+
'-t',
|
|
205
|
+
'1719207629.000100',
|
|
206
|
+
]);
|
|
151
207
|
|
|
152
208
|
expect(fs.readFile).toHaveBeenCalledWith('reply.txt', 'utf-8');
|
|
153
|
-
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
|
|
154
|
-
|
|
209
|
+
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
|
|
210
|
+
'general',
|
|
211
|
+
fileContent,
|
|
212
|
+
'1719207629.000100'
|
|
213
|
+
);
|
|
214
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
215
|
+
expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('schedule message', () => {
|
|
221
|
+
it('should schedule message with --at', async () => {
|
|
222
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
223
|
+
token: 'test-token',
|
|
224
|
+
updatedAt: new Date().toISOString(),
|
|
225
|
+
});
|
|
226
|
+
vi.mocked(mockSlackClient.scheduleMessage).mockResolvedValue({
|
|
227
|
+
ok: true,
|
|
228
|
+
scheduled_message_id: 'Q1298393284',
|
|
229
|
+
post_at: 2051258400,
|
|
230
|
+
} as any);
|
|
231
|
+
|
|
232
|
+
await program.parseAsync([
|
|
233
|
+
'node',
|
|
234
|
+
'slack-cli',
|
|
235
|
+
'send',
|
|
236
|
+
'-c',
|
|
237
|
+
'general',
|
|
238
|
+
'-m',
|
|
239
|
+
'Future message',
|
|
240
|
+
'--at',
|
|
241
|
+
'2035-01-01T10:00:00Z',
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
expect(mockSlackClient.scheduleMessage).toHaveBeenCalledWith(
|
|
245
|
+
'general',
|
|
246
|
+
'Future message',
|
|
247
|
+
2051258400,
|
|
248
|
+
undefined
|
|
249
|
+
);
|
|
250
|
+
expect(mockSlackClient.sendMessage).not.toHaveBeenCalled();
|
|
251
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(
|
|
252
|
+
expect.stringContaining('Message scheduled to #general')
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should schedule message with --after', async () => {
|
|
257
|
+
vi.useFakeTimers();
|
|
258
|
+
vi.setSystemTime(new Date('2026-02-12T00:00:00Z'));
|
|
259
|
+
|
|
260
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
261
|
+
token: 'test-token',
|
|
262
|
+
updatedAt: new Date().toISOString(),
|
|
263
|
+
});
|
|
264
|
+
vi.mocked(mockSlackClient.scheduleMessage).mockResolvedValue({
|
|
265
|
+
ok: true,
|
|
266
|
+
scheduled_message_id: 'Q1298393284',
|
|
267
|
+
post_at: 1770868800,
|
|
268
|
+
} as any);
|
|
269
|
+
|
|
270
|
+
await program.parseAsync([
|
|
271
|
+
'node',
|
|
272
|
+
'slack-cli',
|
|
273
|
+
'send',
|
|
274
|
+
'-c',
|
|
275
|
+
'general',
|
|
276
|
+
'-m',
|
|
277
|
+
'Future message',
|
|
278
|
+
'--after',
|
|
279
|
+
'10',
|
|
280
|
+
]);
|
|
281
|
+
|
|
282
|
+
expect(mockSlackClient.scheduleMessage).toHaveBeenCalledWith(
|
|
283
|
+
'general',
|
|
284
|
+
'Future message',
|
|
285
|
+
1770855000,
|
|
286
|
+
undefined
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
vi.useRealTimers();
|
|
155
290
|
});
|
|
156
291
|
});
|
|
157
292
|
|
|
@@ -160,9 +295,9 @@ describe('send command', () => {
|
|
|
160
295
|
const sendCommand = setupSendCommand();
|
|
161
296
|
sendCommand.exitOverride();
|
|
162
297
|
|
|
163
|
-
await expect(
|
|
164
|
-
|
|
165
|
-
)
|
|
298
|
+
await expect(sendCommand.parseAsync(['-c', 'general'], { from: 'user' })).rejects.toThrow(
|
|
299
|
+
`Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`
|
|
300
|
+
);
|
|
166
301
|
});
|
|
167
302
|
|
|
168
303
|
it('should fail when both message and file are provided', async () => {
|
|
@@ -170,17 +305,64 @@ describe('send command', () => {
|
|
|
170
305
|
sendCommand.exitOverride();
|
|
171
306
|
|
|
172
307
|
await expect(
|
|
173
|
-
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '-f', 'file.txt'], {
|
|
308
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '-f', 'file.txt'], {
|
|
309
|
+
from: 'user',
|
|
310
|
+
})
|
|
174
311
|
).rejects.toThrow(`Error: ${ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE}`);
|
|
175
312
|
});
|
|
176
313
|
|
|
177
|
-
it('should fail when
|
|
314
|
+
it('should fail when both --at and --after are provided', async () => {
|
|
315
|
+
const sendCommand = setupSendCommand();
|
|
316
|
+
sendCommand.exitOverride();
|
|
317
|
+
|
|
318
|
+
await expect(
|
|
319
|
+
sendCommand.parseAsync(
|
|
320
|
+
['-c', 'general', '-m', 'Hello', '--at', '2030-01-01T10:00:00Z', '--after', '10'],
|
|
321
|
+
{
|
|
322
|
+
from: 'user',
|
|
323
|
+
}
|
|
324
|
+
)
|
|
325
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.BOTH_SCHEDULE_OPTIONS}`);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should fail with invalid --at', async () => {
|
|
329
|
+
const sendCommand = setupSendCommand();
|
|
330
|
+
sendCommand.exitOverride();
|
|
331
|
+
|
|
332
|
+
await expect(
|
|
333
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--at', 'invalid-date'], {
|
|
334
|
+
from: 'user',
|
|
335
|
+
})
|
|
336
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.INVALID_SCHEDULE_AT}`);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should fail with past --at', async () => {
|
|
178
340
|
const sendCommand = setupSendCommand();
|
|
179
341
|
sendCommand.exitOverride();
|
|
180
|
-
|
|
342
|
+
|
|
181
343
|
await expect(
|
|
182
|
-
sendCommand.parseAsync(['-m', 'Hello'
|
|
183
|
-
|
|
344
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--at', '1'], {
|
|
345
|
+
from: 'user',
|
|
346
|
+
})
|
|
347
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.SCHEDULE_TIME_IN_PAST}`);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should fail with invalid --after', async () => {
|
|
351
|
+
const sendCommand = setupSendCommand();
|
|
352
|
+
sendCommand.exitOverride();
|
|
353
|
+
|
|
354
|
+
await expect(
|
|
355
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--after', '0'], {
|
|
356
|
+
from: 'user',
|
|
357
|
+
})
|
|
358
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.INVALID_SCHEDULE_AFTER}`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should fail when no channel is provided', async () => {
|
|
362
|
+
const sendCommand = setupSendCommand();
|
|
363
|
+
sendCommand.exitOverride();
|
|
364
|
+
|
|
365
|
+
await expect(sendCommand.parseAsync(['-m', 'Hello'], { from: 'user' })).rejects.toThrow();
|
|
184
366
|
});
|
|
185
367
|
});
|
|
186
368
|
|
|
@@ -190,20 +372,26 @@ describe('send command', () => {
|
|
|
190
372
|
|
|
191
373
|
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello']);
|
|
192
374
|
|
|
193
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
375
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
376
|
+
expect.stringContaining('Error:'),
|
|
377
|
+
expect.any(String)
|
|
378
|
+
);
|
|
194
379
|
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
195
380
|
});
|
|
196
381
|
|
|
197
382
|
it('should handle Slack API errors', async () => {
|
|
198
383
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
199
384
|
token: 'test-token',
|
|
200
|
-
updatedAt: new Date().toISOString()
|
|
385
|
+
updatedAt: new Date().toISOString(),
|
|
201
386
|
});
|
|
202
387
|
vi.mocked(mockSlackClient.sendMessage).mockRejectedValue(new Error('channel_not_found'));
|
|
203
388
|
|
|
204
389
|
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'nonexistent', '-m', 'Hello']);
|
|
205
390
|
|
|
206
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
391
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
392
|
+
expect.stringContaining('Error:'),
|
|
393
|
+
expect.any(String)
|
|
394
|
+
);
|
|
207
395
|
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
208
396
|
});
|
|
209
397
|
|
|
@@ -211,13 +399,16 @@ describe('send command', () => {
|
|
|
211
399
|
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT: no such file'));
|
|
212
400
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
213
401
|
token: 'test-token',
|
|
214
|
-
updatedAt: new Date().toISOString()
|
|
402
|
+
updatedAt: new Date().toISOString(),
|
|
215
403
|
});
|
|
216
404
|
|
|
217
405
|
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'nonexistent.txt']);
|
|
218
406
|
|
|
219
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
407
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(
|
|
408
|
+
expect.stringContaining('Error:'),
|
|
409
|
+
expect.any(String)
|
|
410
|
+
);
|
|
220
411
|
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
221
412
|
});
|
|
222
413
|
});
|
|
223
|
-
});
|
|
414
|
+
});
|
package/tests/index.test.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'fs';
|
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
|
|
6
6
|
describe('slack-cli version', () => {
|
|
7
|
-
it('should display the correct version from package.json', () => {
|
|
7
|
+
it('should display the correct version from package.json', { timeout: 20000 }, () => {
|
|
8
8
|
// Read the expected version from package.json
|
|
9
9
|
const packageJson = JSON.parse(
|
|
10
10
|
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
@@ -21,7 +21,7 @@ describe('slack-cli version', () => {
|
|
|
21
21
|
expect(output).toBe(expectedVersion);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('should display version with -V flag', () => {
|
|
24
|
+
it('should display version with -V flag', { timeout: 20000 }, () => {
|
|
25
25
|
// Read the expected version from package.json
|
|
26
26
|
const packageJson = JSON.parse(
|
|
27
27
|
readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { parseScheduledTimestamp, resolvePostAt } from '../../src/utils/schedule-utils';
|
|
3
|
+
import { optionValidators } from '../../src/utils/validators';
|
|
4
|
+
|
|
5
|
+
describe('schedule utils', () => {
|
|
6
|
+
describe('parseScheduledTimestamp', () => {
|
|
7
|
+
it('parses unix timestamp seconds', () => {
|
|
8
|
+
expect(parseScheduledTimestamp('1770855000')).toBe(1770855000);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('parses ISO date string', () => {
|
|
12
|
+
expect(parseScheduledTimestamp('2026-02-12T00:10:00Z')).toBe(1770855000);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns null for invalid input', () => {
|
|
16
|
+
expect(parseScheduledTimestamp('invalid')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('resolvePostAt', () => {
|
|
21
|
+
it('returns parsed timestamp for --at', () => {
|
|
22
|
+
expect(resolvePostAt('1770855000', undefined)).toBe(1770855000);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns now + minutes for --after', () => {
|
|
26
|
+
expect(resolvePostAt(undefined, '10', Date.parse('2026-02-12T00:00:00Z'))).toBe(1770855000);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns null when invalid --after is provided', () => {
|
|
30
|
+
expect(resolvePostAt(undefined, '0')).toBeNull();
|
|
31
|
+
expect(resolvePostAt(undefined, '1.5')).toBeNull();
|
|
32
|
+
expect(resolvePostAt(undefined, '10minutes')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('optionValidators.scheduleTiming', () => {
|
|
37
|
+
it('rejects both --at and --after', () => {
|
|
38
|
+
expect(optionValidators.scheduleTiming({ at: '1770855000', after: '10' })).toBe(
|
|
39
|
+
'Cannot use both --at and --after'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rejects past --at timestamp', () => {
|
|
44
|
+
vi.useFakeTimers();
|
|
45
|
+
vi.setSystemTime(new Date('2026-02-12T00:00:00Z'));
|
|
46
|
+
|
|
47
|
+
expect(optionValidators.scheduleTiming({ at: '1770854300' })).toBe(
|
|
48
|
+
'Schedule time must be in the future'
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rejects non-integer --after values', () => {
|
|
55
|
+
expect(optionValidators.scheduleTiming({ after: '1.5' })).toBe(
|
|
56
|
+
'--after must be a positive integer (minutes)'
|
|
57
|
+
);
|
|
58
|
+
expect(optionValidators.scheduleTiming({ after: '10minutes' })).toBe(
|
|
59
|
+
'--after must be a positive integer (minutes)'
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -12,7 +12,8 @@ describe('SlackApiClient', () => {
|
|
|
12
12
|
vi.clearAllMocks();
|
|
13
13
|
mockWebClient = {
|
|
14
14
|
chat: {
|
|
15
|
-
postMessage: vi.fn()
|
|
15
|
+
postMessage: vi.fn(),
|
|
16
|
+
scheduleMessage: vi.fn()
|
|
16
17
|
},
|
|
17
18
|
conversations: {
|
|
18
19
|
list: vi.fn(),
|
|
@@ -84,6 +85,22 @@ describe('SlackApiClient', () => {
|
|
|
84
85
|
});
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
describe('scheduleMessage', () => {
|
|
89
|
+
it('should schedule message to channel', async () => {
|
|
90
|
+
const mockResponse = { ok: true, scheduled_message_id: 'Q123', post_at: 1770855000 };
|
|
91
|
+
vi.mocked(mockWebClient.chat.scheduleMessage).mockResolvedValue(mockResponse as any);
|
|
92
|
+
|
|
93
|
+
const result = await client.scheduleMessage('general', 'Hello, future!', 1770855000);
|
|
94
|
+
|
|
95
|
+
expect(mockWebClient.chat.scheduleMessage).toHaveBeenCalledWith({
|
|
96
|
+
channel: 'general',
|
|
97
|
+
text: 'Hello, future!',
|
|
98
|
+
post_at: 1770855000
|
|
99
|
+
});
|
|
100
|
+
expect(result).toEqual(mockResponse);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
87
104
|
describe('listChannels', () => {
|
|
88
105
|
it('should list channels with default options', async () => {
|
|
89
106
|
const mockChannels = [
|
|
@@ -12,6 +12,7 @@ vi.mock('@slack/web-api', () => ({
|
|
|
12
12
|
},
|
|
13
13
|
chat: {
|
|
14
14
|
postMessage: vi.fn(),
|
|
15
|
+
scheduleMessage: vi.fn(),
|
|
15
16
|
},
|
|
16
17
|
})),
|
|
17
18
|
LogLevel: {
|
|
@@ -31,6 +32,23 @@ describe('MessageOperations', () => {
|
|
|
31
32
|
mockClient = (messageOps as any).client;
|
|
32
33
|
});
|
|
33
34
|
|
|
35
|
+
describe('scheduleMessage', () => {
|
|
36
|
+
it('should call chat.scheduleMessage with post_at', async () => {
|
|
37
|
+
mockClient.chat.scheduleMessage.mockResolvedValue({
|
|
38
|
+
ok: true,
|
|
39
|
+
scheduled_message_id: 'Q123',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await messageOps.scheduleMessage('C1234567890', 'Hello', 1770855000);
|
|
43
|
+
|
|
44
|
+
expect(mockClient.chat.scheduleMessage).toHaveBeenCalledWith({
|
|
45
|
+
channel: 'C1234567890',
|
|
46
|
+
text: 'Hello',
|
|
47
|
+
post_at: 1770855000,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
34
52
|
describe('getHistory with mentions', () => {
|
|
35
53
|
it('should fetch user info for mentioned users in message text', async () => {
|
|
36
54
|
const mockMessages = [
|
|
@@ -123,4 +141,4 @@ describe('MessageOperations', () => {
|
|
|
123
141
|
expect(result.users.get('U07L5D50RAL')).toBe('koguchi_s');
|
|
124
142
|
});
|
|
125
143
|
});
|
|
126
|
-
});
|
|
144
|
+
});
|
package/.eslintrc.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"parser": "@typescript-eslint/parser",
|
|
3
|
-
"extends": [
|
|
4
|
-
"eslint:recommended",
|
|
5
|
-
"plugin:@typescript-eslint/recommended",
|
|
6
|
-
"prettier"
|
|
7
|
-
],
|
|
8
|
-
"plugins": ["@typescript-eslint"],
|
|
9
|
-
"parserOptions": {
|
|
10
|
-
"ecmaVersion": 2020,
|
|
11
|
-
"sourceType": "module",
|
|
12
|
-
"project": "./tsconfig.json"
|
|
13
|
-
},
|
|
14
|
-
"env": {
|
|
15
|
-
"node": true,
|
|
16
|
-
"es2020": true
|
|
17
|
-
},
|
|
18
|
-
"rules": {
|
|
19
|
-
"@typescript-eslint/explicit-function-return-type": "off",
|
|
20
|
-
"@typescript-eslint/no-explicit-any": "warn",
|
|
21
|
-
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
|
22
|
-
"no-console": "off"
|
|
23
|
-
},
|
|
24
|
-
"ignorePatterns": ["dist", "node_modules", "coverage", "vitest.config.ts"]
|
|
25
|
-
}
|