#![expect(clippy::unwrap_used)]
use bzr::test_helpers::{capture_stdout, extract_json, setup_test_env};
use bzr::ENV_LOCK;
use clap::Parser;
use wiremock::matchers::{method, path, query_param};
use wiremock::{Mock, ResponseTemplate};
#[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 = bzr::cli::BugAction::List {
product: vec![],
component: vec![],
status: vec![],
assignee: vec![],
creator: vec![],
priority: vec![],
severity: vec![],
id: vec![],
alias: None,
limit: 50,
fields: None,
exclude_fields: None,
};
let (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug list should succeed: {result:?}");
let parsed = extract_json(&output);
assert_eq!(parsed[0]["id"], 1);
assert_eq!(parsed[0]["summary"], "Test bug");
assert_eq!(parsed[0]["status"], "NEW");
}
#[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 {
id: "42".to_string(),
fields: None,
exclude_fields: None,
};
let (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug view should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug search should succeed: {result:?}");
let parsed = extract_json(&output);
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: "New bug".to_string(),
version: Some("unspecified".to_string()),
description: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug create should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "comment list should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::whoami::execute(
&bzr::cli::WhoamiAction::Show,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "whoami should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::product::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "product list should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::server::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "server info should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::field::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "field list should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::classification::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(
result.is_ok(),
"classification view should succeed: {result:?}"
);
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::user::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "user search should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::group::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "group view should succeed: {result:?}");
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::component::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(
result.is_ok(),
"component create should succeed: {result:?}"
);
let parsed = extract_json(&output);
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 (result, output) = capture_stdout(bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "attachment list should succeed: {result:?}");
let parsed = extract_json(&output);
assert_eq!(parsed[0]["file_name"], "patch.diff");
}
#[tokio::test]
async fn config_show_integration() {
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).await;
assert!(result.is_ok(), "config show should succeed: {result:?}");
}
#[tokio::test]
async fn command_with_unknown_server_returns_error() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = bzr::cli::BugAction::List {
product: vec![],
component: vec![],
status: vec![],
assignee: vec![],
creator: vec![],
priority: vec![],
severity: vec![],
id: vec![],
alias: None,
limit: 50,
fields: None,
exclude_fields: None,
};
let result = bzr::commands::bug::execute(
&action,
Some("nonexistent"),
bzr::types::OutputFormat::Json,
None,
)
.await;
assert!(result.is_err(), "should fail with unknown server");
}
#[tokio::test]
async fn api_error_propagates() {
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 {
id: "99999".to_string(),
fields: None,
exclude_fields: None,
};
let result =
bzr::commands::bug::execute(&action, Some("test"), bzr::types::OutputFormat::Json, None)
.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 (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug history should succeed: {result:?}");
let parsed = extract_json(&output);
assert_eq!(parsed[0]["who"], "dev@example.com");
assert_eq!(parsed[0]["changes"][0]["field_name"], "status");
}
#[tokio::test]
async fn bug_update_integration() {
let (_lock, mock, _tmp) = setup_test_env().await;
Mock::given(method("PUT"))
.and(path("/rest/bug/42"))
.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()),
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![],
};
let (result, output) = capture_stdout(bzr::commands::bug::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "bug update should succeed: {result:?}");
let parsed = extract_json(&output);
assert_eq!(parsed["id"], 42);
assert_eq!(parsed["action"], "updated");
}
#[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 (result, output) = capture_stdout(bzr::commands::comment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
))
.await;
assert!(result.is_ok(), "comment add should succeed: {result:?}");
let parsed = extract_json(&output);
assert_eq!(parsed["id"], 999);
}
#[tokio::test]
async fn comment_tag_integration() {
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,
)
.await;
assert!(result.is_ok(), "comment tag should succeed: {result:?}");
}
#[tokio::test]
async fn comment_search_tags_integration() {
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,
)
.await;
assert!(
result.is_ok(),
"comment search-tags should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_download_integration() {
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 {
id: 99,
out: Some(out_path.to_string_lossy().into_owned()),
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
)
.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_upload_integration() {
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,
flag: vec![],
};
let result = bzr::commands::attachment::execute(
&action,
Some("test"),
bzr::types::OutputFormat::Json,
None,
)
.await;
assert!(
result.is_ok(),
"attachment upload should succeed: {result:?}"
);
}
#[tokio::test]
async fn attachment_update_integration() {
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,
)
.await;
assert!(
result.is_ok(),
"attachment update should succeed: {result:?}"
);
}
#[tokio::test]
async fn component_update_integration() {
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,
)
.await;
assert!(
result.is_ok(),
"component update should succeed: {result:?}"
);
}
#[tokio::test]
async fn product_view_integration() {
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,
)
.await;
assert!(result.is_ok(), "product view should succeed: {result:?}");
}
#[tokio::test]
async fn product_create_integration() {
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,
)
.await;
assert!(result.is_ok(), "product create should succeed: {result:?}");
}
#[tokio::test]
async fn product_update_integration() {
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,
)
.await;
assert!(result.is_ok(), "product update should succeed: {result:?}");
}
#[tokio::test]
async fn user_create_integration() {
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)
.await;
assert!(result.is_ok(), "user create should succeed: {result:?}");
}
#[tokio::test]
async fn user_update_integration() {
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)
.await;
assert!(result.is_ok(), "user update should succeed: {result:?}");
}
#[tokio::test]
async fn group_create_integration() {
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)
.await;
assert!(result.is_ok(), "group create should succeed: {result:?}");
}
#[tokio::test]
async fn group_update_integration() {
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)
.await;
assert!(result.is_ok(), "group update should succeed: {result:?}");
}
#[tokio::test]
async fn group_add_user_integration() {
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)
.await;
assert!(result.is_ok(), "group add-user should succeed: {result:?}");
}
#[tokio::test]
async fn group_remove_user_integration() {
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)
.await;
assert!(
result.is_ok(),
"group remove-user should succeed: {result:?}"
);
}
#[tokio::test]
async fn group_list_users_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": [{"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)
.await;
assert!(
result.is_ok(),
"group list-users should succeed: {result:?}"
);
}
#[tokio::test]
async fn config_set_server_integration() {
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).await;
assert!(
result.is_ok(),
"config set-server should succeed: {result:?}"
);
}
#[tokio::test]
async fn config_set_default_integration() {
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).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)
};
bzr::dispatch(&cli, format).await
}
#[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) = capture_stdout(dispatch_cli(&[
"bzr",
"--server",
"test",
"--json",
"bug",
"list",
"--product",
"Firefox",
]))
.await;
assert!(result.is_ok(), "e2e bug list: {result:?}");
let parsed = extract_json(&output);
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) = capture_stdout(dispatch_cli(&[
"bzr", "--server", "test", "--json", "bug", "view", "42",
]))
.await;
assert!(result.is_ok(), "e2e bug view: {result:?}");
let parsed = extract_json(&output);
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) = capture_stdout(dispatch_cli(&[
"bzr", "--server", "test", "--json", "whoami",
]))
.await;
assert!(result.is_ok(), "e2e whoami: {result:?}");
let parsed = extract_json(&output);
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
);
}