@urugus/slack-cli 0.2.2 → 0.2.3

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.
Files changed (59) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/dist/commands/channels.d.ts.map +1 -1
  3. package/dist/commands/channels.js +7 -4
  4. package/dist/commands/channels.js.map +1 -1
  5. package/dist/commands/unread.d.ts.map +1 -1
  6. package/dist/commands/unread.js +17 -21
  7. package/dist/commands/unread.js.map +1 -1
  8. package/dist/utils/config/config-file-manager.d.ts +13 -0
  9. package/dist/utils/config/config-file-manager.d.ts.map +1 -0
  10. package/dist/utils/config/config-file-manager.js +85 -0
  11. package/dist/utils/config/config-file-manager.js.map +1 -0
  12. package/dist/utils/config/profile-manager.d.ts +16 -0
  13. package/dist/utils/config/profile-manager.d.ts.map +1 -0
  14. package/dist/utils/config/profile-manager.js +64 -0
  15. package/dist/utils/config/profile-manager.js.map +1 -0
  16. package/dist/utils/config/token-crypto-service.d.ts +11 -0
  17. package/dist/utils/config/token-crypto-service.d.ts.map +1 -0
  18. package/dist/utils/config/token-crypto-service.js +111 -0
  19. package/dist/utils/config/token-crypto-service.js.map +1 -0
  20. package/dist/utils/formatters/base-formatter.d.ts +23 -0
  21. package/dist/utils/formatters/base-formatter.d.ts.map +1 -0
  22. package/dist/utils/formatters/base-formatter.js +26 -0
  23. package/dist/utils/formatters/base-formatter.js.map +1 -0
  24. package/dist/utils/formatters/channel-formatters.d.ts +4 -13
  25. package/dist/utils/formatters/channel-formatters.d.ts.map +1 -1
  26. package/dist/utils/formatters/channel-formatters.js +18 -26
  27. package/dist/utils/formatters/channel-formatters.js.map +1 -1
  28. package/dist/utils/formatters/channels-list-formatters.d.ts +3 -10
  29. package/dist/utils/formatters/channels-list-formatters.d.ts.map +1 -1
  30. package/dist/utils/formatters/channels-list-formatters.js +15 -22
  31. package/dist/utils/formatters/channels-list-formatters.js.map +1 -1
  32. package/dist/utils/formatters/message-formatters.d.ts +9 -0
  33. package/dist/utils/formatters/message-formatters.d.ts.map +1 -0
  34. package/dist/utils/formatters/message-formatters.js +72 -0
  35. package/dist/utils/formatters/message-formatters.js.map +1 -0
  36. package/dist/utils/option-parsers.d.ts +47 -0
  37. package/dist/utils/option-parsers.d.ts.map +1 -0
  38. package/dist/utils/option-parsers.js +75 -0
  39. package/dist/utils/option-parsers.js.map +1 -0
  40. package/dist/utils/profile-config-refactored.d.ts +20 -0
  41. package/dist/utils/profile-config-refactored.d.ts.map +1 -0
  42. package/dist/utils/profile-config-refactored.js +174 -0
  43. package/dist/utils/profile-config-refactored.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/commands/channels.ts +7 -4
  46. package/src/commands/unread.ts +18 -21
  47. package/src/utils/config/config-file-manager.ts +56 -0
  48. package/src/utils/config/profile-manager.ts +79 -0
  49. package/src/utils/config/token-crypto-service.ts +80 -0
  50. package/src/utils/formatters/base-formatter.ts +34 -0
  51. package/src/utils/formatters/channel-formatters.ts +25 -23
  52. package/src/utils/formatters/channels-list-formatters.ts +27 -31
  53. package/src/utils/formatters/message-formatters.ts +85 -0
  54. package/src/utils/option-parsers.ts +100 -0
  55. package/src/utils/profile-config-refactored.ts +161 -0
  56. package/tests/commands/unread.test.ts +112 -0
  57. package/tests/utils/config/config-file-manager.test.ts +118 -0
  58. package/tests/utils/config/profile-manager.test.ts +266 -0
  59. package/tests/utils/config/token-crypto-service.test.ts +98 -0
@@ -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
+ });