#![expect(clippy::unwrap_used)]
use bzr::test_helpers::setup_test_env;
use bzr::ENV_LOCK;
use clap::Parser;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
fn empty_list_action() -> bzr::cli::BugAction {
bzr::cli::BugAction::List {
product: vec![],
component: vec![],
status: vec![],
assignee: vec![],
creator: vec![],
priority: vec![],
severity: vec![],
id: vec![],
alias: None,
summary: None,
limit: 50,
fields: None,
exclude_fields: None,
created_since: None,
changed_since: None,
whiteboard: vec![],
target_milestone: vec![],
version: vec![],
op_sys: vec![],
platform: vec![],
resolution: vec![],
qa_contact: vec![],
url: vec![],
}
}
#[tokio::test]
async fn bug_list_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [
{"id": 1, "summary": "Test bug", "status": "NEW"}
]
})))
.expect(1)
.mount(&mock)
.await;
let action = empty_list_action();
let mut __io = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io.writers(),
)
.await;
let output = __io.out_str().to_string();
assert!(result.is_ok(), "bug list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["id"], 1);
assert_eq!(parsed[0]["summary"], "Test bug");
assert_eq!(parsed[0]["status"], "NEW");
}
#[tokio::test]
async fn bug_list_changed_since_canonicalizes_bare_date_on_wire() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug"))
.and(query_param("product", "Firefox"))
.and(query_param("last_change_time", "2026-04-01T00:00:00Z"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"bugs": []})))
.expect(1)
.mount(&mock)
.await;
let mut action = empty_list_action();
if let bzr::cli::BugAction::List {
product,
changed_since,
..
} = &mut action
{
*product = vec!["Firefox".into()];
*changed_since = Some("2026-04-01".into());
}
let mut __io2 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io2.writers(),
)
.await;
let _output = __io2.out_str().to_string();
assert!(
result.is_ok(),
"bug list with --changed-since should succeed: {result:?}"
);
}
#[tokio::test]
async fn bug_view_integration() {
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(serde_json::json!({
"bugs": [{"id": 42, "summary": "Test bug", "status": "NEW"}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::View {
ids: vec!["42".to_string()],
permissive: false,
fields: None,
exclude_fields: None,
};
let mut __io3 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io3.writers(),
)
.await;
let output = __io3.out_str().to_string();
assert!(result.is_ok(), "bug view should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 42);
assert_eq!(parsed["summary"], "Test bug");
}
#[tokio::test]
async fn bug_search_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug"))
.and(query_param("quicksearch", "crash"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{"id": 99, "summary": "Crash on startup", "status": "NEW"}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::Search {
query: Some("crash".to_string()),
from_url: None,
save_as: None,
limit: None,
fields: None,
exclude_fields: None,
};
let mut __io4 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io4.writers(),
)
.await;
let output = __io4.out_str().to_string();
assert!(result.is_ok(), "bug search should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["id"], 99);
assert_eq!(parsed[0]["summary"], "Crash on startup");
}
#[tokio::test]
async fn bug_create_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/bug"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 100})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::Create {
template: None,
product: Some("TestProduct".to_string()),
component: Some("General".to_string()),
summary: Some("New bug".to_string()),
version: Some("unspecified".to_string()),
description: Some("body".to_string()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io5 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io5.writers(),
)
.await;
let output = __io5.out_str().to_string();
assert!(result.is_ok(), "bug create should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 100);
}
#[tokio::test]
async fn comment_list_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/comment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": {
"comments": [
{"id": 1, "bug_id": 42, "text": "First comment", "count": 0}
]
}
}
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::CommentAction::List {
bug_id: 42,
since: None,
};
let mut __io6 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io6.writers(),
)
.await;
let output = __io6.out_str().to_string();
assert!(result.is_ok(), "comment list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["id"], 1);
assert_eq!(parsed[0]["text"], "First comment");
}
#[tokio::test]
async fn whoami_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/whoami"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 1,
"name": "admin@example.com",
"real_name": "Admin User"
})))
.expect(1)
.mount(&mock)
.await;
let mut __io7 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::whoami::execute(
&bzr::cli::WhoamiAction::Show,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io7.writers(),
)
.await;
let output = __io7.out_str().to_string();
assert!(result.is_ok(), "whoami should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["name"], "admin@example.com");
assert_eq!(parsed["real_name"], "Admin User");
}
#[tokio::test]
async fn product_list_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/product_accessible"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ids": [1]})))
.expect(1)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/product"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"products": [{
"id": 1, "name": "Firefox", "description": "Browser",
"is_active": true, "components": [], "versions": [], "milestones": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ProductAction::List {
r#type: bzr::types::ProductListType::Accessible,
};
let mut __io8 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::product::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io8.writers(),
)
.await;
let output = __io8.out_str().to_string();
assert!(result.is_ok(), "product list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["name"], "Firefox");
}
#[tokio::test]
async fn server_info_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/version"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"version": "5.1.2"})),
)
.expect(1)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/extensions"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"extensions": {}
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ServerAction::Info;
let mut __io9 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::server::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io9.writers(),
)
.await;
let output = __io9.out_str().to_string();
assert!(result.is_ok(), "server info should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["version"], "5.1.2");
}
#[tokio::test]
async fn field_list_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/field/bug/bug_status"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"fields": [{
"values": [
{"name": "NEW", "sort_key": 100, "is_active": true},
{"name": "RESOLVED", "sort_key": 500, "is_active": true}
]
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::FieldAction::List {
name: "status".to_string(),
};
let mut __io10 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::field::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io10.writers(),
)
.await;
let output = __io10.out_str().to_string();
assert!(result.is_ok(), "field list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["name"], "NEW");
}
#[tokio::test]
async fn classification_view_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/classification/Unclassified"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"classifications": [{
"id": 1,
"name": "Unclassified",
"description": "Default",
"sort_key": 0,
"products": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ClassificationAction::View {
name: "Unclassified".to_string(),
};
let mut __io11 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::classification::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io11.writers(),
)
.await;
let output = __io11.out_str().to_string();
assert!(
result.is_ok(),
"classification view should succeed: {result:?}"
);
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["name"], "Unclassified");
}
#[tokio::test]
async fn user_search_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{
"id": 1,
"name": "alice@example.com",
"real_name": "Alice",
"email": "alice@example.com",
"groups": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::UserAction::Search {
query: "alice".to_string(),
details: false,
};
let mut __io12 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::user::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io12.writers(),
)
.await;
let output = __io12.out_str().to_string();
assert!(result.is_ok(), "user search should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["name"], "alice@example.com");
assert_eq!(parsed[0]["real_name"], "Alice");
}
#[tokio::test]
async fn group_view_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/group"))
.and(query_param("names", "admin"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"groups": [{
"id": 1,
"name": "admin",
"description": "Administrators",
"is_active": true,
"membership": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::View {
group: "admin".to_string(),
};
let mut __io13 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io13.writers(),
)
.await;
let output = __io13.out_str().to_string();
assert!(result.is_ok(), "group view should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["name"], "admin");
assert_eq!(parsed["description"], "Administrators");
}
#[tokio::test]
async fn component_create_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/component"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 10})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ComponentAction::Create {
product: "TestProduct".to_string(),
name: "Backend".to_string(),
description: "Backend component".to_string(),
default_assignee: "dev@test.com".to_string(),
};
let mut __io14 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::component::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io14.writers(),
)
.await;
let output = __io14.out_str().to_string();
assert!(
result.is_ok(),
"component create should succeed: {result:?}"
);
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 10);
}
#[tokio::test]
async fn attachment_list_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/attachment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": [{
"id": 1,
"bug_id": 42,
"file_name": "patch.diff",
"summary": "Fix",
"content_type": "text/plain",
"size": 100
}]
}
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::List { bug_id: 42 };
let mut __io15 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io15.writers(),
)
.await;
let output = __io15.out_str().to_string();
assert!(result.is_ok(), "attachment list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["file_name"], "patch.diff");
}
#[tokio::test]
async fn config_show_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let _lock = 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();
std::fs::write(
config_dir.join("config.toml"),
r#"
default_server = "local"
[servers.local]
url = "https://bugzilla.local"
api_key = "key-1234567890"
"#,
)
.unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
let action = bzr::cli::ConfigAction::Show;
let result = bzr::commands::config::execute(
&action,
None,
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "config show should succeed: {result:?}");
}
#[tokio::test]
async fn command_with_unknown_server_returns_error() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = empty_list_action();
let result = bzr::commands::bug::execute(
&action,
Some("nonexistent"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_err(), "should fail with unknown server");
}
#[tokio::test]
async fn api_error_propagates() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/99999"))
.respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
"error": true,
"code": 101,
"message": "Bug #99999 does not exist."
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::View {
ids: vec!["99999".to_string()],
permissive: false,
fields: None,
exclude_fields: None,
};
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_err(), "should propagate API error");
}
#[tokio::test]
async fn bug_history_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/history"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{
"id": 42,
"history": [{
"when": "2025-01-01T00:00:00Z",
"who": "dev@example.com",
"changes": [{
"field_name": "status",
"removed": "NEW",
"added": "ASSIGNED"
}]
}]
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::History {
id: 42,
since: None,
};
let mut __io16 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io16.writers(),
)
.await;
let output = __io16.out_str().to_string();
assert!(result.is_ok(), "bug history should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["who"], "dev@example.com");
assert_eq!(parsed[0]["changes"][0]["field_name"], "status");
}
#[tokio::test]
async fn bug_update_integration() {
use wiremock::matchers::body_partial_json;
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/42"))
.and(body_partial_json(serde_json::json!({
"keywords": {"add": ["fix-needed"], "remove": ["wontfix"]},
"cc": {"add": ["alice@example.com"]},
"groups": {"remove": ["secret"]},
"see_also": {"add": ["https://example.com/issue/1"]},
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{"id": 42, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::Update {
ids: vec![42],
status: Some("RESOLVED".to_string()),
resolution: Some("FIXED".to_string()),
dupe_of: None,
alias: None,
deadline: None,
estimated_time: None,
remaining_time: None,
work_time: None,
reset_assigned_to: false,
reset_qa_contact: false,
assignee: None,
priority: None,
severity: None,
summary: None,
whiteboard: None,
flag: vec![],
blocks_add: vec![],
blocks_remove: vec![],
depends_on_add: vec![],
depends_on_remove: vec![],
keywords_add: vec!["fix-needed".to_string()],
keywords_remove: vec!["wontfix".to_string()],
cc_add: vec!["alice@example.com".to_string()],
cc_remove: vec![],
groups_add: vec![],
groups_remove: vec!["secret".to_string()],
see_also_add: vec!["https://example.com/issue/1".to_string()],
see_also_remove: vec![],
comment: None,
comment_file: None,
comment_private: false,
};
let mut __io17 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io17.writers(),
)
.await;
let output = __io17.out_str().to_string();
assert!(result.is_ok(), "bug update should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 42);
assert_eq!(parsed["action"], "updated");
}
#[tokio::test]
async fn bug_update_scalar_parity_fields_integration() {
use wiremock::matchers::body_partial_json;
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/42"))
.and(body_partial_json(serde_json::json!({
"alias": "short-name",
"deadline": "2026-12-31",
"estimated_time": 3.5,
"remaining_time": 1.25,
"work_time": 0.5,
"reset_assigned_to": true,
"reset_qa_contact": true,
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{"id": 42, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::Update {
ids: vec![42],
status: None,
resolution: None,
dupe_of: None,
alias: Some("short-name".to_string()),
deadline: Some("2026-12-31".to_string()),
estimated_time: Some(3.5),
remaining_time: Some(1.25),
work_time: Some(0.5),
reset_assigned_to: true,
reset_qa_contact: true,
assignee: None,
priority: None,
severity: None,
summary: None,
whiteboard: None,
flag: vec![],
blocks_add: vec![],
blocks_remove: vec![],
depends_on_add: vec![],
depends_on_remove: vec![],
keywords_add: vec![],
keywords_remove: vec![],
cc_add: vec![],
cc_remove: vec![],
groups_add: vec![],
groups_remove: vec![],
see_also_add: vec![],
see_also_remove: vec![],
comment: None,
comment_file: None,
comment_private: false,
};
let mut io = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut io.writers(),
)
.await;
assert!(result.is_ok(), "bug update should succeed: {result:?}");
}
#[tokio::test]
async fn bug_update_with_comment_integration() {
use wiremock::matchers::body_partial_json;
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/42"))
.and(body_partial_json(serde_json::json!({
"status": "RESOLVED",
"resolution": "FIXED",
"comment": {
"body": "see #other",
"is_private": true,
},
})))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{"id": 42, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::BugAction::Update {
ids: vec![42],
status: Some("RESOLVED".to_string()),
resolution: Some("FIXED".to_string()),
dupe_of: None,
alias: None,
deadline: None,
estimated_time: None,
remaining_time: None,
work_time: None,
reset_assigned_to: false,
reset_qa_contact: false,
assignee: None,
priority: None,
severity: None,
summary: None,
whiteboard: None,
flag: vec![],
blocks_add: vec![],
blocks_remove: vec![],
depends_on_add: vec![],
depends_on_remove: vec![],
keywords_add: vec![],
keywords_remove: vec![],
cc_add: vec![],
cc_remove: vec![],
groups_add: vec![],
groups_remove: vec![],
see_also_add: vec![],
see_also_remove: vec![],
comment: Some("see #other".to_string()),
comment_file: None,
comment_private: true,
};
let mut __io18 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io18.writers(),
)
.await;
let _output = __io18.out_str().to_string();
assert!(
result.is_ok(),
"bug update with comment should succeed: {result:?}"
);
}
#[tokio::test]
async fn comment_add_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/bug/42/comment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 999})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::CommentAction::Add {
bug_id: 42,
body: Some("This is a test comment".to_string()),
private: false,
};
let mut __io19 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io19.writers(),
)
.await;
let output = __io19.out_str().to_string();
assert!(result.is_ok(), "comment add should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 999);
}
#[tokio::test]
async fn comment_tag_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/comment/100/tags"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["spam"])))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::CommentAction::Tag {
comment_id: 100,
add: vec!["spam".to_string()],
remove: vec![],
};
let result = bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "comment tag should succeed: {result:?}");
}
#[tokio::test]
async fn comment_search_tags_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/comment/tags/spam"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!(["spam"])))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::CommentAction::SearchTags {
query: "spam".to_string(),
};
let result = bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"comment search-tags should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_download_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/attachment/99"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"attachments": {
"99": {
"id": 99,
"file_name": "test.txt",
"data": "SGVsbG8gd29ybGQ=",
"content_type": "text/plain",
"size": 11,
"summary": "Test file",
"bug_id": 42
}
}
})))
.expect(1)
.mount(&mock)
.await;
let out_path = tmp.path().join("downloaded.txt");
let action = bzr::cli::AttachmentAction::Download {
ids: vec![99],
bug_ids: vec![],
out: Some(out_path.to_string_lossy().into_owned()),
out_dir: "./attachments".into(),
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"attachment download should succeed: {result:?}"
);
assert!(out_path.exists(), "downloaded file should exist");
let content = std::fs::read_to_string(&out_path).unwrap();
assert_eq!(content, "Hello world");
}
#[tokio::test]
async fn attachment_download_bulk_per_bug_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/77/attachment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"77": [
{
"id": 1001,
"bug_id": 77,
"file_name": "a.txt",
"summary": "first",
"content_type": "text/plain",
"size": 5,
"is_obsolete": false,
"is_patch": false,
"is_private": false,
"data": "QUFBQUE="
},
{
"id": 1002,
"bug_id": 77,
"file_name": "b.txt",
"summary": "second",
"content_type": "text/plain",
"size": 4,
"is_obsolete": false,
"is_patch": false,
"is_private": false,
"data": "QkJCQg=="
}
]
}
})))
.expect(1)
.mount(&mock)
.await;
let out_dir = tmp.path().to_string_lossy().into_owned();
let action = bzr::cli::AttachmentAction::Download {
ids: vec![],
bug_ids: vec![77],
out: None,
out_dir,
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "expected ok: {result:?}");
assert!(tmp.path().join("77").join("1001.a.txt").exists());
assert!(tmp.path().join("77").join("1002.b.txt").exists());
assert_eq!(
std::fs::read(tmp.path().join("77").join("1001.a.txt")).unwrap(),
b"AAAAA"
);
assert_eq!(
std::fs::read(tmp.path().join("77").join("1002.b.txt")).unwrap(),
b"BBBB"
);
}
#[tokio::test]
async fn attachment_upload_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
let upload_file = tmp.path().join("upload.txt");
std::fs::write(&upload_file, "test content").unwrap();
Mock::given(method("POST"))
.and(path("/rest/bug/42/attachment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ids": [101]})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::Upload {
bug_id: 42,
file: upload_file.to_string_lossy().into_owned(),
summary: Some("Test upload".to_string()),
content_type: Some("text/plain".to_string()),
private: false,
is_patch: false,
comment: None,
comment_private: false,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"attachment upload should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_upload_with_comment_integration() {
use wiremock::matchers::body_string_contains;
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
let upload_file = tmp.path().join("upload.txt");
std::fs::write(&upload_file, "test content").unwrap();
Mock::given(method("POST"))
.and(path("/rest/bug/42/attachment"))
.and(body_string_contains(
"\"comment\":\"see #6789 for context\"",
))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ids": [102]})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::Upload {
bug_id: 42,
file: upload_file.to_string_lossy().into_owned(),
summary: Some("Test upload".to_string()),
content_type: Some("text/plain".to_string()),
private: false,
is_patch: false,
comment: Some("see #6789 for context".to_string()),
comment_private: false,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"upload with --comment should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_upload_with_is_patch_integration() {
use wiremock::matchers::body_string_contains;
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
let upload_file = tmp.path().join("fix.patch");
std::fs::write(&upload_file, "diff --git a/x b/x").unwrap();
Mock::given(method("POST"))
.and(path("/rest/bug/42/attachment"))
.and(body_string_contains("\"is_patch\":true"))
.and(body_string_contains("\"content_type\":\"text/plain\""))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ids": [103]})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::Upload {
bug_id: 42,
file: upload_file.to_string_lossy().into_owned(),
summary: Some("Test patch".to_string()),
content_type: None,
private: false,
is_patch: true,
comment: None,
comment_private: false,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"upload with --is-patch should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_upload_with_comment_private_integration() {
use wiremock::matchers::body_string_contains;
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, tmp) = setup_test_env().await;
let upload_file = tmp.path().join("upload.txt");
std::fs::write(&upload_file, "test content").unwrap();
Mock::given(method("POST"))
.and(path("/rest/bug/42/attachment"))
.and(body_string_contains("\"comment\":\"sensitive\""))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"ids": [202]})))
.expect(1)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/comment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": {
"comments": [
{"id": 800, "bug_id": 42, "text": "sensitive", "attachment_id": 202}
]
}
}
})))
.expect(1)
.mount(&mock)
.await;
Mock::given(method("PUT"))
.and(path("/rest/bug/42"))
.and(body_string_contains("\"comment_is_private\""))
.and(body_string_contains("\"800\":true"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"bugs": []})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::Upload {
bug_id: 42,
file: upload_file.to_string_lossy().into_owned(),
summary: Some("test".into()),
content_type: Some("text/plain".into()),
private: false,
is_patch: false,
comment: Some("sensitive".into()),
comment_private: true,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"upload --comment-private should drive POST→GET→PUT to success: {result:?}"
);
}
#[tokio::test]
async fn attachment_list_returns_is_patch_field_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/attachment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": [{
"id": 100,
"bug_id": 42,
"file_name": "fix.patch",
"summary": "patch",
"content_type": "text/plain",
"creation_time": "2026-05-06T00:00:00Z",
"is_obsolete": false,
"is_private": false,
"is_patch": true,
"size": 12
}]
}
})))
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::List { bug_id: 42 };
let mut __io20 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io20.writers(),
)
.await;
let output = __io20.out_str().to_string();
assert!(result.is_ok(), "list should succeed: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["is_patch"], true);
}
#[tokio::test]
async fn attachment_update_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/attachment/99"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"attachments": [{"id": 99, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::AttachmentAction::Update {
id: 99,
summary: Some("Updated summary".to_string()),
file_name: None,
content_type: None,
obsolete: None,
is_patch: None,
is_private: None,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"attachment update should succeed: {result:?}"
);
}
#[tokio::test]
async fn component_update_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/component/10"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 10})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ComponentAction::Update {
id: 10,
name: Some("Updated".to_string()),
description: None,
default_assignee: None,
};
let result = bzr::commands::component::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"component update should succeed: {result:?}"
);
}
#[tokio::test]
async fn product_view_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/product"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"products": [{
"id": 1, "name": "Firefox", "description": "Browser",
"is_active": true, "components": [], "versions": [], "milestones": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ProductAction::View {
name: "Firefox".to_string(),
};
let result = bzr::commands::product::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "product view should succeed: {result:?}");
}
#[tokio::test]
async fn product_create_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/product"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 5})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ProductAction::Create {
name: "NewProduct".to_string(),
description: "A new product".to_string(),
version: "1.0".to_string(),
is_open: true,
};
let result = bzr::commands::product::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "product create should succeed: {result:?}");
}
#[tokio::test]
async fn product_update_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/product/Firefox"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"products": [{"id": 1, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::ProductAction::Update {
name: "Firefox".to_string(),
description: Some("Updated description".to_string()),
default_milestone: None,
is_open: None,
};
let result = bzr::commands::product::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "product update should succeed: {result:?}");
}
#[tokio::test]
async fn user_create_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 42})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::UserAction::Create {
email: "new@example.com".to_string(),
login: None,
full_name: Some("New User".to_string()),
password: None,
};
let result = bzr::commands::user::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "user create should succeed: {result:?}");
}
#[tokio::test]
async fn user_update_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/user/alice%40example%2Ecom"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{"id": 1, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::UserAction::Update {
user: "alice@example.com".to_string(),
real_name: Some("Alice Updated".to_string()),
email: None,
disable_login: None,
login_denied_text: None,
};
let result = bzr::commands::user::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "user update should succeed: {result:?}");
}
#[tokio::test]
async fn group_create_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/group"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 10})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::Create {
name: "testers".to_string(),
description: "Tester group".to_string(),
is_active: true,
};
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "group create should succeed: {result:?}");
}
#[tokio::test]
async fn group_update_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/group/testers"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 10, "changes": {}
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::Update {
group: "testers".to_string(),
description: Some("Updated testers".to_string()),
is_active: None,
};
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "group update should succeed: {result:?}");
}
#[tokio::test]
async fn group_add_user_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/user/alice%40example%2Ecom"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{"id": 1, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::AddUser {
group: "admin".to_string(),
user: "alice@example.com".to_string(),
};
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(result.is_ok(), "group add-user should succeed: {result:?}");
}
#[tokio::test]
async fn group_remove_user_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/user/alice%40example%2Ecom"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{"id": 1, "changes": {}}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::RemoveUser {
group: "admin".to_string(),
user: "alice@example.com".to_string(),
};
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"group remove-user should succeed: {result:?}"
);
}
#[tokio::test]
async fn group_list_users_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{
"id": 1,
"name": "alice@example.com",
"real_name": "Alice",
"email": "alice@example.com",
"groups": [{"name": "admin"}]
}]
})))
.expect(1)
.mount(&mock)
.await;
let action = bzr::cli::GroupAction::ListUsers {
group: "admin".to_string(),
details: false,
};
let result = bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"group list-users should succeed: {result:?}"
);
}
#[tokio::test]
async fn config_set_server_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let _lock = 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();
std::fs::write(
config_dir.join("config.toml"),
"default_server = \"local\"\n\n[servers.local]\nurl = \"https://bugzilla.local\"\napi_key = \"key-1234567890\"\n",
)
.unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
let action = bzr::cli::ConfigAction::SetServer {
name: "staging".to_string(),
url: "https://staging.bugzilla.example".to_string(),
api_key: Some("staging-key-abc".to_string()),
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,
};
let result = bzr::commands::config::execute(
&action,
None,
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"config set-server should succeed: {result:?}"
);
}
#[tokio::test]
async fn config_set_default_integration() {
let mut __cap_io = bzr::test_helpers::CapturedIo::new();
let _lock = 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();
std::fs::write(
config_dir.join("config.toml"),
"default_server = \"local\"\n\n[servers.local]\nurl = \"https://bugzilla.local\"\napi_key = \"key-1234567890\"\n\n[servers.staging]\nurl = \"https://staging.example\"\napi_key = \"staging-key\"\n",
)
.unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", tmp.path()) };
let action = bzr::cli::ConfigAction::SetDefault {
name: "staging".to_string(),
};
let result = bzr::commands::config::execute(
&action,
None,
bzr::types::OutputFormat::Json,
None,
&mut __cap_io.writers(),
)
.await;
assert!(
result.is_ok(),
"config set-default should succeed: {result:?}"
);
}
async fn dispatch_cli(args: &[&str]) -> bzr::error::Result<()> {
let cli = bzr::cli::Cli::try_parse_from(args)
.map_err(|e| bzr::error::BzrError::InputValidation(e.to_string()))?;
let format = if cli.json {
bzr::types::OutputFormat::Json
} else {
cli.output.unwrap_or(bzr::types::OutputFormat::Json)
};
let mut io = bzr::test_helpers::CapturedIo::new();
bzr::dispatch(&cli, format, &mut io.writers()).await
}
async fn dispatch_cli_with_output(args: &[&str]) -> (bzr::error::Result<()>, String) {
let cli = match bzr::cli::Cli::try_parse_from(args) {
Ok(c) => c,
Err(e) => {
return (
Err(bzr::error::BzrError::InputValidation(e.to_string())),
String::new(),
);
}
};
let format = if cli.json {
bzr::types::OutputFormat::Json
} else {
cli.output.unwrap_or(bzr::types::OutputFormat::Json)
};
let mut io = bzr::test_helpers::CapturedIo::new();
let result = bzr::dispatch(&cli, format, &mut io.writers()).await;
(result, io.out_str().to_string())
}
#[tokio::test]
async fn e2e_bug_list_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": [{"id": 1, "summary": "CLI test", "status": "NEW"}]
})))
.expect(1)
.mount(&mock)
.await;
let (result, output) = dispatch_cli_with_output(&[
"bzr",
"--server",
"test",
"--json",
"bug",
"list",
"--product",
"Firefox",
])
.await;
assert!(result.is_ok(), "e2e bug list: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed[0]["summary"], "CLI test");
}
#[tokio::test]
async fn e2e_bug_view_via_cli_args() {
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(serde_json::json!({
"bugs": [{"id": 42, "summary": "CLI view test", "status": "NEW"}]
})))
.expect(1)
.mount(&mock)
.await;
let (result, output) =
dispatch_cli_with_output(&["bzr", "--server", "test", "--json", "bug", "view", "42"]).await;
assert!(result.is_ok(), "e2e bug view: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 42);
assert_eq!(parsed["summary"], "CLI view test");
}
#[tokio::test]
async fn e2e_whoami_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/whoami"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": 1,
"name": "admin@example.com",
"real_name": "Admin"
})))
.expect(1)
.mount(&mock)
.await;
let (result, output) =
dispatch_cli_with_output(&["bzr", "--server", "test", "--json", "whoami"]).await;
assert!(result.is_ok(), "e2e whoami: {result:?}");
let parsed = serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["name"], "admin@example.com");
}
#[tokio::test]
async fn e2e_config_show_via_cli_args() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let result = dispatch_cli(&["bzr", "--json", "config", "show"]).await;
assert!(result.is_ok(), "e2e config show: {result:?}");
}
#[tokio::test]
async fn e2e_server_info_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/version"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"version": "5.2"})),
)
.expect(1)
.mount(&mock)
.await;
Mock::given(method("GET"))
.and(path("/rest/extensions"))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"extensions": {}})),
)
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&["bzr", "--server", "test", "--json", "server", "info"]).await;
assert!(result.is_ok(), "e2e server info: {result:?}");
}
#[tokio::test]
async fn e2e_comment_list_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/comment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": {
"comments": [
{"id": 1, "bug_id": 42, "text": "First", "count": 0}
]
}
}
})))
.expect(1)
.mount(&mock)
.await;
let result =
dispatch_cli(&["bzr", "--server", "test", "--json", "comment", "list", "42"]).await;
assert!(result.is_ok(), "e2e comment list: {result:?}");
}
#[tokio::test]
async fn e2e_attachment_list_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug/42/attachment"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"bugs": {
"42": [{
"id": 1,
"bug_id": 42,
"file_name": "patch.diff",
"summary": "Fix",
"content_type": "text/plain",
"size": 100
}]
}
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr",
"--server",
"test",
"--json",
"attachment",
"list",
"42",
])
.await;
assert!(result.is_ok(), "e2e attachment list: {result:?}");
}
#[tokio::test]
async fn e2e_product_view_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/product"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"products": [{
"id": 1, "name": "Firefox", "description": "Browser",
"is_active": true, "components": [], "versions": [], "milestones": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr", "--server", "test", "--json", "product", "view", "Firefox",
])
.await;
assert!(result.is_ok(), "e2e product view: {result:?}");
}
#[tokio::test]
async fn e2e_field_list_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/field/bug/bug_status"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"fields": [{
"values": [
{"name": "NEW", "sort_key": 100, "is_active": true}
]
}]
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr", "--server", "test", "--json", "field", "list", "status",
])
.await;
assert!(result.is_ok(), "e2e field list: {result:?}");
}
#[tokio::test]
async fn e2e_user_search_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"users": [{
"id": 1,
"name": "alice@example.com",
"real_name": "Alice",
"email": "alice@example.com",
"groups": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr", "--server", "test", "--json", "user", "search", "alice",
])
.await;
assert!(result.is_ok(), "e2e user search: {result:?}");
}
#[tokio::test]
async fn e2e_group_view_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/group"))
.and(query_param("names", "admin"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"groups": [{
"id": 1,
"name": "admin",
"description": "Administrators",
"is_active": true,
"membership": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr", "--server", "test", "--json", "group", "view", "admin",
])
.await;
assert!(result.is_ok(), "e2e group view: {result:?}");
}
#[tokio::test]
async fn e2e_classification_view_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/classification/Unclassified"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"classifications": [{
"id": 1,
"name": "Unclassified",
"description": "Default",
"sort_key": 0,
"products": []
}]
})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr",
"--server",
"test",
"--json",
"classification",
"view",
"Unclassified",
])
.await;
assert!(result.is_ok(), "e2e classification view: {result:?}");
}
#[tokio::test]
async fn e2e_component_create_via_cli_args() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("POST"))
.and(path("/rest/component"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 11})))
.expect(1)
.mount(&mock)
.await;
let result = dispatch_cli(&[
"bzr",
"--server",
"test",
"--json",
"component",
"create",
"--product",
"TestProduct",
"--name",
"Backend",
"--description",
"Backend component",
"--default-assignee",
"dev@test.com",
])
.await;
assert!(result.is_ok(), "e2e component create: {result:?}");
}
#[tokio::test]
async fn e2e_template_list_via_cli_args() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let result = dispatch_cli(&["bzr", "--json", "template", "list"]).await;
assert!(result.is_ok(), "e2e template list: {result:?}");
}
#[tokio::test]
async fn e2e_query_list_via_cli_args() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let result = dispatch_cli(&["bzr", "--json", "query", "list"]).await;
assert!(result.is_ok(), "e2e query list: {result:?}");
}
#[test]
fn cli_version_flag_exits_with_display_version() {
let result = bzr::cli::Cli::try_parse_from(["bzr", "--version"]);
let Err(err) = result else {
unreachable!("--version should not produce a parsed Cli");
};
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
}
#[test]
fn cli_help_flag_exits_with_display_help() {
let result = bzr::cli::Cli::try_parse_from(["bzr", "--help"]);
let Err(err) = result else {
unreachable!("--help should not produce a parsed Cli");
};
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn cli_missing_subcommand_errors() {
let result = bzr::cli::Cli::try_parse_from(["bzr"]);
let Err(err) = result else {
unreachable!("bzr without a subcommand should require one");
};
assert_eq!(
err.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
);
}
#[tokio::test]
async fn bug_list_issue_158_mixed_positive_and_negation_reaches_wire() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("GET"))
.and(path("/rest/bug"))
.and(query_param("product", "P"))
.and(query_param("f1", "status_whiteboard"))
.and(query_param("o1", "notsubstring"))
.and(query_param("v1", "wip"))
.and(query_param("f2", "resolution"))
.and(query_param("o2", "notequals"))
.and(query_param("v2", "FIXED"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"bugs": []})))
.expect(1)
.mount(&mock)
.await;
let mut action = empty_list_action();
if let bzr::cli::BugAction::List {
product,
whiteboard,
resolution,
..
} = &mut action
{
*product = vec!["P".into()];
*whiteboard = vec!["!wip".into()];
*resolution = vec!["!FIXED".into()];
}
let mut __io24 = bzr::test_helpers::CapturedIo::new();
let result = bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
&mut __io24.writers(),
)
.await;
let _output = __io24.out_str().to_string();
assert!(result.is_ok(), "bug list should succeed: {result:?}");
}