#![expect(clippy::unwrap_used)]
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
use crate::cli::BugAction;
use crate::test_helpers::setup_test_env;
use crate::types::OutputFormat;
fn make_view_action(ids: &[&str], permissive: bool) -> BugAction {
BugAction::View {
ids: ids.iter().map(|s| (*s).to_string()).collect(),
permissive,
field_args: crate::cli::FieldArgs {
fields: None,
exclude_fields: None,
},
}
}
fn ok_bug_body(id: u64, summary: &str) -> serde_json::Value {
serde_json::json!({
"bugs": [{
"id": id,
"summary": summary,
"status": "NEW",
"resolution": "",
"assigned_to": "nobody@test.com",
"priority": "P1",
"severity": "normal",
"product": "TestProduct",
"component": "General",
"creation_time": "2025-01-01T00:00:00Z",
"last_change_time": "2025-01-01T00:00:00Z"
}]
})
}
fn ok_bug_body_with_custom(id: u64, summary: &str) -> serde_json::Value {
serde_json::json!({
"bugs": [{
"id": id,
"summary": summary,
"status": "NEW",
"cf_release": "9.6"
}]
})
}
fn api_error_body(code: i64, message: &str) -> serde_json::Value {
serde_json::json!({
"error": true,
"code": code,
"message": message
})
}
#[tokio::test]
async fn view_single_unchanged_table() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(42, "Test bug")))
.mount(&mock)
.await;
let action = make_view_action(&["42"], false);
let mut __io = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io.writers(),
)
.await;
let output = __io.out_str().to_string();
assert!(result.is_ok(), "{:?}", result.err());
assert!(output.contains("Bug #42"));
assert!(output.contains("Test bug"));
assert!(!output.contains(&"─".repeat(60)));
}
#[tokio::test]
async fn view_single_unchanged_json() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(42, "Test bug")))
.mount(&mock)
.await;
let action = make_view_action(&["42"], false);
let mut __io2 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io2.writers(),
)
.await;
let output = __io2.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 42);
assert!(parsed.get("bugs").is_none());
assert!(parsed.get("failed").is_none());
}
#[tokio::test]
async fn view_single_json_custom_only_field_omits_forced_id() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42"))
.and(query_param("include_fields", "id,cf_release"))
.respond_with(
ResponseTemplate::new(200).set_body_json(ok_bug_body_with_custom(42, "Test bug")),
)
.expect(1)
.mount(&mock)
.await;
let mut action = make_view_action(&["42"], false);
let BugAction::View { field_args, .. } = &mut action else {
unreachable!()
};
field_args.fields = Some("cf_release".into());
let mut __io = crate::test_helpers::CapturedIo::new();
let result =
crate::commands::bug::execute(&action, None, OutputFormat::Json, None, &mut __io.writers())
.await;
assert!(result.is_ok(), "single view custom JSON failed: {result:?}");
let parsed: serde_json::Value = serde_json::from_str(__io.out_str().trim()).unwrap();
let obj = parsed.as_object().unwrap();
assert_eq!(
obj.keys().map(String::as_str).collect::<Vec<_>>(),
vec!["cf_release"]
);
assert_eq!(obj["cf_release"], "9.6");
}
#[tokio::test]
async fn view_single_table_renders_requested_custom_row() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42"))
.and(query_param("include_fields", "id,cf_release"))
.respond_with(
ResponseTemplate::new(200).set_body_json(ok_bug_body_with_custom(42, "Test bug")),
)
.expect(1)
.mount(&mock)
.await;
let mut action = make_view_action(&["42"], false);
let BugAction::View { field_args, .. } = &mut action else {
unreachable!()
};
field_args.fields = Some("cf_release".into());
let mut __io = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io.writers(),
)
.await;
assert!(
result.is_ok(),
"single view custom table failed: {result:?}"
);
assert!(
__io.out_str().contains("cf_release") && __io.out_str().contains("9.6"),
"custom detail row:\n{}",
__io.out_str()
);
}
#[tokio::test]
async fn view_single_table_warns_for_unknown_field() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42"))
.and(query_param("include_fields", "id,sumary"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(42, "Test bug")))
.expect(1)
.mount(&mock)
.await;
let mut action = make_view_action(&["42"], false);
let BugAction::View { field_args, .. } = &mut action else {
unreachable!()
};
field_args.fields = Some("sumary".into());
let mut __io = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io.writers(),
)
.await;
assert!(result.is_ok(), "single view typo table failed: {result:?}");
assert!(
__io.err_str().contains("ignoring unknown field(s): sumary"),
"unknown field warning:\n{}",
__io.err_str()
);
}
#[tokio::test]
async fn view_single_failure_propagates() {
let mut __cap_io = crate::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/999999"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(api_error_body(101, "Bug #999999 does not exist.")),
)
.mount(&mock)
.await;
let action = make_view_action(&["999999"], false);
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("does not exist") || err.contains("101"),
"expected not-found error message, got: {err}"
);
}
#[tokio::test]
async fn view_multi_strict_all_succeed_table() {
let (_lock, mock, _tmp) = setup_test_env().await;
for (id, summary) in [(1, "first"), (2, "second"), (3, "third")] {
Mock::given(method("GET"))
.and(path(format!("/rest/bug/{id}")))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(id, summary)))
.mount(&mock)
.await;
}
let action = make_view_action(&["1", "2", "3"], false);
let mut __io3 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io3.writers(),
)
.await;
let output = __io3.out_str().to_string();
assert!(result.is_ok());
assert!(output.contains("Bug #1"));
assert!(output.contains("Bug #2"));
assert!(output.contains("Bug #3"));
assert_eq!(output.matches(&"─".repeat(60)).count(), 2);
}
#[tokio::test]
async fn view_multi_strict_failure_emits_no_partial_table() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(101, "Bug #2 does not exist.")),
)
.mount(&mock)
.await;
let action = make_view_action(&["1", "2", "3"], false);
let mut __io4 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io4.writers(),
)
.await;
let output = __io4.out_str().to_string();
assert!(result.is_err());
assert!(
output.trim().is_empty(),
"strict multi-ID table output should be all-or-nothing, got: {output}"
);
assert!(!output.contains("Bug #3"));
}
#[tokio::test]
async fn view_multi_strict_json_all_succeed_emits_wrapped_shape() {
let (_lock, mock, _tmp) = setup_test_env().await;
for (id, summary) in [(1, "first"), (2, "second")] {
Mock::given(method("GET"))
.and(path(format!("/rest/bug/{id}")))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(id, summary)))
.mount(&mock)
.await;
}
let action = make_view_action(&["1", "2"], false);
let mut __io5 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io5.writers(),
)
.await;
let output = __io5.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["bugs"].as_array().unwrap().len(), 2);
assert_eq!(parsed["failed"].as_array().unwrap().len(), 0);
assert_eq!(parsed["bugs"][0]["id"], 1);
assert_eq!(parsed["bugs"][1]["id"], 2);
}
#[tokio::test]
async fn view_multi_strict_json_projects_custom_fields_inside_wrapper() {
let (_lock, mock, _tmp) = setup_test_env().await;
for (id, summary, release) in [(1, "first", "9.6"), (2, "second", "9.7")] {
Mock::given(method("GET"))
.and(path(format!("/rest/bug/{id}")))
.and(query_param("include_fields", "id,cf_release"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{
"id": id,
"summary": summary,
"status": "NEW",
"cf_release": release
}]
})))
.expect(1)
.mount(&mock)
.await;
}
let mut action = make_view_action(&["1", "2"], false);
let BugAction::View { field_args, .. } = &mut action else {
unreachable!()
};
field_args.fields = Some("id,cf_release".into());
let mut __io = crate::test_helpers::CapturedIo::new();
let result =
crate::commands::bug::execute(&action, None, OutputFormat::Json, None, &mut __io.writers())
.await;
assert!(result.is_ok(), "multi view custom JSON failed: {result:?}");
let parsed: serde_json::Value = serde_json::from_str(__io.out_str().trim()).unwrap();
assert_eq!(parsed["bugs"][0]["id"], 1);
assert_eq!(parsed["bugs"][0]["cf_release"], "9.6");
assert!(parsed["bugs"][0].get("summary").is_none());
assert_eq!(parsed["bugs"][1]["id"], 2);
assert_eq!(parsed["bugs"][1]["cf_release"], "9.7");
assert_eq!(parsed["failed"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn view_multi_strict_json_failure_emits_no_partial_json() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(101, "Bug #2 does not exist.")),
)
.mount(&mock)
.await;
let action = make_view_action(&["1", "2"], false);
let mut __io6 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io6.writers(),
)
.await;
let output = __io6.out_str().to_string();
assert!(result.is_err());
assert!(
output.trim().is_empty(),
"expected empty stdout, got: {output}"
);
}
#[tokio::test]
async fn view_multi_permissive_partial_table() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(101, "Bug #2 does not exist.")),
)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/3"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(3, "third")))
.mount(&mock)
.await;
let action = make_view_action(&["1", "2", "3"], true);
let mut __io7 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Table,
None,
&mut __io7.writers(),
)
.await;
let output = __io7.out_str().to_string();
assert!(
result.is_ok(),
"permissive must exit Ok, got: {:?}",
result.err()
);
assert!(output.contains("Bug #1"));
assert!(output.contains("Bug #2"));
assert!(output.contains("UNAVAILABLE"));
assert!(output.contains("Bug #3"));
let pos1 = output.find("Bug #1").unwrap();
let pos2 = output.find("Bug #2").unwrap();
let pos3 = output.find("Bug #3").unwrap();
assert!(pos1 < pos2 && pos2 < pos3);
}
#[tokio::test]
async fn view_multi_permissive_json_shape() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(101, "Bug #2 does not exist.")),
)
.mount(&mock)
.await;
let action = make_view_action(&["1", "2"], true);
let mut __io8 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io8.writers(),
)
.await;
let output = __io8.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["bugs"].as_array().unwrap().len(), 1);
assert_eq!(parsed["bugs"][0]["id"], 1);
let failed = parsed["failed"].as_array().unwrap();
assert_eq!(failed.len(), 1);
assert_eq!(failed[0]["id"], "2");
assert!(
failed[0]["error"]
.as_str()
.unwrap()
.contains("does not exist")
|| failed[0]["error"].as_str().unwrap().contains("101")
);
}
#[tokio::test]
async fn view_multi_permissive_all_fail_returns_empty_bugs() {
let (_lock, mock, _tmp) = setup_test_env().await;
for id in [1_u64, 2, 3] {
Mock::given(method("GET"))
.and(path(format!("/rest/bug/{id}")))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(101, "no such bug")),
)
.mount(&mock)
.await;
}
let action = make_view_action(&["1", "2", "3"], true);
let mut __io9 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io9.writers(),
)
.await;
let output = __io9.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["bugs"].as_array().unwrap().len(), 0);
assert_eq!(parsed["failed"].as_array().unwrap().len(), 3);
}
#[tokio::test]
async fn view_multi_permissive_with_alias_preserves_id_string() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/my-alias"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(100, "Invalid Bug Alias")),
)
.mount(&mock)
.await;
let action = make_view_action(&["1", "my-alias"], true);
let mut __io10 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io10.writers(),
)
.await;
let output = __io10.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
let failed = parsed["failed"].as_array().unwrap();
assert_eq!(
failed[0]["id"], "my-alias",
"alias preserved verbatim in failure id"
);
}
#[tokio::test]
async fn view_permissive_single_id_rejected() {
let mut __cap_io = crate::test_helpers::CapturedIo::new();
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = make_view_action(&["42"], true);
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, crate::error::BzrError::InputValidation(_)));
assert_eq!(err.exit_code(), 7);
assert!(err
.to_string()
.contains("only meaningful with multiple IDs"));
}
#[tokio::test]
async fn view_multi_permissive_transport_error_bails() {
let mut __cap_io = crate::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(ResponseTemplate::new(500).set_body_string(""))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/3"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(3, "third")))
.mount(&mock)
.await;
let action = make_view_action(&["1", "2", "3"], true);
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_err(),
"transport error must bail despite --permissive"
);
assert!(!result.unwrap_err().is_bug_get_per_resource());
}
#[tokio::test]
async fn view_multi_permissive_api_session_wide_bails() {
let mut __cap_io = crate::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(32000, "Login Required")),
)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/3"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(3, "third")))
.mount(&mock)
.await;
let action = make_view_action(&["1", "2", "3"], true);
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_err(),
"session-wide Api code must bail despite --permissive"
);
}
#[tokio::test]
async fn view_multi_permissive_api_102_suppressed() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(
ResponseTemplate::new(200).set_body_json(api_error_body(102, "Access Denied")),
)
.mount(&mock)
.await;
let action = make_view_action(&["1", "2"], true);
let mut __io11 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io11.writers(),
)
.await;
let output = __io11.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["failed"].as_array().unwrap().len(), 1);
assert_eq!(parsed["failed"][0]["id"], "2");
}
#[tokio::test]
async fn view_multi_permissive_api_410_bails() {
let mut __cap_io = crate::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/1"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(1, "first")))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/2"))
.respond_with(ResponseTemplate::new(200).set_body_json(api_error_body(
410,
"You must log in to access this resource.",
)))
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/3"))
.respond_with(ResponseTemplate::new(200).set_body_json(ok_bug_body(3, "third")))
.mount(&mock)
.await;
let action = make_view_action(&["1", "2", "3"], true);
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_err(),
"auth-flavored Api code must bail despite --permissive"
);
assert!(!result.unwrap_err().is_bug_get_per_resource());
}