@urugus/slack-cli 0.2.2 → 0.2.4
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/dist/commands/channels.d.ts.map +1 -1
- package/dist/commands/channels.js +7 -4
- package/dist/commands/channels.js.map +1 -1
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +17 -21
- package/dist/commands/unread.js.map +1 -1
- package/dist/utils/config/config-file-manager.d.ts +13 -0
- package/dist/utils/config/config-file-manager.d.ts.map +1 -0
- package/dist/utils/config/config-file-manager.js +85 -0
- package/dist/utils/config/config-file-manager.js.map +1 -0
- package/dist/utils/config/profile-manager.d.ts +16 -0
- package/dist/utils/config/profile-manager.d.ts.map +1 -0
- package/dist/utils/config/profile-manager.js +64 -0
- package/dist/utils/config/profile-manager.js.map +1 -0
- package/dist/utils/config/token-crypto-service.d.ts +11 -0
- package/dist/utils/config/token-crypto-service.d.ts.map +1 -0
- package/dist/utils/config/token-crypto-service.js +111 -0
- package/dist/utils/config/token-crypto-service.js.map +1 -0
- package/dist/utils/format-utils.d.ts.map +1 -1
- package/dist/utils/format-utils.js +2 -1
- package/dist/utils/format-utils.js.map +1 -1
- package/dist/utils/formatters/base-formatter.d.ts +23 -0
- package/dist/utils/formatters/base-formatter.d.ts.map +1 -0
- package/dist/utils/formatters/base-formatter.js +26 -0
- package/dist/utils/formatters/base-formatter.js.map +1 -0
- package/dist/utils/formatters/channel-formatters.d.ts +4 -13
- package/dist/utils/formatters/channel-formatters.d.ts.map +1 -1
- package/dist/utils/formatters/channel-formatters.js +18 -26
- package/dist/utils/formatters/channel-formatters.js.map +1 -1
- package/dist/utils/formatters/channels-list-formatters.d.ts +3 -10
- package/dist/utils/formatters/channels-list-formatters.d.ts.map +1 -1
- package/dist/utils/formatters/channels-list-formatters.js +15 -22
- package/dist/utils/formatters/channels-list-formatters.js.map +1 -1
- package/dist/utils/formatters/message-formatters.d.ts +9 -0
- package/dist/utils/formatters/message-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/message-formatters.js +72 -0
- package/dist/utils/formatters/message-formatters.js.map +1 -0
- package/dist/utils/mention-utils.d.ts +17 -0
- package/dist/utils/mention-utils.d.ts.map +1 -0
- package/dist/utils/mention-utils.js +45 -0
- package/dist/utils/mention-utils.js.map +1 -0
- package/dist/utils/option-parsers.d.ts +47 -0
- package/dist/utils/option-parsers.d.ts.map +1 -0
- package/dist/utils/option-parsers.js +75 -0
- package/dist/utils/option-parsers.js.map +1 -0
- package/dist/utils/profile-config-refactored.d.ts +20 -0
- package/dist/utils/profile-config-refactored.d.ts.map +1 -0
- package/dist/utils/profile-config-refactored.js +174 -0
- package/dist/utils/profile-config-refactored.js.map +1 -0
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +3 -2
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/slack-patterns.d.ts +6 -0
- package/dist/utils/slack-patterns.d.ts.map +1 -0
- package/dist/utils/slack-patterns.js +11 -0
- package/dist/utils/slack-patterns.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/channels.ts +7 -4
- package/src/commands/unread.ts +18 -21
- package/src/utils/config/config-file-manager.ts +56 -0
- package/src/utils/config/profile-manager.ts +79 -0
- package/src/utils/config/token-crypto-service.ts +80 -0
- package/src/utils/format-utils.ts +3 -1
- package/src/utils/formatters/base-formatter.ts +34 -0
- package/src/utils/formatters/channel-formatters.ts +25 -23
- package/src/utils/formatters/channels-list-formatters.ts +27 -31
- package/src/utils/formatters/message-formatters.ts +85 -0
- package/src/utils/mention-utils.ts +47 -0
- package/src/utils/option-parsers.ts +100 -0
- package/src/utils/profile-config-refactored.ts +161 -0
- package/src/utils/slack-operations/message-operations.ts +3 -2
- package/src/utils/slack-patterns.ts +9 -0
- package/tests/commands/unread.test.ts +112 -0
- package/tests/utils/config/config-file-manager.test.ts +118 -0
- package/tests/utils/config/profile-manager.test.ts +266 -0
- package/tests/utils/config/token-crypto-service.test.ts +98 -0
- package/tests/utils/mention-utils.test.ts +100 -0
- package/tests/utils/slack-operations/message-operations.test.ts +126 -0
|
@@ -174,6 +174,118 @@ describe('unread command', () => {
|
|
|
174
174
|
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
175
175
|
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Test message'));
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
it('should display channel unread in JSON format when specified', async () => {
|
|
179
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
180
|
+
token: 'test-token',
|
|
181
|
+
updatedAt: new Date().toISOString()
|
|
182
|
+
});
|
|
183
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
184
|
+
channel: mockChannelsWithUnread[0],
|
|
185
|
+
messages: mockUnreadMessages,
|
|
186
|
+
users: new Map([
|
|
187
|
+
['U123', 'john.doe'],
|
|
188
|
+
['U456', 'jane.smith']
|
|
189
|
+
])
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'general', '--format', 'json']);
|
|
193
|
+
|
|
194
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('general');
|
|
195
|
+
|
|
196
|
+
const logCall = mockConsole.logSpy.mock.calls[0][0];
|
|
197
|
+
const parsed = JSON.parse(logCall);
|
|
198
|
+
|
|
199
|
+
expect(parsed).toEqual({
|
|
200
|
+
channel: '#general',
|
|
201
|
+
channelId: 'C123',
|
|
202
|
+
unreadCount: 5,
|
|
203
|
+
messages: [
|
|
204
|
+
{
|
|
205
|
+
timestamp: expect.any(String),
|
|
206
|
+
author: 'john.doe',
|
|
207
|
+
text: 'Hello world'
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
timestamp: expect.any(String),
|
|
211
|
+
author: 'jane.smith',
|
|
212
|
+
text: 'Test message'
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should display channel unread in simple format when specified', async () => {
|
|
219
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
220
|
+
token: 'test-token',
|
|
221
|
+
updatedAt: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
224
|
+
channel: mockChannelsWithUnread[0],
|
|
225
|
+
messages: mockUnreadMessages,
|
|
226
|
+
users: new Map([
|
|
227
|
+
['U123', 'john.doe'],
|
|
228
|
+
['U456', 'jane.smith']
|
|
229
|
+
])
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'general', '--format', 'simple']);
|
|
233
|
+
|
|
234
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('general');
|
|
235
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('#general (5)');
|
|
236
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringMatching(/^\[.*\] john\.doe: Hello world$/));
|
|
237
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringMatching(/^\[.*\] jane\.smith: Test message$/));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should show only count for channel when --count-only is specified', async () => {
|
|
241
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
242
|
+
token: 'test-token',
|
|
243
|
+
updatedAt: new Date().toISOString()
|
|
244
|
+
});
|
|
245
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
246
|
+
channel: mockChannelsWithUnread[0],
|
|
247
|
+
messages: mockUnreadMessages,
|
|
248
|
+
users: new Map([
|
|
249
|
+
['U123', 'john.doe'],
|
|
250
|
+
['U456', 'jane.smith']
|
|
251
|
+
])
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'general', '--count-only']);
|
|
255
|
+
|
|
256
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('general');
|
|
257
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.bold(`#general: 5 unread messages`));
|
|
258
|
+
expect(mockConsole.logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
259
|
+
expect(mockConsole.logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Test message'));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should show count only in JSON format when both --count-only and --format json are specified', async () => {
|
|
263
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
264
|
+
token: 'test-token',
|
|
265
|
+
updatedAt: new Date().toISOString()
|
|
266
|
+
});
|
|
267
|
+
vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
|
|
268
|
+
channel: mockChannelsWithUnread[0],
|
|
269
|
+
messages: mockUnreadMessages,
|
|
270
|
+
users: new Map([
|
|
271
|
+
['U123', 'john.doe'],
|
|
272
|
+
['U456', 'jane.smith']
|
|
273
|
+
])
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'general', '--count-only', '--format', 'json']);
|
|
277
|
+
|
|
278
|
+
expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('general');
|
|
279
|
+
|
|
280
|
+
const logCall = mockConsole.logSpy.mock.calls[0][0];
|
|
281
|
+
const parsed = JSON.parse(logCall);
|
|
282
|
+
|
|
283
|
+
expect(parsed).toEqual({
|
|
284
|
+
channel: '#general',
|
|
285
|
+
channelId: 'C123',
|
|
286
|
+
unreadCount: 5
|
|
287
|
+
});
|
|
288
|
+
});
|
|
177
289
|
});
|
|
178
290
|
|
|
179
291
|
describe('limit option', () => {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { ConfigFileManager } from '../../../src/utils/config/config-file-manager';
|
|
6
|
+
|
|
7
|
+
vi.mock('fs/promises');
|
|
8
|
+
vi.mock('os');
|
|
9
|
+
|
|
10
|
+
describe('ConfigFileManager', () => {
|
|
11
|
+
let manager: ConfigFileManager;
|
|
12
|
+
const mockHomeDir = '/home/user';
|
|
13
|
+
const mockConfigPath = path.join(mockHomeDir, '.slack-cli', 'config.json');
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.mocked(os.homedir).mockReturnValue(mockHomeDir);
|
|
17
|
+
manager = new ConfigFileManager();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('read', () => {
|
|
25
|
+
it('should read and parse config file when it exists', async () => {
|
|
26
|
+
const mockConfig = {
|
|
27
|
+
profiles: { default: { token: 'encrypted-token' } },
|
|
28
|
+
currentProfile: 'default'
|
|
29
|
+
};
|
|
30
|
+
vi.mocked(fs.access).mockResolvedValueOnce(undefined);
|
|
31
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce(JSON.stringify(mockConfig));
|
|
32
|
+
|
|
33
|
+
const result = await manager.read();
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual(mockConfig);
|
|
36
|
+
expect(fs.readFile).toHaveBeenCalledWith(mockConfigPath, 'utf-8');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return default config when file does not exist', async () => {
|
|
40
|
+
vi.mocked(fs.access).mockRejectedValueOnce(new Error('ENOENT'));
|
|
41
|
+
|
|
42
|
+
const result = await manager.read();
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
profiles: {},
|
|
46
|
+
currentProfile: 'default'
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should throw error for invalid JSON', async () => {
|
|
51
|
+
vi.mocked(fs.access).mockResolvedValueOnce(undefined);
|
|
52
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('invalid json');
|
|
53
|
+
|
|
54
|
+
await expect(manager.read()).rejects.toThrow('Invalid configuration file');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('write', () => {
|
|
59
|
+
it('should create directory and write config file', async () => {
|
|
60
|
+
const mockConfig = {
|
|
61
|
+
profiles: { default: { token: 'encrypted-token' } },
|
|
62
|
+
currentProfile: 'default'
|
|
63
|
+
};
|
|
64
|
+
vi.mocked(fs.mkdir).mockResolvedValueOnce(undefined as any);
|
|
65
|
+
vi.mocked(fs.writeFile).mockResolvedValueOnce(undefined);
|
|
66
|
+
|
|
67
|
+
await manager.write(mockConfig);
|
|
68
|
+
|
|
69
|
+
expect(fs.mkdir).toHaveBeenCalledWith(
|
|
70
|
+
path.dirname(mockConfigPath),
|
|
71
|
+
{ recursive: true }
|
|
72
|
+
);
|
|
73
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
74
|
+
mockConfigPath,
|
|
75
|
+
JSON.stringify(mockConfig, null, 2),
|
|
76
|
+
'utf-8'
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle write errors', async () => {
|
|
81
|
+
const mockConfig = {
|
|
82
|
+
profiles: {},
|
|
83
|
+
currentProfile: 'default'
|
|
84
|
+
};
|
|
85
|
+
vi.mocked(fs.mkdir).mockResolvedValueOnce(undefined as any);
|
|
86
|
+
vi.mocked(fs.writeFile).mockRejectedValueOnce(new Error('Write failed'));
|
|
87
|
+
|
|
88
|
+
await expect(manager.write(mockConfig)).rejects.toThrow('Write failed');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('exists', () => {
|
|
93
|
+
it('should return true when config file exists', async () => {
|
|
94
|
+
vi.mocked(fs.access).mockResolvedValueOnce(undefined);
|
|
95
|
+
|
|
96
|
+
const result = await manager.exists();
|
|
97
|
+
|
|
98
|
+
expect(result).toBe(true);
|
|
99
|
+
expect(fs.access).toHaveBeenCalledWith(mockConfigPath);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return false when config file does not exist', async () => {
|
|
103
|
+
vi.mocked(fs.access).mockRejectedValueOnce(new Error('ENOENT'));
|
|
104
|
+
|
|
105
|
+
const result = await manager.exists();
|
|
106
|
+
|
|
107
|
+
expect(result).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getConfigPath', () => {
|
|
112
|
+
it('should return the correct config path', () => {
|
|
113
|
+
const result = manager.getConfigPath();
|
|
114
|
+
|
|
115
|
+
expect(result).toBe(mockConfigPath);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ProfileManager } from '../../../src/utils/config/profile-manager';
|
|
3
|
+
import { ConfigFileManager } from '../../../src/utils/config/config-file-manager';
|
|
4
|
+
import { TokenCryptoService } from '../../../src/utils/config/token-crypto-service';
|
|
5
|
+
import { Config } from '../../../src/types/config';
|
|
6
|
+
|
|
7
|
+
vi.mock('../../../src/utils/config/config-file-manager');
|
|
8
|
+
vi.mock('../../../src/utils/config/token-crypto-service');
|
|
9
|
+
|
|
10
|
+
describe('ProfileManager', () => {
|
|
11
|
+
let manager: ProfileManager;
|
|
12
|
+
let mockFileManager: ConfigFileManager;
|
|
13
|
+
let mockCryptoService: TokenCryptoService;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockFileManager = new ConfigFileManager();
|
|
17
|
+
mockCryptoService = new TokenCryptoService();
|
|
18
|
+
manager = new ProfileManager(mockFileManager, mockCryptoService);
|
|
19
|
+
|
|
20
|
+
vi.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('getProfile', () => {
|
|
24
|
+
it('should get and decrypt a profile token', async () => {
|
|
25
|
+
const mockConfig = {
|
|
26
|
+
profiles: {
|
|
27
|
+
test: { token: 'encrypted-token', updatedAt: '2024-01-01' }
|
|
28
|
+
},
|
|
29
|
+
currentProfile: 'test'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
33
|
+
vi.mocked(mockCryptoService.isEncrypted).mockReturnValueOnce(true);
|
|
34
|
+
vi.mocked(mockCryptoService.decrypt).mockReturnValueOnce('decrypted-token');
|
|
35
|
+
|
|
36
|
+
const result = await manager.getProfile('test');
|
|
37
|
+
|
|
38
|
+
expect(result).toEqual({
|
|
39
|
+
token: 'decrypted-token',
|
|
40
|
+
updatedAt: '2024-01-01'
|
|
41
|
+
});
|
|
42
|
+
expect(mockCryptoService.decrypt).toHaveBeenCalledWith('encrypted-token');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should throw error if profile does not exist', async () => {
|
|
46
|
+
const mockConfig = {
|
|
47
|
+
profiles: {},
|
|
48
|
+
currentProfile: 'default'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
52
|
+
|
|
53
|
+
await expect(manager.getProfile('nonexistent')).rejects.toThrow(
|
|
54
|
+
'Profile "nonexistent" not found'
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should handle already decrypted tokens', async () => {
|
|
59
|
+
const mockConfig = {
|
|
60
|
+
profiles: {
|
|
61
|
+
test: { token: 'plain-token', updatedAt: '2024-01-01' }
|
|
62
|
+
},
|
|
63
|
+
currentProfile: 'test'
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
67
|
+
vi.mocked(mockCryptoService.isEncrypted).mockReturnValueOnce(false);
|
|
68
|
+
|
|
69
|
+
const result = await manager.getProfile('test');
|
|
70
|
+
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
token: 'plain-token',
|
|
73
|
+
updatedAt: '2024-01-01'
|
|
74
|
+
});
|
|
75
|
+
expect(mockCryptoService.decrypt).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('setProfile', () => {
|
|
80
|
+
it('should set and encrypt a profile token', async () => {
|
|
81
|
+
const mockConfig = {
|
|
82
|
+
profiles: {},
|
|
83
|
+
currentProfile: 'default'
|
|
84
|
+
};
|
|
85
|
+
const newProfile: Config = {
|
|
86
|
+
token: 'new-token',
|
|
87
|
+
updatedAt: '2024-01-01'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
91
|
+
vi.mocked(mockCryptoService.encrypt).mockReturnValueOnce('encrypted-new-token');
|
|
92
|
+
vi.mocked(mockFileManager.write).mockResolvedValueOnce(undefined);
|
|
93
|
+
|
|
94
|
+
await manager.setProfile('test', newProfile);
|
|
95
|
+
|
|
96
|
+
expect(mockCryptoService.encrypt).toHaveBeenCalledWith('new-token');
|
|
97
|
+
expect(mockFileManager.write).toHaveBeenCalledWith({
|
|
98
|
+
profiles: {
|
|
99
|
+
test: { token: 'encrypted-new-token', updatedAt: '2024-01-01' }
|
|
100
|
+
},
|
|
101
|
+
currentProfile: 'default'
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should update existing profile', async () => {
|
|
106
|
+
const mockConfig = {
|
|
107
|
+
profiles: {
|
|
108
|
+
test: { token: 'old-encrypted', updatedAt: '2024-01-01' }
|
|
109
|
+
},
|
|
110
|
+
currentProfile: 'test'
|
|
111
|
+
};
|
|
112
|
+
const updatedProfile: Config = {
|
|
113
|
+
token: 'updated-token',
|
|
114
|
+
updatedAt: '2024-01-02'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
118
|
+
vi.mocked(mockCryptoService.encrypt).mockReturnValueOnce('encrypted-updated-token');
|
|
119
|
+
vi.mocked(mockFileManager.write).mockResolvedValueOnce(undefined);
|
|
120
|
+
|
|
121
|
+
await manager.setProfile('test', updatedProfile);
|
|
122
|
+
|
|
123
|
+
expect(mockFileManager.write).toHaveBeenCalledWith({
|
|
124
|
+
profiles: {
|
|
125
|
+
test: { token: 'encrypted-updated-token', updatedAt: '2024-01-02' }
|
|
126
|
+
},
|
|
127
|
+
currentProfile: 'test'
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('deleteProfile', () => {
|
|
133
|
+
it('should delete a profile', async () => {
|
|
134
|
+
const mockConfig = {
|
|
135
|
+
profiles: {
|
|
136
|
+
test: { token: 'encrypted-token', updatedAt: '2024-01-01' },
|
|
137
|
+
another: { token: 'another-token', updatedAt: '2024-01-01' }
|
|
138
|
+
},
|
|
139
|
+
currentProfile: 'test'
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
143
|
+
vi.mocked(mockFileManager.write).mockResolvedValueOnce(undefined);
|
|
144
|
+
|
|
145
|
+
await manager.deleteProfile('test');
|
|
146
|
+
|
|
147
|
+
expect(mockFileManager.write).toHaveBeenCalledWith({
|
|
148
|
+
profiles: {
|
|
149
|
+
another: { token: 'another-token', updatedAt: '2024-01-01' }
|
|
150
|
+
},
|
|
151
|
+
currentProfile: 'test'
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should throw error if trying to delete non-existent profile', async () => {
|
|
156
|
+
const mockConfig = {
|
|
157
|
+
profiles: {},
|
|
158
|
+
currentProfile: 'default'
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
162
|
+
|
|
163
|
+
await expect(manager.deleteProfile('nonexistent')).rejects.toThrow(
|
|
164
|
+
'Profile "nonexistent" not found'
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('listProfiles', () => {
|
|
170
|
+
it('should list all profile names', async () => {
|
|
171
|
+
const mockConfig = {
|
|
172
|
+
profiles: {
|
|
173
|
+
default: { token: 'token1', updatedAt: '2024-01-01' },
|
|
174
|
+
work: { token: 'token2', updatedAt: '2024-01-01' },
|
|
175
|
+
personal: { token: 'token3', updatedAt: '2024-01-01' }
|
|
176
|
+
},
|
|
177
|
+
currentProfile: 'default'
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
181
|
+
|
|
182
|
+
const result = await manager.listProfiles();
|
|
183
|
+
|
|
184
|
+
expect(result).toEqual(['default', 'work', 'personal']);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return empty array if no profiles', async () => {
|
|
188
|
+
const mockConfig = {
|
|
189
|
+
profiles: {},
|
|
190
|
+
currentProfile: 'default'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
194
|
+
|
|
195
|
+
const result = await manager.listProfiles();
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('getCurrentProfile', () => {
|
|
202
|
+
it('should return the current profile name', async () => {
|
|
203
|
+
const mockConfig = {
|
|
204
|
+
profiles: {
|
|
205
|
+
test: { token: 'token', updatedAt: '2024-01-01' }
|
|
206
|
+
},
|
|
207
|
+
currentProfile: 'test'
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
211
|
+
|
|
212
|
+
const result = await manager.getCurrentProfile();
|
|
213
|
+
|
|
214
|
+
expect(result).toBe('test');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should return default if no current profile set', async () => {
|
|
218
|
+
const mockConfig = {
|
|
219
|
+
profiles: {},
|
|
220
|
+
currentProfile: 'default'
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
224
|
+
|
|
225
|
+
const result = await manager.getCurrentProfile();
|
|
226
|
+
|
|
227
|
+
expect(result).toBe('default');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('setCurrentProfile', () => {
|
|
232
|
+
it('should set the current profile', async () => {
|
|
233
|
+
const mockConfig = {
|
|
234
|
+
profiles: {
|
|
235
|
+
test: { token: 'token', updatedAt: '2024-01-01' }
|
|
236
|
+
},
|
|
237
|
+
currentProfile: 'default'
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
241
|
+
vi.mocked(mockFileManager.write).mockResolvedValueOnce(undefined);
|
|
242
|
+
|
|
243
|
+
await manager.setCurrentProfile('test');
|
|
244
|
+
|
|
245
|
+
expect(mockFileManager.write).toHaveBeenCalledWith({
|
|
246
|
+
profiles: {
|
|
247
|
+
test: { token: 'token', updatedAt: '2024-01-01' }
|
|
248
|
+
},
|
|
249
|
+
currentProfile: 'test'
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should throw error if profile does not exist', async () => {
|
|
254
|
+
const mockConfig = {
|
|
255
|
+
profiles: {},
|
|
256
|
+
currentProfile: 'default'
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
vi.mocked(mockFileManager.read).mockResolvedValueOnce(mockConfig);
|
|
260
|
+
|
|
261
|
+
await expect(manager.setCurrentProfile('nonexistent')).rejects.toThrow(
|
|
262
|
+
'Profile "nonexistent" not found'
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { TokenCryptoService } from '../../../src/utils/config/token-crypto-service';
|
|
3
|
+
|
|
4
|
+
describe('TokenCryptoService', () => {
|
|
5
|
+
let service: TokenCryptoService;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
service = new TokenCryptoService();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('encrypt and decrypt', () => {
|
|
12
|
+
it('should encrypt and decrypt a token correctly', () => {
|
|
13
|
+
const originalToken = 'test-token-1234567890-abcdefghijklmnop';
|
|
14
|
+
|
|
15
|
+
const encrypted = service.encrypt(originalToken);
|
|
16
|
+
expect(encrypted).not.toBe(originalToken);
|
|
17
|
+
expect(encrypted.length).toBeGreaterThan(0);
|
|
18
|
+
|
|
19
|
+
const decrypted = service.decrypt(encrypted);
|
|
20
|
+
expect(decrypted).toBe(originalToken);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should produce different encrypted values for the same token', () => {
|
|
24
|
+
const token = 'test-token-1234567890-abcdefghijklmnop';
|
|
25
|
+
|
|
26
|
+
const encrypted1 = service.encrypt(token);
|
|
27
|
+
const encrypted2 = service.encrypt(token);
|
|
28
|
+
|
|
29
|
+
// Different encrypted values due to random IV
|
|
30
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
31
|
+
|
|
32
|
+
// But both decrypt to the same value
|
|
33
|
+
expect(service.decrypt(encrypted1)).toBe(token);
|
|
34
|
+
expect(service.decrypt(encrypted2)).toBe(token);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should handle empty token', () => {
|
|
38
|
+
const emptyToken = '';
|
|
39
|
+
|
|
40
|
+
const encrypted = service.encrypt(emptyToken);
|
|
41
|
+
expect(encrypted.length).toBeGreaterThan(0);
|
|
42
|
+
|
|
43
|
+
const decrypted = service.decrypt(encrypted);
|
|
44
|
+
expect(decrypted).toBe(emptyToken);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle special characters in token', () => {
|
|
48
|
+
const specialToken = 'test-!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
49
|
+
|
|
50
|
+
const encrypted = service.encrypt(specialToken);
|
|
51
|
+
const decrypted = service.decrypt(encrypted);
|
|
52
|
+
|
|
53
|
+
expect(decrypted).toBe(specialToken);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle very long tokens', () => {
|
|
57
|
+
const longToken = 'x'.repeat(1000);
|
|
58
|
+
|
|
59
|
+
const encrypted = service.encrypt(longToken);
|
|
60
|
+
const decrypted = service.decrypt(encrypted);
|
|
61
|
+
|
|
62
|
+
expect(decrypted).toBe(longToken);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('decrypt error handling', () => {
|
|
67
|
+
it('should throw error for invalid encrypted data', () => {
|
|
68
|
+
expect(() => service.decrypt('invalid-data')).toThrow('Failed to decrypt token');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should throw error for empty encrypted data', () => {
|
|
72
|
+
expect(() => service.decrypt('')).toThrow('Failed to decrypt token');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should throw error for malformed encrypted data', () => {
|
|
76
|
+
// Missing IV separator
|
|
77
|
+
expect(() => service.decrypt('aabbccdd')).toThrow('Failed to decrypt token');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('isEncrypted', () => {
|
|
82
|
+
it('should return true for encrypted tokens', () => {
|
|
83
|
+
const token = 'test-token-1234567890';
|
|
84
|
+
const encrypted = service.encrypt(token);
|
|
85
|
+
|
|
86
|
+
expect(service.isEncrypted(encrypted)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should return false for plain tokens', () => {
|
|
90
|
+
expect(service.isEncrypted('test-token-1234567890')).toBe(false);
|
|
91
|
+
expect(service.isEncrypted('plain-text')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return false for empty string', () => {
|
|
95
|
+
expect(service.isEncrypted('')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractUserIdsFromMentions, extractAllUserIds } from '../../src/utils/mention-utils';
|
|
3
|
+
|
|
4
|
+
describe('mention-utils', () => {
|
|
5
|
+
describe('extractUserIdsFromMentions', () => {
|
|
6
|
+
it('should extract single user ID from mention', () => {
|
|
7
|
+
const text = 'Hello <@U123456789>';
|
|
8
|
+
const userIds = extractUserIdsFromMentions(text);
|
|
9
|
+
expect(userIds).toEqual(['U123456789']);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should extract multiple user IDs from mentions', () => {
|
|
13
|
+
const text = 'Hey <@U123456789> and <@U987654321>, please check this';
|
|
14
|
+
const userIds = extractUserIdsFromMentions(text);
|
|
15
|
+
expect(userIds).toEqual(['U123456789', 'U987654321']);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should handle duplicate mentions', () => {
|
|
19
|
+
const text = '<@U123456789> mentioned <@U123456789> again';
|
|
20
|
+
const userIds = extractUserIdsFromMentions(text);
|
|
21
|
+
expect(userIds).toEqual(['U123456789', 'U123456789']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return empty array for text without mentions', () => {
|
|
25
|
+
const text = 'No mentions here';
|
|
26
|
+
const userIds = extractUserIdsFromMentions(text);
|
|
27
|
+
expect(userIds).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should handle empty text', () => {
|
|
31
|
+
const userIds = extractUserIdsFromMentions('');
|
|
32
|
+
expect(userIds).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should ignore malformed mentions', () => {
|
|
36
|
+
const text = 'Invalid <@> mention and <@lowercase> mention';
|
|
37
|
+
const userIds = extractUserIdsFromMentions(text);
|
|
38
|
+
expect(userIds).toEqual([]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('extractAllUserIds', () => {
|
|
43
|
+
it('should extract user IDs from message authors only', () => {
|
|
44
|
+
const messages = [
|
|
45
|
+
{ user: 'U111111111', text: 'Hello world' },
|
|
46
|
+
{ user: 'U222222222', text: 'Hi there' },
|
|
47
|
+
];
|
|
48
|
+
const userIds = extractAllUserIds(messages);
|
|
49
|
+
expect(userIds).toEqual(['U111111111', 'U222222222']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should extract user IDs from mentions only', () => {
|
|
53
|
+
const messages = [
|
|
54
|
+
{ text: 'Hello <@U333333333>' },
|
|
55
|
+
{ text: 'Hi <@U444444444>' },
|
|
56
|
+
];
|
|
57
|
+
const userIds = extractAllUserIds(messages);
|
|
58
|
+
expect(userIds).toEqual(['U333333333', 'U444444444']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should extract both authors and mentioned users', () => {
|
|
62
|
+
const messages = [
|
|
63
|
+
{ user: 'U111111111', text: 'Hello <@U222222222>' },
|
|
64
|
+
{ user: 'U333333333', text: 'Hi <@U444444444> and <@U555555555>' },
|
|
65
|
+
];
|
|
66
|
+
const userIds = extractAllUserIds(messages);
|
|
67
|
+
expect(userIds.sort()).toEqual([
|
|
68
|
+
'U111111111',
|
|
69
|
+
'U222222222',
|
|
70
|
+
'U333333333',
|
|
71
|
+
'U444444444',
|
|
72
|
+
'U555555555',
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should remove duplicate user IDs', () => {
|
|
77
|
+
const messages = [
|
|
78
|
+
{ user: 'U111111111', text: 'Hello <@U111111111>' },
|
|
79
|
+
{ user: 'U111111111', text: 'Another message' },
|
|
80
|
+
];
|
|
81
|
+
const userIds = extractAllUserIds(messages);
|
|
82
|
+
expect(userIds).toEqual(['U111111111']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle messages without user or text', () => {
|
|
86
|
+
const messages = [
|
|
87
|
+
{ user: 'U111111111' },
|
|
88
|
+
{ text: 'No user here' },
|
|
89
|
+
{},
|
|
90
|
+
];
|
|
91
|
+
const userIds = extractAllUserIds(messages);
|
|
92
|
+
expect(userIds).toEqual(['U111111111']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should handle empty messages array', () => {
|
|
96
|
+
const userIds = extractAllUserIds([]);
|
|
97
|
+
expect(userIds).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|