use super::*;
use crate::tests::support::schema;
use crate::tests::support::operation_plan::*;
#[test]
fn canonical_digest_and_authority_bind_every_owner_without_recursive_intent() {
let plan = OperationPlanRecord::new(request()).expect("plan");
assert_eq!(
plan.digest().hash(),
"436d23bca3ad214e39d2722851f0df7627d6d924f8e6e5159a55c3d05103e97b"
);
let mut reversed = request();
reversed.operations.reverse();
assert_eq!(
OperationPlanRecord::new(reversed).expect("canonical order"),
plan
);
let mut value = serde_json::to_value(&plan).expect("json");
for pointer in [
"/context/network",
"/context/caller",
"/context/release",
"/selected_targets/0",
"/operations/0/target",
"/operations/0/request",
] {
let text = value
.pointer(pointer)
.expect("field")
.as_str()
.expect("text")
.to_uppercase();
*value.pointer_mut(pointer).expect("field") = serde_json::json!(text);
}
assert_eq!(
serde_json::from_value::<OperationPlanRecord>(value).expect("canonical aliases"),
plan
);
let authority = plan
.attempt_authority(7)
.expect("exact derived declaration");
assert_eq!(authority.binding().intent(), plan.digest().hash());
assert_eq!(authority.binding().operation_sequence(), 7);
assert_eq!(authority.binding().network(), plan.context().network());
assert_eq!(authority.binding().caller(), plan.context().caller());
assert_eq!(authority.binding().target(), APP);
assert_eq!(authority.binding().release(), plan.context().release());
assert_eq!(
authority.binding().request(),
plan.operation(7).expect("binding").request()
);
assert_eq!(
authority.budget(),
plan.operation(7).expect("budget").budget()
);
assert_eq!(
plan.attempt_authority(7).expect("repeat does not consume"),
authority
);
assert_eq!(
plan.allocated_attempts(),
AllocatedAttemptsView {
mutations: 3,
observations: 2
}
);
assert_eq!(plan.inventory().targets().len(), 2);
assert_eq!(plan.selected_targets(), &[APP.to_owned()]);
assert_eq!(plan.operations().len(), 2);
assert_eq!(plan.budget().mutations(), 3);
assert_eq!(plan.budget().observations(), 2);
assert!(matches!(
plan.attempt_authority(99),
Err(OperationPlanError::UnknownOperation(99))
));
assert_eq!(
serde_json::from_slice::<OperationPlanRecord>(&serde_json::to_vec(&plan).expect("encode"))
.expect("decode"),
plan
);
}
#[test]
fn changed_context_inventory_dependencies_request_and_limits_change_original_intent_and_authority()
{
let base = OperationPlanRecord::new(request()).expect("plan");
for field in [
"network",
"caller",
"release",
"inventory",
"selection",
"dependencies",
"request",
"operation_budget",
"plan_budget",
] {
let mut changed = request();
match field {
"network" => {
changed.context = PlanContextRecord::new(&PlanContextRequest {
network: "23".repeat(32),
caller: changed.context.caller().into(),
release: changed.context.release().into(),
})
.expect("network");
}
"caller" => {
changed.context = PlanContextRecord::new(&PlanContextRequest {
network: changed.context.network().into(),
caller: ROOT.into(),
release: changed.context.release().into(),
})
.expect("caller");
}
"release" => {
changed.context = PlanContextRecord::new(&PlanContextRequest {
network: changed.context.network().into(),
caller: changed.context.caller().into(),
release: "23".repeat(32),
})
.expect("release");
}
"inventory" => {
let mut value = serde_json::to_value(&changed.inventory).expect("json");
value["targets"][0]["role"] = serde_json::json!("changed unselected parent");
changed.inventory = serde_json::from_value(value).expect("inventory");
}
"selection" => {
changed.selected_targets = vec![ROOT.into()];
changed.operations = vec![
operation(0, ROOT, "ef", 1, 1),
operation(7, ROOT, "01", 2, 1),
];
}
"dependencies" => {
let mut row = serde_json::to_value(&changed.operations[1]).unwrap();
row["depends_on"] = serde_json::json!([]);
changed.operations[1] = serde_json::from_value(row).unwrap();
}
"request" => changed.operations[0] = operation(0, APP, "23", 1, 1),
"operation_budget" => changed.operations[0] = operation(0, APP, "ef", 0, 1),
"plan_budget" => {
changed.budget = PlanBudgetRecord::new(4, 2).expect("different original ceiling");
}
_ => unreachable!(),
}
let changed = OperationPlanRecord::new(changed).expect("different valid plan");
assert_ne!(changed.digest(), base.digest(), "{field}");
assert_ne!(
changed.attempt_authority(7).expect("derived"),
base.attempt_authority(7).expect("base"),
"{field}"
);
}
}
#[test]
fn operation_identity_physical_selection_and_original_budget_mismatches_reject() {
let mut input = request();
input.selected_targets.clear();
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::EmptySelection)
));
let mut input = request();
input.selected_targets.push(APP.to_uppercase());
assert!(
matches!(OperationPlanRecord::new(input),Err(OperationPlanError::DuplicateSelected(id)) if id==APP)
);
let mut input = request();
input.selected_targets = vec!["2vxsx-fae".into()];
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::Inventory(
InventoryRecordError::UnknownTarget(_)
))
));
let mut input = request();
input.operations[1] = input.operations[0].clone();
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::DuplicateOperation(0))
));
let mut input = request();
input.operations[1] = operation(7, ROOT, "01", 2, 1);
assert!(
matches!(OperationPlanRecord::new(input),Err(OperationPlanError::UnselectedTarget(id)) if id==ROOT)
);
let mut input = request();
input.selected_targets.push(ROOT.into());
assert!(
matches!(OperationPlanRecord::new(input),Err(OperationPlanError::UnusedTarget(id)) if id==ROOT)
);
for budget in [
PlanBudgetRecord::new(2, 2).expect("mutation limit"),
PlanBudgetRecord::new(3, 1).expect("observation limit"),
] {
let mut input = request();
input.budget = budget;
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::AssignedBudgetExceeded)
));
}
}
#[test]
fn closed_schema_revalidates_all_bindings_and_never_accepts_derived_authority_or_views() {
let value =
serde_json::to_value(OperationPlanRecord::new(request()).expect("plan")).expect("json");
for (path, fields) in [
(
"",
vec![
"version",
"context",
"inventory",
"selected_targets",
"operations",
"budget",
],
),
("/context", vec!["network", "caller", "release"]),
(
"/operations/0",
vec![
"operation_sequence",
"depends_on",
"target",
"request",
"budget",
],
),
("/budget", vec!["mutations", "observations"]),
] {
schema::closed_fields::<OperationPlanRecord>(&value, path, &fields);
}
for (path, bad_entry) in [
("/version", serde_json::json!(2)),
("/context/network", serde_json::json!("label")),
("/context/caller", serde_json::json!("invalid")),
("/operations/0/target", serde_json::json!("invalid")),
("/operations/0/request", serde_json::json!("not a hash")),
("/budget/mutations", serde_json::json!(65537)),
("/operations/0/budget/mutations", serde_json::json!(1025)),
("/operations/0/operation_sequence", serde_json::json!(9)),
("/operations/0/target", serde_json::json!(ROOT)),
] {
let mut bad = value.clone();
*bad.pointer_mut(path).expect("field") = bad_entry;
assert!(serde_json::from_value::<OperationPlanRecord>(bad).is_err());
}
for field in ["intent", "authority", "allocated", "preflight_accepted"] {
let mut bad = value.clone();
bad[field] = serde_json::json!(true);
assert!(serde_json::from_value::<OperationPlanRecord>(bad).is_err());
}
schema::duplicate_versions::<OperationPlanRecord>(&value);
}
#[test]
fn selection_operation_and_assigned_attempt_bounds_reject_without_reallocating() {
assert!(matches!(
PlanBudgetRecord::new(u32::MAX, 1),
Err(OperationPlanError::BudgetTooLarge)
));
assert!(matches!(
PlanBudgetRecord::new(MAX_PLAN_ATTEMPTS, 1),
Err(OperationPlanError::BudgetTooLarge)
));
let mut input = request();
input.selected_targets = vec![APP.into(); MAX_INVENTORY_TARGETS + 1];
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::TooManySelected)
));
let mut input = request();
input.operations = vec![input.operations[0].clone(); MAX_PLAN_OPERATIONS + 1];
assert!(matches!(
OperationPlanRecord::new(input),
Err(OperationPlanError::TooManyOperations)
));
let value =
serde_json::to_value(OperationPlanRecord::new(request()).expect("plan")).expect("json");
for (field, entries) in [
(
"selected_targets",
serde_json::json!(vec![APP; MAX_INVENTORY_TARGETS + 1]),
),
(
"operations",
serde_json::json!(vec![
value["operations"][0].clone();
MAX_PLAN_OPERATIONS + 1
]),
),
] {
let mut bad = value.clone();
bad[field] = entries;
assert!(
serde_json::from_value::<OperationPlanRecord>(bad)
.expect_err("decode count bound before admission")
.to_string()
.contains("exceeds")
);
}
let mut input = request();
input.operations = (0..64)
.map(|id| operation(id, APP, "ef", 1024, 0))
.collect();
input.budget = PlanBudgetRecord::new(MAX_PLAN_ATTEMPTS, 0).expect("maximum");
let plan = OperationPlanRecord::new(input).expect("exact assigned bound");
assert_eq!(plan.allocated_attempts().mutations, MAX_PLAN_ATTEMPTS);
let mut bad = serde_json::to_value(&plan).expect("json");
bad["operations"].as_array_mut().expect("ops").push(serde_json::json!({"operation_sequence":64,"depends_on":[],"target":APP,"request":"ef".repeat(32),"budget":{"mutations":1,"observations":0}}));
assert!(
serde_json::from_value::<OperationPlanRecord>(bad)
.expect_err("assigned total bound")
.to_string()
.contains("ceilings")
);
let mut input = request();
input.budget = PlanBudgetRecord::new(0, 0).expect("zero");
input.operations = vec![operation(0, APP, "ef", 0, 0), operation(7, APP, "01", 0, 0)];
assert_eq!(
OperationPlanRecord::new(input)
.expect("zero allowance declaration")
.allocated_attempts(),
AllocatedAttemptsView {
mutations: 0,
observations: 0
}
);
}
#[test]
fn retired_split_graph_and_missing_row_dependencies_are_rejected_even_with_version_one() {
let value = serde_json::to_value(plan()).unwrap();
let mut old = value.clone();
old["graph"] = serde_json::json!({"version":1,"nodes":[]});
assert!(serde_json::from_value::<OperationPlanRecord>(old.clone()).is_err());
for row in old["operations"].as_array_mut().unwrap() {
row.as_object_mut().unwrap().remove("depends_on");
}
assert!(serde_json::from_value::<OperationPlanRecord>(old).is_err());
let mut missing = value;
missing["operations"][0]
.as_object_mut()
.unwrap()
.remove("depends_on");
assert!(serde_json::from_value::<OperationPlanRecord>(missing).is_err());
}