ic-backup 0.59.0

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use serde_json::json;

const ROOT: &str = "aaaaa-aa";
const APP: &str = "renrk-eyaaa-aaaaa-aaada-cai";

fn plan(targets: &[String]) -> OperationPlanRecord {
    use crate::model::inventory::{InventoryRecord, InventoryTargetRecord, InventoryTargetRequest};
    use crate::model::operation_plan::PlanBudgetRecord;
    use crate::tests::support::operation_plan::{graph, operation, request};

    let mut declaration = request();
    declaration.inventory = InventoryRecord::new(
        targets
            .iter()
            .map(|id| {
                InventoryTargetRecord::new(&InventoryTargetRequest {
                    canister_id: id.clone(),
                    parent_canister_id: None,
                    role: None,
                    module_hash: None,
                })
                .unwrap()
            })
            .collect(),
    )
    .unwrap();
    declaration.selected_targets = targets.to_vec();
    declaration.graph = graph(
        targets
            .iter()
            .enumerate()
            .map(|(index, _)| (u64::try_from(index).unwrap(), vec![])),
    );
    declaration.operations = targets
        .iter()
        .enumerate()
        .map(|(index, id)| operation(u64::try_from(index).unwrap(), id, "ef", 1, 0))
        .collect();
    declaration.budget = PlanBudgetRecord::new(u32::try_from(targets.len()).unwrap(), 0).unwrap();
    OperationPlanRecord::new(declaration).expect("bounded original plan")
}

fn journal(
    plan: &OperationPlanRecord,
    targets: &[&str],
    state: ArtifactStateRecord,
) -> DownloadJournalRecord {
    let checksum = matches!(
        state,
        ArtifactStateRecord::ChecksumVerified | ArtifactStateRecord::Durable
    )
    .then(|| json!({"algorithm":"sha256","hash":"ef".repeat(32)}));
    serde_json::from_value(json!({
        "version":1,"intent":plan.digest().hash(),
        "artifacts":targets.iter().enumerate().map(|(i,id)|json!({
            "canister_id":id,"snapshot_id":format!("snap-{i}"),
            "snapshot_taken_at_timestamp":u64::MAX,"snapshot_total_size_bytes":u64::MAX,
            "staging_path":format!("artifacts/{id}.tmp"),"artifact_path":format!("artifacts/{id}"),
            "state":state,"checksum":checksum
        })).collect::<Vec<_>>()
    }))
    .expect("validated journal declaration")
}

#[test]
fn exact_durable_set_borrows_original_identity_metadata_and_checksums() {
    let plan = plan(&[ROOT.into()]);
    let journal = journal(&plan, &[ROOT], ArtifactStateRecord::Durable);
    let checksum = validate(&plan, &journal).expect("pure structural admission");
    assert!(std::ptr::eq(
        checksum,
        journal.artifacts()[0].checksum().unwrap()
    ));
    assert_eq!(checksum.hash(), "ef".repeat(32));
}

#[test]
fn rejects_changed_release_and_original_allowances_even_with_equal_targets() {
    let original = plan(&[ROOT.into()]);
    let journal = journal(&original, &[ROOT], ArtifactStateRecord::Durable);
    for change_release in [true, false] {
        let mut value = serde_json::to_value(&original).expect("original declaration");
        if change_release {
            value["context"]["release"] = json!("01".repeat(32));
        } else {
            value["budget"]["mutations"] = json!(2);
        }
        let changed: OperationPlanRecord =
            serde_json::from_value(value).expect("different valid intent");
        assert_eq!(
            validate(&changed, &journal).unwrap_err(),
            DownloadIntegrityPolicyError::IntentMismatch
        );
    }
}

#[test]
fn rejects_missing_extra_and_equal_count_wrong_target_sets() {
    let plan = plan(&[ROOT.into(), APP.into()]);
    for targets in [[ROOT], ["2vxsx-fae"]] {
        let journal = journal(&plan, &targets, ArtifactStateRecord::Durable);
        assert_eq!(
            validate(&plan, &journal).unwrap_err(),
            DownloadIntegrityPolicyError::TargetSetMismatch
        );
    }
}

#[test]
fn rejects_each_non_durable_state() {
    let plan = plan(&[ROOT.into()]);
    let original = journal(&plan, &[ROOT], ArtifactStateRecord::Durable);
    for state in [
        ArtifactStateRecord::Created,
        ArtifactStateRecord::ChecksumVerified,
    ] {
        let mut value = serde_json::to_value(&original).expect("declaration");
        value["artifacts"][0]["state"] = json!(state);
        if state != ArtifactStateRecord::ChecksumVerified {
            value["artifacts"][0]["checksum"] = json!(null);
        }
        let journal = serde_json::from_value(value).expect("valid earlier state");
        assert_eq!(
            validate(&plan, &journal).unwrap_err(),
            DownloadIntegrityPolicyError::NonDurableArtifact {
                canister_id: ROOT.into(),
                state,
            }
        );
    }
}