#![expect(clippy::unwrap_used)]
use std::io::Write;
use wiremock::matchers::{body_string_contains, method, path};
use wiremock::{Mock, ResponseTemplate};
use crate::cli::{BugAction, TemplateAction};
use crate::error::BzrError;
use crate::test_helpers::setup_test_env;
use crate::types::OutputFormat;
fn create_action() -> BugAction {
BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: Some("New bug".into()),
version: Some("unspecified".into()),
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
}
}
#[tokio::test]
async fn bug_create_sends_post() {
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": 99})))
.expect(1)
.mount(&mock)
.await;
let mut __io = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&create_action(),
None,
OutputFormat::Json,
None,
&mut __io.writers(),
)
.await;
let output = __io.out_str().to_string();
assert!(result.is_ok());
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["action"], "created");
assert_eq!(parsed["id"], 99);
}
#[tokio::test]
async fn bug_create_missing_product_returns_input_validation() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = BugAction::Create {
template: None,
product: None,
component: Some("General".into()),
summary: Some("Needs product".into()),
version: None,
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io2 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io2.writers(),
)
.await;
let _output = __io2.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(msg) if msg.contains("--product")),
"got {err:?}"
);
}
#[tokio::test]
async fn bug_create_missing_component_returns_input_validation() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: None,
summary: Some("Needs component".into()),
version: None,
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io3 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io3.writers(),
)
.await;
let _output = __io3.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(msg) if msg.contains("--component")),
"got {err:?}"
);
}
#[tokio::test]
async fn bug_create_with_unknown_template_errors() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = BugAction::Create {
template: Some("does-not-exist".into()),
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: Some("Bad template".into()),
version: None,
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io4 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io4.writers(),
)
.await;
let _output = __io4.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::Config(msg) if msg.contains("does-not-exist")),
"got {err:?}"
);
}
#[tokio::test]
async fn bug_create_with_template_fills_missing_fields() {
let (_lock, mock, _tmp) = setup_test_env().await;
let save = TemplateAction::Save {
name: "tpl".into(),
product: Some("TplProduct".into()),
component: Some("TplComponent".into()),
version: Some("9.9".into()),
priority: Some("P2".into()),
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
description: Some("from template".into()),
};
let mut __io5 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::template::execute(
&save,
None,
OutputFormat::Json,
None,
&mut __io5.writers(),
)
.await;
let _ = __io5.out_str().to_string();
assert!(result.is_ok(), "template save failed: {result:?}");
Mock::given(method("POST"))
.and(path("/rest/bug"))
.and(body_string_contains("TplProduct"))
.and(body_string_contains("TplComponent"))
.and(body_string_contains("9.9"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 7})))
.expect(1)
.mount(&mock)
.await;
let action = BugAction::Create {
template: Some("tpl".into()),
product: None,
component: None,
summary: Some("From template".into()),
version: None,
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io6 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io6.writers(),
)
.await;
let output = __io6.out_str().to_string();
assert!(
result.is_ok(),
"bug create with template failed: {result:?}"
);
let parsed: serde_json::Value =
serde_json::from_str::<serde_json::Value>(output.trim()).unwrap();
assert_eq!(parsed["id"], 7);
assert_eq!(parsed["action"], "created");
}
#[tokio::test]
async fn bug_create_reads_description_from_file() {
let (_lock, mock, _tmp) = setup_test_env().await;
let dir = std::env::temp_dir();
let desc_path = dir.join(format!("bzr-create-desc-{}.txt", std::process::id()));
std::fs::write(&desc_path, "description from file\n").unwrap();
Mock::given(method("POST"))
.and(path("/rest/bug"))
.and(body_string_contains("description from file"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 11})))
.expect(1)
.mount(&mock)
.await;
let action = BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: Some("Bug from file".into()),
version: None,
description: None,
description_file: Some(desc_path.clone()),
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io7 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io7.writers(),
)
.await;
let _output = __io7.out_str().to_string();
assert!(result.is_ok(), "got {result:?}");
let _ = std::fs::remove_file(&desc_path);
}
#[tokio::test]
async fn bug_create_description_file_missing_returns_input_validation() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: Some("Bug".into()),
version: None,
description: None,
description_file: Some(std::path::PathBuf::from("/nonexistent-bzr-test-path-xyz")),
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io8 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io8.writers(),
)
.await;
let _output = __io8.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(m) if m.contains("description-file")),
"got {err:?}"
);
}
#[tokio::test]
async fn bug_create_description_file_non_utf8_returns_input_validation() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let dir = std::env::temp_dir();
let bad_path = dir.join(format!("bzr-create-bad-utf8-{}.bin", std::process::id()));
std::fs::write(&bad_path, [0xff_u8, 0xfe_u8, 0xfd_u8]).unwrap();
let action = BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: Some("Bug".into()),
version: None,
description: None,
description_file: Some(bad_path.clone()),
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io9 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io9.writers(),
)
.await;
let _output = __io9.out_str().to_string();
let err = result.unwrap_err();
let _ = std::fs::remove_file(&bad_path);
assert!(
matches!(&err, BzrError::InputValidation(m) if m.contains("description-file")),
"got {err:?}"
);
}
#[tokio::test]
async fn bug_create_missing_summary_without_editor_flow_is_rejected() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let action = BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: None,
version: None,
description: Some("body".into()),
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io10 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io10.writers(),
)
.await;
let _output = __io10.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(m) if m.contains("--summary")),
"got {err:?}"
);
}
#[test]
fn parse_editor_buffer_strips_sentinel_and_extracts_summary() {
let buf = "\
My bug summary
This is the description.
# ------------------------ >8 ------------------------
# Do not modify or remove the line above.
# Product: Foo
";
let (summary, description) = super::parse_editor_buffer(buf).unwrap();
assert_eq!(summary, "My bug summary");
assert_eq!(description, "This is the description.");
}
#[test]
fn parse_editor_buffer_handles_multi_line_summary_block() {
let buf = "\
Summary line
overflow line
Description here
# ------------------------ >8 ------------------------
# trailer
";
let (summary, description) = super::parse_editor_buffer(buf).unwrap();
assert_eq!(summary, "Summary line");
assert_eq!(description, "overflow line\n\nDescription here");
}
#[test]
fn parse_editor_buffer_skips_leading_blank_lines() {
let buf =
"\n\nReal summary\n\nReal body\n\n# ------------------------ >8 ------------------------\n";
let (summary, description) = super::parse_editor_buffer(buf).unwrap();
assert_eq!(summary, "Real summary");
assert_eq!(description, "Real body");
}
#[test]
fn parse_editor_buffer_empty_above_sentinel_errors() {
let buf = "\
# ------------------------ >8 ------------------------
# Product: Foo
# Component: Bar
";
let err = super::parse_editor_buffer(buf).unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(m) if m.contains("empty buffer")),
"got {err:?}"
);
}
#[test]
fn parse_editor_buffer_no_sentinel_uses_full_buffer() {
let buf = "Summary\n\nDescription\n";
let (summary, description) = super::parse_editor_buffer(buf).unwrap();
assert_eq!(summary, "Summary");
assert_eq!(description, "Description");
}
#[test]
fn parse_editor_buffer_only_summary_no_description() {
let buf = "\
Just a summary
# ------------------------ >8 ------------------------
";
let (summary, description) = super::parse_editor_buffer(buf).unwrap();
assert_eq!(summary, "Just a summary");
assert_eq!(description, "");
}
#[test]
fn build_editor_template_includes_summary_and_field_reminder() {
use crate::types::CreateBugParams;
let params = CreateBugParams {
product: "Foo".into(),
component: "Bar".into(),
summary: String::new(),
version: "1.0".into(),
description: None,
priority: None,
severity: Some("High".into()),
assigned_to: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
cc: vec![],
keywords: vec![],
};
let buf = super::build_editor_template(Some("Pre-filled summary"), None, ¶ms);
assert!(buf.starts_with("Pre-filled summary\n"));
assert!(buf.contains("# ------------------------ >8 ------------------------"));
assert!(buf.contains("# Product: Foo"));
assert!(buf.contains("# Component: Bar"));
assert!(buf.contains("# Severity: High"));
assert!(buf.contains("# Priority: <unset>"));
}
#[test]
fn build_editor_template_includes_template_description_body() {
use crate::types::CreateBugParams;
let params = CreateBugParams {
product: "Foo".into(),
component: "Bar".into(),
summary: String::new(),
version: "1.0".into(),
description: None,
priority: None,
severity: None,
assigned_to: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
cc: vec![],
keywords: vec![],
};
let buf = super::build_editor_template(None, Some("## Steps\n\n## Expected"), ¶ms);
assert!(buf.contains("## Steps"));
assert!(buf.contains("## Expected"));
}
fn install_fake_editor() -> std::path::PathBuf {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir();
let script = dir.join(format!("bzr-bc-editor-{}.sh", std::process::id()));
std::fs::write(
&script,
"#!/bin/sh\nprintf 'Editor summary\\n\\nEditor description\\n' > \"$1\"\n",
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
fn editor_action_no_summary_no_description() -> BugAction {
BugAction::Create {
template: None,
product: Some("TestProduct".into()),
component: Some("General".into()),
summary: None,
version: None,
description: None,
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
}
}
#[tokio::test]
async fn bug_create_editor_flow_resolves_via_editor_when_stdin_is_tty() {
use std::io::IsTerminal;
if !std::io::stdin().is_terminal() {
let _ = writeln!(
std::io::stderr(),
"Skipping: editor flow requires TTY stdin (cargo test runs non-TTY)."
);
return;
}
let (_lock, mock, _tmp) = setup_test_env().await;
let script = install_fake_editor();
let prev = std::env::var("EDITOR").ok();
unsafe { std::env::set_var("EDITOR", &script) };
Mock::given(method("POST"))
.and(path("/rest/bug"))
.and(body_string_contains("Editor summary"))
.and(body_string_contains("Editor description"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 33})))
.expect(1)
.mount(&mock)
.await;
let mut __io11 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&editor_action_no_summary_no_description(),
None,
OutputFormat::Json,
None,
&mut __io11.writers(),
)
.await;
let _output = __io11.out_str().to_string();
unsafe {
if let Some(p) = prev {
std::env::set_var("EDITOR", p);
} else {
std::env::remove_var("EDITOR");
}
}
let _ = std::fs::remove_file(&script);
assert!(result.is_ok(), "editor flow should succeed: {result:?}");
}
#[tokio::test]
async fn bug_create_editor_branch_unreachable_when_stdin_piped() {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
let _ = writeln!(
std::io::stderr(),
"Skipping: this test asserts the non-editor path under piped stdin."
);
return;
}
let (_lock, mock, _tmp) = setup_test_env().await;
let script = install_fake_editor();
let prev = std::env::var("EDITOR").ok();
unsafe { std::env::set_var("EDITOR", &script) };
Mock::given(method("POST"))
.and(path("/rest/bug"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": 0})))
.expect(0)
.mount(&mock)
.await;
let mut __io12 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&editor_action_no_summary_no_description(),
None,
OutputFormat::Json,
None,
&mut __io12.writers(),
)
.await;
let _output = __io12.out_str().to_string();
unsafe {
if let Some(p) = prev {
std::env::set_var("EDITOR", p);
} else {
std::env::remove_var("EDITOR");
}
}
let _ = std::fs::remove_file(&script);
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(m) if m.contains("piped stdin")),
"expected InputValidation about empty piped stdin, got {err:?}"
);
}
#[tokio::test]
async fn bug_create_template_description_does_not_fall_back_outside_editor_flow() {
let (_lock, _mock, _tmp) = setup_test_env().await;
let save = TemplateAction::Save {
name: "tpl-with-desc".into(),
product: Some("TestProduct".into()),
component: Some("General".into()),
version: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
description: Some("template body".into()),
};
let mut __io13 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::template::execute(
&save,
None,
OutputFormat::Json,
None,
&mut __io13.writers(),
)
.await;
let _ = __io13.out_str().to_string();
assert!(result.is_ok(), "template save failed: {result:?}");
let action = BugAction::Create {
template: Some("tpl-with-desc".into()),
product: None,
component: None,
summary: Some("Bug from template".into()),
version: None,
description: None,
description_file: None,
priority: None,
severity: None,
assignee: None,
op_sys: None,
rep_platform: None,
blocks: vec![],
depends_on: vec![],
};
let mut __io14 = crate::test_helpers::CapturedIo::new();
let result = crate::commands::bug::execute(
&action,
None,
OutputFormat::Json,
None,
&mut __io14.writers(),
)
.await;
let _output = __io14.out_str().to_string();
let err = result.unwrap_err();
assert!(
matches!(&err, BzrError::InputValidation(_)),
"expected InputValidation (template body should not auto-fill outside the editor flow), got {err:?}"
);
}