ic-backup 0.3.7

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use crate::{
    model::{
        attempt_journal::{ObservationOutcomeRecord, ObservationReceiptRequest},
        ic_request::{IcManagementMethodRecord as Method, IcManagementRequest},
    },
    test_support::{
        ic_observation::{input, original},
        membership::hash,
    },
};

#[test]
fn status_and_list_bind_exact_original_bytes_and_both_spent_attempts() {
    for method in [Method::CanisterStatus, Method::ListCanisterSnapshots] {
        let original = original(method);
        let before = original.journal.clone();
        let request = IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &original.mutation,
            &original.payload,
        )
        .unwrap();
        assert_eq!(request.plan(), &original.plan);
        assert_eq!(request.mutation(), &original.mutation);
        assert_eq!(request.payload(), &original.payload);
        assert_eq!(request.authority(), original.journal.authority());
        assert_eq!(
            (request.mutation_attempt(), request.observation_attempt()),
            (1, 2)
        );
        request.validate_journal(&original.journal).unwrap();
        assert_eq!(original.journal, before);
    }
}

#[test]
fn changed_plan_mutation_target_class_and_reserved_observation_are_rejected() {
    let original = original(Method::CanisterStatus);
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            99,
            &original.journal,
            &original.mutation,
            &original.payload
        ),
        Err(IcObservationRequestError::Plan(_))
    ));
    let mut plan = serde_json::to_value(&original.plan).unwrap();
    plan["operations"][1]["budget"]["observations"] = serde_json::json!(1);
    let plan: OperationPlanRecord = serde_json::from_value(plan).unwrap();
    assert!(matches!(
        IcObservationRequest::new(
            &plan,
            7,
            &original.journal,
            &original.mutation,
            &original.payload
        ),
        Err(IcObservationRequestError::AuthorityMismatch)
    ));
    let changed = IcManagementRequestRecord::new(IcManagementRequest {
        method: Method::StopCanister,
        target: original.payload.target().into(),
        snapshot_id: None,
    })
    .unwrap();
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &changed,
            &original.payload
        ),
        Err(IcObservationRequestError::Payload(
            IcRequestError::DigestMismatch
        ))
    ));
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &original.mutation,
            &changed
        ),
        Err(IcObservationRequestError::Payload(
            IcRequestError::EffectMismatch { .. }
        ))
    ));
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &original.payload,
            &original.payload
        ),
        Err(IcObservationRequestError::Payload(
            IcRequestError::EffectMismatch { .. }
        ))
    ));
    let other = IcManagementRequestRecord::new(IcManagementRequest {
        method: Method::CanisterStatus,
        target: "aaaaa-aa".into(),
        snapshot_id: None,
    })
    .unwrap();
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &original.mutation,
            &other
        ),
        Err(IcObservationRequestError::Payload(
            IcRequestError::TargetMismatch
        ))
    ));
    let list = IcManagementRequestRecord::new(IcManagementRequest {
        method: Method::ListCanisterSnapshots,
        target: original.payload.target().into(),
        snapshot_id: None,
    })
    .unwrap();
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &original.journal,
            &original.mutation,
            &list
        ),
        Err(IcObservationRequestError::RequestMismatch)
    ));
}

#[test]
fn missing_settled_and_replaced_reservations_keep_original_consumption() {
    let mut original = original(Method::CanisterStatus);
    let mut empty = AttemptJournalRecord::new(original.journal.authority().clone());
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &empty,
            &original.mutation,
            &original.payload
        ),
        Err(IcObservationRequestError::NoPendingMutation)
    ));
    empty.reserve_mutation().unwrap();
    assert!(matches!(
        IcObservationRequest::new(
            &original.plan,
            7,
            &empty,
            &original.mutation,
            &original.payload
        ),
        Err(IcObservationRequestError::NoPendingObservation)
    ));
    let request = IcObservationRequest::new(
        &original.plan,
        7,
        &original.journal,
        &original.mutation,
        &original.payload,
    )
    .unwrap();
    original
        .journal
        .record_observation(ObservationReceiptRequest {
            attempt: 2,
            request: original.payload.digest().hash().into(),
            outcome: ObservationOutcomeRecord::Uncertain,
            evidence: hash("12").hash().into(),
        })
        .unwrap();
    assert!(matches!(
        request.validate_journal(&original.journal),
        Err(IcObservationRequestError::ObservationMismatch)
    ));
    original
        .journal
        .reserve_observation(1, original.payload.digest().hash())
        .unwrap();
    assert!(matches!(
        request.validate_journal(&original.journal),
        Err(IcObservationRequestError::ObservationMismatch)
    ));
    assert_eq!(original.journal.view().observations_remaining, 0);
    original
        .journal
        .record_observation(ObservationReceiptRequest {
            attempt: 3,
            request: original.payload.digest().hash().into(),
            outcome: ObservationOutcomeRecord::Applied,
            evidence: hash("12").hash().into(),
        })
        .unwrap();
    assert!(matches!(
        request.validate_journal(&original.journal),
        Err(IcObservationRequestError::MutationMismatch)
    ));
    assert_eq!(original.journal.view().observations_used, 2);
}

#[test]
fn passive_reply_owns_chronological_attempt_byte_and_principal_bounds() {
    let original = original(Method::CanisterStatus);
    let request = IcObservationRequest::new(
        &original.plan,
        7,
        &original.journal,
        &original.mutation,
        &original.payload,
    )
    .unwrap();
    for (mutation, observation) in [
        (0, 2),
        (1, 0),
        (2, 2),
        (3, 2),
        (1, MAX_OPERATION_ATTEMPTS + 1),
        (MAX_OPERATION_ATTEMPTS + 1, 2),
    ] {
        let mut fields = input(&request);
        fields.mutation_attempt = mutation;
        fields.observation_attempt = observation;
        assert_eq!(
            IcObservationResponse::new(fields).unwrap_err(),
            IcObservationResponseError::InvalidAttempts
        );
    }
    let mut fields = input(&request);
    fields.target = "invalid".into();
    assert_eq!(
        IcObservationResponse::new(fields).unwrap_err(),
        IcObservationResponseError::InvalidTarget
    );
    let mut fields = input(&request);
    fields.reply = vec![b'X'; MAX_IC_OBSERVATION_REPLY_BYTES + 1];
    assert_eq!(
        IcObservationResponse::new(fields).unwrap_err(),
        IcObservationResponseError::ReplyTooLarge
    );
    let mut fields = input(&request);
    fields.mutation_attempt = MAX_OPERATION_ATTEMPTS - 1;
    fields.observation_attempt = MAX_OPERATION_ATTEMPTS;
    fields.target = fields.target.to_ascii_uppercase();
    fields.reply = vec![b'X'; MAX_IC_OBSERVATION_REPLY_BYTES];
    let response = IcObservationResponse::new(fields).unwrap();
    assert_eq!(response.input().reply.len(), MAX_IC_OBSERVATION_REPLY_BYTES);
    assert_eq!(response.input().target, original.payload.target());
    assert!(!format!("{response:?}").contains("XXXX"));
}