use super::*;
#[test]
fn in_place_plan_orders_parent_before_child() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let ordered = plan.ordered_members();
let value = serde_json::to_value(&plan).expect("serialize restore plan");
assert_eq!(plan.plan_version, 1);
assert_eq!(plan.backup_id, "fbk_test_001");
assert_eq!(plan.source_environment, "local");
assert_eq!(value["source_environment"], "local");
assert_eq!(plan.source_root_canister, ROOT);
assert_eq!(plan.topology_hash, HASH);
assert_eq!(plan.member_count, 2);
assert_eq!(plan.identity_summary.fixed_members, 1);
assert_eq!(plan.identity_summary.relocatable_members, 1);
assert_eq!(plan.identity_summary.in_place_members, 2);
assert_eq!(plan.identity_summary.mapped_members, 0);
assert_eq!(plan.identity_summary.remapped_members, 0);
assert!(plan.verification_summary.verification_required);
assert!(plan.verification_summary.all_members_have_checks);
assert!(plan.readiness_summary.ready);
assert!(plan.readiness_summary.reasons.is_empty());
assert_eq!(plan.verification_summary.deployment_checks, 0);
assert_eq!(plan.verification_summary.member_checks, 2);
assert_eq!(plan.verification_summary.members_with_checks, 2);
assert_eq!(plan.verification_summary.total_checks, 2);
assert_eq!(plan.ordering_summary.ordered_members, 2);
assert_eq!(plan.ordering_summary.dependency_free_members, 1);
assert_eq!(plan.ordering_summary.parent_edges, 1);
assert_eq!(ordered[0].member_order, 0);
assert_eq!(ordered[1].member_order, 1);
assert_eq!(ordered[0].source_canister, ROOT);
assert_eq!(ordered[1].source_canister, CHILD);
assert_eq!(
ordered[1].ordering_dependency,
Some(RestoreOrderingDependency {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
relationship: RestoreOrderingRelationship::ParentBeforeChild,
})
);
}
#[test]
fn restore_plan_unknown_field_fails_deserialize() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let mut value = serde_json::to_value(plan).expect("serialize restore plan");
value["unexpected_field"] = serde_json::Value::Bool(true);
let err = serde_json::from_value::<RestorePlan>(value).expect_err("unknown field rejects");
assert!(err.is_data());
}
#[test]
fn restore_plan_requires_and_validates_plan_version() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let mut value = serde_json::to_value(&plan).expect("serialize restore plan");
value
.as_object_mut()
.expect("plan object")
.remove("plan_version");
let err = serde_json::from_value::<RestorePlan>(value).expect_err("plan version is required");
assert!(err.is_data());
let mut unsupported = plan;
unsupported.plan_version = 2;
let err = unsupported
.validate()
.expect_err("unsupported restore plan version rejects");
std::assert_matches!(err, RestorePlanError::UnsupportedVersion(2));
}
#[test]
fn restore_plan_rejects_contradictory_derived_projection() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let mut plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
plan.readiness_summary.ready = false;
let err = plan
.validate()
.expect_err("contradictory readiness projection rejects");
std::assert_matches!(
err,
RestorePlanError::ProjectionMismatch("readiness_summary")
);
}
#[test]
fn restore_plan_rejects_contradictory_parent_projection() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let mut plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let child = plan
.members
.iter_mut()
.find(|member| member.source_canister == CHILD)
.expect("child member should exist");
child.parent_target_canister = Some(TARGET.to_string());
let err = plan
.validate()
.expect_err("contradictory parent target rejects");
std::assert_matches!(
err,
RestorePlanError::ProjectionMismatch("members[].parent_target_canister")
);
}
#[test]
fn restore_plan_requires_current_verification_and_lifecycle_fields() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
for path in [
&["deployment_verification_checks"][..],
&["operation_summary", "planned_canister_stops"][..],
&["operation_summary", "planned_canister_starts"][..],
] {
let mut value = serde_json::to_value(&plan).expect("serialize restore plan");
remove_json_field(&mut value, path);
let err = serde_json::from_value::<RestorePlan>(value)
.expect_err("current restore-plan field must be present");
assert!(err.is_data());
}
}
#[test]
fn restore_plan_requires_exact_current_optional_fields() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
for field in [
"parent_source_canister",
"parent_target_canister",
"ordering_dependency",
] {
let mut value =
serde_json::to_value(&plan.members[0]).expect("serialize restore plan member");
value
.as_object_mut()
.expect("restore plan member object")
.remove(field);
let err = serde_json::from_value::<RestorePlanMember>(value)
.expect_err("current restore plan member field must be present");
assert!(err.is_data());
}
}
#[test]
fn fixed_identity_member_cannot_be_remapped() {
let manifest = valid_manifest(IdentityMode::Fixed);
let mapping = RestoreMapping {
members: vec![
RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
},
RestoreMappingEntry {
source_canister: CHILD.to_string(),
target_canister: TARGET.to_string(),
},
],
};
let err = RestorePlanner::plan(&manifest, Some(&mapping))
.expect_err("fixed member remap should fail");
std::assert_matches!(err, RestorePlanError::FixedIdentityRemap { .. });
}
#[test]
fn relocatable_member_can_be_mapped() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let mapping = RestoreMapping {
members: vec![
RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
},
RestoreMappingEntry {
source_canister: CHILD.to_string(),
target_canister: TARGET.to_string(),
},
],
};
let plan = RestorePlanner::plan(&manifest, Some(&mapping)).expect("plan should build");
let child = plan
.ordered_members()
.into_iter()
.find(|member| member.source_canister == CHILD)
.expect("child member should be planned");
assert_eq!(plan.identity_summary.fixed_members, 1);
assert_eq!(plan.identity_summary.relocatable_members, 1);
assert_eq!(plan.identity_summary.in_place_members, 1);
assert_eq!(plan.identity_summary.mapped_members, 2);
assert_eq!(plan.identity_summary.remapped_members, 1);
assert_eq!(child.target_canister, TARGET);
assert_eq!(child.parent_target_canister, Some(ROOT.to_string()));
}
#[test]
fn plan_members_include_snapshot_and_verification_metadata() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let root = plan
.ordered_members()
.into_iter()
.find(|member| member.source_canister == ROOT)
.expect("root member should be planned");
assert_eq!(root.identity_mode, IdentityMode::Fixed);
assert_eq!(root.verification_checks[0].kind, "status");
assert_eq!(root.source_snapshot.snapshot_id, "snap-root");
assert_eq!(root.source_snapshot.artifact_path, "artifacts/root");
}
#[test]
fn plan_includes_mapping_summary() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let in_place = RestorePlanner::plan(&manifest, None).expect("plan should build");
assert!(!in_place.identity_summary.mapping_supplied);
assert!(!in_place.identity_summary.all_sources_mapped);
assert_eq!(in_place.identity_summary.mapped_members, 0);
let mapping = RestoreMapping {
members: vec![
RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
},
RestoreMappingEntry {
source_canister: CHILD.to_string(),
target_canister: TARGET.to_string(),
},
],
};
let mapped = RestorePlanner::plan(&manifest, Some(&mapping)).expect("plan should build");
assert!(mapped.identity_summary.mapping_supplied);
assert!(mapped.identity_summary.all_sources_mapped);
assert_eq!(mapped.identity_summary.mapped_members, 2);
assert_eq!(mapped.identity_summary.remapped_members, 1);
}
#[test]
fn plan_includes_snapshot_summary() {
let mut manifest = valid_manifest(IdentityMode::Relocatable);
manifest.deployment.members[1].source_snapshot.module_hash = None;
manifest.deployment.members[1].source_snapshot.checksum = None;
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
assert!(!plan.snapshot_summary.all_members_have_module_hash);
assert!(plan.snapshot_summary.all_members_have_code_version);
assert!(!plan.snapshot_summary.all_members_have_checksum);
assert_eq!(plan.snapshot_summary.members_with_module_hash, 1);
assert_eq!(plan.snapshot_summary.members_with_code_version, 2);
assert_eq!(plan.snapshot_summary.members_with_checksum, 1);
assert!(!plan.readiness_summary.ready);
assert_eq!(
plan.readiness_summary.reasons,
["missing-snapshot-checksum"]
);
}
#[test]
fn plan_includes_verification_summary() {
let mut manifest = valid_manifest(IdentityMode::Relocatable);
manifest
.verification
.deployment_checks
.push(VerificationCheck {
kind: "status".to_string(),
roles: Vec::new(),
});
manifest
.verification
.member_checks
.push(MemberVerificationChecks {
role: "app".to_string(),
checks: vec![VerificationCheck {
kind: "status".to_string(),
roles: Vec::new(),
}],
});
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
assert!(plan.verification_summary.verification_required);
assert!(plan.verification_summary.all_members_have_checks);
let app = plan
.ordered_members()
.into_iter()
.find(|member| member.role == "app")
.expect("app member should be planned");
assert_eq!(app.verification_checks.len(), 2);
assert_eq!(plan.deployment_verification_checks.len(), 1);
assert_eq!(plan.deployment_verification_checks[0].kind, "status");
assert_eq!(plan.verification_summary.deployment_checks, 1);
assert_eq!(plan.verification_summary.member_checks, 3);
assert_eq!(plan.verification_summary.members_with_checks, 2);
assert_eq!(plan.verification_summary.total_checks, 4);
}
#[test]
fn plan_includes_operation_summary() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
assert_eq!(plan.operation_summary.planned_canister_stops, 2);
assert_eq!(plan.operation_summary.planned_canister_starts, 2);
assert_eq!(plan.operation_summary.planned_snapshot_uploads, 2);
assert_eq!(plan.operation_summary.planned_snapshot_loads, 2);
assert_eq!(plan.operation_summary.planned_verification_checks, 2);
assert_eq!(plan.operation_summary.planned_operations, 10);
}
#[test]
fn plan_expands_role_verification_checks_per_matching_member() {
let mut manifest = valid_manifest(IdentityMode::Relocatable);
manifest.deployment.members.push(deployment_member(
"app",
CHILD_TWO,
Some(ROOT),
IdentityMode::Relocatable,
));
manifest
.verification
.member_checks
.push(MemberVerificationChecks {
role: "app".to_string(),
checks: vec![VerificationCheck {
kind: "status".to_string(),
roles: Vec::new(),
}],
});
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
assert_eq!(plan.verification_summary.deployment_checks, 0);
assert_eq!(plan.verification_summary.member_checks, 5);
assert_eq!(plan.verification_summary.members_with_checks, 3);
assert_eq!(plan.verification_summary.total_checks, 5);
}
#[test]
fn plan_applies_member_verification_role_filters() {
let mut manifest = valid_manifest(IdentityMode::Relocatable);
manifest.deployment.members[0]
.verification_checks
.push(VerificationCheck {
kind: "status".to_string(),
roles: vec!["root".to_string()],
});
manifest
.verification
.member_checks
.push(MemberVerificationChecks {
role: "app".to_string(),
checks: vec![
VerificationCheck {
kind: "status".to_string(),
roles: vec!["app".to_string()],
},
VerificationCheck {
kind: "status".to_string(),
roles: vec!["root".to_string()],
},
],
});
let plan = RestorePlanner::plan(&manifest, None).expect("plan should build");
let app = plan
.ordered_members()
.into_iter()
.find(|member| member.role == "app")
.expect("app member should be planned");
let dry_run = RestoreApplyDryRun::from_plan(&plan).expect("build restore dry-run");
let app_verification_kinds = dry_run
.operations
.iter()
.filter(|operation| {
operation.source_canister == CHILD
&& operation.operation == RestoreApplyOperationKind::VerifyMember
})
.filter_map(|operation| operation.verification_kind.as_deref())
.collect::<Vec<_>>();
assert_eq!(app.verification_checks.len(), 2);
assert_eq!(
app.verification_checks
.iter()
.map(|check| check.kind.as_str())
.collect::<Vec<_>>(),
["status", "status"]
);
assert_eq!(plan.verification_summary.member_checks, 3);
assert_eq!(plan.verification_summary.total_checks, 3);
assert_eq!(dry_run.rendered_operations, 11);
assert_eq!(app_verification_kinds, ["status", "status"]);
}
#[test]
fn mapped_restore_requires_complete_mapping() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let mapping = RestoreMapping {
members: vec![RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
}],
};
let err = RestorePlanner::plan(&manifest, Some(&mapping))
.expect_err("incomplete mapping should fail");
std::assert_matches!(err, RestorePlanError::MissingMappingSource(_));
}
#[test]
fn mapped_restore_rejects_unknown_mapping_sources() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let unknown = "rdmx6-jaaaa-aaaaa-aaadq-cai";
let mapping = RestoreMapping {
members: vec![
RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
},
RestoreMappingEntry {
source_canister: CHILD.to_string(),
target_canister: TARGET.to_string(),
},
RestoreMappingEntry {
source_canister: unknown.to_string(),
target_canister: unknown.to_string(),
},
],
};
let err = RestorePlanner::plan(&manifest, Some(&mapping))
.expect_err("unknown mapping source should fail");
std::assert_matches!(err, RestorePlanError::UnknownMappingSource(_));
}
#[test]
fn duplicate_mapping_targets_fail_validation() {
let manifest = valid_manifest(IdentityMode::Relocatable);
let mapping = RestoreMapping {
members: vec![
RestoreMappingEntry {
source_canister: ROOT.to_string(),
target_canister: ROOT.to_string(),
},
RestoreMappingEntry {
source_canister: CHILD.to_string(),
target_canister: ROOT.to_string(),
},
],
};
let err =
RestorePlanner::plan(&manifest, Some(&mapping)).expect_err("duplicate targets should fail");
std::assert_matches!(err, RestorePlanError::DuplicateMappingTarget(_));
}