@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
package/src/utils/validators.ts
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { ERROR_MESSAGES } from './constants';
|
|
3
|
-
import { parseScheduledTimestamp } from './schedule-utils';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Common validation functions for CLI commands
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export interface ValidationRule<T = unknown> {
|
|
10
|
-
validate: (value: T) => boolean | string;
|
|
11
|
-
errorMessage?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ValidationOptions {
|
|
15
|
-
required?: boolean;
|
|
16
|
-
rules?: ValidationRule[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validates that a value exists (not undefined, null, or empty string)
|
|
21
|
-
*/
|
|
22
|
-
export function validateRequired(value: unknown, fieldName: string): string | null {
|
|
23
|
-
if (value === undefined || value === null || value === '') {
|
|
24
|
-
return `${fieldName} is required`;
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Validates mutually exclusive options
|
|
31
|
-
*/
|
|
32
|
-
export function validateMutuallyExclusive(
|
|
33
|
-
options: Record<string, unknown>,
|
|
34
|
-
fields: string[],
|
|
35
|
-
errorMessage?: string
|
|
36
|
-
): string | null {
|
|
37
|
-
const presentFields = fields.filter((field) => options[field] !== undefined);
|
|
38
|
-
if (presentFields.length > 1) {
|
|
39
|
-
return errorMessage || `Cannot use both ${presentFields.join(' and ')}`;
|
|
40
|
-
}
|
|
41
|
-
if (presentFields.length === 0) {
|
|
42
|
-
return errorMessage || `Must specify one of: ${fields.join(', ')}`;
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Validates string format using regex
|
|
49
|
-
*/
|
|
50
|
-
export function validateFormat(
|
|
51
|
-
value: string,
|
|
52
|
-
pattern: RegExp,
|
|
53
|
-
errorMessage: string
|
|
54
|
-
): string | null {
|
|
55
|
-
if (!pattern.test(value)) {
|
|
56
|
-
return errorMessage;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Validates numeric range
|
|
63
|
-
*/
|
|
64
|
-
export function validateRange(
|
|
65
|
-
value: number,
|
|
66
|
-
min?: number,
|
|
67
|
-
max?: number,
|
|
68
|
-
fieldName = 'Value'
|
|
69
|
-
): string | null {
|
|
70
|
-
if (min !== undefined && value < min) {
|
|
71
|
-
return `${fieldName} must be at least ${min}`;
|
|
72
|
-
}
|
|
73
|
-
if (max !== undefined && value > max) {
|
|
74
|
-
return `${fieldName} must be at most ${max}`;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Validates date format
|
|
81
|
-
*/
|
|
82
|
-
export function validateDateFormat(dateString: string): string | null {
|
|
83
|
-
const date = new Date(dateString);
|
|
84
|
-
if (isNaN(date.getTime())) {
|
|
85
|
-
return 'Invalid date format';
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Common format validators
|
|
92
|
-
*/
|
|
93
|
-
export const formatValidators = {
|
|
94
|
-
/**
|
|
95
|
-
* Validates Slack thread timestamp format (1234567890.123456)
|
|
96
|
-
*/
|
|
97
|
-
threadTimestamp: (value: string): string | null => {
|
|
98
|
-
const pattern = /^\d{10}\.\d{6}$/;
|
|
99
|
-
return validateFormat(value, pattern, ERROR_MESSAGES.INVALID_THREAD_TIMESTAMP);
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Validates Slack channel ID format (C1234567890, D1234567890, G1234567890)
|
|
104
|
-
*/
|
|
105
|
-
channelId: (value: string): string | null => {
|
|
106
|
-
const pattern = /^[CDG][A-Z0-9]{10,}$/;
|
|
107
|
-
return validateFormat(value, pattern, 'Invalid channel ID format');
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Validates output format options
|
|
112
|
-
*/
|
|
113
|
-
outputFormat: (value: string): string | null => {
|
|
114
|
-
const validFormats = ['table', 'simple', 'json', 'compact'];
|
|
115
|
-
if (!validFormats.includes(value)) {
|
|
116
|
-
return `Invalid format. Must be one of: ${validFormats.join(', ')}`;
|
|
117
|
-
}
|
|
118
|
-
return null;
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Creates a preAction hook for command validation
|
|
124
|
-
*/
|
|
125
|
-
export function createValidationHook(
|
|
126
|
-
validations: Array<(options: Record<string, unknown>, command: Command) => string | null>
|
|
127
|
-
): (thisCommand: Command) => void {
|
|
128
|
-
return (thisCommand: Command) => {
|
|
129
|
-
const options = thisCommand.opts();
|
|
130
|
-
|
|
131
|
-
for (const validation of validations) {
|
|
132
|
-
const error = validation(options, thisCommand);
|
|
133
|
-
if (error) {
|
|
134
|
-
thisCommand.error(`Error: ${error}`);
|
|
135
|
-
break; // Stop processing after first error
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Common command option validators
|
|
143
|
-
*/
|
|
144
|
-
export const optionValidators = {
|
|
145
|
-
/**
|
|
146
|
-
* Validates message/file options for send command
|
|
147
|
-
*/
|
|
148
|
-
messageOrFile: (options: Record<string, unknown>): string | null => {
|
|
149
|
-
if (!options.message && !options.file) {
|
|
150
|
-
return ERROR_MESSAGES.NO_MESSAGE_OR_FILE;
|
|
151
|
-
}
|
|
152
|
-
if (options.message && options.file) {
|
|
153
|
-
return ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE;
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Validates thread timestamp if provided
|
|
160
|
-
*/
|
|
161
|
-
threadTimestamp: (options: Record<string, unknown>): string | null => {
|
|
162
|
-
if (options.thread) {
|
|
163
|
-
return formatValidators.threadTimestamp(options.thread as string);
|
|
164
|
-
}
|
|
165
|
-
return null;
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Validates schedule options for send command
|
|
170
|
-
*/
|
|
171
|
-
scheduleTiming: (options: Record<string, unknown>): string | null => {
|
|
172
|
-
const at = options.at as string | undefined;
|
|
173
|
-
const after = options.after as string | undefined;
|
|
174
|
-
|
|
175
|
-
if (at && after) {
|
|
176
|
-
return ERROR_MESSAGES.BOTH_SCHEDULE_OPTIONS;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (at) {
|
|
180
|
-
const postAt = parseScheduledTimestamp(at);
|
|
181
|
-
if (postAt === null) {
|
|
182
|
-
return ERROR_MESSAGES.INVALID_SCHEDULE_AT;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (postAt <= Math.floor(Date.now() / 1000)) {
|
|
186
|
-
return ERROR_MESSAGES.SCHEDULE_TIME_IN_PAST;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (after) {
|
|
191
|
-
const trimmedAfter = after.trim();
|
|
192
|
-
if (!/^\d+$/.test(trimmedAfter)) {
|
|
193
|
-
return ERROR_MESSAGES.INVALID_SCHEDULE_AFTER;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const minutes = Number.parseInt(trimmedAfter, 10);
|
|
197
|
-
if (!Number.isSafeInteger(minutes) || minutes <= 0) {
|
|
198
|
-
return ERROR_MESSAGES.INVALID_SCHEDULE_AFTER;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return null;
|
|
203
|
-
},
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Validates message count for history command
|
|
207
|
-
*/
|
|
208
|
-
messageCount: (options: Record<string, unknown>): string | null => {
|
|
209
|
-
if (options.number) {
|
|
210
|
-
const count = parseInt(options.number as string, 10);
|
|
211
|
-
if (isNaN(count)) {
|
|
212
|
-
return 'Message count must be a number';
|
|
213
|
-
}
|
|
214
|
-
return validateRange(count, 1, 1000, 'Message count');
|
|
215
|
-
}
|
|
216
|
-
return null;
|
|
217
|
-
},
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Validates date format for history command
|
|
221
|
-
*/
|
|
222
|
-
sinceDate: (options: Record<string, unknown>): string | null => {
|
|
223
|
-
if (options.since) {
|
|
224
|
-
const error = validateDateFormat(options.since as string);
|
|
225
|
-
if (error) {
|
|
226
|
-
return 'Invalid date format. Use YYYY-MM-DD HH:MM:SS';
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
},
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Validates format option
|
|
234
|
-
*/
|
|
235
|
-
format: (options: Record<string, unknown>): string | null => {
|
|
236
|
-
if (options.format) {
|
|
237
|
-
const validFormats = ['table', 'simple', 'json'];
|
|
238
|
-
if (!validFormats.includes(options.format as string)) {
|
|
239
|
-
return `Invalid format '${options.format}'. Must be one of: ${validFormats.join(', ')}`;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return null;
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Creates a validated option parser
|
|
248
|
-
*/
|
|
249
|
-
export function createOptionParser<T>(
|
|
250
|
-
parser: (value: string | undefined, defaultValue: T) => T,
|
|
251
|
-
validator?: (value: T) => string | null
|
|
252
|
-
): (value: string | undefined, defaultValue: T) => T {
|
|
253
|
-
return (value: string | undefined, defaultValue: T): T => {
|
|
254
|
-
const parsed = parser(value, defaultValue);
|
|
255
|
-
if (validator) {
|
|
256
|
-
const error = validator(parsed);
|
|
257
|
-
if (error) {
|
|
258
|
-
throw new Error(error);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return parsed;
|
|
262
|
-
};
|
|
263
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { setupChannelsCommand } from '../../src/commands/channels';
|
|
3
|
-
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
4
|
-
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
5
|
-
import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
|
|
6
|
-
import { ERROR_MESSAGES } from '../../src/utils/constants';
|
|
7
|
-
|
|
8
|
-
vi.mock('../../src/utils/slack-api-client');
|
|
9
|
-
vi.mock('../../src/utils/profile-config');
|
|
10
|
-
|
|
11
|
-
describe('channels command', () => {
|
|
12
|
-
let program: any;
|
|
13
|
-
let mockSlackClient: SlackApiClient;
|
|
14
|
-
let mockConfigManager: ProfileConfigManager;
|
|
15
|
-
let mockConsole: any;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
vi.clearAllMocks();
|
|
19
|
-
|
|
20
|
-
mockConfigManager = new ProfileConfigManager();
|
|
21
|
-
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
22
|
-
|
|
23
|
-
mockSlackClient = new SlackApiClient('test-token');
|
|
24
|
-
vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
|
|
25
|
-
|
|
26
|
-
mockConsole = setupMockConsole();
|
|
27
|
-
program = createTestProgram();
|
|
28
|
-
program.addCommand(setupChannelsCommand());
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
restoreMocks();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const mockChannels = [
|
|
36
|
-
{
|
|
37
|
-
id: 'C1234567890',
|
|
38
|
-
name: 'general',
|
|
39
|
-
is_channel: true,
|
|
40
|
-
is_private: false,
|
|
41
|
-
num_members: 250,
|
|
42
|
-
created: 1579075200,
|
|
43
|
-
purpose: { value: 'Company announcements' }
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
id: 'C0987654321',
|
|
47
|
-
name: 'random',
|
|
48
|
-
is_channel: true,
|
|
49
|
-
is_private: false,
|
|
50
|
-
num_members: 145,
|
|
51
|
-
created: 1579075200,
|
|
52
|
-
purpose: { value: 'Random discussions' }
|
|
53
|
-
}
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
describe('basic functionality', () => {
|
|
57
|
-
it('should list public channels by default', async () => {
|
|
58
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
59
|
-
token: 'test-token',
|
|
60
|
-
updatedAt: new Date().toISOString()
|
|
61
|
-
});
|
|
62
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
63
|
-
|
|
64
|
-
await program.parseAsync(['node', 'slack-cli', 'channels']);
|
|
65
|
-
|
|
66
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
67
|
-
types: 'public_channel',
|
|
68
|
-
exclude_archived: true,
|
|
69
|
-
limit: 100
|
|
70
|
-
});
|
|
71
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('general'));
|
|
72
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('random'));
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should show error when no token is configured', async () => {
|
|
76
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(null);
|
|
77
|
-
vi.mocked(mockConfigManager.listProfiles).mockResolvedValue([]);
|
|
78
|
-
|
|
79
|
-
await program.parseAsync(['node', 'slack-cli', 'channels']);
|
|
80
|
-
|
|
81
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith('✗ Error:', ERROR_MESSAGES.NO_CONFIG('default'));
|
|
82
|
-
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('channel type filtering', () => {
|
|
87
|
-
it('should list private channels when type is private', async () => {
|
|
88
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
89
|
-
token: 'test-token',
|
|
90
|
-
updatedAt: new Date().toISOString()
|
|
91
|
-
});
|
|
92
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
93
|
-
|
|
94
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'private']);
|
|
95
|
-
|
|
96
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
97
|
-
types: 'private_channel',
|
|
98
|
-
exclude_archived: true,
|
|
99
|
-
limit: 100
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should list all channel types when type is all', async () => {
|
|
104
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
105
|
-
token: 'test-token',
|
|
106
|
-
updatedAt: new Date().toISOString()
|
|
107
|
-
});
|
|
108
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
109
|
-
|
|
110
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'all']);
|
|
111
|
-
|
|
112
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
113
|
-
types: 'public_channel,private_channel,mpim,im',
|
|
114
|
-
exclude_archived: true,
|
|
115
|
-
limit: 100
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should list direct messages when type is im', async () => {
|
|
120
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
121
|
-
token: 'test-token',
|
|
122
|
-
updatedAt: new Date().toISOString()
|
|
123
|
-
});
|
|
124
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
125
|
-
|
|
126
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--type', 'im']);
|
|
127
|
-
|
|
128
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
129
|
-
types: 'im',
|
|
130
|
-
exclude_archived: true,
|
|
131
|
-
limit: 100
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('output formatting', () => {
|
|
137
|
-
it('should output in table format by default', async () => {
|
|
138
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
139
|
-
token: 'test-token',
|
|
140
|
-
updatedAt: new Date().toISOString()
|
|
141
|
-
});
|
|
142
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
143
|
-
|
|
144
|
-
await program.parseAsync(['node', 'slack-cli', 'channels']);
|
|
145
|
-
|
|
146
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Name'));
|
|
147
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Type'));
|
|
148
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Members'));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should output in simple format when specified', async () => {
|
|
152
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
153
|
-
token: 'test-token',
|
|
154
|
-
updatedAt: new Date().toISOString()
|
|
155
|
-
});
|
|
156
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
157
|
-
|
|
158
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--format', 'simple']);
|
|
159
|
-
|
|
160
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith('general');
|
|
161
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith('random');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should output in JSON format when specified', async () => {
|
|
165
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
166
|
-
token: 'test-token',
|
|
167
|
-
updatedAt: new Date().toISOString()
|
|
168
|
-
});
|
|
169
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
170
|
-
|
|
171
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--format', 'json']);
|
|
172
|
-
|
|
173
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('"name": "general"'));
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('additional options', () => {
|
|
178
|
-
it('should include archived channels when flag is set', async () => {
|
|
179
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
180
|
-
token: 'test-token',
|
|
181
|
-
updatedAt: new Date().toISOString()
|
|
182
|
-
});
|
|
183
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
184
|
-
|
|
185
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--include-archived']);
|
|
186
|
-
|
|
187
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
188
|
-
types: 'public_channel',
|
|
189
|
-
exclude_archived: false,
|
|
190
|
-
limit: 100
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should respect custom limit', async () => {
|
|
195
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
196
|
-
token: 'test-token',
|
|
197
|
-
updatedAt: new Date().toISOString()
|
|
198
|
-
});
|
|
199
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
200
|
-
|
|
201
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--limit', '50']);
|
|
202
|
-
|
|
203
|
-
expect(mockSlackClient.listChannels).toHaveBeenCalledWith({
|
|
204
|
-
types: 'public_channel',
|
|
205
|
-
exclude_archived: true,
|
|
206
|
-
limit: 50
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('should use specified profile', async () => {
|
|
211
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
212
|
-
token: 'work-token',
|
|
213
|
-
updatedAt: new Date().toISOString()
|
|
214
|
-
});
|
|
215
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue(mockChannels);
|
|
216
|
-
|
|
217
|
-
await program.parseAsync(['node', 'slack-cli', 'channels', '--profile', 'work']);
|
|
218
|
-
|
|
219
|
-
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
220
|
-
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('error handling', () => {
|
|
225
|
-
it('should handle API errors gracefully', async () => {
|
|
226
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
227
|
-
token: 'test-token',
|
|
228
|
-
updatedAt: new Date().toISOString()
|
|
229
|
-
});
|
|
230
|
-
vi.mocked(mockSlackClient.listChannels).mockRejectedValue(new Error('API Error'));
|
|
231
|
-
|
|
232
|
-
await program.parseAsync(['node', 'slack-cli', 'channels']);
|
|
233
|
-
|
|
234
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith('✗ Error:', 'API Error');
|
|
235
|
-
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should show message when no channels found', async () => {
|
|
239
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
240
|
-
token: 'test-token',
|
|
241
|
-
updatedAt: new Date().toISOString()
|
|
242
|
-
});
|
|
243
|
-
vi.mocked(mockSlackClient.listChannels).mockResolvedValue([]);
|
|
244
|
-
|
|
245
|
-
await program.parseAsync(['node', 'slack-cli', 'channels']);
|
|
246
|
-
|
|
247
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(ERROR_MESSAGES.NO_CHANNELS_FOUND);
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
});
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { setupConfigCommand } from '../../src/commands/config';
|
|
3
|
-
import { ProfileConfigManager } from '../../src/utils/profile-config';
|
|
4
|
-
import type { Config, Profile } from '../../src/types/config';
|
|
5
|
-
import { setupMockConsole, createTestProgram, restoreMocks } from '../test-utils';
|
|
6
|
-
import { SUCCESS_MESSAGES, ERROR_MESSAGES } from '../../src/utils/constants';
|
|
7
|
-
|
|
8
|
-
vi.mock('../../src/utils/profile-config');
|
|
9
|
-
|
|
10
|
-
describe('profile config command', () => {
|
|
11
|
-
let program: any;
|
|
12
|
-
let mockConfigManager: ProfileConfigManager;
|
|
13
|
-
let mockConsole: any;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
vi.clearAllMocks();
|
|
17
|
-
|
|
18
|
-
mockConfigManager = new ProfileConfigManager();
|
|
19
|
-
vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
|
|
20
|
-
|
|
21
|
-
mockConsole = setupMockConsole();
|
|
22
|
-
program = createTestProgram();
|
|
23
|
-
program.addCommand(setupConfigCommand());
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
restoreMocks();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('config set with profile', () => {
|
|
31
|
-
it('should set token for specified profile', async () => {
|
|
32
|
-
vi.mocked(mockConfigManager.setToken).mockResolvedValue(undefined);
|
|
33
|
-
|
|
34
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'set', '--token', 'test-token-123', '--profile', 'work']);
|
|
35
|
-
|
|
36
|
-
expect(mockConfigManager.setToken).toHaveBeenCalledWith('test-token-123', 'work');
|
|
37
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.TOKEN_SAVED('work')));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should set token for default profile when no profile specified', async () => {
|
|
41
|
-
vi.mocked(mockConfigManager.setToken).mockResolvedValue(undefined);
|
|
42
|
-
vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('default');
|
|
43
|
-
|
|
44
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'set', '--token', 'test-token-123']);
|
|
45
|
-
|
|
46
|
-
expect(mockConfigManager.setToken).toHaveBeenCalledWith('test-token-123', undefined);
|
|
47
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Token saved successfully for profile "default"'));
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('config get with profile', () => {
|
|
52
|
-
it('should display config for specified profile', async () => {
|
|
53
|
-
const mockConfig: Config = {
|
|
54
|
-
token: 'test-token-1234567890-abcdefghijklmnop',
|
|
55
|
-
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
vi.mocked(mockConfigManager.getConfig).mockResolvedValue(mockConfig);
|
|
59
|
-
vi.mocked(mockConfigManager.maskToken).mockReturnValue('test-****-****-mnop');
|
|
60
|
-
|
|
61
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'get', '--profile', 'work']);
|
|
62
|
-
|
|
63
|
-
expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
|
|
64
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Configuration for profile "work":'));
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('config profiles', () => {
|
|
69
|
-
it('should list all profiles', async () => {
|
|
70
|
-
const mockProfiles: Profile[] = [
|
|
71
|
-
{
|
|
72
|
-
name: 'default',
|
|
73
|
-
config: {
|
|
74
|
-
token: 'default-token',
|
|
75
|
-
updatedAt: '2025-06-21T10:00:00.000Z'
|
|
76
|
-
}
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
name: 'work',
|
|
80
|
-
config: {
|
|
81
|
-
token: 'work-token',
|
|
82
|
-
updatedAt: '2025-06-21T11:00:00.000Z'
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
vi.mocked(mockConfigManager.listProfiles).mockResolvedValue(mockProfiles);
|
|
88
|
-
vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('work');
|
|
89
|
-
|
|
90
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'profiles']);
|
|
91
|
-
|
|
92
|
-
expect(mockConfigManager.listProfiles).toHaveBeenCalled();
|
|
93
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Available profiles:'));
|
|
94
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('default'));
|
|
95
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('* work')); // current profile marked
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should show message when no profiles exist', async () => {
|
|
99
|
-
vi.mocked(mockConfigManager.listProfiles).mockResolvedValue([]);
|
|
100
|
-
|
|
101
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'profiles']);
|
|
102
|
-
|
|
103
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('No profiles found'));
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('config use', () => {
|
|
108
|
-
it('should switch default profile', async () => {
|
|
109
|
-
vi.mocked(mockConfigManager.useProfile).mockResolvedValue(undefined);
|
|
110
|
-
|
|
111
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'use', 'work']);
|
|
112
|
-
|
|
113
|
-
expect(mockConfigManager.useProfile).toHaveBeenCalledWith('work');
|
|
114
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Switched to profile "work"'));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('should show error when profile does not exist', async () => {
|
|
118
|
-
vi.mocked(mockConfigManager.useProfile).mockRejectedValue(new Error('Profile "nonexistent" does not exist'));
|
|
119
|
-
|
|
120
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'use', 'nonexistent']);
|
|
121
|
-
|
|
122
|
-
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
|
|
123
|
-
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('config current', () => {
|
|
128
|
-
it('should show current active profile', async () => {
|
|
129
|
-
vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('work');
|
|
130
|
-
|
|
131
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'current']);
|
|
132
|
-
|
|
133
|
-
expect(mockConfigManager.getCurrentProfile).toHaveBeenCalled();
|
|
134
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Current profile: work'));
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('config clear with profile', () => {
|
|
139
|
-
it('should clear specific profile', async () => {
|
|
140
|
-
vi.mocked(mockConfigManager.clearConfig).mockResolvedValue(undefined);
|
|
141
|
-
|
|
142
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'clear', '--profile', 'work']);
|
|
143
|
-
|
|
144
|
-
expect(mockConfigManager.clearConfig).toHaveBeenCalledWith('work');
|
|
145
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Profile "work" cleared successfully'));
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('should clear current profile when no profile specified', async () => {
|
|
149
|
-
vi.mocked(mockConfigManager.clearConfig).mockResolvedValue(undefined);
|
|
150
|
-
vi.mocked(mockConfigManager.getCurrentProfile).mockResolvedValue('default');
|
|
151
|
-
|
|
152
|
-
await program.parseAsync(['node', 'slack-cli', 'config', 'clear']);
|
|
153
|
-
|
|
154
|
-
expect(mockConfigManager.clearConfig).toHaveBeenCalledWith(undefined);
|
|
155
|
-
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Profile "default" cleared successfully'));
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
});
|