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