use super::*;
use crate::model::settlement::EffectAttribution;
use crate::tests::support::encode_fixture;
use crate::{
model::{
attempt_journal::ObservationReceiptRequest,
ic_lifecycle_reply::IcLifecycleReply,
ic_observation::{IcObservationRequest, IcObservationResponseInput},
},
tests::support::{
ic_observation::{self, original_for_mutation as original},
operation_plan::hash,
response_fault::ObservationFault,
},
};
use ic_platform::management_canister_types::CanisterStatusType as Status;
struct Settings {
controllers: Vec<ic_platform::Principal>,
}
ic_platform::candid_record!(Settings {
570_880_087 => controllers: Vec<ic_platform::Principal>,
});
struct WireStatus {
status: Status,
settings: Settings,
marker: u64,
}
ic_platform::candid_record!(WireStatus {
100_394_802 => status: Status,
2_336_062_691 => settings: Settings,
3_127_932_346 => marker: u64,
});
fn input(
request: &IcObservationRequest<'_>,
status: Status,
marker: u64,
) -> IcObservationResponseInput {
let mut input = ic_observation::input(request);
input.reply = encode_fixture(WireStatus {
status,
settings: Settings {
controllers: vec![ic_platform::Principal::anonymous()],
},
marker,
})
.unwrap();
input
}
fn settlement(
request: &IcObservationRequest<'_>,
response: &IcObservationResponse,
) -> IcLifecycleSettlement {
IcLifecycleSettlement {
context: crate::model::settlement::SettlementContext {
authority: request.authority().digest(),
mutation_attempt: request.mutation_attempt(),
observation_attempt: request.observation_attempt(),
challenge: hash("56"),
observation_evidence: response.input().evidence.clone(),
evidence: hash("90"),
},
status: IcLifecycleReply::decode(request.payload(), &response.input().reply)
.unwrap()
.digest(),
attribution: EffectAttribution::Unresolved {
uncertainty: hash("78"),
},
}
}
#[test]
fn statuses_never_infer_original_outcomes_or_change_spending() {
for method in [
Method::StopCanister,
Method::StartCanister,
Method::LoadCanisterSnapshot,
] {
let original = original(method, Method::CanisterStatus);
let before = original.journal.clone();
let request = original.request();
for status in [Status::Stopped, Status::Running, Status::Stopping] {
let response = IcObservationResponse::new(input(&request, status, 42)).unwrap();
let mut claim = settlement(&request, &response);
for (attribution, outcome) in [
(
EffectAttribution::Applied {
attribution: hash("78"),
},
ObservationOutcomeRecord::Applied,
),
(
EffectAttribution::NotApplied {
exclusion: hash("78"),
},
ObservationOutcomeRecord::NotApplied,
),
(
EffectAttribution::Unresolved {
uncertainty: hash("78"),
},
ObservationOutcomeRecord::Uncertain,
),
] {
claim.attribution = attribution;
let view = validate_lifecycle_settlement(
&request,
&original.journal,
&response,
&hash("56"),
&claim,
)
.unwrap();
assert_eq!(view.outcome(), outcome);
assert!(std::ptr::eq(view.settlement(), &raw const claim));
assert!(std::ptr::eq(
view.observation().response(),
&raw const response
));
assert_eq!(original.journal, before);
}
}
}
}
#[test]
fn capture_and_inventory_have_no_lifecycle_settlement_lane() {
for (method, observation) in [
(Method::TakeCanisterSnapshot, Method::CanisterStatus),
(Method::StopCanister, Method::ListCanisterSnapshots),
(Method::StartCanister, Method::ListCanisterSnapshots),
(Method::LoadCanisterSnapshot, Method::ListCanisterSnapshots),
] {
let original = original(method, observation);
let request = original.request();
let response = IcObservationResponse::new(ic_observation::input(&request)).unwrap();
let claim = IcLifecycleSettlement {
context: crate::model::settlement::SettlementContext {
authority: request.authority().digest(),
mutation_attempt: 1,
observation_attempt: 2,
challenge: hash("56"),
observation_evidence: hash("34"),
evidence: hash("90"),
},
status: hash("78"),
attribution: EffectAttribution::Unresolved {
uncertainty: hash("78"),
},
};
assert!(matches!(
validate_lifecycle_settlement(
&request,
&original.journal,
&response,
&hash("56"),
&claim
),
Err(IcLifecycleSettlementError::UnsupportedMethod)
));
}
}
#[test]
fn stale_challenge_precedes_status_mismatch_and_exact_raw_evidence_remains_bound() {
let original = original(Method::LoadCanisterSnapshot, Method::CanisterStatus);
let request = original.request();
let response = IcObservationResponse::new(input(&request, Status::Stopped, 42)).unwrap();
let initial = settlement(&request, &response);
let before = original.journal.clone();
let mut stale = initial.clone();
stale.context.challenge = hash("12");
stale.status = hash("12");
assert!(matches!(
validate_lifecycle_settlement(&request, &original.journal, &response, &hash("56"), &stale),
Err(IcLifecycleSettlementError::Identity(
SettlementIdentityMismatch::Challenge
))
));
let changed = IcObservationResponse::new(input(&request, Status::Stopped, 43)).unwrap();
assert!(matches!(
validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &initial),
Err(IcLifecycleSettlementError::StatusMismatch)
));
let mut fields = response.input().clone();
fields.evidence = hash("12");
let changed = IcObservationResponse::new(fields).unwrap();
assert!(matches!(
validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &initial),
Err(IcLifecycleSettlementError::ObservationEvidenceMismatch)
));
assert_eq!(original.journal, before);
}
#[test]
fn existing_association_wire_and_current_journal_admission_remain_mandatory() {
let original = original(Method::StopCanister, Method::CanisterStatus);
let request = original.request();
let response = IcObservationResponse::new(input(&request, Status::Stopped, 42)).unwrap();
let claim = settlement(&request, &response);
for fault in [
ObservationFault::Authority,
ObservationFault::Request,
ObservationFault::Target,
ObservationFault::BothAttempts,
ObservationFault::Network,
] {
let mut fields = response.input().clone();
fault.apply(&mut fields, hash("12"), hash("12"));
let changed = IcObservationResponse::new(fields).unwrap();
let error = validate_lifecycle_settlement(
&request,
&original.journal,
&changed,
&hash("56"),
&claim,
)
.unwrap_err();
let IcLifecycleSettlementError::Observation(error) = error else {
panic!("existing association denial");
};
fault.assert_error(&error);
}
let mut fields = response.input().clone();
fields.reply = b"DIDL\0\0".to_vec();
let changed = IcObservationResponse::new(fields).unwrap();
assert!(matches!(
validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &claim,),
Err(IcLifecycleSettlementError::Observation(
IcObservationAssociationError::Status(_)
))
));
let mut settled = original.journal.clone();
settled
.record_observation(ObservationReceiptRequest {
attempt: 2,
request: request.payload().digest(),
outcome: ObservationOutcomeRecord::Uncertain,
evidence: hash("90").clone(),
})
.unwrap();
assert!(matches!(
validate_lifecycle_settlement(&request, &settled, &response, &hash("56"), &claim),
Err(IcLifecycleSettlementError::Observation(
IcObservationAssociationError::Reservation(_)
))
));
assert_eq!(settled.view().pending_mutation, Some(1));
assert_eq!(
settled.view().observations_used,
original.journal.view().observations_used
);
}