use super::*;
#[test]
fn validates_subtree_plan_with_authority_preflights() {
let plan = subtree_plan();
plan.validate().expect("valid subtree plan");
plan.validate_for_execution()
.expect("executable subtree plan");
}
#[test]
fn rejects_root_in_normal_scope() {
let mut plan = subtree_plan();
plan.root_included = true;
plan.targets.push(BackupTarget {
canister_id: ROOT.to_string(),
role: Some("root".to_string()),
parent_canister_id: None,
depth: 0,
control_authority: proven_root_control(),
snapshot_read_authority: proven_root_read(),
identity_mode: IdentityMode::Fixed,
expected_module_hash: None,
});
let err = plan
.validate()
.expect_err("root should require maintenance");
std::assert_matches!(err, BackupPlanError::RootIncludedWithoutMaintenance);
}
#[test]
fn planning_allows_declared_authority() {
let mut plan = subtree_plan();
plan.snapshot_read_authority =
SnapshotReadAuthority::root_configured_read(AuthorityEvidence::Declared);
plan.targets[0].control_authority =
ControlAuthority::root_controller(AuthorityEvidence::Declared);
plan.targets[0].snapshot_read_authority =
SnapshotReadAuthority::root_configured_read(AuthorityEvidence::Declared);
plan.validate().expect("declared authority can plan");
let err = plan
.validate_for_execution()
.expect_err("declared authority cannot execute");
std::assert_matches!(
err,
BackupPlanError::UnprovenControlAuthority(canister) if canister == APP
);
}
#[test]
fn planning_allows_unknown_authority() {
let mut plan = subtree_plan();
plan.snapshot_read_authority = SnapshotReadAuthority::unknown();
plan.targets[0].control_authority = ControlAuthority::unknown();
plan.targets[0].snapshot_read_authority = SnapshotReadAuthority::unknown();
plan.validate().expect("unknown authority can plan");
let err = plan
.validate_for_execution()
.expect_err("unknown authority cannot execute");
std::assert_matches!(
err,
BackupPlanError::UnprovenControlAuthority(canister) if canister == APP
);
}
#[test]
fn rejects_unproven_control_authority() {
let mut plan = subtree_plan();
plan.targets[0].control_authority =
ControlAuthority::root_controller(AuthorityEvidence::Declared);
let err = plan
.validate_for_execution()
.expect_err("control authority should be proven");
std::assert_matches!(
err,
BackupPlanError::UnprovenControlAuthority(canister) if canister == APP
);
}
#[test]
fn rejects_unproven_snapshot_read_authority() {
let mut plan = subtree_plan();
plan.targets[0].snapshot_read_authority =
SnapshotReadAuthority::root_configured_read(AuthorityEvidence::Declared);
let err = plan
.validate_for_execution()
.expect_err("snapshot read authority should be proven");
std::assert_matches!(
err,
BackupPlanError::UnprovenTargetSnapshotReadAuthority(canister) if canister == APP
);
}
#[test]
fn rejects_incomplete_operation_projection() {
let mut plan = subtree_plan();
plan.phases.pop();
let err = plan
.validate()
.expect_err("incomplete operation projection should reject");
std::assert_matches!(
err,
BackupPlanError::OperationCountMismatch {
expected: 10,
actual: 9
}
);
}
#[test]
fn rejects_root_omitted_deployment_scope_with_selected_root() {
let mut plan = subtree_plan();
plan.selected_scope_kind = BackupScopeKind::NonRootDeployment;
let err = plan
.validate()
.expect_err("root-omitted deployment scope should not name one root");
std::assert_matches!(err, BackupPlanError::NonRootDeploymentHasSelectedRoot);
}
#[test]
fn rejects_operation_order_mismatch() {
let mut plan = subtree_plan();
plan.phases[1].order = 42;
let err = plan
.validate()
.expect_err("operation order mismatch should reject");
std::assert_matches!(
err,
BackupPlanError::OperationProjectionMismatch {
index: 1,
field: "order"
}
);
}
#[test]
fn rejects_operation_id_mismatch() {
let mut plan = subtree_plan();
plan.phases[4].operation_id = "stop-other".to_string();
let err = plan
.validate()
.expect_err("operation id mismatch should reject");
std::assert_matches!(
err,
BackupPlanError::OperationProjectionMismatch {
index: 4,
field: "operation_id"
}
);
}
#[test]
fn rejects_operation_kind_mismatch() {
let mut plan = subtree_plan();
plan.phases[4].kind = BackupOperationKind::CreateSnapshot;
let err = plan
.validate()
.expect_err("operation kind mismatch should reject");
std::assert_matches!(
err,
BackupPlanError::OperationProjectionMismatch {
index: 4,
field: "kind"
}
);
}
#[test]
fn rejects_operation_target_mismatch() {
let mut plan = subtree_plan();
plan.phases[4].target_canister_id = Some(WORKER.to_string());
let err = plan
.validate()
.expect_err("operation target mismatch should reject");
std::assert_matches!(
err,
BackupPlanError::OperationProjectionMismatch {
index: 4,
field: "target_canister_id"
}
);
}
#[test]
fn rejects_invalid_topology_hash() {
let mut plan = subtree_plan();
plan.topology_hash_before_quiesce = "not-a-hash".to_string();
let err = plan
.validate()
.expect_err("invalid topology hash should reject");
std::assert_matches!(
err,
BackupPlanError::InvalidTopologyHash {
field: "topology_hash_before_quiesce",
value
} if value == "not-a-hash"
);
}
#[test]
fn rejects_target_parent_cycle() {
let mut plan = subtree_plan();
plan.targets[0].parent_canister_id = Some(APP.to_string());
let err = plan
.validate()
.expect_err("target parent cycle should reject");
std::assert_matches!(
err,
BackupPlanError::TargetParentCycle { canister_id } if canister_id == APP
);
}
#[test]
fn rejects_target_depth_mismatch() {
let mut plan = subtree_plan();
plan.targets.push(worker_target(APP, 3));
plan.phases = build_backup_phases(&plan.targets);
let err = plan
.validate()
.expect_err("target depth mismatch should reject");
std::assert_matches!(
err,
BackupPlanError::TargetDepthMismatch {
canister_id,
parent_canister_id,
expected: 2,
actual: 3
} if canister_id == WORKER && parent_canister_id == APP
);
}
#[test]
fn rejects_selected_root_with_internal_parent() {
let mut plan = subtree_plan();
plan.targets[0].parent_canister_id = Some(WORKER.to_string());
plan.targets[0].depth = 2;
plan.targets.push(worker_target(ROOT, 1));
plan.phases = build_backup_phases(&plan.targets);
let err = plan
.validate()
.expect_err("selected root with internal parent should reject");
std::assert_matches!(
err,
BackupPlanError::SelectedRootHasInternalParent {
selected_root,
parent_canister_id
} if selected_root == APP && parent_canister_id == WORKER
);
}
#[test]
fn rejects_target_disconnected_from_selected_root() {
let mut plan = subtree_plan();
plan.targets.push(worker_target(ROOT, 1));
plan.phases = build_backup_phases(&plan.targets);
let err = plan
.validate()
.expect_err("disconnected target should reject");
std::assert_matches!(
err,
BackupPlanError::TargetDisconnected {
canister_id,
expected_root
} if canister_id == WORKER && expected_root == APP
);
}
#[test]
fn rejects_non_root_deployment_target_disconnected_from_root() {
let mut plan = subtree_plan();
plan.selected_scope_kind = BackupScopeKind::NonRootDeployment;
plan.selected_subtree_root = None;
plan.targets[0].parent_canister_id = Some(WORKER.to_string());
let err = plan
.validate()
.expect_err("root-disconnected deployment target should reject");
std::assert_matches!(
err,
BackupPlanError::TargetDisconnected {
canister_id,
expected_root
} if canister_id == APP && expected_root == ROOT
);
}
fn worker_target(parent_canister_id: &str, depth: u32) -> BackupTarget {
BackupTarget {
canister_id: WORKER.to_string(),
role: Some("worker".to_string()),
parent_canister_id: Some(parent_canister_id.to_string()),
depth,
control_authority: proven_root_control(),
snapshot_read_authority: proven_root_read(),
identity_mode: IdentityMode::Relocatable,
expected_module_hash: None,
}
}