@urugus/slack-cli 0.1.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/.claude/settings.local.json +53 -0
- package/.eslintrc.json +25 -0
- package/.github/dependabot.yml +18 -0
- package/.github/workflows/ci.yml +70 -0
- package/.github/workflows/pr-validation.yml +51 -0
- package/.prettierignore +11 -0
- package/.prettierrc +10 -0
- package/CLAUDE.md +16 -0
- package/README.md +161 -0
- package/dist/commands/channels.d.ts +3 -0
- package/dist/commands/channels.d.ts.map +1 -0
- package/dist/commands/channels.js +50 -0
- package/dist/commands/channels.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +87 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +79 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +85 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/unread.d.ts +3 -0
- package/dist/commands/unread.d.ts.map +1 -0
- package/dist/commands/unread.js +104 -0
- package/dist/commands/unread.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/types/commands.d.ts +40 -0
- package/dist/types/commands.d.ts.map +1 -0
- package/dist/types/commands.js +3 -0
- package/dist/types/commands.js.map +1 -0
- package/dist/types/config.d.ts +18 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/utils/channel-formatter.d.ts +16 -0
- package/dist/utils/channel-formatter.d.ts.map +1 -0
- package/dist/utils/channel-formatter.js +77 -0
- package/dist/utils/channel-formatter.js.map +1 -0
- package/dist/utils/client-factory.d.ts +6 -0
- package/dist/utils/client-factory.d.ts.map +1 -0
- package/dist/utils/client-factory.js +13 -0
- package/dist/utils/client-factory.js.map +1 -0
- package/dist/utils/command-wrapper.d.ts +6 -0
- package/dist/utils/command-wrapper.d.ts.map +1 -0
- package/dist/utils/command-wrapper.js +27 -0
- package/dist/utils/command-wrapper.js.map +1 -0
- package/dist/utils/config-helper.d.ts +8 -0
- package/dist/utils/config-helper.d.ts.map +1 -0
- package/dist/utils/config-helper.js +19 -0
- package/dist/utils/config-helper.js.map +1 -0
- package/dist/utils/config.d.ts +10 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +94 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/constants.d.ts +32 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +42 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/date-utils.d.ts +3 -0
- package/dist/utils/date-utils.d.ts.map +1 -0
- package/dist/utils/date-utils.js +12 -0
- package/dist/utils/date-utils.js.map +1 -0
- package/dist/utils/error-utils.d.ts +2 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +10 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/errors.d.ts +17 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +40 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/profile-config.d.ts +21 -0
- package/dist/utils/profile-config.d.ts.map +1 -0
- package/dist/utils/profile-config.js +173 -0
- package/dist/utils/profile-config.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +74 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -0
- package/dist/utils/slack-api-client.js +132 -0
- package/dist/utils/slack-api-client.js.map +1 -0
- package/package.json +56 -0
- package/src/commands/channels.ts +65 -0
- package/src/commands/config.ts +104 -0
- package/src/commands/history.ts +96 -0
- package/src/commands/send.ts +52 -0
- package/src/commands/unread.ts +118 -0
- package/src/index.ts +19 -0
- package/src/types/commands.ts +46 -0
- package/src/types/config.ts +20 -0
- package/src/utils/channel-formatter.ts +89 -0
- package/src/utils/client-factory.ts +10 -0
- package/src/utils/command-wrapper.ts +27 -0
- package/src/utils/config-helper.ts +21 -0
- package/src/utils/constants.ts +47 -0
- package/src/utils/date-utils.ts +8 -0
- package/src/utils/error-utils.ts +6 -0
- package/src/utils/errors.ts +37 -0
- package/src/utils/profile-config.ts +171 -0
- package/src/utils/slack-api-client.ts +218 -0
- package/tests/commands/channels.test.ts +250 -0
- package/tests/commands/config.test.ts +158 -0
- package/tests/commands/history.test.ts +250 -0
- package/tests/commands/send.test.ts +156 -0
- package/tests/commands/unread.test.ts +248 -0
- package/tests/test-utils.ts +28 -0
- package/tests/utils/config.test.ts +400 -0
- package/tests/utils/date-utils.test.ts +30 -0
- package/tests/utils/error-utils.test.ts +34 -0
- package/tests/utils/slack-api-client.test.ts +170 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { setupHistoryCommand } from '../../src/commands/history';
|
|
3
|
+
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
4
|
+
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
5
|
+
import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
|
|
6
|
+
import { ERROR_MESSAGES } from '../../src/utils/constants';
|
|
7
|
+
|
|
8
|
+
vi.mock('../../src/utils/slack-api-client');
|
|
9
|
+
vi.mock('../../src/utils/profile-config');
|
|
10
|
+
|
|
11
|
+
describe('history command', () => {
|
|
12
|
+
let program: any;
|
|
13
|
+
let mockSlackClient: SlackApiClient;
|
|
14
|
+
let mockConfigManager: ProfileConfigManager;
|
|
15
|
+
let mockConsole: any;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
|
|
20
|
+
mockConfigManager = new ProfileConfigManager();
|
|
21
|
+
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
22
|
+
|
|
23
|
+
mockSlackClient = new SlackApiClient('test-token');
|
|
24
|
+
vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
|
|
25
|
+
|
|
26
|
+
mockConsole = setupMockConsole();
|
|
27
|
+
program = createTestProgram();
|
|
28
|
+
program.addCommand(setupHistoryCommand());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
restoreMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('basic functionality', () => {
|
|
36
|
+
it('should fetch channel history with default options', async () => {
|
|
37
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
38
|
+
token: 'test-token',
|
|
39
|
+
updatedAt: new Date().toISOString()
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const mockMessages = [
|
|
43
|
+
{
|
|
44
|
+
type: 'message',
|
|
45
|
+
text: 'Hello world',
|
|
46
|
+
user: 'U123456',
|
|
47
|
+
ts: '1609459200.000100',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'message',
|
|
51
|
+
text: 'Another message',
|
|
52
|
+
user: 'U789012',
|
|
53
|
+
ts: '1609459300.000200',
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
57
|
+
messages: mockMessages,
|
|
58
|
+
users: new Map([
|
|
59
|
+
['U123456', 'john.doe'],
|
|
60
|
+
['U789012', 'jane.smith']
|
|
61
|
+
])
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
65
|
+
|
|
66
|
+
expect(mockSlackClient.getHistory).toHaveBeenCalledWith('general', {
|
|
67
|
+
limit: 10,
|
|
68
|
+
});
|
|
69
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Message History for #general'));
|
|
70
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('john.doe'));
|
|
71
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should fetch history with custom message count', async () => {
|
|
75
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
76
|
+
token: 'test-token',
|
|
77
|
+
updatedAt: new Date().toISOString()
|
|
78
|
+
});
|
|
79
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
80
|
+
messages: [],
|
|
81
|
+
users: new Map()
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '-n', '20']);
|
|
85
|
+
|
|
86
|
+
expect(mockSlackClient.getHistory).toHaveBeenCalledWith('general', {
|
|
87
|
+
limit: 20,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should fetch history since specific date', async () => {
|
|
92
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
93
|
+
token: 'test-token',
|
|
94
|
+
updatedAt: new Date().toISOString()
|
|
95
|
+
});
|
|
96
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
97
|
+
messages: [],
|
|
98
|
+
users: new Map()
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const testDate = '2024-01-01 00:00:00';
|
|
102
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '--since', testDate]);
|
|
103
|
+
|
|
104
|
+
// Calculate expected timestamp based on the actual date parsing behavior
|
|
105
|
+
const expectedTimestamp = Math.floor(Date.parse(testDate) / 1000).toString();
|
|
106
|
+
|
|
107
|
+
expect(mockSlackClient.getHistory).toHaveBeenCalledWith('general', {
|
|
108
|
+
limit: 10,
|
|
109
|
+
oldest: expectedTimestamp,
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should use specific profile when provided', async () => {
|
|
114
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
115
|
+
token: 'work-token',
|
|
116
|
+
updatedAt: new Date().toISOString()
|
|
117
|
+
});
|
|
118
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
119
|
+
messages: [],
|
|
120
|
+
users: new Map()
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '--profile', 'work']);
|
|
124
|
+
|
|
125
|
+
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
126
|
+
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('error handling', () => {
|
|
131
|
+
it('should show error when no configuration exists', async () => {
|
|
132
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
|
|
133
|
+
|
|
134
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
135
|
+
|
|
136
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
137
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should show error when profile not found', async () => {
|
|
141
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
|
|
142
|
+
|
|
143
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '--profile', 'unknown']);
|
|
144
|
+
|
|
145
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
146
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should show error for invalid date format', async () => {
|
|
150
|
+
const historyCommand = setupHistoryCommand();
|
|
151
|
+
historyCommand.exitOverride();
|
|
152
|
+
|
|
153
|
+
await expect(
|
|
154
|
+
historyCommand.parseAsync(['-c', 'general', '--since', 'invalid-date'], { from: 'user' })
|
|
155
|
+
).rejects.toThrow();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should show error for invalid message count', async () => {
|
|
159
|
+
const historyCommand = setupHistoryCommand();
|
|
160
|
+
historyCommand.exitOverride();
|
|
161
|
+
|
|
162
|
+
await expect(
|
|
163
|
+
historyCommand.parseAsync(['-c', 'general', '-n', '-5'], { from: 'user' })
|
|
164
|
+
).rejects.toThrow();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle Slack API errors', async () => {
|
|
168
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
169
|
+
token: 'test-token',
|
|
170
|
+
updatedAt: new Date().toISOString()
|
|
171
|
+
});
|
|
172
|
+
vi.mocked(mockSlackClient.getHistory).mockRejectedValue(new Error('channel_not_found'));
|
|
173
|
+
|
|
174
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'nonexistent']);
|
|
175
|
+
|
|
176
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
177
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('output formatting', () => {
|
|
182
|
+
it('should format messages with user names and timestamps', async () => {
|
|
183
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
184
|
+
token: 'test-token',
|
|
185
|
+
updatedAt: new Date().toISOString()
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const mockMessages = [
|
|
189
|
+
{
|
|
190
|
+
type: 'message',
|
|
191
|
+
text: 'Hello world',
|
|
192
|
+
user: 'U123456',
|
|
193
|
+
ts: '1609459200.000100',
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
198
|
+
messages: mockMessages,
|
|
199
|
+
users: new Map([['U123456', 'john.doe']])
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
203
|
+
|
|
204
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('john.doe'));
|
|
205
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
206
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('✓ Displayed 1 message(s)'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle messages without user info gracefully', async () => {
|
|
210
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
211
|
+
token: 'test-token',
|
|
212
|
+
updatedAt: new Date().toISOString()
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const mockMessages = [
|
|
216
|
+
{
|
|
217
|
+
type: 'message',
|
|
218
|
+
text: 'Bot message',
|
|
219
|
+
bot_id: 'B123456',
|
|
220
|
+
ts: '1609459200.000100',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
225
|
+
messages: mockMessages,
|
|
226
|
+
users: new Map()
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
230
|
+
|
|
231
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Bot'));
|
|
232
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Bot message'));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should show message when no messages found', async () => {
|
|
236
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
237
|
+
token: 'test-token',
|
|
238
|
+
updatedAt: new Date().toISOString()
|
|
239
|
+
});
|
|
240
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
241
|
+
messages: [],
|
|
242
|
+
users: new Map()
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
246
|
+
|
|
247
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('No messages found'));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { setupSendCommand } from '../../src/commands/send';
|
|
3
|
+
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
4
|
+
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
5
|
+
import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
|
|
6
|
+
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '../../src/utils/constants';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
|
|
9
|
+
vi.mock('../../src/utils/slack-api-client');
|
|
10
|
+
vi.mock('../../src/utils/profile-config');
|
|
11
|
+
vi.mock('fs/promises');
|
|
12
|
+
|
|
13
|
+
describe('send command', () => {
|
|
14
|
+
let program: any;
|
|
15
|
+
let mockSlackClient: SlackApiClient;
|
|
16
|
+
let mockConfigManager: ProfileConfigManager;
|
|
17
|
+
let mockConsole: any;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
|
|
22
|
+
mockConfigManager = new ProfileConfigManager();
|
|
23
|
+
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
24
|
+
|
|
25
|
+
mockSlackClient = new SlackApiClient('test-token');
|
|
26
|
+
vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
|
|
27
|
+
|
|
28
|
+
mockConsole = setupMockConsole();
|
|
29
|
+
program = createTestProgram();
|
|
30
|
+
program.addCommand(setupSendCommand());
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
restoreMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('send message with -m option', () => {
|
|
38
|
+
it('should send a message to specified channel', async () => {
|
|
39
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
40
|
+
token: 'test-token',
|
|
41
|
+
updatedAt: new Date().toISOString()
|
|
42
|
+
});
|
|
43
|
+
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
44
|
+
ok: true,
|
|
45
|
+
ts: '1234567890.123456'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello, World!']);
|
|
49
|
+
|
|
50
|
+
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', 'Hello, World!');
|
|
51
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general')));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should use specified profile', async () => {
|
|
55
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
56
|
+
token: 'work-token',
|
|
57
|
+
updatedAt: new Date().toISOString()
|
|
58
|
+
});
|
|
59
|
+
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
60
|
+
ok: true,
|
|
61
|
+
ts: '1234567890.123456'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello', '--profile', 'work']);
|
|
65
|
+
|
|
66
|
+
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
67
|
+
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('send message with -f option', () => {
|
|
72
|
+
it('should send message from file', async () => {
|
|
73
|
+
const fileContent = 'Message from file\nLine 2';
|
|
74
|
+
vi.mocked(fs.readFile).mockResolvedValue(fileContent);
|
|
75
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
76
|
+
token: 'test-token',
|
|
77
|
+
updatedAt: new Date().toISOString()
|
|
78
|
+
});
|
|
79
|
+
vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
|
|
80
|
+
ok: true,
|
|
81
|
+
ts: '1234567890.123456'
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'message.txt']);
|
|
85
|
+
|
|
86
|
+
expect(fs.readFile).toHaveBeenCalledWith('message.txt', 'utf-8');
|
|
87
|
+
expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', fileContent);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('validation', () => {
|
|
92
|
+
it('should fail when no message or file is provided', async () => {
|
|
93
|
+
const sendCommand = setupSendCommand();
|
|
94
|
+
sendCommand.exitOverride();
|
|
95
|
+
|
|
96
|
+
await expect(
|
|
97
|
+
sendCommand.parseAsync(['-c', 'general'], { from: 'user' })
|
|
98
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should fail when both message and file are provided', async () => {
|
|
102
|
+
const sendCommand = setupSendCommand();
|
|
103
|
+
sendCommand.exitOverride();
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '-f', 'file.txt'], { from: 'user' })
|
|
107
|
+
).rejects.toThrow(`Error: ${ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE}`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should fail when no channel is provided', async () => {
|
|
111
|
+
const sendCommand = setupSendCommand();
|
|
112
|
+
sendCommand.exitOverride();
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
sendCommand.parseAsync(['-m', 'Hello'], { from: 'user' })
|
|
116
|
+
).rejects.toThrow();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('error handling', () => {
|
|
121
|
+
it('should handle missing configuration', async () => {
|
|
122
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
|
|
123
|
+
|
|
124
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello']);
|
|
125
|
+
|
|
126
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
127
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should handle Slack API errors', async () => {
|
|
131
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
132
|
+
token: 'test-token',
|
|
133
|
+
updatedAt: new Date().toISOString()
|
|
134
|
+
});
|
|
135
|
+
vi.mocked(mockSlackClient.sendMessage).mockRejectedValue(new Error('channel_not_found'));
|
|
136
|
+
|
|
137
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'nonexistent', '-m', 'Hello']);
|
|
138
|
+
|
|
139
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
140
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should handle file read errors', async () => {
|
|
144
|
+
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT: no such file'));
|
|
145
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
146
|
+
token: 'test-token',
|
|
147
|
+
updatedAt: new Date().toISOString()
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'nonexistent.txt']);
|
|
151
|
+
|
|
152
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
153
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { setupUnreadCommand } from '../../src/commands/unread';
|
|
3
|
+
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
4
|
+
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
5
|
+
import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
|
|
6
|
+
import { ERROR_MESSAGES } from '../../src/utils/constants';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
vi.mock('../../src/utils/slack-api-client');
|
|
10
|
+
vi.mock('../../src/utils/profile-config');
|
|
11
|
+
|
|
12
|
+
describe('unread command', () => {
|
|
13
|
+
let program: any;
|
|
14
|
+
let mockSlackClient: SlackApiClient;
|
|
15
|
+
let mockConfigManager: ProfileConfigManager;
|
|
16
|
+
let mockConsole: any;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
|
|
21
|
+
mockConfigManager = new ProfileConfigManager();
|
|
22
|
+
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
23
|
+
|
|
24
|
+
mockSlackClient = new SlackApiClient('test-token');
|
|
25
|
+
vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
|
|
26
|
+
|
|
27
|
+
mockConsole = setupMockConsole();
|
|
28
|
+
program = createTestProgram();
|
|
29
|
+
program.addCommand(setupUnreadCommand());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
restoreMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mockChannelsWithUnread = [
|
|
37
|
+
{
|
|
38
|
+
id: 'C123',
|
|
39
|
+
name: 'general',
|
|
40
|
+
is_channel: true,
|
|
41
|
+
is_member: true,
|
|
42
|
+
is_archived: false,
|
|
43
|
+
unread_count: 5,
|
|
44
|
+
unread_count_display: 5,
|
|
45
|
+
last_read: '1705286300.000000',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'C456',
|
|
49
|
+
name: 'random',
|
|
50
|
+
is_channel: true,
|
|
51
|
+
is_member: true,
|
|
52
|
+
is_archived: false,
|
|
53
|
+
unread_count: 2,
|
|
54
|
+
unread_count_display: 2,
|
|
55
|
+
last_read: '1705286400.000000',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'C789',
|
|
59
|
+
name: 'dev',
|
|
60
|
+
is_channel: true,
|
|
61
|
+
is_member: true,
|
|
62
|
+
is_archived: false,
|
|
63
|
+
unread_count: 0,
|
|
64
|
+
unread_count_display: 0,
|
|
65
|
+
last_read: '1705286500.000000',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const mockUnreadMessages = [
|
|
70
|
+
{
|
|
71
|
+
ts: '1705286400.000001',
|
|
72
|
+
user: 'U123',
|
|
73
|
+
text: 'Hello world',
|
|
74
|
+
type: 'message',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
ts: '1705286500.000002',
|
|
78
|
+
user: 'U456',
|
|
79
|
+
text: 'Test message',
|
|
80
|
+
type: 'message',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
describe('basic functionality', () => {
|
|
85
|
+
it('should display unread counts in table format by default', async () => {
|
|
86
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
87
|
+
token: 'test-token',
|
|
88
|
+
updatedAt: new Date().toISOString()
|
|
89
|
+
});
|
|
90
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue(
|
|
91
|
+
mockChannelsWithUnread.filter(ch => (ch.unread_count_display || 0) > 0)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
await program.parseAsync(['node', 'slack-cli', 'unread']);
|
|
95
|
+
|
|
96
|
+
expect(mockSlackClient.listUnreadChannels).toHaveBeenCalled();
|
|
97
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Channel'));
|
|
98
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Unread'));
|
|
99
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('#general'));
|
|
100
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('5'));
|
|
101
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('#random'));
|
|
102
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('2'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should display only count when --count-only is specified', async () => {
|
|
106
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
107
|
+
token: 'test-token',
|
|
108
|
+
updatedAt: new Date().toISOString()
|
|
109
|
+
});
|
|
110
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue(
|
|
111
|
+
mockChannelsWithUnread.filter(ch => (ch.unread_count_display || 0) > 0)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--count-only']);
|
|
115
|
+
|
|
116
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('#general: 5');
|
|
117
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('#random: 2');
|
|
118
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.bold('Total: 7 unread messages'));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should display in JSON format when specified', async () => {
|
|
122
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
123
|
+
token: 'test-token',
|
|
124
|
+
updatedAt: new Date().toISOString()
|
|
125
|
+
});
|
|
126
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue(
|
|
127
|
+
mockChannelsWithUnread.filter(ch => (ch.unread_count_display || 0) > 0)
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--format', 'json']);
|
|
131
|
+
|
|
132
|
+
const expectedOutput = [
|
|
133
|
+
{ channel: '#general', channelId: 'C123', unreadCount: 5 },
|
|
134
|
+
{ channel: '#random', channelId: 'C456', unreadCount: 2 },
|
|
135
|
+
];
|
|
136
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(JSON.stringify(expectedOutput, null, 2));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should display in simple format when specified', async () => {
|
|
140
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
141
|
+
token: 'test-token',
|
|
142
|
+
updatedAt: new Date().toISOString()
|
|
143
|
+
});
|
|
144
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue(
|
|
145
|
+
mockChannelsWithUnread.filter(ch => (ch.unread_count_display || 0) > 0)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--format', 'simple']);
|
|
149
|
+
|
|
150
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('#general (5)');
|
|
151
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('#random (2)');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('channel filtering', () => {
|
|
156
|
+
it('should filter by specific channel when --channel is specified', async () => {
|
|
157
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
158
|
+
token: 'test-token',
|
|
159
|
+
updatedAt: new Date().toISOString()
|
|
160
|
+
});
|
|
161
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
162
|
+
channel: mockChannelsWithUnread[0],
|
|
163
|
+
messages: mockUnreadMessages,
|
|
164
|
+
users: new Map([
|
|
165
|
+
['U123', 'john.doe'],
|
|
166
|
+
['U456', 'jane.smith']
|
|
167
|
+
])
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'general']);
|
|
171
|
+
|
|
172
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('general');
|
|
173
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.bold(`#general: 5 unread messages`));
|
|
174
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
175
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Test message'));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('limit option', () => {
|
|
180
|
+
it('should limit the number of channels displayed', async () => {
|
|
181
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
182
|
+
token: 'test-token',
|
|
183
|
+
updatedAt: new Date().toISOString()
|
|
184
|
+
});
|
|
185
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue(
|
|
186
|
+
mockChannelsWithUnread.filter(ch => (ch.unread_count_display || 0) > 0)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--limit', '1']);
|
|
190
|
+
|
|
191
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('#general'));
|
|
192
|
+
expect(mockConsole.logSpy).not.toHaveBeenCalledWith(expect.stringContaining('#random'));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('error handling', () => {
|
|
197
|
+
it('should display message when no unread messages found', async () => {
|
|
198
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
199
|
+
token: 'test-token',
|
|
200
|
+
updatedAt: new Date().toISOString()
|
|
201
|
+
});
|
|
202
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue([]);
|
|
203
|
+
|
|
204
|
+
await program.parseAsync(['node', 'slack-cli', 'unread']);
|
|
205
|
+
|
|
206
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.green('✓ No unread messages'));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should handle channel not found error', async () => {
|
|
210
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
211
|
+
token: 'test-token',
|
|
212
|
+
updatedAt: new Date().toISOString()
|
|
213
|
+
});
|
|
214
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockRejectedValue(
|
|
215
|
+
new Error('channel_not_found')
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'nonexistent']);
|
|
219
|
+
|
|
220
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
221
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle missing configuration', async () => {
|
|
225
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
|
|
226
|
+
|
|
227
|
+
await program.parseAsync(['node', 'slack-cli', 'unread']);
|
|
228
|
+
|
|
229
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
230
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('profile option', () => {
|
|
235
|
+
it('should use specified profile', async () => {
|
|
236
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
237
|
+
token: 'work-token',
|
|
238
|
+
updatedAt: new Date().toISOString()
|
|
239
|
+
});
|
|
240
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockResolvedValue([]);
|
|
241
|
+
|
|
242
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--profile', 'work']);
|
|
243
|
+
|
|
244
|
+
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
245
|
+
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
export interface MockConsole {
|
|
5
|
+
logSpy: any;
|
|
6
|
+
errorSpy: any;
|
|
7
|
+
exitSpy: any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setupMockConsole(): MockConsole {
|
|
11
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
12
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
13
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
14
|
+
|
|
15
|
+
return { logSpy, errorSpy, exitSpy };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createTestProgram(): Command {
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program.exitOverride((err) => {
|
|
21
|
+
throw new Error('process.exit');
|
|
22
|
+
});
|
|
23
|
+
return program;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function restoreMocks(): void {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
}
|