#![allow(clippy::unwrap_used)]
use assert_cmd::Command;
use predicates::str::contains;
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn auth_status_redacts_the_token() {
for args in [vec!["auth", "status"], vec!["auth", "status", "--json"]] {
let out = Command::cargo_bin("bb")
.unwrap()
.args(&args)
.env("BB_EMAIL", "dev@example.com")
.env("BB_TOKEN", "ATATT3xFfGF0_super_secret_value")
.env("BB_API_BASE", "http://127.0.0.1:1")
.output()
.unwrap();
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains("ATATT3xFfGF0"),
"token leaked for {args:?}: {combined}"
);
assert!(
!combined.contains("super_secret"),
"token body leaked for {args:?}: {combined}"
);
}
}
#[test]
fn auth_status_shows_email_and_redacted_tail() {
Command::cargo_bin("bb")
.unwrap()
.args(["auth", "status"])
.env("BB_EMAIL", "dev@example.com")
.env("BB_TOKEN", "ATATT3xFfGF0abcd")
.env("BB_API_BASE", "http://127.0.0.1:1")
.assert()
.stdout(contains("dev@example.com"))
.stdout(contains("****abcd"));
}
#[test]
fn auth_status_without_credentials_exits_two() {
Command::cargo_bin("bb")
.unwrap()
.args(["auth", "status"])
.env("BB_EMAIL", "")
.env("BB_TOKEN", "")
.env("BB_KEYRING_DISABLE", "1")
.assert()
.code(2)
.stderr(contains("bb auth login"));
}
#[test]
fn auth_logout_json_emits_parseable_json() {
let out = Command::cargo_bin("bb")
.unwrap()
.args(["auth", "logout", "--json"])
.env("BB_KEYRING_DISABLE", "1")
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let _value: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout did not parse as JSON: {e}\nstdout: {stdout}"));
}
#[test]
fn auth_login_non_tty_names_required_flags() {
let assert = Command::cargo_bin("bb")
.unwrap()
.args(["auth", "login"])
.env("BB_API_BASE", "http://127.0.0.1:1")
.write_stdin("")
.assert()
.failure();
let output = assert.get_output();
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(combined.contains("--email"), "missing --email: {combined}");
assert!(
combined.contains("--token-stdin"),
"missing --token-stdin: {combined}"
);
}
#[test]
fn auth_login_non_tty_never_leaks_secret() {
for args in [vec!["auth", "login"], vec!["auth", "login", "--json"]] {
let out = Command::cargo_bin("bb")
.unwrap()
.args(&args)
.env("BB_API_BASE", "http://127.0.0.1:1")
.write_stdin("super-secret-token-value")
.output()
.unwrap();
let combined = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
assert!(
!combined.contains("super-secret-token-value"),
"secret leaked for {args:?}: {combined}"
);
}
}
#[test]
fn auth_help_mentions_api_token_not_app_password() {
let out = Command::cargo_bin("bb")
.unwrap()
.args(["auth", "login", "--help"])
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout).to_lowercase();
assert!(text.contains("api token"));
assert!(!text.contains("app password"));
}
#[tokio::test]
async fn login_verifies_the_token_and_reports_the_account() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/user"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"display_name": "Dev Person"})),
)
.mount(&server)
.await;
let out = Command::cargo_bin("bb")
.unwrap()
.args([
"auth",
"login",
"--email",
"dev@example.com",
"--token-stdin",
])
.env("BB_API_BASE", server.uri())
.env("BB_KEYRING_DISABLE", "1")
.env("NO_COLOR", "1")
.env_remove("BB_EMAIL")
.env_remove("BB_TOKEN")
.write_stdin("ATATT3xFfGF0abcd")
.output()
.unwrap();
assert!(out.status.success(), "login failed: {out:?}");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Dev Person"), "no account name: {stdout}");
assert!(stdout.contains("****abcd"), "no redacted tail: {stdout}");
assert!(
!stdout.contains("ATATT3xFfGF0abcd"),
"token leaked: {stdout}"
);
}
#[tokio::test]
async fn login_json_emits_pure_json() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/user"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"display_name": "Dev Person"})),
)
.mount(&server)
.await;
let out = Command::cargo_bin("bb")
.unwrap()
.args([
"auth",
"login",
"--email",
"dev@example.com",
"--token-stdin",
"--json",
])
.env("BB_API_BASE", server.uri())
.env("BB_KEYRING_DISABLE", "1")
.env("NO_COLOR", "1")
.env_remove("BB_EMAIL")
.env_remove("BB_TOKEN")
.write_stdin("ATATT3xFfGF0abcd")
.output()
.unwrap();
assert!(out.status.success(), "login failed: {out:?}");
let stdout = String::from_utf8_lossy(&out.stdout);
let value: serde_json::Value = serde_json::from_str(stdout.trim())
.unwrap_or_else(|e| panic!("stdout was not pure JSON: {e}\nstdout: {stdout}"));
assert_eq!(value["account"], "Dev Person");
assert_eq!(value["email"], "dev@example.com");
assert_eq!(value["token"], "****abcd");
}
#[test]
fn login_rejects_an_email_without_an_at_sign() {
Command::cargo_bin("bb")
.unwrap()
.args(["auth", "login", "--email", "not-an-email", "--token-stdin"])
.env("BB_API_BASE", "http://127.0.0.1:1")
.env("BB_KEYRING_DISABLE", "1")
.env("NO_COLOR", "1")
.env_remove("BB_EMAIL")
.env_remove("BB_TOKEN")
.write_stdin("some-token")
.assert()
.failure()
.stderr(contains("atlassian account email"));
}
#[test]
fn logout_warns_about_a_legacy_plaintext_credential_file() {
let home = tempdir().unwrap();
std::fs::write(
home.path().join(".bitbucket-rest-cli-config.json"),
"{\"token\":\"whatever\"}",
)
.unwrap();
Command::cargo_bin("bb")
.unwrap()
.args(["auth", "logout"])
.env("HOME", home.path())
.env("BB_KEYRING_DISABLE", "1")
.env("NO_COLOR", "1")
.assert()
.success()
.stderr(contains("legacy plaintext credential file"));
}
#[tokio::test]
async fn status_reports_an_unverified_account_when_the_identity_check_fails() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/user"))
.respond_with(ResponseTemplate::new(500))
.mount(&server)
.await;
let out = Command::cargo_bin("bb")
.unwrap()
.args(["auth", "status", "--json"])
.env("BB_EMAIL", "dev@example.com")
.env("BB_TOKEN", "ATATT3xFfGF0abcd")
.env("BB_API_BASE", server.uri())
.env("BB_KEYRING_DISABLE", "1")
.output()
.unwrap();
assert!(out.status.success(), "status failed: {out:?}");
let stdout = String::from_utf8_lossy(&out.stdout);
let value: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert!(
value["account"].is_null(),
"expected a null account, got {value}"
);
}