@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,400 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
6
|
+
import type { Config, ConfigStore } from '../../src/types/config';
|
|
7
|
+
|
|
8
|
+
vi.mock('fs/promises');
|
|
9
|
+
vi.mock('os');
|
|
10
|
+
|
|
11
|
+
describe('ProfileConfigManager', () => {
|
|
12
|
+
let configManager: ProfileConfigManager;
|
|
13
|
+
const mockHomeDir = '/mock/home';
|
|
14
|
+
const mockConfigPath = path.join(mockHomeDir, '.slack-cli', 'config.json');
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
vi.mocked(os.homedir).mockReturnValue(mockHomeDir);
|
|
19
|
+
configManager = new ProfileConfigManager();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('setToken', () => {
|
|
27
|
+
it('should save token to default profile when no profile specified and no default set', async () => {
|
|
28
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
29
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
30
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
31
|
+
vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
|
|
32
|
+
|
|
33
|
+
await configManager.setToken('test-token-123');
|
|
34
|
+
|
|
35
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
36
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
37
|
+
|
|
38
|
+
expect(writtenData.profiles.default).toBeDefined();
|
|
39
|
+
expect(writtenData.profiles.default.token).toBe('test-token-123');
|
|
40
|
+
expect(writtenData.defaultProfile).toBe('default');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should save token to current default profile when no profile specified', async () => {
|
|
44
|
+
const existingStore: ConfigStore = {
|
|
45
|
+
profiles: {
|
|
46
|
+
personal: {
|
|
47
|
+
token: 'personal-token',
|
|
48
|
+
updatedAt: '2025-01-01T00:00:00.000Z'
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
defaultProfile: 'personal'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingStore));
|
|
55
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
56
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
57
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
58
|
+
|
|
59
|
+
await configManager.setToken('updated-token-123');
|
|
60
|
+
|
|
61
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
62
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
63
|
+
|
|
64
|
+
expect(writtenData.profiles.personal.token).toBe('updated-token-123');
|
|
65
|
+
expect(writtenData.defaultProfile).toBe('personal');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should save token to specified profile', async () => {
|
|
69
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
70
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
71
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
72
|
+
vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
|
|
73
|
+
|
|
74
|
+
await configManager.setToken('work-token-123', 'work');
|
|
75
|
+
|
|
76
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
77
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
78
|
+
|
|
79
|
+
expect(writtenData.profiles.work).toBeDefined();
|
|
80
|
+
expect(writtenData.profiles.work.token).toBe('work-token-123');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should preserve existing profiles when adding new one', async () => {
|
|
84
|
+
const existingStore: ConfigStore = {
|
|
85
|
+
profiles: {
|
|
86
|
+
personal: {
|
|
87
|
+
token: 'personal-token',
|
|
88
|
+
updatedAt: '2025-01-01T00:00:00.000Z'
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
defaultProfile: 'personal'
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingStore));
|
|
95
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
96
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
97
|
+
|
|
98
|
+
await configManager.setToken('work-token-123', 'work');
|
|
99
|
+
|
|
100
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
101
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
102
|
+
|
|
103
|
+
expect(writtenData.profiles.personal).toBeDefined();
|
|
104
|
+
expect(writtenData.profiles.work).toBeDefined();
|
|
105
|
+
expect(writtenData.profiles.work.token).toBe('work-token-123');
|
|
106
|
+
expect(writtenData.defaultProfile).toBe('personal'); // default unchanged
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('getConfig', () => {
|
|
111
|
+
it('should return config from default profile', async () => {
|
|
112
|
+
const mockStore: ConfigStore = {
|
|
113
|
+
profiles: {
|
|
114
|
+
default: {
|
|
115
|
+
token: 'default-token',
|
|
116
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
117
|
+
},
|
|
118
|
+
work: {
|
|
119
|
+
token: 'work-token',
|
|
120
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
defaultProfile: 'default'
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
127
|
+
|
|
128
|
+
const config = await configManager.getConfig();
|
|
129
|
+
|
|
130
|
+
expect(config).toEqual(mockStore.profiles.default);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should return config from specified profile', async () => {
|
|
134
|
+
const mockStore: ConfigStore = {
|
|
135
|
+
profiles: {
|
|
136
|
+
default: {
|
|
137
|
+
token: 'default-token',
|
|
138
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
139
|
+
},
|
|
140
|
+
work: {
|
|
141
|
+
token: 'work-token',
|
|
142
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
defaultProfile: 'default'
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
149
|
+
|
|
150
|
+
const config = await configManager.getConfig('work');
|
|
151
|
+
|
|
152
|
+
expect(config).toEqual(mockStore.profiles.work);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should return null when profile does not exist', async () => {
|
|
156
|
+
const mockStore: ConfigStore = {
|
|
157
|
+
profiles: {
|
|
158
|
+
default: {
|
|
159
|
+
token: 'default-token',
|
|
160
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
defaultProfile: 'default'
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
167
|
+
|
|
168
|
+
const config = await configManager.getConfig('nonexistent');
|
|
169
|
+
|
|
170
|
+
expect(config).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('listProfiles', () => {
|
|
175
|
+
it('should return all profiles with current profile marked', async () => {
|
|
176
|
+
const mockStore: ConfigStore = {
|
|
177
|
+
profiles: {
|
|
178
|
+
default: {
|
|
179
|
+
token: 'default-token',
|
|
180
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
181
|
+
},
|
|
182
|
+
work: {
|
|
183
|
+
token: 'work-token',
|
|
184
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
185
|
+
},
|
|
186
|
+
personal: {
|
|
187
|
+
token: 'personal-token',
|
|
188
|
+
updatedAt: '2025-06-21T12:00:00.000Z'
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
defaultProfile: 'work'
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
195
|
+
|
|
196
|
+
const profiles = await configManager.listProfiles();
|
|
197
|
+
|
|
198
|
+
expect(profiles).toHaveLength(3);
|
|
199
|
+
expect(profiles.find(p => p.name === 'work')?.isDefault).toBe(true);
|
|
200
|
+
expect(profiles.find(p => p.name === 'default')?.isDefault).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should return empty array when no profiles exist', async () => {
|
|
204
|
+
vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' });
|
|
205
|
+
|
|
206
|
+
const profiles = await configManager.listProfiles();
|
|
207
|
+
|
|
208
|
+
expect(profiles).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('useProfile', () => {
|
|
213
|
+
it('should set default profile', async () => {
|
|
214
|
+
const mockStore: ConfigStore = {
|
|
215
|
+
profiles: {
|
|
216
|
+
default: {
|
|
217
|
+
token: 'default-token',
|
|
218
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
219
|
+
},
|
|
220
|
+
work: {
|
|
221
|
+
token: 'work-token',
|
|
222
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
defaultProfile: 'default'
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
229
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
230
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
231
|
+
|
|
232
|
+
await configManager.useProfile('work');
|
|
233
|
+
|
|
234
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
235
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
236
|
+
|
|
237
|
+
expect(writtenData.defaultProfile).toBe('work');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should throw error when profile does not exist', async () => {
|
|
241
|
+
const mockStore: ConfigStore = {
|
|
242
|
+
profiles: {
|
|
243
|
+
default: {
|
|
244
|
+
token: 'default-token',
|
|
245
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
defaultProfile: 'default'
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
252
|
+
|
|
253
|
+
await expect(configManager.useProfile('nonexistent')).rejects.toThrow('Profile "nonexistent" does not exist');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('getCurrentProfile', () => {
|
|
258
|
+
it('should return current default profile name', async () => {
|
|
259
|
+
const mockStore: ConfigStore = {
|
|
260
|
+
profiles: {
|
|
261
|
+
default: {
|
|
262
|
+
token: 'default-token',
|
|
263
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
264
|
+
},
|
|
265
|
+
work: {
|
|
266
|
+
token: 'work-token',
|
|
267
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
defaultProfile: 'work'
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
274
|
+
|
|
275
|
+
const currentProfile = await configManager.getCurrentProfile();
|
|
276
|
+
|
|
277
|
+
expect(currentProfile).toBe('work');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should return "default" when no default profile set', async () => {
|
|
281
|
+
const mockStore: ConfigStore = {
|
|
282
|
+
profiles: {
|
|
283
|
+
default: {
|
|
284
|
+
token: 'default-token',
|
|
285
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
291
|
+
|
|
292
|
+
const currentProfile = await configManager.getCurrentProfile();
|
|
293
|
+
|
|
294
|
+
expect(currentProfile).toBe('default');
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('clearConfig', () => {
|
|
299
|
+
it('should clear specific profile', async () => {
|
|
300
|
+
const mockStore: ConfigStore = {
|
|
301
|
+
profiles: {
|
|
302
|
+
default: {
|
|
303
|
+
token: 'default-token',
|
|
304
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
305
|
+
},
|
|
306
|
+
work: {
|
|
307
|
+
token: 'work-token',
|
|
308
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
defaultProfile: 'work'
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
315
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
316
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
317
|
+
|
|
318
|
+
await configManager.clearConfig('work');
|
|
319
|
+
|
|
320
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
321
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
322
|
+
|
|
323
|
+
expect(writtenData.profiles.work).toBeUndefined();
|
|
324
|
+
expect(writtenData.profiles.default).toBeDefined();
|
|
325
|
+
expect(writtenData.defaultProfile).toBe('default'); // fallback to default
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should clear default profile and reset defaultProfile', async () => {
|
|
329
|
+
const mockStore: ConfigStore = {
|
|
330
|
+
profiles: {
|
|
331
|
+
default: {
|
|
332
|
+
token: 'default-token',
|
|
333
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
334
|
+
},
|
|
335
|
+
work: {
|
|
336
|
+
token: 'work-token',
|
|
337
|
+
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
defaultProfile: 'default'
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
344
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
345
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
346
|
+
|
|
347
|
+
await configManager.clearConfig('default');
|
|
348
|
+
|
|
349
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
350
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
351
|
+
|
|
352
|
+
expect(writtenData.profiles.default).toBeUndefined();
|
|
353
|
+
expect(writtenData.profiles.work).toBeDefined();
|
|
354
|
+
expect(writtenData.defaultProfile).toBe('work'); // switch to remaining profile
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should delete config file when clearing last profile', async () => {
|
|
358
|
+
const mockStore: ConfigStore = {
|
|
359
|
+
profiles: {
|
|
360
|
+
default: {
|
|
361
|
+
token: 'default-token',
|
|
362
|
+
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
defaultProfile: 'default'
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockStore));
|
|
369
|
+
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
370
|
+
|
|
371
|
+
await configManager.clearConfig('default');
|
|
372
|
+
|
|
373
|
+
expect(fs.unlink).toHaveBeenCalledWith(mockConfigPath);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
describe('migration from old config', () => {
|
|
378
|
+
it('should automatically migrate old single-token config to profile format', async () => {
|
|
379
|
+
const oldConfig = {
|
|
380
|
+
token: 'old-token-123',
|
|
381
|
+
updatedAt: '2025-01-01T00:00:00.000Z'
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(oldConfig));
|
|
385
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
386
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
387
|
+
|
|
388
|
+
const config = await configManager.getConfig();
|
|
389
|
+
|
|
390
|
+
expect(config).toEqual(oldConfig);
|
|
391
|
+
|
|
392
|
+
// Verify migration happened
|
|
393
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
394
|
+
const writtenData = JSON.parse(writeCall[1] as string) as ConfigStore;
|
|
395
|
+
|
|
396
|
+
expect(writtenData.profiles.default).toEqual(oldConfig);
|
|
397
|
+
expect(writtenData.defaultProfile).toBe('default');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatUnixTimestamp, formatSlackTimestamp } from '../../src/utils/date-utils';
|
|
3
|
+
|
|
4
|
+
describe('date-utils', () => {
|
|
5
|
+
describe('formatUnixTimestamp', () => {
|
|
6
|
+
it('should format unix timestamp to ISO date string', () => {
|
|
7
|
+
const timestamp = 1640995200; // 2022-01-01 00:00:00 UTC
|
|
8
|
+
expect(formatUnixTimestamp(timestamp)).toBe('2022-01-01');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should handle different timestamps correctly', () => {
|
|
12
|
+
const timestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
|
13
|
+
expect(formatUnixTimestamp(timestamp)).toBe('2023-01-01');
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('formatSlackTimestamp', () => {
|
|
18
|
+
it('should format Slack timestamp to locale string', () => {
|
|
19
|
+
const slackTimestamp = '1640995200.000000';
|
|
20
|
+
const result = formatSlackTimestamp(slackTimestamp);
|
|
21
|
+
expect(result).toContain('2022');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle timestamps with milliseconds', () => {
|
|
25
|
+
const slackTimestamp = '1640995200.123456';
|
|
26
|
+
const result = formatSlackTimestamp(slackTimestamp);
|
|
27
|
+
expect(result).toContain('2022');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractErrorMessage } from '../../src/utils/error-utils';
|
|
3
|
+
|
|
4
|
+
describe('extractErrorMessage', () => {
|
|
5
|
+
it('should extract message from Error instance', () => {
|
|
6
|
+
const error = new Error('Test error message');
|
|
7
|
+
expect(extractErrorMessage(error)).toBe('Test error message');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should convert string to string', () => {
|
|
11
|
+
const error = 'String error';
|
|
12
|
+
expect(extractErrorMessage(error)).toBe('String error');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should convert number to string', () => {
|
|
16
|
+
const error = 404;
|
|
17
|
+
expect(extractErrorMessage(error)).toBe('404');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should convert object to string', () => {
|
|
21
|
+
const error = { code: 'ERROR_CODE', detail: 'Some detail' };
|
|
22
|
+
expect(extractErrorMessage(error)).toBe('[object Object]');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle null', () => {
|
|
26
|
+
const error = null;
|
|
27
|
+
expect(extractErrorMessage(error)).toBe('null');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle undefined', () => {
|
|
31
|
+
const error = undefined;
|
|
32
|
+
expect(extractErrorMessage(error)).toBe('undefined');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
3
|
+
import { WebClient } from '@slack/web-api';
|
|
4
|
+
|
|
5
|
+
vi.mock('@slack/web-api');
|
|
6
|
+
|
|
7
|
+
describe('SlackApiClient', () => {
|
|
8
|
+
let client: SlackApiClient;
|
|
9
|
+
let mockWebClient: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
mockWebClient = {
|
|
14
|
+
chat: {
|
|
15
|
+
postMessage: vi.fn()
|
|
16
|
+
},
|
|
17
|
+
conversations: {
|
|
18
|
+
list: vi.fn()
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
vi.mocked(WebClient).mockReturnValue(mockWebClient);
|
|
22
|
+
client = new SlackApiClient('test-token');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('constructor', () => {
|
|
26
|
+
it('should create WebClient with provided token', () => {
|
|
27
|
+
expect(WebClient).toHaveBeenCalledWith('test-token');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('sendMessage', () => {
|
|
32
|
+
it('should send message to channel', async () => {
|
|
33
|
+
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
34
|
+
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
35
|
+
|
|
36
|
+
const result = await client.sendMessage('general', 'Hello, World!');
|
|
37
|
+
|
|
38
|
+
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
39
|
+
channel: 'general',
|
|
40
|
+
text: 'Hello, World!'
|
|
41
|
+
});
|
|
42
|
+
expect(result).toEqual(mockResponse);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle channel ID format', async () => {
|
|
46
|
+
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
47
|
+
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
48
|
+
|
|
49
|
+
await client.sendMessage('C1234567890', 'Hello!');
|
|
50
|
+
|
|
51
|
+
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
52
|
+
channel: 'C1234567890',
|
|
53
|
+
text: 'Hello!'
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle multi-line messages', async () => {
|
|
58
|
+
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
59
|
+
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
60
|
+
|
|
61
|
+
const multiLineMessage = 'Line 1\nLine 2\nLine 3';
|
|
62
|
+
await client.sendMessage('general', multiLineMessage);
|
|
63
|
+
|
|
64
|
+
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
65
|
+
channel: 'general',
|
|
66
|
+
text: multiLineMessage
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should throw error on API failure', async () => {
|
|
71
|
+
const mockError = new Error('channel_not_found');
|
|
72
|
+
vi.mocked(mockWebClient.chat.postMessage).mockRejectedValue(mockError);
|
|
73
|
+
|
|
74
|
+
await expect(client.sendMessage('nonexistent', 'Hello')).rejects.toThrow('channel_not_found');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('listChannels', () => {
|
|
79
|
+
it('should list channels with default options', async () => {
|
|
80
|
+
const mockChannels = [
|
|
81
|
+
{
|
|
82
|
+
id: 'C1234567890',
|
|
83
|
+
name: 'general',
|
|
84
|
+
is_channel: true,
|
|
85
|
+
is_private: false,
|
|
86
|
+
num_members: 150,
|
|
87
|
+
created: 1579075200,
|
|
88
|
+
purpose: { value: 'Company announcements' }
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
92
|
+
ok: true,
|
|
93
|
+
channels: mockChannels
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
const result = await client.listChannels({
|
|
97
|
+
types: 'public_channel',
|
|
98
|
+
exclude_archived: true,
|
|
99
|
+
limit: 100
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
103
|
+
types: 'public_channel',
|
|
104
|
+
exclude_archived: true,
|
|
105
|
+
limit: 100
|
|
106
|
+
});
|
|
107
|
+
expect(result).toEqual(mockChannels);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle private channels', async () => {
|
|
111
|
+
const mockChannels = [
|
|
112
|
+
{
|
|
113
|
+
id: 'G1234567890',
|
|
114
|
+
name: 'private-channel',
|
|
115
|
+
is_group: true,
|
|
116
|
+
is_private: true,
|
|
117
|
+
num_members: 10,
|
|
118
|
+
created: 1579075200,
|
|
119
|
+
purpose: { value: 'Private discussions' }
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
123
|
+
ok: true,
|
|
124
|
+
channels: mockChannels
|
|
125
|
+
} as any);
|
|
126
|
+
|
|
127
|
+
const result = await client.listChannels({
|
|
128
|
+
types: 'private_channel',
|
|
129
|
+
exclude_archived: true,
|
|
130
|
+
limit: 50
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
134
|
+
types: 'private_channel',
|
|
135
|
+
exclude_archived: true,
|
|
136
|
+
limit: 50
|
|
137
|
+
});
|
|
138
|
+
expect(result).toEqual(mockChannels);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should handle multiple channel types', async () => {
|
|
142
|
+
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
143
|
+
ok: true,
|
|
144
|
+
channels: []
|
|
145
|
+
} as any);
|
|
146
|
+
|
|
147
|
+
await client.listChannels({
|
|
148
|
+
types: 'public_channel,private_channel,im',
|
|
149
|
+
exclude_archived: false,
|
|
150
|
+
limit: 200
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
154
|
+
types: 'public_channel,private_channel,im',
|
|
155
|
+
exclude_archived: false,
|
|
156
|
+
limit: 200
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle API errors', async () => {
|
|
161
|
+
vi.mocked(mockWebClient.conversations.list).mockRejectedValue(new Error('invalid_auth'));
|
|
162
|
+
|
|
163
|
+
await expect(client.listChannels({
|
|
164
|
+
types: 'public_channel',
|
|
165
|
+
exclude_archived: true,
|
|
166
|
+
limit: 100
|
|
167
|
+
})).rejects.toThrow('invalid_auth');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"noImplicitAny": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"moduleResolution": "node",
|
|
18
|
+
"allowSyntheticDefaultImports": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src/**/*"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
22
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
reporter: ['text', 'json', 'html'],
|
|
10
|
+
exclude: [
|
|
11
|
+
'node_modules/',
|
|
12
|
+
'dist/',
|
|
13
|
+
'**/*.d.ts',
|
|
14
|
+
'**/*.config.*',
|
|
15
|
+
'**/mockData/**',
|
|
16
|
+
'**/tests/**',
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
20
|
+
testTimeout: 10000,
|
|
21
|
+
},
|
|
22
|
+
resolve: {
|
|
23
|
+
alias: {
|
|
24
|
+
'@': '/src',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|