use std::process::Command;
use tempfile::tempdir;
#[test]
fn test_cli_version() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "--version"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("atlassian-cli"));
const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
assert!(
stdout.contains(EXPECTED_VERSION),
"CLI should output version {}, got: {}",
EXPECTED_VERSION,
stdout
);
}
#[test]
fn test_cli_help() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Usage:"));
assert!(stdout.contains("jira"));
assert!(stdout.contains("confluence"));
assert!(stdout.contains("bitbucket"));
assert!(stdout.contains("bb") || stdout.contains("[alias"));
}
#[test]
fn test_jira_help() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "jira", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Jira commands"));
assert!(stdout.contains("search"));
assert!(stdout.contains("create"));
}
#[test]
fn test_confluence_help() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "confluence", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Confluence commands"));
}
#[test]
fn test_auth_help() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "auth", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Authentication commands"));
}
#[test]
fn test_output_format_flag() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "--format", "json", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
}
#[test]
fn test_invalid_command() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "nonexistent"])
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("unrecognized subcommand") || stderr.contains("error:"));
}
#[test]
fn test_bb_alias_works() {
let bb_output = Command::new("cargo")
.args(["run", "--quiet", "--", "bb", "--help"])
.output()
.expect("Failed to execute bb alias");
let bitbucket_output = Command::new("cargo")
.args(["run", "--quiet", "--", "bitbucket", "--help"])
.output()
.expect("Failed to execute bitbucket command");
assert!(bb_output.status.success());
assert!(bitbucket_output.status.success());
let bb_help = String::from_utf8_lossy(&bb_output.stdout);
let bitbucket_help = String::from_utf8_lossy(&bitbucket_output.stdout);
assert!(bb_help.contains("repo") || bb_help.contains("Repo"));
assert!(bb_help.contains("pipeline") || bb_help.contains("Pipeline"));
assert!(bitbucket_help.contains("repo") || bitbucket_help.contains("Repo"));
assert!(bitbucket_help.contains("pipeline") || bitbucket_help.contains("Pipeline"));
}
#[test]
fn test_bitbucket_alias_backwards_compatible() {
let bitbucket_output = Command::new("cargo")
.args(["run", "--quiet", "--", "bitbucket", "whoami"])
.env("ATLASSIAN_CLI_PROFILE", "nonexistent")
.output()
.expect("Failed to execute bitbucket whoami");
let bb_output = Command::new("cargo")
.args(["run", "--quiet", "--", "bb", "whoami"])
.env("ATLASSIAN_CLI_PROFILE", "nonexistent")
.output()
.expect("Failed to execute bb whoami");
assert_eq!(
bitbucket_output.status.success(),
bb_output.status.success(),
"Both commands should have identical exit status"
);
let bitbucket_stderr = String::from_utf8_lossy(&bitbucket_output.stderr);
let bb_stderr = String::from_utf8_lossy(&bb_output.stderr);
assert_eq!(
bitbucket_stderr, bb_stderr,
"Both commands should produce identical error messages"
);
}
#[test]
fn test_bitbucket_only_profile_no_base_url_error() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("config.yaml");
let config_content = r#"
default_profile: bbonly
profiles:
bbonly:
email: test@example.com
workspace: myworkspace
"#;
std::fs::write(&config_path, config_content).expect("Failed to write config");
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"--config",
config_path.to_str().unwrap(),
"bitbucket",
"repo",
"list",
])
.env("ATLASSIAN_CLI_BITBUCKET_TOKEN_BBONLY", "fake-token")
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("missing a base_url"),
"Bitbucket-only profile should not require base_url. Got: {stderr}"
);
}
#[test]
fn test_auth_login_help_shows_bearer_flag() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "auth", "login", "--help"])
.output()
.expect("Failed to execute auth login help");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--bearer"),
"auth login help should mention --bearer flag. Got:\n{stdout}"
);
assert!(
stdout.contains("deprecated") || stdout.contains("App passwords"),
"auth login help should mention app password deprecation. Got:\n{stdout}"
);
assert!(
stdout.contains("access token"),
"auth login help should mention access tokens. Got:\n{stdout}"
);
}
#[test]
fn test_bitbucket_bearer_profile_no_email_required() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("config.yaml");
let config_content = r#"
default_profile: ci
profiles:
ci:
workspace: myworkspace
bitbucket_token_type: bearer
"#;
std::fs::write(&config_path, config_content).expect("Failed to write config");
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"--config",
config_path.to_str().unwrap(),
"bitbucket",
"repo",
"list",
])
.env("ATLASSIAN_CLI_BITBUCKET_TOKEN_CI", "fake-bearer-token")
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("missing an email"),
"Bearer profile should not require email. Got: {stderr}"
);
}
#[test]
fn test_jira_still_requires_base_url() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("config.yaml");
let config_content = r#"
default_profile: nobaseurl
profiles:
nobaseurl:
email: test@example.com
"#;
std::fs::write(&config_path, config_content).expect("Failed to write config");
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"--config",
config_path.to_str().unwrap(),
"jira",
"issue",
"search",
])
.env("ATLASSIAN_CLI_TOKEN_NOBASEURL", "fake-token")
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("missing a base_url") || stderr.contains("base-url"),
"Jira commands should require base_url. Got: {stderr}"
);
}
#[test]
fn test_jira_issue_create_help_mentions_custom_fields() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "jira", "issue", "create", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("KEY=JSON_VALUE"),
"expected --field value_name in help, got: {stdout}"
);
assert!(
stdout.contains("jira fields list"),
"expected fields-discovery hint in help, got: {stdout}"
);
}
#[test]
fn test_jira_attachment_help_lists_subcommands() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "jira", "attachment", "--help"])
.output()
.expect("Failed to execute command");
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
for sub in ["list", "get", "download", "upload", "delete"] {
assert!(
stdout.contains(sub),
"attachment help should list `{sub}`, got:\n{stdout}"
);
}
}
#[test]
fn test_jira_attachment_download_requires_a_source() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "jira", "attachment", "download"])
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("required"),
"expected a required-argument error, got: {stderr}"
);
}
#[test]
fn test_jira_attachment_download_rejects_id_with_issue() {
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"jira",
"attachment",
"download",
"10001",
"--issue",
"TEST-1",
])
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot be used with"),
"expected a conflict error, got: {stderr}"
);
}
#[test]
fn test_jira_attachment_download_rejects_dir_without_issue() {
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"jira",
"attachment",
"download",
"10001",
"--dir",
"./out",
])
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot be used with"),
"expected a conflict error, got: {stderr}"
);
}
#[test]
fn test_jira_attachment_download_rejects_output_with_issue() {
let output = Command::new("cargo")
.args([
"run",
"--quiet",
"--",
"jira",
"attachment",
"download",
"--issue",
"TEST-1",
"--output",
"f.bin",
])
.output()
.expect("Failed to execute command");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("cannot be used with"),
"expected a conflict error, got: {stderr}"
);
}
#[test]
fn test_profile_and_config_are_accepted_after_the_subcommand() {
let temp_dir = tempdir().expect("Failed to create temp dir");
let config_path = temp_dir.path().join("config.yaml");
std::fs::write(
&config_path,
"default_profile: t\nprofiles:\n t:\n email: a@b.c\n base_url: http://127.0.0.1:1\n",
)
.expect("Failed to write config");
for args in [
vec!["jira", "issue", "get", "T-1"],
vec!["confluence", "attachment", "list", "123"],
vec!["jira", "attachment", "download", "10001", "--output", "-"],
] {
let output = Command::new("cargo")
.args(["run", "--quiet", "--"])
.args(&args)
.args(["--profile", "t", "--config", config_path.to_str().unwrap()])
.env("ATLASSIAN_CLI_TOKEN_T", "x")
.output()
.expect("Failed to execute command");
assert_ne!(
output.status.code(),
Some(2),
"trailing --profile/--config rejected for {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn test_auth_login_without_flags_is_not_a_parse_error() {
let output = Command::new("cargo")
.args(["run", "--quiet", "--", "auth", "login"])
.stdin(std::process::Stdio::null())
.output()
.expect("Failed to execute command");
assert_ne!(
output.status.code(),
Some(2),
"bare `auth login` should reach the command, not fail argument parsing"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--profile"),
"should name the flag to pass when it cannot prompt, got: {stderr}"
);
assert!(
!output.status.success(),
"it must still fail without a terminal rather than proceeding"
);
}