@urugus/slack-cli 0.2.11 → 0.2.13
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/README.md +4 -0
- package/dist/commands/history-display.d.ts +5 -1
- package/dist/commands/history-display.d.ts.map +1 -1
- package/dist/commands/history-display.js +3 -3
- package/dist/commands/history-display.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +28 -11
- package/dist/commands/history.js.map +1 -1
- package/dist/types/commands.d.ts +1 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/slack-api-client.d.ts +1 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +3 -0
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +1 -0
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +21 -0
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/package.json +5 -2
- package/.claude/settings.local.json +0 -75
- package/.github/dependabot.yml +0 -18
- package/.github/workflows/ci.yml +0 -70
- package/.github/workflows/pr-validation.yml +0 -41
- package/.prettierignore +0 -11
- package/.prettierrc +0 -10
- package/CHANGELOG.md +0 -61
- package/CLAUDE.md +0 -16
- package/eslint.config.js +0 -38
- package/src/commands/channels.ts +0 -50
- package/src/commands/config-subcommands.ts +0 -63
- package/src/commands/config.ts +0 -50
- package/src/commands/history-display.ts +0 -19
- package/src/commands/history-validators.ts +0 -46
- package/src/commands/history.ts +0 -61
- package/src/commands/scheduled.ts +0 -71
- package/src/commands/send.ts +0 -69
- package/src/commands/unread.ts +0 -122
- package/src/index.ts +0 -27
- package/src/types/commands.ts +0 -58
- package/src/types/config.ts +0 -20
- package/src/utils/channel-formatter.ts +0 -45
- package/src/utils/channel-resolver.ts +0 -82
- package/src/utils/client-factory.ts +0 -10
- package/src/utils/command-wrapper.ts +0 -27
- package/src/utils/config/config-file-manager.ts +0 -56
- package/src/utils/config/profile-manager.ts +0 -79
- package/src/utils/config/token-crypto-service.ts +0 -80
- package/src/utils/config-helper.ts +0 -21
- package/src/utils/constants.ts +0 -78
- package/src/utils/date-utils.ts +0 -8
- package/src/utils/error-utils.ts +0 -6
- package/src/utils/errors.ts +0 -33
- package/src/utils/format-utils.ts +0 -9
- package/src/utils/formatters/base-formatter.ts +0 -34
- package/src/utils/formatters/channel-formatters.ts +0 -71
- package/src/utils/formatters/channels-list-formatters.ts +0 -55
- package/src/utils/formatters/history-formatters.ts +0 -123
- package/src/utils/formatters/message-formatters.ts +0 -85
- package/src/utils/mention-utils.ts +0 -47
- package/src/utils/option-parsers.ts +0 -100
- package/src/utils/profile-config.ts +0 -161
- package/src/utils/schedule-utils.ts +0 -41
- package/src/utils/slack-api-client.ts +0 -135
- package/src/utils/slack-operations/base-client.ts +0 -30
- package/src/utils/slack-operations/channel-operations.ts +0 -161
- package/src/utils/slack-operations/index.ts +0 -3
- package/src/utils/slack-operations/message-operations.ts +0 -176
- package/src/utils/slack-patterns.ts +0 -9
- package/src/utils/token-utils.ts +0 -17
- package/src/utils/validators.ts +0 -263
- package/tests/commands/channels.test.ts +0 -250
- package/tests/commands/config.test.ts +0 -158
- package/tests/commands/history.test.ts +0 -403
- package/tests/commands/scheduled.test.ts +0 -131
- package/tests/commands/send.test.ts +0 -414
- package/tests/commands/unread.test.ts +0 -492
- package/tests/index.test.ts +0 -40
- package/tests/test-utils.ts +0 -28
- package/tests/utils/channel-resolver.test.ts +0 -161
- package/tests/utils/config/config-file-manager.test.ts +0 -118
- package/tests/utils/config/profile-manager.test.ts +0 -266
- package/tests/utils/config/token-crypto-service.test.ts +0 -98
- package/tests/utils/config.test.ts +0 -400
- package/tests/utils/date-utils.test.ts +0 -30
- package/tests/utils/error-utils.test.ts +0 -34
- package/tests/utils/format-utils.test.ts +0 -61
- package/tests/utils/mention-utils.test.ts +0 -100
- package/tests/utils/option-parsers.test.ts +0 -173
- package/tests/utils/profile-config.test.ts +0 -282
- package/tests/utils/schedule-utils.test.ts +0 -63
- package/tests/utils/slack-api-client.test.ts +0 -313
- package/tests/utils/slack-operations/channel-operations.test.ts +0 -248
- package/tests/utils/slack-operations/message-operations.test.ts +0 -163
- package/tests/utils/token-utils.test.ts +0 -33
- package/tests/utils/validators.test.ts +0 -307
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -27
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { ChannelResolver } from '../../src/utils/channel-resolver';
|
|
3
|
-
import { Channel } from '../../src/utils/slack-api-client';
|
|
4
|
-
|
|
5
|
-
describe('ChannelResolver', () => {
|
|
6
|
-
let resolver: ChannelResolver;
|
|
7
|
-
let mockChannels: Channel[];
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
resolver = new ChannelResolver();
|
|
11
|
-
mockChannels = [
|
|
12
|
-
{
|
|
13
|
-
id: 'C1234567890',
|
|
14
|
-
name: 'general',
|
|
15
|
-
is_private: false,
|
|
16
|
-
created: 1234567890,
|
|
17
|
-
is_member: true,
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
id: 'C0987654321',
|
|
21
|
-
name: 'random',
|
|
22
|
-
is_private: false,
|
|
23
|
-
created: 1234567890,
|
|
24
|
-
is_member: true,
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
id: 'C1111111111',
|
|
28
|
-
name: 'dev-team',
|
|
29
|
-
is_private: false,
|
|
30
|
-
created: 1234567890,
|
|
31
|
-
is_member: true,
|
|
32
|
-
name_normalized: 'dev-team',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: 'G1234567890',
|
|
36
|
-
name: 'private-channel',
|
|
37
|
-
is_private: true,
|
|
38
|
-
created: 1234567890,
|
|
39
|
-
is_member: true,
|
|
40
|
-
},
|
|
41
|
-
];
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('isChannelId', () => {
|
|
45
|
-
it('should identify channel IDs with valid format', () => {
|
|
46
|
-
expect(resolver.isChannelId('C1234567890')).toBe(true);
|
|
47
|
-
expect(resolver.isChannelId('D1234567890')).toBe(true);
|
|
48
|
-
expect(resolver.isChannelId('G1234567890')).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should return false for channel names or malformed IDs', () => {
|
|
52
|
-
expect(resolver.isChannelId('general')).toBe(false);
|
|
53
|
-
expect(resolver.isChannelId('#general')).toBe(false);
|
|
54
|
-
expect(resolver.isChannelId('General')).toBe(false);
|
|
55
|
-
expect(resolver.isChannelId('Dev')).toBe(false);
|
|
56
|
-
expect(resolver.isChannelId('C123')).toBe(false);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('findChannel', () => {
|
|
61
|
-
it('should find channel by exact name match', () => {
|
|
62
|
-
const result = resolver.findChannel('general', mockChannels);
|
|
63
|
-
expect(result).toEqual(mockChannels[0]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should find channel by name without # prefix', () => {
|
|
67
|
-
const result = resolver.findChannel('#general', mockChannels);
|
|
68
|
-
expect(result).toEqual(mockChannels[0]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should find channel by case-insensitive match', () => {
|
|
72
|
-
const result = resolver.findChannel('GENERAL', mockChannels);
|
|
73
|
-
expect(result).toEqual(mockChannels[0]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should find channel by normalized name', () => {
|
|
77
|
-
const result = resolver.findChannel('dev-team', mockChannels);
|
|
78
|
-
expect(result).toEqual(mockChannels[2]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should return undefined when channel not found', () => {
|
|
82
|
-
const result = resolver.findChannel('nonexistent', mockChannels);
|
|
83
|
-
expect(result).toBeUndefined();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('getSimilarChannels', () => {
|
|
88
|
-
it('should find similar channels by partial match', () => {
|
|
89
|
-
const result = resolver.getSimilarChannels('gen', mockChannels);
|
|
90
|
-
expect(result).toEqual(['general']);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should limit results to specified count', () => {
|
|
94
|
-
const manyChannels = [
|
|
95
|
-
...mockChannels,
|
|
96
|
-
{ id: 'C9999999999', name: 'general-2', is_private: false, created: 0 },
|
|
97
|
-
{ id: 'C8888888888', name: 'general-3', is_private: false, created: 0 },
|
|
98
|
-
];
|
|
99
|
-
const result = resolver.getSimilarChannels('general', manyChannels, 2);
|
|
100
|
-
expect(result).toHaveLength(2);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should return empty array when no similar channels found', () => {
|
|
104
|
-
const result = resolver.getSimilarChannels('xyz', mockChannels);
|
|
105
|
-
expect(result).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('resolveChannelError', () => {
|
|
110
|
-
it('should create error with suggestions when similar channels exist', () => {
|
|
111
|
-
const error = resolver.resolveChannelError('genera', mockChannels);
|
|
112
|
-
expect(error.message).toContain("Channel 'genera' not found");
|
|
113
|
-
expect(error.message).toContain('Did you mean one of these?');
|
|
114
|
-
expect(error.message).toContain('general');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should create error without suggestions when no similar channels', () => {
|
|
118
|
-
const error = resolver.resolveChannelError('xyz', mockChannels);
|
|
119
|
-
expect(error.message).toContain("Channel 'xyz' not found");
|
|
120
|
-
expect(error.message).toContain('Make sure you are a member of this channel');
|
|
121
|
-
expect(error.message).not.toContain('Did you mean');
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
describe('resolveChannelId', () => {
|
|
126
|
-
it('should return ID directly if already an ID', async () => {
|
|
127
|
-
const getChannelsFn = vi.fn();
|
|
128
|
-
const result = await resolver.resolveChannelId('C1234567890', getChannelsFn);
|
|
129
|
-
expect(result).toBe('C1234567890');
|
|
130
|
-
expect(getChannelsFn).not.toHaveBeenCalled();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('should resolve channel name to ID', async () => {
|
|
134
|
-
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
135
|
-
const result = await resolver.resolveChannelId('general', getChannelsFn);
|
|
136
|
-
expect(result).toBe('C1234567890');
|
|
137
|
-
expect(getChannelsFn).toHaveBeenCalled();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should resolve mixed-case channel names to ID', async () => {
|
|
141
|
-
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
142
|
-
const result = await resolver.resolveChannelId('General', getChannelsFn);
|
|
143
|
-
expect(result).toBe('C1234567890');
|
|
144
|
-
expect(getChannelsFn).toHaveBeenCalled();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should throw error when channel not found', async () => {
|
|
148
|
-
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
149
|
-
await expect(resolver.resolveChannelId('nonexistent', getChannelsFn)).rejects.toThrow(
|
|
150
|
-
"Channel 'nonexistent' not found"
|
|
151
|
-
);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should include suggestions in error when similar channels exist', async () => {
|
|
155
|
-
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
156
|
-
await expect(resolver.resolveChannelId('genera', getChannelsFn)).rejects.toThrow(
|
|
157
|
-
'Did you mean one of these? general'
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,266 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
});
|