@urugus/slack-cli 0.2.5 → 0.2.7
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 +2 -1
- package/README.md +7 -0
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +7 -7
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +7 -11
- package/dist/commands/send.js.map +1 -1
- package/dist/types/commands.d.ts +1 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +1 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/profile-config.d.ts.map +1 -1
- package/dist/utils/profile-config.js +2 -6
- package/dist/utils/profile-config.js.map +1 -1
- package/dist/utils/slack-api-client.d.ts +1 -1
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +2 -2
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/channel-operations.d.ts +9 -0
- package/dist/utils/slack-operations/channel-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/channel-operations.js +77 -50
- package/dist/utils/slack-operations/channel-operations.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +7 -3
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/token-utils.d.ts +7 -0
- package/dist/utils/token-utils.d.ts.map +1 -0
- package/dist/utils/token-utils.js +18 -0
- package/dist/utils/token-utils.js.map +1 -0
- package/dist/utils/validators.d.ts +79 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +175 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/history.ts +17 -12
- package/src/commands/send.ts +10 -11
- package/src/types/commands.ts +1 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/profile-config.ts +3 -15
- package/src/utils/slack-api-client.ts +6 -2
- package/src/utils/slack-operations/channel-operations.ts +91 -54
- package/src/utils/slack-operations/message-operations.ts +14 -4
- package/src/utils/token-utils.ts +17 -0
- package/src/utils/validators.ts +212 -0
- package/tests/commands/send.test.ts +69 -2
- package/tests/utils/option-parsers.test.ts +173 -0
- package/tests/utils/profile-config.test.ts +282 -0
- package/tests/utils/slack-operations/channel-operations-refactored.test.ts +179 -0
- package/tests/utils/token-utils.test.ts +33 -0
- package/tests/utils/validators.test.ts +307 -0
- package/src/utils/profile-config-refactored.ts +0 -161
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, 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
|
+
|
|
7
|
+
vi.mock('fs/promises');
|
|
8
|
+
vi.mock('os');
|
|
9
|
+
|
|
10
|
+
describe('ProfileConfigManager', () => {
|
|
11
|
+
let configManager: ProfileConfigManager;
|
|
12
|
+
const mockConfigPath = '/home/user/.slack-cli/config.json';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.resetAllMocks();
|
|
16
|
+
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
17
|
+
configManager = new ProfileConfigManager();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('setToken', () => {
|
|
21
|
+
it('should set token for default profile when no profile specified', async () => {
|
|
22
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
23
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
24
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
25
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
26
|
+
|
|
27
|
+
await configManager.setToken('test-token');
|
|
28
|
+
|
|
29
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
30
|
+
mockConfigPath,
|
|
31
|
+
expect.stringContaining('"default"'),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should set token for specified profile', async () => {
|
|
36
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
37
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
38
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
39
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
40
|
+
|
|
41
|
+
await configManager.setToken('test-token', 'production');
|
|
42
|
+
|
|
43
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
44
|
+
const savedData = JSON.parse(writeCall[1] as string);
|
|
45
|
+
expect(savedData.profiles.production.token).toBe('test-token');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getConfig', () => {
|
|
50
|
+
it('should return null when no config exists', async () => {
|
|
51
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
52
|
+
|
|
53
|
+
const config = await configManager.getConfig();
|
|
54
|
+
|
|
55
|
+
expect(config).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return config for default profile', async () => {
|
|
59
|
+
const mockStore = {
|
|
60
|
+
profiles: {
|
|
61
|
+
default: {
|
|
62
|
+
token: 'test-token',
|
|
63
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
defaultProfile: 'default',
|
|
67
|
+
};
|
|
68
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
69
|
+
|
|
70
|
+
const config = await configManager.getConfig();
|
|
71
|
+
|
|
72
|
+
expect(config).toEqual(mockStore.profiles.default);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return config for specified profile', async () => {
|
|
76
|
+
const mockStore = {
|
|
77
|
+
profiles: {
|
|
78
|
+
production: {
|
|
79
|
+
token: 'prod-token',
|
|
80
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
defaultProfile: 'default',
|
|
84
|
+
};
|
|
85
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
86
|
+
|
|
87
|
+
const config = await configManager.getConfig('production');
|
|
88
|
+
|
|
89
|
+
expect(config).toEqual(mockStore.profiles.production);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('listProfiles', () => {
|
|
94
|
+
it('should return empty array when no profiles exist', async () => {
|
|
95
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
96
|
+
|
|
97
|
+
const profiles = await configManager.listProfiles();
|
|
98
|
+
|
|
99
|
+
expect(profiles).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return all profiles with default flag', async () => {
|
|
103
|
+
const mockStore = {
|
|
104
|
+
profiles: {
|
|
105
|
+
default: {
|
|
106
|
+
token: 'default-token',
|
|
107
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
108
|
+
},
|
|
109
|
+
production: {
|
|
110
|
+
token: 'prod-token',
|
|
111
|
+
updatedAt: '2024-01-02T00:00:00.000Z',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
defaultProfile: 'default',
|
|
115
|
+
};
|
|
116
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
117
|
+
|
|
118
|
+
const profiles = await configManager.listProfiles();
|
|
119
|
+
|
|
120
|
+
expect(profiles).toHaveLength(2);
|
|
121
|
+
expect(profiles.find((p) => p.name === 'default')?.isDefault).toBe(true);
|
|
122
|
+
expect(profiles.find((p) => p.name === 'production')?.isDefault).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('useProfile', () => {
|
|
127
|
+
it('should switch to existing profile', async () => {
|
|
128
|
+
const mockStore = {
|
|
129
|
+
profiles: {
|
|
130
|
+
default: { token: 'default-token', updatedAt: '2024-01-01T00:00:00.000Z' },
|
|
131
|
+
production: { token: 'prod-token', updatedAt: '2024-01-02T00:00:00.000Z' },
|
|
132
|
+
},
|
|
133
|
+
defaultProfile: 'default',
|
|
134
|
+
};
|
|
135
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
136
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
137
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
138
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
139
|
+
|
|
140
|
+
await configManager.useProfile('production');
|
|
141
|
+
|
|
142
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
143
|
+
const savedData = JSON.parse(writeCall[1] as string);
|
|
144
|
+
expect(savedData.defaultProfile).toBe('production');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should throw error when profile does not exist', async () => {
|
|
148
|
+
const mockStore = {
|
|
149
|
+
profiles: {
|
|
150
|
+
default: { token: 'default-token', updatedAt: '2024-01-01T00:00:00.000Z' },
|
|
151
|
+
},
|
|
152
|
+
defaultProfile: 'default',
|
|
153
|
+
};
|
|
154
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
155
|
+
|
|
156
|
+
await expect(configManager.useProfile('nonexistent')).rejects.toThrow(
|
|
157
|
+
'Profile "nonexistent" does not exist',
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('getCurrentProfile', () => {
|
|
163
|
+
it('should return default profile when none set', async () => {
|
|
164
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce({ code: 'ENOENT' });
|
|
165
|
+
|
|
166
|
+
const profile = await configManager.getCurrentProfile();
|
|
167
|
+
|
|
168
|
+
expect(profile).toBe('default');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should return current profile', async () => {
|
|
172
|
+
const mockStore = {
|
|
173
|
+
profiles: {},
|
|
174
|
+
defaultProfile: 'production',
|
|
175
|
+
};
|
|
176
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
177
|
+
|
|
178
|
+
const profile = await configManager.getCurrentProfile();
|
|
179
|
+
|
|
180
|
+
expect(profile).toBe('production');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('clearConfig', () => {
|
|
185
|
+
it('should remove specified profile', async () => {
|
|
186
|
+
const mockStore = {
|
|
187
|
+
profiles: {
|
|
188
|
+
default: { token: 'default-token', updatedAt: '2024-01-01T00:00:00.000Z' },
|
|
189
|
+
production: { token: 'prod-token', updatedAt: '2024-01-02T00:00:00.000Z' },
|
|
190
|
+
},
|
|
191
|
+
defaultProfile: 'default',
|
|
192
|
+
};
|
|
193
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
194
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
195
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
196
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
197
|
+
|
|
198
|
+
await configManager.clearConfig('production');
|
|
199
|
+
|
|
200
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
201
|
+
const savedData = JSON.parse(writeCall[1] as string);
|
|
202
|
+
expect(savedData.profiles.production).toBeUndefined();
|
|
203
|
+
expect(savedData.profiles.default).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should delete config file when last profile removed', async () => {
|
|
207
|
+
const mockStore = {
|
|
208
|
+
profiles: {
|
|
209
|
+
default: { token: 'default-token', updatedAt: '2024-01-01T00:00:00.000Z' },
|
|
210
|
+
},
|
|
211
|
+
defaultProfile: 'default',
|
|
212
|
+
};
|
|
213
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
214
|
+
vi.mocked(fs.unlink).mockResolvedValue();
|
|
215
|
+
|
|
216
|
+
await configManager.clearConfig('default');
|
|
217
|
+
|
|
218
|
+
expect(fs.unlink).toHaveBeenCalledWith(mockConfigPath);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should set new default when current default is removed', async () => {
|
|
222
|
+
const mockStore = {
|
|
223
|
+
profiles: {
|
|
224
|
+
default: { token: 'default-token', updatedAt: '2024-01-01T00:00:00.000Z' },
|
|
225
|
+
production: { token: 'prod-token', updatedAt: '2024-01-02T00:00:00.000Z' },
|
|
226
|
+
},
|
|
227
|
+
defaultProfile: 'default',
|
|
228
|
+
};
|
|
229
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockStore));
|
|
230
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
231
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
232
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
233
|
+
|
|
234
|
+
await configManager.clearConfig('default');
|
|
235
|
+
|
|
236
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
237
|
+
const savedData = JSON.parse(writeCall[1] as string);
|
|
238
|
+
expect(savedData.defaultProfile).toBe('production');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('maskToken', () => {
|
|
243
|
+
it('should mask short tokens completely', () => {
|
|
244
|
+
const masked = configManager.maskToken('short');
|
|
245
|
+
expect(masked).toBe('****');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should mask long tokens showing prefix and suffix', () => {
|
|
249
|
+
const token = 'test-1234567890-abcdefghijklmnop';
|
|
250
|
+
const masked = configManager.maskToken(token);
|
|
251
|
+
expect(masked).toBe('test-****-****-mnop');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('migration', () => {
|
|
256
|
+
it('should migrate old format to new format', async () => {
|
|
257
|
+
const oldConfig = {
|
|
258
|
+
token: 'old-token',
|
|
259
|
+
updatedAt: '2024-01-01T00:00:00.000Z',
|
|
260
|
+
};
|
|
261
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(oldConfig));
|
|
262
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
263
|
+
vi.mocked(fs.writeFile).mockResolvedValue();
|
|
264
|
+
vi.mocked(fs.chmod).mockResolvedValue();
|
|
265
|
+
|
|
266
|
+
const config = await configManager.getConfig();
|
|
267
|
+
|
|
268
|
+
expect(config).toEqual(oldConfig);
|
|
269
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
270
|
+
const savedData = JSON.parse(writeCall[1] as string);
|
|
271
|
+
expect(savedData.profiles.default).toEqual(oldConfig);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('error handling', () => {
|
|
276
|
+
it('should throw error for invalid JSON', async () => {
|
|
277
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('invalid json');
|
|
278
|
+
|
|
279
|
+
await expect(configManager.getConfig()).rejects.toThrow('Invalid config file format');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ChannelOperations } from '../../../src/utils/slack-operations/channel-operations';
|
|
3
|
+
import { WebClient } from '@slack/web-api';
|
|
4
|
+
|
|
5
|
+
vi.mock('@slack/web-api');
|
|
6
|
+
|
|
7
|
+
describe('ChannelOperations - refactored listUnreadChannels', () => {
|
|
8
|
+
let channelOps: ChannelOperations;
|
|
9
|
+
let mockClient: any;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetAllMocks();
|
|
13
|
+
mockClient = {
|
|
14
|
+
conversations: {
|
|
15
|
+
list: vi.fn(),
|
|
16
|
+
info: vi.fn(),
|
|
17
|
+
history: vi.fn(),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
channelOps = new ChannelOperations(mockClient as WebClient);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('listUnreadChannels', () => {
|
|
24
|
+
it('should fetch unread channels with proper separation of concerns', async () => {
|
|
25
|
+
const mockChannels = [
|
|
26
|
+
{ id: 'C1', name: 'general' },
|
|
27
|
+
{ id: 'C2', name: 'random' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
mockClient.conversations.list.mockResolvedValueOnce({
|
|
31
|
+
channels: mockChannels,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Channel 1 - has unread messages
|
|
35
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
36
|
+
channel: { id: 'C1', last_read: '1234567890.000000' },
|
|
37
|
+
});
|
|
38
|
+
mockClient.conversations.history
|
|
39
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567900.000000' }] }) // latest message
|
|
40
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567900.000000' }, { ts: '1234567895.000000' }] }); // messages after last_read
|
|
41
|
+
|
|
42
|
+
// Channel 2 - no unread messages
|
|
43
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
44
|
+
channel: { id: 'C2', last_read: '1234567900.000000' },
|
|
45
|
+
});
|
|
46
|
+
mockClient.conversations.history
|
|
47
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567890.000000' }] }) // latest message
|
|
48
|
+
.mockResolvedValueOnce({ messages: [] }); // no messages after last_read
|
|
49
|
+
|
|
50
|
+
const result = await channelOps.listUnreadChannels();
|
|
51
|
+
|
|
52
|
+
expect(result).toHaveLength(1);
|
|
53
|
+
expect(result[0]).toMatchObject({
|
|
54
|
+
id: 'C1',
|
|
55
|
+
name: 'general',
|
|
56
|
+
unread_count: 2,
|
|
57
|
+
unread_count_display: 2,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Verify the separation of concerns - each method is called appropriately
|
|
61
|
+
expect(mockClient.conversations.list).toHaveBeenCalledOnce();
|
|
62
|
+
expect(mockClient.conversations.info).toHaveBeenCalledTimes(2);
|
|
63
|
+
expect(mockClient.conversations.history).toHaveBeenCalledTimes(4);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle channels with no last_read timestamp', async () => {
|
|
67
|
+
const mockChannels = [
|
|
68
|
+
{ id: 'C1', name: 'general' },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
mockClient.conversations.list.mockResolvedValueOnce({
|
|
72
|
+
channels: mockChannels,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
76
|
+
channel: { id: 'C1' }, // no last_read
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
mockClient.conversations.history
|
|
80
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567900.000000' }] }) // check if has messages
|
|
81
|
+
.mockResolvedValueOnce({
|
|
82
|
+
messages: [
|
|
83
|
+
{ ts: '1234567900.000000' },
|
|
84
|
+
{ ts: '1234567895.000000' },
|
|
85
|
+
{ ts: '1234567890.000000' },
|
|
86
|
+
]
|
|
87
|
+
}); // all messages are unread
|
|
88
|
+
|
|
89
|
+
const result = await channelOps.listUnreadChannels();
|
|
90
|
+
|
|
91
|
+
expect(result).toHaveLength(1);
|
|
92
|
+
expect(result[0]).toMatchObject({
|
|
93
|
+
id: 'C1',
|
|
94
|
+
name: 'general',
|
|
95
|
+
unread_count: 3,
|
|
96
|
+
unread_count_display: 3,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should skip channels with no messages', async () => {
|
|
101
|
+
const mockChannels = [
|
|
102
|
+
{ id: 'C1', name: 'general' },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
mockClient.conversations.list.mockResolvedValueOnce({
|
|
106
|
+
channels: mockChannels,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
110
|
+
channel: { id: 'C1', last_read: '1234567890.000000' },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
mockClient.conversations.history
|
|
114
|
+
.mockResolvedValueOnce({ messages: [] }); // no messages at all
|
|
115
|
+
|
|
116
|
+
const result = await channelOps.listUnreadChannels();
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle rate limit errors gracefully', async () => {
|
|
122
|
+
const mockChannels = [
|
|
123
|
+
{ id: 'C1', name: 'general' },
|
|
124
|
+
{ id: 'C2', name: 'random' },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
mockClient.conversations.list.mockResolvedValueOnce({
|
|
128
|
+
channels: mockChannels,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Channel 1 - rate limited
|
|
132
|
+
mockClient.conversations.info.mockRejectedValueOnce(new Error('rate_limited'));
|
|
133
|
+
|
|
134
|
+
// Channel 2 - successful
|
|
135
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
136
|
+
channel: { id: 'C2', last_read: '1234567890.000000' },
|
|
137
|
+
});
|
|
138
|
+
mockClient.conversations.history
|
|
139
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567900.000000' }] })
|
|
140
|
+
.mockResolvedValueOnce({ messages: [{ ts: '1234567900.000000' }] });
|
|
141
|
+
|
|
142
|
+
const result = await channelOps.listUnreadChannels();
|
|
143
|
+
|
|
144
|
+
expect(result).toHaveLength(1);
|
|
145
|
+
expect(result[0].id).toBe('C2');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('private methods (indirectly tested)', () => {
|
|
150
|
+
it('fetchAllChannels should handle large channel lists', async () => {
|
|
151
|
+
const mockChannels = [
|
|
152
|
+
{ id: 'C1', name: 'channel-1' },
|
|
153
|
+
{ id: 'C2', name: 'channel-2' },
|
|
154
|
+
{ id: 'C3', name: 'channel-3' },
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
mockClient.conversations.list.mockResolvedValueOnce({
|
|
158
|
+
channels: mockChannels,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Mock all channels having no unread messages for simplicity
|
|
162
|
+
mockChannels.forEach(() => {
|
|
163
|
+
mockClient.conversations.info.mockResolvedValueOnce({
|
|
164
|
+
channel: { last_read: '9999999999.000000' },
|
|
165
|
+
});
|
|
166
|
+
mockClient.conversations.history.mockResolvedValueOnce({ messages: [] });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await channelOps.listUnreadChannels();
|
|
170
|
+
|
|
171
|
+
expect(result).toHaveLength(0);
|
|
172
|
+
expect(mockClient.conversations.list).toHaveBeenCalledWith({
|
|
173
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
174
|
+
exclude_archived: true,
|
|
175
|
+
limit: 1000,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { maskToken } from '../../src/utils/token-utils';
|
|
3
|
+
|
|
4
|
+
describe('token-utils', () => {
|
|
5
|
+
describe('maskToken', () => {
|
|
6
|
+
it('should mask short tokens completely', () => {
|
|
7
|
+
expect(maskToken('short')).toBe('****');
|
|
8
|
+
expect(maskToken('123456789')).toBe('****');
|
|
9
|
+
expect(maskToken('')).toBe('****');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should mask long tokens showing prefix and suffix', () => {
|
|
13
|
+
const token = 'test-1234567890-abcdefghijklmnop';
|
|
14
|
+
expect(maskToken(token)).toBe('test-****-****-mnop');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should handle tokens of exactly minimum length + 1', () => {
|
|
18
|
+
const token = '1234567890'; // 10 characters
|
|
19
|
+
expect(maskToken(token)).toBe('1234-****-****-7890');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should handle various token formats', () => {
|
|
23
|
+
expect(maskToken('test-123456789012345')).toBe('test-****-****-2345');
|
|
24
|
+
expect(maskToken('demo-2-123456789012345')).toBe('demo-****-****-2345');
|
|
25
|
+
expect(maskToken('1234567890123456789012345')).toBe('1234-****-****-2345');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should handle tokens with special characters', () => {
|
|
29
|
+
expect(maskToken('test-token-with-dashes')).toBe('test-****-****-shes');
|
|
30
|
+
expect(maskToken('token_with_underscores')).toBe('toke-****-****-ores');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|