@urugus/slack-cli 0.2.6 → 0.2.8
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/dist/commands/history-display.d.ts +1 -1
- package/dist/commands/history-display.d.ts.map +1 -1
- package/dist/commands/history-display.js +8 -28
- package/dist/commands/history-display.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +15 -8
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/send.d.ts.map +1 -1
- package/dist/commands/send.js +5 -18
- package/dist/commands/send.js.map +1 -1
- package/dist/types/commands.d.ts +1 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/formatters/history-formatters.d.ts +8 -0
- package/dist/utils/formatters/history-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/history-formatters.js +105 -0
- package/dist/utils/formatters/history-formatters.js.map +1 -0
- package/dist/utils/profile-config.d.ts.map +1 -1
- package/dist/utils/profile-config.js +2 -6
- package/dist/utils/profile-config.js.map +1 -1
- package/dist/utils/slack-operations/channel-operations.d.ts +9 -0
- package/dist/utils/slack-operations/channel-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/channel-operations.js +77 -50
- package/dist/utils/slack-operations/channel-operations.js.map +1 -1
- package/dist/utils/token-utils.d.ts +7 -0
- package/dist/utils/token-utils.d.ts.map +1 -0
- package/dist/utils/token-utils.js +18 -0
- package/dist/utils/token-utils.js.map +1 -0
- package/dist/utils/validators.d.ts +83 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +187 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/history-display.ts +9 -28
- package/src/commands/history.ts +25 -13
- package/src/commands/send.ts +8 -19
- package/src/types/commands.ts +1 -0
- package/src/utils/formatters/history-formatters.ts +123 -0
- package/src/utils/profile-config.ts +3 -15
- package/src/utils/slack-operations/channel-operations.ts +91 -54
- package/src/utils/token-utils.ts +17 -0
- package/src/utils/validators.ts +225 -0
- package/tests/commands/history.test.ts +115 -0
- package/tests/utils/option-parsers.test.ts +173 -0
- package/tests/utils/profile-config.test.ts +282 -0
- package/tests/utils/slack-operations/channel-operations-refactored.test.ts +179 -0
- package/tests/utils/token-utils.test.ts +33 -0
- package/tests/utils/validators.test.ts +307 -0
- package/src/utils/profile-config-refactored.ts +0 -161
|
@@ -2,6 +2,7 @@ import { BaseSlackClient } from './base-client';
|
|
|
2
2
|
import { channelResolver } from '../channel-resolver';
|
|
3
3
|
import { DEFAULTS } from '../constants';
|
|
4
4
|
import { Channel, ListChannelsOptions } from '../slack-api-client';
|
|
5
|
+
import { WebClient } from '@slack/web-api';
|
|
5
6
|
|
|
6
7
|
interface ChannelWithUnreadInfo extends Channel {
|
|
7
8
|
unread_count: number;
|
|
@@ -10,6 +11,15 @@ interface ChannelWithUnreadInfo extends Channel {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export class ChannelOperations extends BaseSlackClient {
|
|
14
|
+
constructor(tokenOrClient: string | WebClient) {
|
|
15
|
+
if (typeof tokenOrClient === 'string') {
|
|
16
|
+
super(tokenOrClient);
|
|
17
|
+
} else {
|
|
18
|
+
super('dummy-token'); // Call parent constructor
|
|
19
|
+
this.client = tokenOrClient; // Override the client for testing
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
async listChannels(options: ListChannelsOptions): Promise<Channel[]> {
|
|
14
24
|
const channels: Channel[] = [];
|
|
15
25
|
let cursor: string | undefined;
|
|
@@ -34,65 +44,15 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
async listUnreadChannels(): Promise<Channel[]> {
|
|
37
|
-
|
|
38
|
-
const response = await this.client.conversations.list({
|
|
39
|
-
types: 'public_channel,private_channel,im,mpim',
|
|
40
|
-
exclude_archived: true,
|
|
41
|
-
limit: 1000,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const channels = response.channels as Channel[];
|
|
47
|
+
const channels = await this.fetchAllChannels();
|
|
45
48
|
const channelsWithUnread: Channel[] = [];
|
|
46
49
|
|
|
47
50
|
// Process channels one by one with delay to avoid rate limits
|
|
48
51
|
for (const channel of channels) {
|
|
49
52
|
try {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const channelInfo = info.channel as ChannelWithUnreadInfo;
|
|
55
|
-
|
|
56
|
-
// Get the latest message in the channel
|
|
57
|
-
const history = await this.client.conversations.history({
|
|
58
|
-
channel: channel.id,
|
|
59
|
-
limit: 1,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Always check for messages after last_read timestamp
|
|
63
|
-
if (channelInfo.last_read) {
|
|
64
|
-
// Fetch messages after last_read
|
|
65
|
-
const unreadHistory = await this.client.conversations.history({
|
|
66
|
-
channel: channel.id,
|
|
67
|
-
oldest: channelInfo.last_read,
|
|
68
|
-
limit: 100, // Get up to 100 unread messages
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const unreadCount = unreadHistory.messages?.length || 0;
|
|
72
|
-
if (unreadCount > 0) {
|
|
73
|
-
channelsWithUnread.push({
|
|
74
|
-
...channel,
|
|
75
|
-
unread_count: unreadCount,
|
|
76
|
-
unread_count_display: unreadCount,
|
|
77
|
-
last_read: channelInfo.last_read,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
} else if (history.messages && history.messages.length > 0) {
|
|
81
|
-
// If no last_read, all messages are unread
|
|
82
|
-
const allHistory = await this.client.conversations.history({
|
|
83
|
-
channel: channel.id,
|
|
84
|
-
limit: 100,
|
|
85
|
-
});
|
|
86
|
-
const unreadCount = allHistory.messages?.length || 0;
|
|
87
|
-
|
|
88
|
-
if (unreadCount > 0) {
|
|
89
|
-
channelsWithUnread.push({
|
|
90
|
-
...channel,
|
|
91
|
-
unread_count: unreadCount,
|
|
92
|
-
unread_count_display: unreadCount,
|
|
93
|
-
last_read: channelInfo.last_read,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
53
|
+
const unreadInfo = await this.getChannelUnreadInfo(channel);
|
|
54
|
+
if (unreadInfo) {
|
|
55
|
+
channelsWithUnread.push(unreadInfo);
|
|
96
56
|
}
|
|
97
57
|
|
|
98
58
|
// Add delay between API calls to avoid rate limiting
|
|
@@ -106,6 +66,83 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
106
66
|
return channelsWithUnread;
|
|
107
67
|
}
|
|
108
68
|
|
|
69
|
+
private async fetchAllChannels(): Promise<Channel[]> {
|
|
70
|
+
const response = await this.client.conversations.list({
|
|
71
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
72
|
+
exclude_archived: true,
|
|
73
|
+
limit: 1000,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return response.channels as Channel[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async getChannelUnreadInfo(channel: Channel): Promise<Channel | null> {
|
|
80
|
+
const channelInfo = await this.fetchChannelInfo(channel.id);
|
|
81
|
+
const unreadCount = await this.calculateUnreadCount(channel.id, channelInfo);
|
|
82
|
+
|
|
83
|
+
if (unreadCount > 0) {
|
|
84
|
+
return {
|
|
85
|
+
...channel,
|
|
86
|
+
unread_count: unreadCount,
|
|
87
|
+
unread_count_display: unreadCount,
|
|
88
|
+
last_read: channelInfo.last_read,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async fetchChannelInfo(channelId: string): Promise<ChannelWithUnreadInfo> {
|
|
96
|
+
const info = await this.client.conversations.info({
|
|
97
|
+
channel: channelId,
|
|
98
|
+
include_num_members: false,
|
|
99
|
+
});
|
|
100
|
+
return info.channel as ChannelWithUnreadInfo;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async calculateUnreadCount(
|
|
104
|
+
channelId: string,
|
|
105
|
+
channelInfo: ChannelWithUnreadInfo
|
|
106
|
+
): Promise<number> {
|
|
107
|
+
// Get the latest message to check if channel has any messages
|
|
108
|
+
const latestMessage = await this.fetchLatestMessage(channelId);
|
|
109
|
+
if (!latestMessage) {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (channelInfo.last_read) {
|
|
114
|
+
return await this.fetchUnreadMessageCount(channelId, channelInfo.last_read);
|
|
115
|
+
} else {
|
|
116
|
+
// If no last_read, all messages are unread
|
|
117
|
+
return await this.fetchAllMessageCount(channelId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async fetchLatestMessage(channelId: string): Promise<any> {
|
|
122
|
+
const history = await this.client.conversations.history({
|
|
123
|
+
channel: channelId,
|
|
124
|
+
limit: 1,
|
|
125
|
+
});
|
|
126
|
+
return history.messages && history.messages.length > 0 ? history.messages[0] : null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async fetchUnreadMessageCount(channelId: string, lastRead: string): Promise<number> {
|
|
130
|
+
const unreadHistory = await this.client.conversations.history({
|
|
131
|
+
channel: channelId,
|
|
132
|
+
oldest: lastRead,
|
|
133
|
+
limit: 100, // Get up to 100 unread messages
|
|
134
|
+
});
|
|
135
|
+
return unreadHistory.messages?.length || 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async fetchAllMessageCount(channelId: string): Promise<number> {
|
|
139
|
+
const allHistory = await this.client.conversations.history({
|
|
140
|
+
channel: channelId,
|
|
141
|
+
limit: 100,
|
|
142
|
+
});
|
|
143
|
+
return allHistory.messages?.length || 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
109
146
|
async getChannelInfo(channelNameOrId: string): Promise<ChannelWithUnreadInfo> {
|
|
110
147
|
const channelId = await channelResolver.resolveChannelId(channelNameOrId, () =>
|
|
111
148
|
this.listChannels({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TOKEN_MASK_LENGTH, TOKEN_MIN_LENGTH } from './constants';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Masks a token for display purposes, showing only first and last few characters
|
|
5
|
+
* @param token The token to mask
|
|
6
|
+
* @returns Masked token in format "xoxb-****-****-abcd"
|
|
7
|
+
*/
|
|
8
|
+
export function maskToken(token: string): string {
|
|
9
|
+
if (token.length <= TOKEN_MIN_LENGTH) {
|
|
10
|
+
return '****';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const prefix = token.substring(0, TOKEN_MASK_LENGTH);
|
|
14
|
+
const suffix = token.substring(token.length - TOKEN_MASK_LENGTH);
|
|
15
|
+
|
|
16
|
+
return `${prefix}-****-****-${suffix}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { ERROR_MESSAGES } from './constants';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Common validation functions for CLI commands
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ValidationRule<T = unknown> {
|
|
9
|
+
validate: (value: T) => boolean | string;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ValidationOptions {
|
|
14
|
+
required?: boolean;
|
|
15
|
+
rules?: ValidationRule[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates that a value exists (not undefined, null, or empty string)
|
|
20
|
+
*/
|
|
21
|
+
export function validateRequired(value: unknown, fieldName: string): string | null {
|
|
22
|
+
if (value === undefined || value === null || value === '') {
|
|
23
|
+
return `${fieldName} is required`;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates mutually exclusive options
|
|
30
|
+
*/
|
|
31
|
+
export function validateMutuallyExclusive(
|
|
32
|
+
options: Record<string, unknown>,
|
|
33
|
+
fields: string[],
|
|
34
|
+
errorMessage?: string
|
|
35
|
+
): string | null {
|
|
36
|
+
const presentFields = fields.filter((field) => options[field] !== undefined);
|
|
37
|
+
if (presentFields.length > 1) {
|
|
38
|
+
return errorMessage || `Cannot use both ${presentFields.join(' and ')}`;
|
|
39
|
+
}
|
|
40
|
+
if (presentFields.length === 0) {
|
|
41
|
+
return errorMessage || `Must specify one of: ${fields.join(', ')}`;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates string format using regex
|
|
48
|
+
*/
|
|
49
|
+
export function validateFormat(
|
|
50
|
+
value: string,
|
|
51
|
+
pattern: RegExp,
|
|
52
|
+
errorMessage: string
|
|
53
|
+
): string | null {
|
|
54
|
+
if (!pattern.test(value)) {
|
|
55
|
+
return errorMessage;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validates numeric range
|
|
62
|
+
*/
|
|
63
|
+
export function validateRange(
|
|
64
|
+
value: number,
|
|
65
|
+
min?: number,
|
|
66
|
+
max?: number,
|
|
67
|
+
fieldName = 'Value'
|
|
68
|
+
): string | null {
|
|
69
|
+
if (min !== undefined && value < min) {
|
|
70
|
+
return `${fieldName} must be at least ${min}`;
|
|
71
|
+
}
|
|
72
|
+
if (max !== undefined && value > max) {
|
|
73
|
+
return `${fieldName} must be at most ${max}`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validates date format
|
|
80
|
+
*/
|
|
81
|
+
export function validateDateFormat(dateString: string): string | null {
|
|
82
|
+
const date = new Date(dateString);
|
|
83
|
+
if (isNaN(date.getTime())) {
|
|
84
|
+
return 'Invalid date format';
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Common format validators
|
|
91
|
+
*/
|
|
92
|
+
export const formatValidators = {
|
|
93
|
+
/**
|
|
94
|
+
* Validates Slack thread timestamp format (1234567890.123456)
|
|
95
|
+
*/
|
|
96
|
+
threadTimestamp: (value: string): string | null => {
|
|
97
|
+
const pattern = /^\d{10}\.\d{6}$/;
|
|
98
|
+
return validateFormat(value, pattern, ERROR_MESSAGES.INVALID_THREAD_TIMESTAMP);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Validates Slack channel ID format (C1234567890, D1234567890, G1234567890)
|
|
103
|
+
*/
|
|
104
|
+
channelId: (value: string): string | null => {
|
|
105
|
+
const pattern = /^[CDG][A-Z0-9]{10,}$/;
|
|
106
|
+
return validateFormat(value, pattern, 'Invalid channel ID format');
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Validates output format options
|
|
111
|
+
*/
|
|
112
|
+
outputFormat: (value: string): string | null => {
|
|
113
|
+
const validFormats = ['table', 'simple', 'json', 'compact'];
|
|
114
|
+
if (!validFormats.includes(value)) {
|
|
115
|
+
return `Invalid format. Must be one of: ${validFormats.join(', ')}`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Creates a preAction hook for command validation
|
|
123
|
+
*/
|
|
124
|
+
export function createValidationHook(
|
|
125
|
+
validations: Array<(options: Record<string, unknown>, command: Command) => string | null>
|
|
126
|
+
): (thisCommand: Command) => void {
|
|
127
|
+
return (thisCommand: Command) => {
|
|
128
|
+
const options = thisCommand.opts();
|
|
129
|
+
|
|
130
|
+
for (const validation of validations) {
|
|
131
|
+
const error = validation(options, thisCommand);
|
|
132
|
+
if (error) {
|
|
133
|
+
thisCommand.error(`Error: ${error}`);
|
|
134
|
+
break; // Stop processing after first error
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Common command option validators
|
|
142
|
+
*/
|
|
143
|
+
export const optionValidators = {
|
|
144
|
+
/**
|
|
145
|
+
* Validates message/file options for send command
|
|
146
|
+
*/
|
|
147
|
+
messageOrFile: (options: Record<string, unknown>): string | null => {
|
|
148
|
+
if (!options.message && !options.file) {
|
|
149
|
+
return ERROR_MESSAGES.NO_MESSAGE_OR_FILE;
|
|
150
|
+
}
|
|
151
|
+
if (options.message && options.file) {
|
|
152
|
+
return ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validates thread timestamp if provided
|
|
159
|
+
*/
|
|
160
|
+
threadTimestamp: (options: Record<string, unknown>): string | null => {
|
|
161
|
+
if (options.thread) {
|
|
162
|
+
return formatValidators.threadTimestamp(options.thread as string);
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validates message count for history command
|
|
169
|
+
*/
|
|
170
|
+
messageCount: (options: Record<string, unknown>): string | null => {
|
|
171
|
+
if (options.number) {
|
|
172
|
+
const count = parseInt(options.number as string, 10);
|
|
173
|
+
if (isNaN(count)) {
|
|
174
|
+
return 'Message count must be a number';
|
|
175
|
+
}
|
|
176
|
+
return validateRange(count, 1, 1000, 'Message count');
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validates date format for history command
|
|
183
|
+
*/
|
|
184
|
+
sinceDate: (options: Record<string, unknown>): string | null => {
|
|
185
|
+
if (options.since) {
|
|
186
|
+
const error = validateDateFormat(options.since as string);
|
|
187
|
+
if (error) {
|
|
188
|
+
return 'Invalid date format. Use YYYY-MM-DD HH:MM:SS';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validates format option
|
|
196
|
+
*/
|
|
197
|
+
format: (options: Record<string, unknown>): string | null => {
|
|
198
|
+
if (options.format) {
|
|
199
|
+
const validFormats = ['table', 'simple', 'json'];
|
|
200
|
+
if (!validFormats.includes(options.format as string)) {
|
|
201
|
+
return `Invalid format '${options.format}'. Must be one of: ${validFormats.join(', ')}`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Creates a validated option parser
|
|
210
|
+
*/
|
|
211
|
+
export function createOptionParser<T>(
|
|
212
|
+
parser: (value: string | undefined, defaultValue: T) => T,
|
|
213
|
+
validator?: (value: T) => string | null
|
|
214
|
+
): (value: string | undefined, defaultValue: T) => T {
|
|
215
|
+
return (value: string | undefined, defaultValue: T): T => {
|
|
216
|
+
const parsed = parser(value, defaultValue);
|
|
217
|
+
if (validator) {
|
|
218
|
+
const error = validator(parsed);
|
|
219
|
+
if (error) {
|
|
220
|
+
throw new Error(error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return parsed;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -217,6 +217,121 @@ describe('history command', () => {
|
|
|
217
217
|
});
|
|
218
218
|
|
|
219
219
|
describe('output formatting', () => {
|
|
220
|
+
describe('format options', () => {
|
|
221
|
+
it('should display messages in JSON format when --format json is specified', async () => {
|
|
222
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
223
|
+
token: 'test-token',
|
|
224
|
+
updatedAt: new Date().toISOString()
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const mockMessages = [
|
|
228
|
+
{
|
|
229
|
+
type: 'message',
|
|
230
|
+
text: 'Hello world',
|
|
231
|
+
user: 'U123456',
|
|
232
|
+
ts: '1609459200.000100',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: 'message',
|
|
236
|
+
text: 'Another message',
|
|
237
|
+
user: 'U789012',
|
|
238
|
+
ts: '1609459300.000200',
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
243
|
+
messages: mockMessages,
|
|
244
|
+
users: new Map([
|
|
245
|
+
['U123456', 'john.doe'],
|
|
246
|
+
['U789012', 'jane.smith']
|
|
247
|
+
])
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '--format', 'json']);
|
|
251
|
+
|
|
252
|
+
const expectedOutput = {
|
|
253
|
+
channel: 'general',
|
|
254
|
+
messages: [
|
|
255
|
+
{
|
|
256
|
+
timestamp: '2021-01-01 00:01:40',
|
|
257
|
+
user: 'jane.smith',
|
|
258
|
+
text: 'Another message'
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
timestamp: '2021-01-01 00:00:00',
|
|
262
|
+
user: 'john.doe',
|
|
263
|
+
text: 'Hello world'
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
total: 2
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(JSON.stringify(expectedOutput, null, 2));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should display messages in simple format when --format simple is specified', async () => {
|
|
273
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
274
|
+
token: 'test-token',
|
|
275
|
+
updatedAt: new Date().toISOString()
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const mockMessages = [
|
|
279
|
+
{
|
|
280
|
+
type: 'message',
|
|
281
|
+
text: 'Hello world',
|
|
282
|
+
user: 'U123456',
|
|
283
|
+
ts: '1609459200.000100',
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
288
|
+
messages: mockMessages,
|
|
289
|
+
users: new Map([['U123456', 'john.doe']])
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general', '--format', 'simple']);
|
|
293
|
+
|
|
294
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith('[2021-01-01 00:00:00] john.doe: Hello world');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should display messages in table format by default', async () => {
|
|
298
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
299
|
+
token: 'test-token',
|
|
300
|
+
updatedAt: new Date().toISOString()
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const mockMessages = [
|
|
304
|
+
{
|
|
305
|
+
type: 'message',
|
|
306
|
+
text: 'Hello world',
|
|
307
|
+
user: 'U123456',
|
|
308
|
+
ts: '1609459200.000100',
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
vi.mocked(mockSlackClient.getHistory).mockResolvedValue({
|
|
313
|
+
messages: mockMessages,
|
|
314
|
+
users: new Map([['U123456', 'john.doe']])
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await program.parseAsync(['node', 'slack-cli', 'history', '-c', 'general']);
|
|
318
|
+
|
|
319
|
+
// Should display in table format (current format)
|
|
320
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Message History for #general'));
|
|
321
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('john.doe'));
|
|
322
|
+
expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should handle invalid format option', async () => {
|
|
326
|
+
const historyCommand = setupHistoryCommand();
|
|
327
|
+
historyCommand.exitOverride();
|
|
328
|
+
|
|
329
|
+
await expect(
|
|
330
|
+
historyCommand.parseAsync(['-c', 'general', '--format', 'invalid'], { from: 'user' })
|
|
331
|
+
).rejects.toThrow();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
220
335
|
it('should format messages with user names and timestamps', async () => {
|
|
221
336
|
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
222
337
|
token: 'test-token',
|