use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains;
use serde_json::Value;
mod common;
use common::{fez_fake, AuditLog, PKG_COLUMNS};
fn package_list_json(args: &[&str]) -> Value {
let output = fez_fake().args(args).output().expect("run fez");
assert!(
output.status.success(),
"fez failed: stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("valid JSON envelope")
}
#[test]
fn packages_list_json_has_packages() {
fez_fake()
.args(["packages", "list", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageList\""))
.stdout(contains(PKG_COLUMNS))
.stdout(contains("\"rows\":"))
.stdout(contains("\"count\":"))
.stdout(contains("\"scope\":\"installed\""))
.stdout(contains("bash"))
.stdout(contains("htop"));
}
#[test]
fn packages_list_human_default() {
fez_fake()
.args(["packages", "list"])
.assert()
.success()
.stdout(contains("bash"))
.stdout(contains("NAME"));
}
#[test]
fn packages_list_repo_filter_is_exact() {
fez_fake()
.args(["packages", "list", "--repo", "fedora", "--json"])
.assert()
.success()
.stdout(contains("bash"))
.stdout(contains("htop"))
.stdout(contains("nginx"))
.stdout(contains("vim-enhanced").not())
.stdout(contains("\"repos\":[\"fedora\"]"));
}
#[test]
fn packages_list_repo_filter_updates_only() {
fez_fake()
.args(["packages", "list", "--repo", "updates", "--json"])
.assert()
.success()
.stdout(contains("vim-enhanced"))
.stdout(contains("\"bash\"").not())
.stdout(contains("\"nginx\"").not());
}
#[test]
fn packages_list_multiple_repo_flags_union() {
fez_fake()
.args([
"packages", "list", "--repo", "fedora", "--repo", "updates", "--json",
])
.assert()
.success()
.stdout(contains("bash"))
.stdout(contains("vim-enhanced"))
.stdout(contains("\"repos\":[\"fedora\",\"updates\"]"));
}
#[test]
fn packages_list_unknown_repo_is_empty() {
fez_fake()
.args(["packages", "list", "--repo", "no-such-repo", "--json"])
.assert()
.success()
.stdout(contains("\"bash\"").not())
.stdout(contains("vim-enhanced").not())
.stdout(contains("\"count\":0"));
}
#[test]
fn packages_list_no_repo_returns_all() {
fez_fake()
.args(["packages", "list", "--json"])
.assert()
.success()
.stdout(contains("bash"))
.stdout(contains("vim-enhanced"));
}
#[test]
fn packages_list_limit_offset_returns_page_metadata() {
let envelope = package_list_json(&[
"packages", "list", "--limit", "2", "--offset", "1", "--json",
]);
let data = &envelope["data"];
let rows = data["rows"].as_array().expect("rows array");
let names: Vec<&str> = rows
.iter()
.map(|row| row[0].as_str().expect("package name"))
.collect();
assert_eq!(names, ["htop", "nginx"]);
assert_eq!(data["count"], 2);
assert_eq!(data["total"], 4);
assert_eq!(data["returned"], 2);
assert_eq!(data["limit"], 2);
assert_eq!(data["offset"], 1);
assert_eq!(data["next_offset"], 3);
}
#[test]
fn packages_list_name_filter_reduces_available_rows() {
let envelope =
package_list_json(&["packages", "list", "--available", "--name", "ngi", "--json"]);
let data = &envelope["data"];
let rows = data["rows"].as_array().expect("rows array");
assert_eq!(rows.len(), 1);
assert_eq!(rows[0][0], "nginx");
assert_eq!(data["name"], "ngi");
assert_eq!(data["total"], 1);
}
#[test]
fn packages_list_large_unpaginated_response_hints_to_filter() {
let output = fez_fake()
.env("FEZ_FAKE_PACKAGE_COUNT", "1001")
.args(["packages", "list", "--available", "--json"])
.output()
.expect("run fez");
assert!(output.status.success());
let envelope: Value = serde_json::from_slice(&output.stdout).expect("valid JSON envelope");
assert_eq!(envelope["data"]["total"], 1001);
assert_eq!(envelope["data"]["limit"], Value::Null);
assert!(
envelope["hints"]
.as_array()
.expect("hints array")
.iter()
.any(|hint| hint
.as_str()
.is_some_and(|h| h.contains("use --limit") && h.contains("--name"))),
"expected pagination/filter hint in {envelope}"
);
}
#[test]
fn packages_info_json() {
fez_fake()
.args(["packages", "info", "bash", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageInfo\""))
.stdout(contains("\"name\":\"bash\""));
}
#[test]
fn packages_search_finds_nginx() {
fez_fake()
.args(["packages", "search", "ngin", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageSearch\""))
.stdout(contains(PKG_COLUMNS))
.stdout(contains("\"pattern\":\"ngin\""))
.stdout(contains("nginx"));
}
#[test]
fn packages_check_update_lists_updates() {
fez_fake()
.args(["packages", "check-update", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageUpdates\""))
.stdout(contains(PKG_COLUMNS))
.stdout(contains("\"rows\":"))
.stdout(contains("\"count\":"));
}
#[test]
fn packages_repolist_shows_enabled_state() {
fez_fake()
.args(["packages", "repolist", "--all", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"RepoList\""))
.stdout(contains("\"columns\":[\"id\",\"name\",\"enabled\"]"))
.stdout(contains("\"rows\":"))
.stdout(contains("\"count\":"))
.stdout(contains("fedora"));
}
#[test]
fn packages_install_dry_run_emits_plan() {
fez_fake()
.env("FEZ_FAKE_PLAN", "install")
.env("FEZ_AUDIT", "off")
.args(["packages", "install", "htop", "--dry-run", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackagePlan\""))
.stdout(contains("\"dry_run\":true"))
.stdout(contains("\"operation\":\"install\""));
}
#[test]
fn packages_remove_small_plan_succeeds() {
fez_fake()
.env("FEZ_FAKE_PLAN", "small")
.env("FEZ_AUDIT", "off")
.args(["packages", "remove", "htop", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageMutation\""))
.stdout(contains("\"operation\":\"remove\""));
}
#[test]
fn packages_upgrade_emits_mutation() {
fez_fake()
.env("FEZ_AUDIT", "off")
.env("FEZ_FAKE_PLAN", "install")
.args(["packages", "upgrade", "nginx", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageMutation\""))
.stdout(contains("\"operation\":\"upgrade\""));
}
#[test]
fn packages_upgrade_all_dry_run_emits_plan() {
fez_fake()
.env("FEZ_AUDIT", "off")
.env("FEZ_FAKE_PLAN", "install")
.args(["packages", "upgrade", "--dry-run", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackagePlan\""))
.stdout(contains("\"dry_run\":true"));
}
#[test]
fn packages_remove_protected_human_error_to_stderr() {
fez_fake()
.env("FEZ_AUDIT", "off")
.env("FEZ_FAKE_PLAN", "protected")
.args(["packages", "remove", "glibc"])
.assert()
.code(10)
.stderr(contains("error:"))
.stderr(contains("dangerous transaction"));
}
#[test]
fn packages_remove_protected_refused_without_force() {
fez_fake()
.env("FEZ_FAKE_PLAN", "protected")
.env("FEZ_AUDIT", "off")
.args(["packages", "remove", "glibc", "--json"])
.assert()
.code(10)
.stdout(contains("\"code\":\"dangerous-transaction\""))
.stdout(contains("glibc"));
}
#[test]
fn packages_remove_protected_allowed_with_force() {
fez_fake()
.env("FEZ_FAKE_PLAN", "protected")
.env("FEZ_AUDIT", "off")
.args(["packages", "remove", "glibc", "--force", "--json"])
.assert()
.success()
.stdout(contains("\"kind\":\"PackageMutation\""));
}
#[test]
fn packages_remove_cascade_refused_without_force() {
fez_fake()
.env("FEZ_FAKE_PLAN", "cascade")
.env("FEZ_AUDIT", "off")
.args(["packages", "remove", "leaf", "--json"])
.assert()
.code(10)
.stdout(contains("\"code\":\"dangerous-transaction\""));
}
#[test]
fn packages_dependency_missing_returns_exit_9() {
fez_fake()
.env("FEZ_FAKE_NO_DNF5", "1")
.env("FEZ_FAKE_NO_PACKAGEKIT", "1")
.args(["packages", "list", "--json"])
.assert()
.code(9)
.stdout(contains("\"code\":\"dependency-missing\""))
.stdout(contains("dnf5daemon"))
.stdout(contains("\"remediation\""));
}
#[test]
fn packages_mutation_writes_attempt_and_result_audit_records() {
let audit = AuditLog::new("pkg-audit");
fez_fake()
.env("FEZ_AUDIT", audit.env_value())
.env("FEZ_FAKE_PLAN", "small")
.args(["packages", "remove", "htop", "--json"])
.assert()
.success();
let records = audit.records();
assert_eq!(records.len(), 2, "expected attempt + result records");
let (attempt, result) = (&records[0], &records[1]);
assert_eq!(attempt["result"], "attempt");
assert_eq!(result["result"], "ok");
assert_eq!(result["operation"], "remove");
}