#![expect(clippy::unwrap_used)]
use super::*;
use crate::error::BzrError;
use crate::test_helpers::capture_stdout;
use crate::types::AuthMethod;
async fn setup_config_env() -> (tokio::sync::MutexGuard<'static, ()>, tempfile::TempDir) {
let lock = crate::ENV_LOCK.lock().await;
let tmp = tempfile::TempDir::new().unwrap();
let config_dir = tmp.path().join("bzr");
std::fs::create_dir_all(&config_dir).unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
(lock, tmp)
}
async fn seed_inline_server(name: &str, url: &str, api_key: &str) {
execute(
&ConfigAction::SetServer {
name: name.into(),
url: url.into(),
api_key: Some(api_key.into()),
api_key_env: None,
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
}
#[cfg(feature = "keyring")]
async fn seed_keyring_secret(server_name: &str, secret: &str) {
unsafe { std::env::set_var("BZR_KEYRING_TEST_SECRET", secret) };
execute(
&ConfigAction::SetKeyring {
name: server_name.into(),
service: None,
account: None,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
unsafe { std::env::remove_var("BZR_KEYRING_TEST_SECRET") };
}
#[tokio::test]
async fn set_default_on_empty_config_returns_error() {
let (_lock, _tmp) = setup_config_env().await;
let config = Config::default();
config.save().unwrap();
let result = execute(
&ConfigAction::SetDefault {
name: "nonexistent".into(),
},
None,
OutputFormat::Table,
None,
)
.await;
assert!(result.is_err());
assert!(
matches!(result.unwrap_err(), BzrError::Config(_)),
"expected Config error for unknown server"
);
}
#[tokio::test]
async fn first_set_server_auto_sets_default() {
let (_lock, _tmp) = setup_config_env().await;
let (result, output) = capture_stdout(execute(
&ConfigAction::SetServer {
name: "first".into(),
url: "https://first.example.com".into(),
api_key: Some("first-key-1234567890".into()),
api_key_env: None,
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
))
.await;
result.unwrap();
let config = Config::load().unwrap();
assert_eq!(config.default_server.as_deref(), Some("first"));
assert!(config.servers.contains_key("first"));
let parsed: serde_json::Value = crate::test_helpers::extract_json(&output);
assert_eq!(parsed["is_default"], true);
}
#[tokio::test]
async fn second_set_server_does_not_override_default() {
let (_lock, _tmp) = setup_config_env().await;
execute(
&ConfigAction::SetServer {
name: "first".into(),
url: "https://first.example.com".into(),
api_key: Some("first-key-1234567890".into()),
api_key_env: None,
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Table,
None,
)
.await
.unwrap();
execute(
&ConfigAction::SetServer {
name: "second".into(),
url: "https://second.example.com".into(),
api_key: Some("second-key-1234567890".into()),
api_key_env: None,
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Table,
None,
)
.await
.unwrap();
let config = Config::load().unwrap();
assert_eq!(
config.default_server.as_deref(),
Some("first"),
"second server should not override existing default"
);
assert_eq!(config.servers.len(), 2);
}
#[tokio::test]
async fn set_server_update_preserves_existing_default() {
let (_lock, _tmp) = setup_config_env().await;
for (name, url) in [
("first", "https://first.example.com"),
("second", "https://second.example.com"),
] {
seed_inline_server(name, url, &format!("{name}-key-1234567890")).await;
}
let (result, output) = capture_stdout(execute(
&ConfigAction::SetServer {
name: "second".into(),
url: "https://updated.example.com".into(),
api_key: Some("updated-key-1234567890".into()),
api_key_env: None,
email: Some("ops@example.com".into()),
auth_method: Some(AuthMethod::QueryParam),
tls_insecure: true,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok());
let parsed = crate::test_helpers::extract_json(&output);
assert_eq!(parsed["name"], "second");
assert_eq!(parsed["action"], "updated");
let config = Config::load().unwrap();
assert_eq!(config.default_server.as_deref(), Some("first"));
let server = &config.servers["second"];
assert_eq!(server.url, "https://updated.example.com");
assert_eq!(server.email.as_deref(), Some("ops@example.com"));
assert_eq!(server.auth_method, Some(AuthMethod::QueryParam));
assert!(server.tls_insecure);
}
#[tokio::test]
async fn set_default_persists_selected_server() {
let (_lock, _tmp) = setup_config_env().await;
for (name, url) in [
("first", "https://first.example.com"),
("second", "https://second.example.com"),
] {
seed_inline_server(name, url, &format!("{name}-key-1234567890")).await;
}
let (result, output) = capture_stdout(execute(
&ConfigAction::SetDefault {
name: "second".into(),
},
None,
OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok());
let parsed = crate::test_helpers::extract_json(&output);
assert_eq!(parsed["name"], "second");
assert_eq!(parsed["action"], "updated");
assert_eq!(
Config::load().unwrap().default_server.as_deref(),
Some("second")
);
}
#[tokio::test]
async fn show_json_includes_populated_server_details() {
let (_lock, _tmp) = setup_config_env().await;
execute(
&ConfigAction::SetServer {
name: "prod".into(),
url: "https://prod.example.com".into(),
api_key: Some("abcdef1234567890".into()),
api_key_env: None,
email: Some("admin@example.com".into()),
auth_method: Some(AuthMethod::Header),
tls_insecure: true,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
let (result, output) =
capture_stdout(execute(&ConfigAction::Show, None, OutputFormat::Json, None)).await;
assert!(result.is_ok());
let parsed = crate::test_helpers::extract_json(&output);
assert_eq!(parsed["default_server"], "prod");
assert_eq!(parsed["servers"]["prod"]["url"], "https://prod.example.com");
assert_eq!(parsed["servers"]["prod"]["email"], "admin@example.com");
assert_eq!(parsed["servers"]["prod"]["auth_method"], "header");
assert_eq!(parsed["servers"]["prod"]["tls_insecure"], true);
assert_eq!(parsed["servers"]["prod"]["api_key"], "abcdef12...");
assert_eq!(parsed["servers"]["prod"]["api_key_source"], "inline");
}
#[tokio::test]
async fn set_server_with_env_var_persists_env_source() {
let (_lock, _tmp) = setup_config_env().await;
execute(
&ConfigAction::SetServer {
name: "prod".into(),
url: "https://prod.example.com".into(),
api_key: None,
api_key_env: Some("BZR_API_KEY".into()),
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
let config = Config::load().unwrap();
let server = &config.servers["prod"];
assert_eq!(server.api_key, None);
assert_eq!(server.api_key_env.as_deref(), Some("BZR_API_KEY"));
}
#[cfg(feature = "keyring")]
#[tokio::test]
async fn set_keyring_stores_secret_and_rewrites_config() {
::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
let (_lock, _tmp) = setup_config_env().await;
seed_inline_server("prod", "https://prod.example.com", "old-inline-value").await;
seed_keyring_secret("prod", "new-keyring-value").await;
let config = Config::load().unwrap();
let server = &config.servers["prod"];
assert!(server.api_key.is_none());
assert!(server.api_key_env.is_none());
assert!(server.api_key_keyring.is_some());
assert_eq!(server.resolve_api_key("prod").unwrap(), "new-keyring-value");
crate::credentials::keyring::delete("bzr", "prod").unwrap();
}
#[cfg(feature = "keyring")]
#[tokio::test]
async fn migrate_to_keyring_from_inline_rewrites_config() {
::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
let (_lock, _tmp) = setup_config_env().await;
seed_inline_server(
"migrate-inline",
"https://migrate-inline.example.com",
"inline-secret-value",
)
.await;
execute(
&ConfigAction::MigrateToKeyring {
name: "migrate-inline".into(),
service: None,
account: None,
yes: true,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
let config = Config::load().unwrap();
let server = &config.servers["migrate-inline"];
assert!(server.api_key.is_none(), "inline key should be cleared");
assert!(server.api_key_keyring.is_some());
assert_eq!(
server.resolve_api_key("migrate-inline").unwrap(),
"inline-secret-value"
);
crate::credentials::keyring::delete("bzr", "migrate-inline").unwrap();
}
#[cfg(feature = "keyring")]
#[tokio::test]
async fn migrate_to_keyring_from_env_preserves_config() {
::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
let (_lock, _tmp) = setup_config_env().await;
unsafe { std::env::set_var("BZR_MIGRATE_TEST_KEY", "env-secret-value") };
execute(
&ConfigAction::SetServer {
name: "migrate-env".into(),
url: "https://migrate-env.example.com".into(),
api_key: None,
api_key_env: Some("BZR_MIGRATE_TEST_KEY".into()),
email: None,
auth_method: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_now: false,
tls_pin_clear: false,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
execute(
&ConfigAction::MigrateToKeyring {
name: "migrate-env".into(),
service: None,
account: None,
yes: true,
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
unsafe { std::env::remove_var("BZR_MIGRATE_TEST_KEY") };
let config = Config::load().unwrap();
let server = &config.servers["migrate-env"];
assert_eq!(server.api_key_env.as_deref(), Some("BZR_MIGRATE_TEST_KEY"));
assert!(server.api_key_keyring.is_none());
let stored = crate::credentials::keyring::retrieve("bzr", "migrate-env").unwrap();
assert_eq!(stored, "env-secret-value");
crate::credentials::keyring::delete("bzr", "migrate-env").unwrap();
}
#[cfg(feature = "keyring")]
#[tokio::test]
async fn migrate_to_keyring_from_keyring_errors_before_storing() {
::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
let (_lock, _tmp) = setup_config_env().await;
seed_inline_server("migrate-already-kr", "https://example.com", "init").await;
seed_keyring_secret("migrate-already-kr", "original-secret").await;
let result = execute(
&ConfigAction::MigrateToKeyring {
name: "migrate-already-kr".into(),
service: Some("different-service".into()),
account: None,
yes: true,
},
None,
OutputFormat::Json,
None,
)
.await;
assert!(result.is_err());
let lookup = crate::credentials::keyring::retrieve("different-service", "migrate-already-kr");
assert!(
lookup.is_err(),
"no entry should have been stored at the different-service location"
);
crate::credentials::keyring::delete("bzr", "migrate-already-kr").unwrap();
}
#[tokio::test]
async fn migrate_to_keyring_without_yes_errors() {
let (_lock, _tmp) = setup_config_env().await;
seed_inline_server(
"migrate-noyes",
"https://migrate-noyes.example.com",
"secret",
)
.await;
let result = execute(
&ConfigAction::MigrateToKeyring {
name: "migrate-noyes".into(),
service: None,
account: None,
yes: false,
},
None,
OutputFormat::Json,
None,
)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::error::BzrError::InputValidation(_)));
assert!(err.to_string().contains("--yes"));
}
#[cfg(feature = "keyring")]
#[tokio::test]
async fn unset_keyring_removes_secret_and_clears_config() {
::keyring::set_default_credential_builder(::keyring::mock::default_credential_builder());
let (_lock, _tmp) = setup_config_env().await;
seed_inline_server("unset-test", "https://unset-test.example.com", "tmp").await;
seed_keyring_secret("unset-test", "unset-test-secret").await;
execute(
&ConfigAction::UnsetKeyring {
name: "unset-test".into(),
},
None,
OutputFormat::Json,
None,
)
.await
.unwrap();
let path = Config::path().unwrap();
let content = std::fs::read_to_string(&path).unwrap();
let config: Config = toml::from_str(&content).unwrap();
let server = &config.servers["unset-test"];
assert!(server.api_key_keyring.is_none());
assert!(server.api_key.is_none());
assert!(server.api_key_env.is_none());
crate::credentials::keyring::delete("bzr", "unset-test").unwrap();
}