1use 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
12pub 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 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 token_store
46 .get(&bot_token_key)
47 .map_err(|e| format!("Failed to get bot token: {}", e))?
48 }
49 Some(TokenType::User) => {
50 token_store
52 .get(&user_token_key)
53 .map_err(|e| format!("Failed to get user token: {}", e))?
54 }
55 None => {
56 match token_store.get(&user_token_key) {
58 Ok(user_token) => user_token,
59 Err(_) => {
60 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#[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
78pub fn has_flag(args: &[String], flag: &str) -> bool {
80 args.iter().any(|arg| arg == flag)
81}
82
83pub 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
105pub 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
113pub fn parse_token_type(args: &[String]) -> Result<Option<TokenType>, String> {
116 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 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 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
168pub 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 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 commands::apply_filters(&mut response, &filters);
199
200 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 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 commands::apply_filters(&mut response, &filters);
235
236 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 let types = get_option(args, "--types=");
251 let profile = get_option(args, "--profile=");
252 let filter_strings = get_all_options(args, "--filter=");
253
254 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 commands::apply_filters(&mut response, &filters);
269
270 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 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 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 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 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 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 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 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 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 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 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 let args1 = vec!["--token-type=user".to_string()];
705 assert_eq!(parse_token_type(&args1).unwrap(), Some(TokenType::User));
706
707 let args2 = vec!["--token-type=bot".to_string()];
709 assert_eq!(parse_token_type(&args2).unwrap(), Some(TokenType::Bot));
710
711 let args3 = vec!["--token-type".to_string(), "user".to_string()];
713 assert_eq!(parse_token_type(&args3).unwrap(), Some(TokenType::User));
714
715 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}