Skip to main content

slack_rs/cli/
mod.rs

1//! CLI command routing and handlers
2
3use crate::api::{ApiClient, CommandResponse};
4use crate::commands;
5use crate::commands::ConversationSelector;
6use crate::profile::{
7    default_config_path, load_config, make_token_key, resolve_profile_full, FileTokenStore,
8    TokenStore, TokenType,
9};
10use serde_json::Value;
11
12/// Get API client for a profile with optional token type selection
13///
14/// # Arguments
15/// * `profile_name` - Optional profile name (defaults to "default")
16/// * `token_type` - Optional token type (bot/user). If None, uses profile default or bot fallback
17///
18/// # Token Resolution Priority
19/// 1. CLI flag token_type parameter (if provided)
20/// 2. Profile's default_token_type (if set)
21/// 3. Try user token first, fall back to bot token
22pub async fn get_api_client_with_token_type(
23    profile_name: Option<String>,
24    token_type: Option<TokenType>,
25) -> Result<ApiClient, String> {
26    let profile_name = profile_name.unwrap_or_else(|| "default".to_string());
27    let config_path = default_config_path().map_err(|e| e.to_string())?;
28    let config = load_config(&config_path).map_err(|e| e.to_string())?;
29
30    let profile = config
31        .get(&profile_name)
32        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
33
34    let token_store = FileTokenStore::new().map_err(|e| e.to_string())?;
35
36    // Resolve token type: CLI flag > profile default > try user first with bot fallback
37    let resolved_token_type = token_type.or(profile.default_token_type);
38
39    let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
40    let user_token_key = format!("{}:{}:user", profile.team_id, profile.user_id);
41
42    let token = match resolved_token_type {
43        Some(TokenType::Bot) => {
44            // Explicitly requested bot token
45            token_store
46                .get(&bot_token_key)
47                .map_err(|e| format!("Failed to get bot token: {}", e))?
48        }
49        Some(TokenType::User) => {
50            // Explicitly requested user token
51            token_store
52                .get(&user_token_key)
53                .map_err(|e| format!("Failed to get user token: {}", e))?
54        }
55        None => {
56            // No explicit preference, try user token first (for APIs that require user scope)
57            match token_store.get(&user_token_key) {
58                Ok(user_token) => user_token,
59                Err(_) => {
60                    // Fall back to bot token
61                    token_store
62                        .get(&bot_token_key)
63                        .map_err(|e| format!("Failed to get token: {}", e))?
64                }
65            }
66        }
67    };
68
69    Ok(ApiClient::with_token(token))
70}
71
72/// Get API client for a profile (legacy function, maintains backward compatibility)
73#[allow(dead_code)]
74pub async fn get_api_client(profile_name: Option<String>) -> Result<ApiClient, String> {
75    get_api_client_with_token_type(profile_name, None).await
76}
77
78/// Check if a flag exists in args
79pub fn has_flag(args: &[String], flag: &str) -> bool {
80    args.iter().any(|arg| arg == flag)
81}
82
83/// Wrap response with unified envelope including metadata
84pub async fn wrap_with_envelope(
85    response: Value,
86    method: &str,
87    command: &str,
88    profile_name: Option<String>,
89) -> Result<CommandResponse, String> {
90    let profile_name_str = profile_name.unwrap_or_else(|| "default".to_string());
91    let config_path = default_config_path().map_err(|e| e.to_string())?;
92    let profile = resolve_profile_full(&config_path, &profile_name_str)
93        .map_err(|e| format!("Failed to resolve profile '{}': {}", profile_name_str, e))?;
94
95    Ok(CommandResponse::new(
96        response,
97        Some(profile_name_str),
98        profile.team_id,
99        profile.user_id,
100        method.to_string(),
101        command.to_string(),
102    ))
103}
104
105/// Get option value from args (e.g., --key=value)
106pub fn get_option(args: &[String], prefix: &str) -> Option<String> {
107    args.iter()
108        .find(|arg| arg.starts_with(prefix))
109        .and_then(|arg| arg.strip_prefix(prefix))
110        .map(|s| s.to_string())
111}
112
113/// Parse token type from command line arguments
114/// Supports both --token-type=VALUE and --token-type VALUE formats
115pub fn parse_token_type(args: &[String]) -> Result<Option<TokenType>, String> {
116    // First try --token-type=VALUE format
117    if let Some(token_type_str) = get_option(args, "--token-type=") {
118        return token_type_str
119            .parse::<TokenType>()
120            .map(Some)
121            .map_err(|e| e.to_string());
122    }
123
124    // Then try --token-type VALUE format (space-separated)
125    if let Some(pos) = args.iter().position(|arg| arg == "--token-type") {
126        if let Some(value) = args.get(pos + 1) {
127            return value
128                .parse::<TokenType>()
129                .map(Some)
130                .map_err(|e| e.to_string());
131        } else {
132            return Err("--token-type requires a value (bot or user)".to_string());
133        }
134    }
135
136    Ok(None)
137}
138
139pub async fn run_search(args: &[String]) -> Result<(), String> {
140    let query = args[2].clone();
141    let count = get_option(args, "--count=").and_then(|s| s.parse().ok());
142    let page = get_option(args, "--page=").and_then(|s| s.parse().ok());
143    let sort = get_option(args, "--sort=");
144    let sort_dir = get_option(args, "--sort_dir=");
145    let profile = get_option(args, "--profile=");
146    let token_type = parse_token_type(args)?;
147    let raw = has_flag(args, "--raw");
148
149    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
150    let response = commands::search(&client, query, count, page, sort, sort_dir)
151        .await
152        .map_err(|e| e.to_string())?;
153
154    // Output with or without envelope
155    let output = if raw {
156        serde_json::to_string_pretty(&response).unwrap()
157    } else {
158        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
159        let wrapped =
160            wrap_with_envelope(response_value, "search.messages", "search", profile).await?;
161        serde_json::to_string_pretty(&wrapped).unwrap()
162    };
163
164    println!("{}", output);
165    Ok(())
166}
167
168/// Get all options with a specific prefix from args
169pub fn get_all_options(args: &[String], prefix: &str) -> Vec<String> {
170    args.iter()
171        .filter(|arg| arg.starts_with(prefix))
172        .filter_map(|arg| arg.strip_prefix(prefix))
173        .map(|s| s.to_string())
174        .collect()
175}
176
177pub async fn run_conv_list(args: &[String]) -> Result<(), String> {
178    let types = get_option(args, "--types=");
179    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
180    let profile = get_option(args, "--profile=");
181    let token_type = parse_token_type(args)?;
182    let filter_strings = get_all_options(args, "--filter=");
183    let raw = has_flag(args, "--raw");
184
185    // Parse filters
186    let filters: Result<Vec<_>, _> = filter_strings
187        .iter()
188        .map(|s| commands::ConversationFilter::parse(s))
189        .collect();
190    let filters = filters.map_err(|e| e.to_string())?;
191
192    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
193    let mut response = commands::conv_list(&client, types, limit)
194        .await
195        .map_err(|e| e.to_string())?;
196
197    // Apply filters
198    commands::apply_filters(&mut response, &filters);
199
200    // Output with or without envelope
201    let output = if raw {
202        serde_json::to_string_pretty(&response).unwrap()
203    } else {
204        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
205        let wrapped =
206            wrap_with_envelope(response_value, "conversations.list", "conv list", profile).await?;
207        serde_json::to_string_pretty(&wrapped).unwrap()
208    };
209
210    println!("{}", output);
211    Ok(())
212}
213
214pub async fn run_conv_select(args: &[String]) -> Result<(), String> {
215    let types = get_option(args, "--types=");
216    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
217    let profile = get_option(args, "--profile=");
218    let token_type = parse_token_type(args)?;
219    let filter_strings = get_all_options(args, "--filter=");
220
221    // Parse filters
222    let filters: Result<Vec<_>, _> = filter_strings
223        .iter()
224        .map(|s| commands::ConversationFilter::parse(s))
225        .collect();
226    let filters = filters.map_err(|e| e.to_string())?;
227
228    let client = get_api_client_with_token_type(profile, token_type).await?;
229    let mut response = commands::conv_list(&client, types, limit)
230        .await
231        .map_err(|e| e.to_string())?;
232
233    // Apply filters
234    commands::apply_filters(&mut response, &filters);
235
236    // Extract conversations and present selection
237    let items = commands::extract_conversations(&response);
238    let selector = commands::StdinSelector;
239    let channel_id = selector.select(&items)?;
240
241    println!("{}", channel_id);
242    Ok(())
243}
244
245pub async fn run_conv_history(args: &[String]) -> Result<(), String> {
246    let interactive = has_flag(args, "--interactive");
247
248    let channel = if interactive {
249        // Use conv_select logic to get channel
250        let types = get_option(args, "--types=");
251        let profile = get_option(args, "--profile=");
252        let filter_strings = get_all_options(args, "--filter=");
253
254        // Parse filters
255        let filters: Result<Vec<_>, _> = filter_strings
256            .iter()
257            .map(|s| commands::ConversationFilter::parse(s))
258            .collect();
259        let filters = filters.map_err(|e| e.to_string())?;
260
261        let token_type_inner = parse_token_type(args)?;
262        let client = get_api_client_with_token_type(profile.clone(), token_type_inner).await?;
263        let mut response = commands::conv_list(&client, types, None)
264            .await
265            .map_err(|e| e.to_string())?;
266
267        // Apply filters
268        commands::apply_filters(&mut response, &filters);
269
270        // Extract conversations and present selection
271        let items = commands::extract_conversations(&response);
272        let selector = commands::StdinSelector;
273        selector.select(&items)?
274    } else {
275        if args.len() < 4 {
276            return Err("Channel argument required when --interactive is not used".to_string());
277        }
278        args[3].clone()
279    };
280
281    let limit = get_option(args, "--limit=").and_then(|s| s.parse().ok());
282    let oldest = get_option(args, "--oldest=");
283    let latest = get_option(args, "--latest=");
284    let profile = get_option(args, "--profile=");
285    let token_type = parse_token_type(args)?;
286    let raw = has_flag(args, "--raw");
287
288    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
289    let response = commands::conv_history(&client, channel, limit, oldest, latest)
290        .await
291        .map_err(|e| e.to_string())?;
292
293    // Output with or without envelope
294    let output = if raw {
295        serde_json::to_string_pretty(&response).unwrap()
296    } else {
297        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
298        let wrapped = wrap_with_envelope(
299            response_value,
300            "conversations.history",
301            "conv history",
302            profile,
303        )
304        .await?;
305        serde_json::to_string_pretty(&wrapped).unwrap()
306    };
307
308    println!("{}", output);
309    Ok(())
310}
311
312pub async fn run_users_info(args: &[String]) -> Result<(), String> {
313    let user = args[3].clone();
314    let profile = get_option(args, "--profile=");
315    let token_type = parse_token_type(args)?;
316    let raw = has_flag(args, "--raw");
317
318    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
319    let response = commands::users_info(&client, user)
320        .await
321        .map_err(|e| e.to_string())?;
322
323    // Output with or without envelope
324    let output = if raw {
325        serde_json::to_string_pretty(&response).unwrap()
326    } else {
327        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
328        let wrapped =
329            wrap_with_envelope(response_value, "users.info", "users info", profile).await?;
330        serde_json::to_string_pretty(&wrapped).unwrap()
331    };
332
333    println!("{}", output);
334    Ok(())
335}
336
337pub async fn run_users_cache_update(args: &[String]) -> Result<(), String> {
338    let profile_name = get_option(args, "--profile=").unwrap_or_else(|| "default".to_string());
339    let force = has_flag(args, "--force");
340    let token_type = parse_token_type(args)?;
341
342    let config_path = default_config_path().map_err(|e| e.to_string())?;
343    let config = load_config(&config_path).map_err(|e| e.to_string())?;
344
345    let profile = config
346        .get(&profile_name)
347        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
348
349    let client = get_api_client_with_token_type(Some(profile_name.clone()), token_type).await?;
350
351    commands::update_cache(&client, profile.team_id.clone(), force)
352        .await
353        .map_err(|e| e.to_string())?;
354
355    println!("Cache updated successfully for team {}", profile.team_id);
356    Ok(())
357}
358
359pub async fn run_users_resolve_mentions(args: &[String]) -> Result<(), String> {
360    if args.len() < 4 {
361        return Err(
362            "Usage: users resolve-mentions <text> [--profile=NAME] [--format=FORMAT]".to_string(),
363        );
364    }
365
366    let text = args[3].clone();
367    let profile_name = get_option(args, "--profile=").unwrap_or_else(|| "default".to_string());
368    let format_str = get_option(args, "--format=").unwrap_or_else(|| "display_name".to_string());
369
370    let format = format_str.parse::<commands::MentionFormat>().map_err(|_| {
371        format!(
372            "Invalid format: {}. Use display_name, real_name, or username",
373            format_str
374        )
375    })?;
376
377    let config_path = default_config_path().map_err(|e| e.to_string())?;
378    let config = load_config(&config_path).map_err(|e| e.to_string())?;
379
380    let profile = config
381        .get(&profile_name)
382        .ok_or_else(|| format!("Profile '{}' not found", profile_name))?;
383
384    let cache_path = commands::UsersCacheFile::default_path()?;
385    let cache_file = commands::UsersCacheFile::load(&cache_path)?;
386
387    let workspace_cache = cache_file.get_workspace(&profile.team_id).ok_or_else(|| {
388        format!(
389            "No cache found for team {}. Run 'users cache-update' first.",
390            profile.team_id
391        )
392    })?;
393
394    let result = commands::resolve_mentions(&text, workspace_cache, format);
395    println!("{}", result);
396    Ok(())
397}
398
399pub async fn run_msg_post(args: &[String]) -> Result<(), String> {
400    if args.len() < 5 {
401        return Err("Usage: msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--profile=NAME] [--token-type=bot|user]".to_string());
402    }
403
404    let channel = args[3].clone();
405    let text = args[4].clone();
406    let thread_ts = get_option(args, "--thread-ts=");
407    let reply_broadcast = has_flag(args, "--reply-broadcast");
408    let profile = get_option(args, "--profile=");
409    let token_type = parse_token_type(args)?;
410
411    // Validate: --reply-broadcast requires --thread-ts
412    if reply_broadcast && thread_ts.is_none() {
413        return Err("Error: --reply-broadcast requires --thread-ts".to_string());
414    }
415
416    let raw = has_flag(args, "--raw");
417    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
418    let response = commands::msg_post(&client, channel, text, thread_ts, reply_broadcast)
419        .await
420        .map_err(|e| e.to_string())?;
421
422    // Output with or without envelope
423    let output = if raw {
424        serde_json::to_string_pretty(&response).unwrap()
425    } else {
426        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
427        let wrapped =
428            wrap_with_envelope(response_value, "chat.postMessage", "msg post", profile).await?;
429        serde_json::to_string_pretty(&wrapped).unwrap()
430    };
431
432    println!("{}", output);
433    Ok(())
434}
435
436pub async fn run_msg_update(args: &[String]) -> Result<(), String> {
437    if args.len() < 6 {
438        return Err("Usage: msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user]".to_string());
439    }
440
441    let channel = args[3].clone();
442    let ts = args[4].clone();
443    let text = args[5].clone();
444    let yes = has_flag(args, "--yes");
445    let profile = get_option(args, "--profile=");
446    let token_type = parse_token_type(args)?;
447    let raw = has_flag(args, "--raw");
448
449    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
450    let response = commands::msg_update(&client, channel, ts, text, yes)
451        .await
452        .map_err(|e| e.to_string())?;
453
454    // Output with or without envelope
455    let output = if raw {
456        serde_json::to_string_pretty(&response).unwrap()
457    } else {
458        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
459        let wrapped =
460            wrap_with_envelope(response_value, "chat.update", "msg update", profile).await?;
461        serde_json::to_string_pretty(&wrapped).unwrap()
462    };
463
464    println!("{}", output);
465    Ok(())
466}
467
468pub async fn run_msg_delete(args: &[String]) -> Result<(), String> {
469    if args.len() < 5 {
470        return Err(
471            "Usage: msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user]"
472                .to_string(),
473        );
474    }
475
476    let channel = args[3].clone();
477    let ts = args[4].clone();
478    let yes = has_flag(args, "--yes");
479    let profile = get_option(args, "--profile=");
480    let token_type = parse_token_type(args)?;
481    let raw = has_flag(args, "--raw");
482
483    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
484    let response = commands::msg_delete(&client, channel, ts, yes)
485        .await
486        .map_err(|e| e.to_string())?;
487
488    // Output with or without envelope
489    let output = if raw {
490        serde_json::to_string_pretty(&response).unwrap()
491    } else {
492        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
493        let wrapped =
494            wrap_with_envelope(response_value, "chat.delete", "msg delete", profile).await?;
495        serde_json::to_string_pretty(&wrapped).unwrap()
496    };
497
498    println!("{}", output);
499    Ok(())
500}
501
502pub async fn run_react_add(args: &[String]) -> Result<(), String> {
503    if args.len() < 6 {
504        return Err(
505            "Usage: react add <channel> <ts> <emoji> [--profile=NAME] [--token-type=bot|user]"
506                .to_string(),
507        );
508    }
509
510    let channel = args[3].clone();
511    let ts = args[4].clone();
512    let emoji = args[5].clone();
513    let profile = get_option(args, "--profile=");
514    let token_type = parse_token_type(args)?;
515    let raw = has_flag(args, "--raw");
516
517    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
518    let response = commands::react_add(&client, channel, ts, emoji)
519        .await
520        .map_err(|e| e.to_string())?;
521
522    // Output with or without envelope
523    let output = if raw {
524        serde_json::to_string_pretty(&response).unwrap()
525    } else {
526        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
527        let wrapped =
528            wrap_with_envelope(response_value, "reactions.add", "react add", profile).await?;
529        serde_json::to_string_pretty(&wrapped).unwrap()
530    };
531
532    println!("{}", output);
533    Ok(())
534}
535
536pub async fn run_react_remove(args: &[String]) -> Result<(), String> {
537    if args.len() < 6 {
538        return Err(
539            "Usage: react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user]".to_string(),
540        );
541    }
542
543    let channel = args[3].clone();
544    let ts = args[4].clone();
545    let emoji = args[5].clone();
546    let yes = has_flag(args, "--yes");
547    let profile = get_option(args, "--profile=");
548    let token_type = parse_token_type(args)?;
549    let raw = has_flag(args, "--raw");
550
551    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
552    let response = commands::react_remove(&client, channel, ts, emoji, yes)
553        .await
554        .map_err(|e| e.to_string())?;
555
556    // Output with or without envelope
557    let output = if raw {
558        serde_json::to_string_pretty(&response).unwrap()
559    } else {
560        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
561        let wrapped =
562            wrap_with_envelope(response_value, "reactions.remove", "react remove", profile).await?;
563        serde_json::to_string_pretty(&wrapped).unwrap()
564    };
565
566    println!("{}", output);
567    Ok(())
568}
569
570pub async fn run_file_upload(args: &[String]) -> Result<(), String> {
571    if args.len() < 4 {
572        return Err(
573            "Usage: file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--profile=NAME] [--token-type=bot|user]"
574                .to_string(),
575        );
576    }
577
578    let file_path = args[3].clone();
579
580    // Support both --channel and --channels
581    let channels = get_option(args, "--channel=").or_else(|| get_option(args, "--channels="));
582    let title = get_option(args, "--title=");
583    let comment = get_option(args, "--comment=");
584    let profile = get_option(args, "--profile=");
585    let token_type = parse_token_type(args)?;
586    let raw = has_flag(args, "--raw");
587
588    let client = get_api_client_with_token_type(profile.clone(), token_type).await?;
589    let response = commands::file_upload(&client, file_path, channels, title, comment)
590        .await
591        .map_err(|e| e.to_string())?;
592
593    // Output with or without envelope
594    let output = if raw {
595        serde_json::to_string_pretty(&response).unwrap()
596    } else {
597        let response_value = serde_json::to_value(&response).map_err(|e| e.to_string())?;
598        let wrapped =
599            wrap_with_envelope(response_value, "files.upload", "file upload", profile).await?;
600        serde_json::to_string_pretty(&wrapped).unwrap()
601    };
602
603    println!("{}", output);
604    Ok(())
605}
606
607pub fn print_conv_usage(prog: &str) {
608    println!("Conv command usage:");
609    println!(
610        "  {} conv list [--types=TYPE] [--limit=N] [--filter=KEY:VALUE]... [--profile=NAME] [--token-type=bot|user]",
611        prog
612    );
613    println!("    Filters: name:<glob>, is_member:true|false, is_private:true|false");
614    println!(
615        "  {} conv select [--types=TYPE] [--filter=KEY:VALUE]... [--profile=NAME]",
616        prog
617    );
618    println!("    Interactively select a conversation and output its channel ID");
619    println!(
620        "  {} conv history <channel> [--limit=N] [--oldest=TS] [--latest=TS] [--profile=NAME] [--token-type=bot|user]",
621        prog
622    );
623    println!(
624        "  {} conv history --interactive [--types=TYPE] [--filter=KEY:VALUE]... [--limit=N] [--profile=NAME]",
625        prog
626    );
627    println!("    Select channel interactively before fetching history");
628}
629
630pub fn print_users_usage(prog: &str) {
631    println!("Users command usage:");
632    println!(
633        "  {} users info <user_id> [--profile=NAME] [--token-type=bot|user]",
634        prog
635    );
636    println!(
637        "  {} users cache-update [--profile=NAME] [--force] [--token-type=bot|user]",
638        prog
639    );
640    println!("  {} users resolve-mentions <text> [--profile=NAME] [--format=display_name|real_name|username]", prog);
641}
642
643pub fn print_msg_usage(prog: &str) {
644    println!("Msg command usage:");
645    println!(
646        "  {} msg post <channel> <text> [--thread-ts=TS] [--reply-broadcast] [--profile=NAME] [--token-type=bot|user]",
647        prog
648    );
649    println!(
650        "  {} msg update <channel> <ts> <text> [--yes] [--profile=NAME] [--token-type=bot|user]",
651        prog
652    );
653    println!(
654        "  {} msg delete <channel> <ts> [--yes] [--profile=NAME] [--token-type=bot|user]",
655        prog
656    );
657}
658
659pub fn print_react_usage(prog: &str) {
660    println!("React command usage:");
661    println!(
662        "  {} react add <channel> <ts> <emoji> [--profile=NAME] [--token-type=bot|user]",
663        prog
664    );
665    println!(
666        "  {} react remove <channel> <ts> <emoji> [--yes] [--profile=NAME] [--token-type=bot|user]",
667        prog
668    );
669}
670
671pub fn print_file_usage(prog: &str) {
672    println!("File command usage:");
673    println!(
674        "  {} file upload <path> [--channel=ID] [--channels=IDs] [--title=TITLE] [--comment=TEXT] [--profile=NAME] [--token-type=bot|user]",
675        prog
676    );
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[test]
684    fn test_parse_token_type_equals_format() {
685        let args = vec!["command".to_string(), "--token-type=user".to_string()];
686        let result = parse_token_type(&args).unwrap();
687        assert_eq!(result, Some(TokenType::User));
688    }
689
690    #[test]
691    fn test_parse_token_type_space_separated() {
692        let args = vec![
693            "command".to_string(),
694            "--token-type".to_string(),
695            "bot".to_string(),
696        ];
697        let result = parse_token_type(&args).unwrap();
698        assert_eq!(result, Some(TokenType::Bot));
699    }
700
701    #[test]
702    fn test_parse_token_type_both_values() {
703        // Test user with equals
704        let args1 = vec!["--token-type=user".to_string()];
705        assert_eq!(parse_token_type(&args1).unwrap(), Some(TokenType::User));
706
707        // Test bot with equals
708        let args2 = vec!["--token-type=bot".to_string()];
709        assert_eq!(parse_token_type(&args2).unwrap(), Some(TokenType::Bot));
710
711        // Test user with space
712        let args3 = vec!["--token-type".to_string(), "user".to_string()];
713        assert_eq!(parse_token_type(&args3).unwrap(), Some(TokenType::User));
714
715        // Test bot with space
716        let args4 = vec!["--token-type".to_string(), "bot".to_string()];
717        assert_eq!(parse_token_type(&args4).unwrap(), Some(TokenType::Bot));
718    }
719
720    #[test]
721    fn test_parse_token_type_missing() {
722        let args = vec!["command".to_string()];
723        let result = parse_token_type(&args).unwrap();
724        assert_eq!(result, None);
725    }
726
727    #[test]
728    fn test_parse_token_type_missing_value() {
729        let args = vec!["--token-type".to_string()];
730        let result = parse_token_type(&args);
731        assert!(result.is_err());
732        assert_eq!(
733            result.unwrap_err(),
734            "--token-type requires a value (bot or user)"
735        );
736    }
737
738    #[test]
739    fn test_parse_token_type_invalid_value() {
740        let args = vec!["--token-type=invalid".to_string()];
741        let result = parse_token_type(&args);
742        assert!(result.is_err());
743    }
744}