use std::io::{self, Write};
use serde::Serialize;
use super::formatting::{mask_api_key, print_field, print_formatted, print_optional_field};
use crate::config::{CredentialSource, CredentialSourceKind};
use crate::types::{AuthMethod, OutputFormat};
fn auth_display(auth_method: Option<&AuthMethod>) -> String {
auth_method.map_or_else(
|| "auto (not yet detected)".to_string(),
ToString::to_string,
)
}
#[derive(Serialize)]
#[non_exhaustive]
pub struct ServerDisplayInfo {
url: String,
email: Option<String>,
api_key: String,
api_key_source: String,
#[serde(skip_serializing_if = "Option::is_none")]
auth_method: Option<AuthMethod>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
tls_insecure: bool,
#[serde(skip_serializing_if = "Option::is_none")]
tls_ca_cert: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tls_pin: Option<String>,
}
impl ServerDisplayInfo {
fn from_config(srv: &crate::config::ServerConfig) -> Self {
let (api_key, api_key_source) = match srv.credential_source() {
Ok(CredentialSource::Inline(api_key)) => {
(mask_api_key(api_key), CredentialSourceKind::Inline.as_str())
}
Ok(CredentialSource::EnvVar(var_name)) => {
(var_name.to_string(), CredentialSourceKind::Env.as_str())
}
Ok(CredentialSource::Keyring { service, account }) => {
let display = if account.is_empty() {
format!("{service}/<server-name>")
} else {
format!("{service}/{account}")
};
(display, CredentialSourceKind::Keyring.as_str())
}
Err(_) => ("[invalid config]".to_string(), "invalid"),
};
Self {
url: srv.url.clone(),
email: srv.email.clone(),
api_key,
api_key_source: api_key_source.to_string(),
auth_method: srv.auth_method,
tls_insecure: srv.tls_insecure,
tls_ca_cert: srv.tls_ca_cert.as_ref().map(|p| p.display().to_string()),
tls_pin: srv.tls_pin_sha256.as_ref().map(|pin| {
if let Some(issuer) = &srv.tls_pin_issuer {
format!("{pin} ({issuer})")
} else {
pin.clone()
}
}),
}
}
}
#[derive(Serialize)]
#[non_exhaustive]
pub struct ConfigView {
pub config_file: String,
pub default_server: Option<String>,
pub servers: std::collections::BTreeMap<String, ServerDisplayInfo>,
}
impl ConfigView {
pub fn from_config(config: &crate::config::Config, path: &std::path::Path) -> Self {
let servers = config
.servers
.iter()
.map(|(name, srv)| (name.clone(), ServerDisplayInfo::from_config(srv)))
.collect();
Self {
config_file: path.to_string_lossy().into_owned(),
default_server: config.default_server.clone(),
servers,
}
}
}
fn print_api_key(s: &ServerDisplayInfo) {
let label = match s.api_key_source.as_str() {
"env" => "API Key Env",
"keyring" => "Keyring",
_ => "API Key",
};
print_field(label, &s.api_key);
}
fn print_server(name: &str, s: &ServerDisplayInfo) {
writeln!(io::stdout(), "\n[{name}]").expect("write to output");
print_field("URL", &s.url);
print_optional_field("Email", s.email.as_deref());
print_api_key(s);
print_field("API Key Source", &s.api_key_source);
print_field("Auth", &auth_display(s.auth_method.as_ref()));
if s.tls_insecure {
print_field("TLS", "insecure (certificate verification disabled)");
}
if let Some(ca) = &s.tls_ca_cert {
print_field("TLS CA Cert", ca);
}
if let Some(pin) = &s.tls_pin {
print_field("TLS Pin", pin);
}
}
pub fn print_config(view: &ConfigView, format: OutputFormat) {
print_formatted(view, format, |v| {
let mut out = io::stdout();
writeln!(out, "Config file: {}\n", v.config_file).expect("write to output");
if let Some(ref def) = v.default_server {
writeln!(out, "Default server: {def}").expect("write to output");
}
if v.servers.is_empty() {
writeln!(out, "No servers configured.").expect("write to output");
return;
}
for (name, s) in &v.servers {
print_server(name, s);
}
});
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::config::{Config, ServerConfig};
use std::collections::HashMap;
use std::path::Path;
#[test]
fn config_view_json_serialization() {
let view = ConfigView {
config_file: "/etc/bzr/config.toml".into(),
default_server: Some("prod".into()),
servers: std::collections::BTreeMap::new(),
};
let json: serde_json::Value = serde_json::to_value(&view).unwrap();
assert_eq!(json["config_file"], "/etc/bzr/config.toml");
assert_eq!(json["default_server"], "prod");
}
#[test]
fn config_view_no_default_server() {
let view = ConfigView {
config_file: "/tmp/config.toml".into(),
default_server: None,
servers: std::collections::BTreeMap::new(),
};
let json: serde_json::Value = serde_json::to_value(&view).unwrap();
assert!(json["default_server"].is_null());
}
#[test]
fn auth_display_formats_detected_and_undetected_states() {
assert_eq!(auth_display(None), "auto (not yet detected)");
assert_eq!(auth_display(Some(&AuthMethod::Header)), "header");
}
#[test]
fn config_view_from_config_masks_key_and_includes_flags() {
let mut servers = HashMap::new();
servers.insert(
"prod".into(),
ServerConfig {
url: "https://bugzilla.example".into(),
email: Some("admin@example.com".into()),
api_key: Some("1234567890abcdef".into()),
api_key_env: None,
api_key_keyring: None,
auth_method: Some(AuthMethod::Header),
api_mode: None,
server_version: None,
tls_insecure: true,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_issuer: None,
tls_pin_issuer_der: None,
},
);
let config = Config {
default_server: Some("prod".into()),
servers,
queries: HashMap::new(),
templates: HashMap::new(),
};
let view = ConfigView::from_config(&config, Path::new("/tmp/bzr/config.toml"));
let server = &view.servers["prod"];
let json: serde_json::Value = serde_json::to_value(server).unwrap();
assert_eq!(json["url"], "https://bugzilla.example");
assert_eq!(json["email"], "admin@example.com");
assert_eq!(json["api_key"], "12345678...");
assert_eq!(json["api_key_source"], "inline");
assert_eq!(json["auth_method"], "header");
assert_eq!(json["tls_insecure"], true);
}
#[test]
fn config_view_from_config_shows_env_backed_keys_without_resolving() {
let mut servers = HashMap::new();
servers.insert(
"prod".into(),
ServerConfig {
url: "https://bugzilla.example".into(),
email: None,
api_key: None,
api_key_env: Some("BZR_API_KEY".into()),
api_key_keyring: None,
auth_method: None,
api_mode: None,
server_version: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_issuer: None,
tls_pin_issuer_der: None,
},
);
let config = Config {
default_server: Some("prod".into()),
servers,
queries: HashMap::new(),
templates: HashMap::new(),
};
let view = ConfigView::from_config(&config, Path::new("/tmp/bzr/config.toml"));
let server = &view.servers["prod"];
let json: serde_json::Value = serde_json::to_value(server).unwrap();
assert_eq!(json["api_key"], "BZR_API_KEY");
assert_eq!(json["api_key_source"], "env");
}
#[test]
fn server_display_info_keyring_source() {
let srv = ServerConfig {
url: "https://example.com".into(),
api_key: None,
api_key_env: None,
api_key_keyring: Some(crate::config::KeyringRef {
service: Some("bzr".into()),
account: Some("prod".into()),
}),
email: None,
auth_method: None,
api_mode: None,
server_version: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_issuer: None,
tls_pin_issuer_der: None,
};
let info = ServerDisplayInfo::from_config(&srv);
assert_eq!(info.api_key_source, "keyring");
assert!(info.api_key.contains("bzr"));
assert!(info.api_key.contains("prod"));
}
#[test]
fn display_server_with_tls_pin() {
let mut servers = HashMap::new();
servers.insert(
"pinned".into(),
ServerConfig {
url: "https://bugzilla.example".into(),
email: None,
api_key: Some("1234567890abcdef".into()),
api_key_env: None,
api_key_keyring: None,
auth_method: Some(AuthMethod::Header),
api_mode: None,
server_version: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: Some("sha256//abc123".into()),
tls_pin_issuer: Some("CN=Test CA".into()),
tls_pin_issuer_der: None,
},
);
let config = Config {
default_server: Some("pinned".into()),
servers,
queries: HashMap::new(),
templates: HashMap::new(),
};
let view = ConfigView::from_config(&config, Path::new("/tmp/bzr/config.toml"));
let server = &view.servers["pinned"];
let json: serde_json::Value = serde_json::to_value(server).unwrap();
assert_eq!(json["tls_pin"], "sha256//abc123 (CN=Test CA)");
assert!(json.get("tls_insecure").is_none());
}
fn make_display_info(
url: &str,
api_key: &str,
source: &str,
tls_insecure: bool,
) -> ServerDisplayInfo {
ServerDisplayInfo {
url: url.into(),
email: None,
api_key: api_key.into(),
api_key_source: source.into(),
auth_method: None,
tls_insecure,
tls_ca_cert: None,
tls_pin: None,
}
}
async fn capture_print_config(view: &ConfigView) -> String {
let ((), output) = crate::test_helpers::capture_stdout(async {
print_config(view, OutputFormat::Table);
})
.await;
output
}
#[tokio::test]
async fn print_config_renders_table_for_inline_server() {
let _lock = crate::ENV_LOCK.lock().await;
let mut info = make_display_info("https://bugzilla.example", "12345678...", "inline", true);
info.email = Some("admin@example.com".into());
info.auth_method = Some(AuthMethod::Header);
let mut servers = std::collections::BTreeMap::new();
servers.insert("prod".into(), info);
let view = ConfigView {
config_file: "/tmp/bzr/config.toml".into(),
default_server: Some("prod".into()),
servers,
};
let output = capture_print_config(&view).await;
for needle in [
"Config file: /tmp/bzr/config.toml",
"Default server: prod",
"[prod]",
"https://bugzilla.example",
"admin@example.com",
"API Key",
"12345678...",
"insecure",
] {
assert!(output.contains(needle), "missing {needle:?} in output");
}
}
#[tokio::test]
async fn print_config_renders_env_and_keyring_labels() {
let _lock = crate::ENV_LOCK.lock().await;
let mut servers = std::collections::BTreeMap::new();
servers.insert(
"env-srv".into(),
make_display_info("https://env.example", "BZR_API_KEY", "env", false),
);
servers.insert(
"kr-srv".into(),
make_display_info("https://kr.example", "bzr/kr-srv", "keyring", false),
);
let view = ConfigView {
config_file: "/tmp/bzr/config.toml".into(),
default_server: None,
servers,
};
let output = capture_print_config(&view).await;
for needle in ["API Key Env", "BZR_API_KEY", "Keyring", "bzr/kr-srv"] {
assert!(output.contains(needle), "missing {needle:?} in output");
}
}
#[tokio::test]
async fn print_config_renders_empty_servers_message() {
let _lock = crate::ENV_LOCK.lock().await;
let view = ConfigView {
config_file: "/tmp/bzr/config.toml".into(),
default_server: None,
servers: std::collections::BTreeMap::new(),
};
let output = capture_print_config(&view).await;
assert!(output.contains("No servers configured."));
}
#[test]
fn server_display_info_keyring_source_default_account() {
let srv = ServerConfig {
url: "https://example.com".into(),
api_key: None,
api_key_env: None,
api_key_keyring: Some(crate::config::KeyringRef {
service: None,
account: None,
}),
email: None,
auth_method: None,
api_mode: None,
server_version: None,
tls_insecure: false,
tls_ca_cert: None,
tls_pin_sha256: None,
tls_pin_issuer: None,
tls_pin_issuer_der: None,
};
let info = ServerDisplayInfo::from_config(&srv);
assert_eq!(info.api_key_source, "keyring");
assert!(info.api_key.contains("bzr"));
assert!(info.api_key.contains("<server-name>"));
}
}