diff --git a/cli/src/commands/rate_limit.rs b/cli/src/commands/rate_limit.rs index 9816baa..a548c15 100644 --- a/cli/src/commands/rate_limit.rs +++ b/cli/src/commands/rate_limit.rs @@ -63,3 +63,47 @@ fn format_timestamp(timestamp: i32) -> String { dt.format("%Y-%m-%d %H:%M:%S %Z").to_string() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_timestamp_valid() { + // 2024-01-15 00:00:00 UTC + let timestamp = 1705276800; + let result = format_timestamp(timestamp); + + // Should contain date components (timezone-independent check) + assert!(result.contains("2024-01-")); + assert!(result.contains(":")); + } + + #[test] + fn test_format_timestamp_epoch() { + // Unix epoch: 1970-01-01 00:00:00 UTC + let timestamp = 0; + let result = format_timestamp(timestamp); + + // Should contain 1970 (timezone-independent check) + assert!(result.contains("1970-")); + } + + #[test] + fn test_format_timestamp_format() { + let timestamp = 1705276800; + let result = format_timestamp(timestamp); + + // Should match format: YYYY-MM-DD HH:MM:SS TZ + let parts: Vec<&str> = result.split_whitespace().collect(); + assert_eq!(parts.len(), 3, "Expected 3 parts: date, time, timezone"); + + // Date format check + let date_parts: Vec<&str> = parts[0].split('-').collect(); + assert_eq!(date_parts.len(), 3, "Date should have 3 parts"); + + // Time format check + let time_parts: Vec<&str> = parts[1].split(':').collect(); + assert_eq!(time_parts.len(), 3, "Time should have 3 parts"); + } +} diff --git a/cli/src/commands/watching.rs b/cli/src/commands/watching.rs index 0a839d5..54f3e32 100644 --- a/cli/src/commands/watching.rs +++ b/cli/src/commands/watching.rs @@ -182,3 +182,34 @@ fn parse_issue_id_or_key(input: &str) -> anyhow::Result { Ok(IssueIdOrKey::Key(IssueKey::from_str(input)?)) } } + +#[cfg(test)] +mod tests { + use super::*; + use backlog_core::identifier::Identifier; + + #[test] + fn test_parse_issue_id_or_key_numeric_id() { + let result = parse_issue_id_or_key("123").unwrap(); + match result { + IssueIdOrKey::Id(id) => assert_eq!(id.value(), 123), + IssueIdOrKey::Key(_) => panic!("Expected Id variant"), + } + } + + #[test] + fn test_parse_issue_id_or_key_string_key() { + let result = parse_issue_id_or_key("PROJECT-456").unwrap(); + match result { + IssueIdOrKey::Key(key) => assert_eq!(key.to_string(), "PROJECT-456"), + IssueIdOrKey::Id(_) => panic!("Expected Key variant"), + } + } + + #[test] + fn test_parse_issue_id_or_key_invalid_format() { + // Invalid key format (no hyphen with number) + let result = parse_issue_id_or_key("invalid"); + assert!(result.is_err()); + } +} diff --git a/cli/src/commands/webhook.rs b/cli/src/commands/webhook.rs index acb902b..37801d4 100644 --- a/cli/src/commands/webhook.rs +++ b/cli/src/commands/webhook.rs @@ -493,3 +493,29 @@ async fn delete_webhook( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_escape_csv_no_special_chars() { + let input = "simple text"; + let result = escape_csv(input); + assert_eq!(result, "simple text"); + } + + #[test] + fn test_escape_csv_with_comma() { + let input = "hello, world"; + let result = escape_csv(input); + assert_eq!(result, "\"hello, world\""); + } + + #[test] + fn test_escape_csv_with_quotes() { + let input = "say \"hello\""; + let result = escape_csv(input); + assert_eq!(result, "\"say \"\"hello\"\"\""); + } +} diff --git a/cli/tests/star_test.rs b/cli/tests/star_test.rs new file mode 100644 index 0000000..21225d6 --- /dev/null +++ b/cli/tests/star_test.rs @@ -0,0 +1,80 @@ +use clap::{Parser, Subcommand}; + +#[derive(Parser, Debug)] +struct Cli { + #[clap(subcommand)] + command: StarCommands, +} + +#[derive(Subcommand, Debug)] +enum StarCommands { + Add { + #[clap(subcommand)] + target: StarTarget, + }, +} + +#[derive(Subcommand, Debug, PartialEq)] +enum StarTarget { + Issue { issue_id: u32 }, + Comment { issue_id: u32, comment_id: u32 }, + Wiki { wiki_id: u32 }, + Pr { pr_id: u32 }, + PrComment { pr_comment_id: u32 }, +} + +#[test] +fn test_star_add_issue() { + let args = Cli::try_parse_from(["prog", "add", "issue", "123"]).unwrap(); + match args.command { + StarCommands::Add { target } => { + assert_eq!(target, StarTarget::Issue { issue_id: 123 }); + } + } +} + +#[test] +fn test_star_add_comment() { + let args = Cli::try_parse_from(["prog", "add", "comment", "100", "200"]).unwrap(); + match args.command { + StarCommands::Add { target } => { + assert_eq!( + target, + StarTarget::Comment { + issue_id: 100, + comment_id: 200 + } + ); + } + } +} + +#[test] +fn test_star_add_wiki() { + let args = Cli::try_parse_from(["prog", "add", "wiki", "456"]).unwrap(); + match args.command { + StarCommands::Add { target } => { + assert_eq!(target, StarTarget::Wiki { wiki_id: 456 }); + } + } +} + +#[test] +fn test_star_add_pr() { + let args = Cli::try_parse_from(["prog", "add", "pr", "789"]).unwrap(); + match args.command { + StarCommands::Add { target } => { + assert_eq!(target, StarTarget::Pr { pr_id: 789 }); + } + } +} + +#[test] +fn test_star_add_pr_comment() { + let args = Cli::try_parse_from(["prog", "add", "pr-comment", "321"]).unwrap(); + match args.command { + StarCommands::Add { target } => { + assert_eq!(target, StarTarget::PrComment { pr_comment_id: 321 }); + } + } +} diff --git a/cli/tests/watching_test.rs b/cli/tests/watching_test.rs new file mode 100644 index 0000000..a7165ac --- /dev/null +++ b/cli/tests/watching_test.rs @@ -0,0 +1,49 @@ +use clap::{Args, Parser, Subcommand}; + +#[derive(Parser, Debug)] +struct Cli { + #[clap(subcommand)] + command: WatchingSubcommand, +} + +#[derive(Subcommand, Debug)] +enum WatchingSubcommand { + Get(GetWatchingArgs), + Add(AddWatchingArgs), +} + +#[derive(Args, Debug, Clone)] +struct GetWatchingArgs { + watching_id: u32, +} + +#[derive(Args, Debug, Clone)] +struct AddWatchingArgs { + issue: String, + #[arg(short, long)] + note: Option, +} + +#[test] +fn test_watching_get_command() { + let args = Cli::try_parse_from(["prog", "get", "12345"]).unwrap(); + match args.command { + WatchingSubcommand::Get(args) => { + assert_eq!(args.watching_id, 12345); + } + _ => panic!("Expected Get command"), + } +} + +#[test] +fn test_watching_add_with_note() { + let args = + Cli::try_parse_from(["prog", "add", "PROJECT-123", "--note", "Important issue"]).unwrap(); + match args.command { + WatchingSubcommand::Add(args) => { + assert_eq!(args.issue, "PROJECT-123"); + assert_eq!(args.note, Some("Important issue".to_string())); + } + _ => panic!("Expected Add command"), + } +} diff --git a/cli/tests/webhook_test.rs b/cli/tests/webhook_test.rs new file mode 100644 index 0000000..a33342d --- /dev/null +++ b/cli/tests/webhook_test.rs @@ -0,0 +1,227 @@ +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Parser, Debug)] +struct Cli { + #[clap(subcommand)] + command: WebhookCommands, +} + +#[derive(Subcommand, Debug)] +enum WebhookCommands { + #[clap(alias = "ls")] + List { + #[arg(short, long)] + project: String, + #[arg(short, long, value_enum, default_value = "table")] + format: OutputFormat, + }, + Get { + #[arg(short, long)] + project: String, + #[arg(short, long)] + webhook_id: u32, + #[arg(short, long, value_enum, default_value = "table")] + format: OutputFormat, + }, + Add { + #[arg(short, long)] + project: String, + #[arg(short, long)] + name: String, + #[arg(short = 'u', long)] + hook_url: String, + #[arg(short, long)] + description: Option, + #[arg(long)] + all_event: Option, + #[arg(long, value_delimiter = ',')] + activity_type_ids: Option>, + }, + Update { + #[arg(short, long)] + project: String, + #[arg(short = 'w', long)] + webhook_id: u32, + #[arg(long)] + name: Option, + #[arg(long)] + description: Option, + #[arg(long)] + hook_url: Option, + #[arg(long)] + all_event: Option, + #[arg(long, value_delimiter = ',')] + activity_type_ids: Option>, + }, + #[clap(alias = "rm")] + Delete { + #[arg(short, long)] + project: String, + #[arg(short = 'w', long)] + webhook_id: u32, + }, +} + +#[derive(Debug, Clone, Copy, ValueEnum, PartialEq)] +enum OutputFormat { + Table, + Json, + Csv, +} + +#[test] +fn test_webhook_list_command() { + let args = Cli::try_parse_from(["prog", "list", "--project", "TEST"]).unwrap(); + match args.command { + WebhookCommands::List { project, format } => { + assert_eq!(project, "TEST"); + assert_eq!(format, OutputFormat::Table); + } + _ => panic!("Expected List command"), + } +} + +#[test] +fn test_webhook_get_command() { + let args = + Cli::try_parse_from(["prog", "get", "--project", "TEST", "--webhook-id", "123"]).unwrap(); + match args.command { + WebhookCommands::Get { + project, + webhook_id, + format, + } => { + assert_eq!(project, "TEST"); + assert_eq!(webhook_id, 123); + assert_eq!(format, OutputFormat::Table); + } + _ => panic!("Expected Get command"), + } +} + +#[test] +fn test_webhook_add_command() { + let args = Cli::try_parse_from([ + "prog", + "add", + "--project", + "TEST", + "--name", + "My Webhook", + "--hook-url", + "https://example.com/hook", + ]) + .unwrap(); + match args.command { + WebhookCommands::Add { + project, + name, + hook_url, + description, + all_event, + activity_type_ids, + } => { + assert_eq!(project, "TEST"); + assert_eq!(name, "My Webhook"); + assert_eq!(hook_url, "https://example.com/hook"); + assert_eq!(description, None); + assert_eq!(all_event, None); + assert_eq!(activity_type_ids, None); + } + _ => panic!("Expected Add command"), + } +} + +#[test] +fn test_webhook_update_minimal() { + let args = Cli::try_parse_from([ + "prog", + "update", + "--project", + "TEST", + "--webhook-id", + "456", + "--name", + "Updated Name", + ]) + .unwrap(); + match args.command { + WebhookCommands::Update { + project, + webhook_id, + name, + description, + hook_url, + all_event, + activity_type_ids, + } => { + assert_eq!(project, "TEST"); + assert_eq!(webhook_id, 456); + assert_eq!(name, Some("Updated Name".to_string())); + assert_eq!(description, None); + assert_eq!(hook_url, None); + assert_eq!(all_event, None); + assert_eq!(activity_type_ids, None); + } + _ => panic!("Expected Update command"), + } +} + +#[test] +fn test_webhook_update_full() { + let args = Cli::try_parse_from([ + "prog", + "update", + "--project", + "TEST", + "--webhook-id", + "789", + "--name", + "Full Update", + "--description", + "A description", + "--hook-url", + "https://new.example.com/hook", + "--all-event", + "true", + "--activity-type-ids", + "1,2,3", + ]) + .unwrap(); + match args.command { + WebhookCommands::Update { + project, + webhook_id, + name, + description, + hook_url, + all_event, + activity_type_ids, + } => { + assert_eq!(project, "TEST"); + assert_eq!(webhook_id, 789); + assert_eq!(name, Some("Full Update".to_string())); + assert_eq!(description, Some("A description".to_string())); + assert_eq!(hook_url, Some("https://new.example.com/hook".to_string())); + assert_eq!(all_event, Some(true)); + assert_eq!(activity_type_ids, Some(vec![1, 2, 3])); + } + _ => panic!("Expected Update command"), + } +} + +#[test] +fn test_webhook_delete_command() { + let args = Cli::try_parse_from(["prog", "delete", "--project", "TEST", "--webhook-id", "999"]) + .unwrap(); + match args.command { + WebhookCommands::Delete { + project, + webhook_id, + } => { + assert_eq!(project, "TEST"); + assert_eq!(webhook_id, 999); + } + _ => panic!("Expected Delete command"), + } +}