#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "test code: unwrap/expect/panic on assert_cmd assertions and JSON-shape assertions is the expected diagnostic"
)]
mod common;
use predicates::prelude::*;
use predicates::str::contains;
use tempfile::tempdir;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::aviso;
fn empty_config_dir() -> tempfile::TempDir {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("config.yaml"), "").unwrap();
dir
}
#[tokio::test]
async fn verbosity_v_sets_debug_in_stderr_tracing() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/schema"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"schema": {},
"event_types": [],
"total_schemas": 0
})))
.mount(&server)
.await;
let dir = empty_config_dir();
let assertion = aviso()
.args([
"--config",
dir.path().join("config.yaml").to_str().unwrap(),
"--base-url",
&server.uri(),
"-v",
"schema",
"list",
])
.env_remove("AVISO_LOG")
.assert()
.success();
let output = assertion.get_output();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("DEBUG") || stderr.contains("\"level\":\"DEBUG\""),
"expected DEBUG-level tracing in stderr; got: {stderr}"
);
}
#[tokio::test]
async fn verbosity_no_v_uses_info_level_default() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/schema"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"schema": {},
"event_types": [],
"total_schemas": 0
})))
.mount(&server)
.await;
let dir = empty_config_dir();
let assertion = aviso()
.args([
"--config",
dir.path().join("config.yaml").to_str().unwrap(),
"--base-url",
&server.uri(),
"schema",
"list",
])
.env_remove("AVISO_LOG")
.assert()
.success();
let output = assertion.get_output();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("DEBUG"),
"default (no -v) should NOT include DEBUG-level events; got: {stderr}"
);
}
#[tokio::test]
async fn danger_accept_invalid_certs_emits_session_warn() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/schema"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"schema": {},
"event_types": [],
"total_schemas": 0
})))
.mount(&server)
.await;
let dir = empty_config_dir();
aviso()
.args([
"--config",
dir.path().join("config.yaml").to_str().unwrap(),
"--base-url",
&server.uri(),
"--danger-accept-invalid-certs",
"schema",
"list",
])
.env_remove("AVISO_LOG")
.assert()
.success()
.stderr(contains("cli.tls.insecure_mode").or(contains("insecure")));
}
#[tokio::test]
async fn otel_log_format_emits_required_fields_per_data_model() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/schema"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"schema": {},
"event_types": [],
"total_schemas": 0
})))
.mount(&server)
.await;
let dir = empty_config_dir();
let assertion = aviso()
.args([
"--config",
dir.path().join("config.yaml").to_str().unwrap(),
"--base-url",
&server.uri(),
"-v",
"schema",
"list",
])
.env_remove("AVISO_LOG")
.assert()
.success();
let stderr = String::from_utf8_lossy(&assertion.get_output().stderr).to_string();
let line = stderr
.lines()
.find(|l| l.contains("\"event.name\":\"cli.config.resolved\""))
.unwrap_or_else(|| {
panic!("expected cli.config.resolved DEBUG event in stderr; got: {stderr}")
});
let parsed: serde_json::Value = serde_json::from_str(line)
.unwrap_or_else(|e| panic!("emitted event must be valid JSON: {e}; line: {line}"));
assert!(
parsed
.get("timestamp")
.and_then(serde_json::Value::as_str)
.is_some_and(|t| t.ends_with('Z')),
"expected RFC3339 UTC timestamp ending in Z; got: {parsed}"
);
assert_eq!(
parsed
.get("severityText")
.and_then(serde_json::Value::as_str),
Some("DEBUG"),
"expected severityText=DEBUG; got: {parsed}"
);
assert_eq!(
parsed
.get("severityNumber")
.and_then(serde_json::Value::as_u64),
Some(5),
"expected severityNumber=5 (OTel lowest-in-range for DEBUG); got: {parsed}"
);
assert!(
parsed
.get("body")
.and_then(serde_json::Value::as_str)
.is_some_and(|b| !b.is_empty()),
"expected non-empty body; got: {parsed}"
);
let resource = parsed.get("resource").expect("missing resource block");
assert_eq!(
resource
.get("service.name")
.and_then(serde_json::Value::as_str),
Some("aviso-cli"),
"resource.service.name must be aviso-cli; got: {resource}"
);
assert!(
resource
.get("service.version")
.and_then(serde_json::Value::as_str)
.is_some_and(|v| !v.is_empty()),
"resource.service.version must be present and non-empty; got: {resource}"
);
let attributes = parsed.get("attributes").expect("missing attributes block");
assert_eq!(
attributes
.get("event.name")
.and_then(serde_json::Value::as_str),
Some("cli.config.resolved"),
"attributes.event.name should carry the stable event identifier; got: {attributes}"
);
assert!(
parsed.get("level").is_none(),
"old tracing-subscriber `level` field should not appear in OTel-shape output; got: {parsed}"
);
assert!(
parsed.get("fields").is_none(),
"old tracing-subscriber `fields` block should not appear in OTel-shape output; got: {parsed}"
);
}
#[tokio::test]
async fn no_danger_flag_does_not_emit_insecure_warn() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/api/v1/schema"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"status": "success",
"schema": {},
"event_types": [],
"total_schemas": 0
})))
.mount(&server)
.await;
let dir = empty_config_dir();
let assertion = aviso()
.args([
"--config",
dir.path().join("config.yaml").to_str().unwrap(),
"--base-url",
&server.uri(),
"schema",
"list",
])
.env_remove("AVISO_LOG")
.assert()
.success();
let output = assertion.get_output();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("cli.tls.insecure_mode"),
"insecure_mode WARN should not fire without --danger-accept-invalid-certs; got: {stderr}"
);
}