use super::*;
use crate::model::attempt_journal::{ObservationOutcomeRecord, ObservationReservationError};
use crate::tests::support::encode_fixture;
use crate::tests::support::ic_observation::snapshot;
use crate::tests::support::ic_snapshot_upload::{SOURCE_ID, observation_input, with_observation};
use ic_platform::management_canister_types::Snapshot;
fn response(
request: &IcSnapshotUploadObservationRequest<'_, '_>,
snapshots: Vec<Snapshot>,
) -> IcObservationResponse {
let mut input = observation_input(request);
input.reply = encode_fixture(snapshots).unwrap();
IcObservationResponse::new(input).unwrap()
}
fn claims(
request: &IcSnapshotUploadObservationRequest<'_, '_>,
baseline: &IcSnapshotReply<'_>,
response: &IcObservationResponse,
attribution: IcInventoryAttribution,
) -> IcInventorySettlement {
IcInventorySettlement {
context: crate::model::settlement::SettlementContext {
authority: request.authority().digest(),
mutation_attempt: 1,
observation_attempt: 2,
challenge: ArtifactChecksumRecord::from_bytes(b"fresh challenge"),
observation_evidence: response.input().evidence.clone(),
evidence: ArtifactChecksumRecord::from_bytes(b"synthetic local qualification only"),
},
baseline: baseline.digest(),
inventory: IcSnapshotReply::decode(request.payload(), &response.input().reply)
.unwrap()
.digest(),
attribution,
}
}
fn applied(id: &[u8]) -> IcInventoryAttribution {
IcInventoryAttribution::Applied {
snapshot_id: id.into(),
attribution: ArtifactChecksumRecord::from_bytes(b"exclusive allocation declaration"),
}
}
fn unresolved() -> IcInventoryAttribution {
IcInventoryAttribution::Unresolved {
uncertainty: ArtifactChecksumRecord::from_bytes(
b"settled authenticated uncertainty declaration",
),
}
}
#[test]
fn explicit_attribution_selects_one_of_several_new_ids_and_retains_exact_originals() {
with_observation(|plan, upload, list, journal| {
let request =
IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
let bytes = encode_fixture(vec![snapshot(&[1])]).unwrap();
let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
let response = response(
&request,
vec![snapshot(&[3]), snapshot(&[1]), snapshot(&[2])],
);
let claims = claims(&request, &baseline, &response, applied(&[3]));
let before = journal.digest();
let view = validate_settlement(
&request,
journal,
&baseline,
&response,
&claims.context.challenge,
&claims,
)
.unwrap();
assert_eq!(view.outcome(), ObservationOutcomeRecord::Applied);
assert_eq!(view.snapshot().unwrap().id(), &[3]);
assert_eq!(view.baseline().digest(), baseline.digest());
assert_eq!(
view.observation().response().input().evidence,
claims.context.observation_evidence
);
assert!(std::ptr::eq(view.settlement(), &raw const claims));
assert_eq!(journal.digest(), before);
assert_eq!(journal.view().pending_mutation, Some(1));
assert_eq!(journal.view().pending_observation, Some(2));
let mut changed = claims.clone();
changed.context.authority = ArtifactChecksumRecord::from_bytes(b"other original authority");
assert!(matches!(
validate_settlement(
&request,
journal,
&baseline,
&response,
&claims.context.challenge,
&changed
),
Err(IcSnapshotUploadSettlementError::Identity(
InventorySettlementMismatch::Authority
))
));
assert_eq!(journal.digest(), before);
});
}
#[test]
fn applied_requires_new_candidate_and_canonical_destination_bounds() {
with_observation(|plan, upload, list, journal| {
let request =
IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
let bytes = encode_fixture(vec![snapshot(&[1])]).unwrap();
let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
let response = response(
&request,
vec![
snapshot(&[1]),
snapshot(SOURCE_ID),
snapshot(&[2]),
snapshot(&[9; 256]),
],
);
let missing = self::response(&request, vec![snapshot(SOURCE_ID)]);
let mut invalid = claims(&request, &baseline, &missing, applied(SOURCE_ID));
invalid.context.authority = ArtifactChecksumRecord::from_bytes(b"wrong authority");
assert!(matches!(
validate_settlement(
&request,
journal,
&baseline,
&missing,
&invalid.context.challenge,
&invalid
),
Err(IcSnapshotUploadSettlementError::Baseline(
SnapshotInventoryDeltaError::LostBaseline
))
));
for id in [vec![], vec![9; 257], SOURCE_ID.to_vec()] {
let claims = claims(&request, &baseline, &response, applied(&id));
assert!(matches!(
validate_settlement(
&request,
journal,
&baseline,
&response,
&claims.context.challenge,
&claims
),
Err(IcSnapshotUploadSettlementError::Destination(_))
));
}
for id in [&[1][..], &[8][..]] {
let claims = claims(&request, &baseline, &response, applied(id));
assert!(matches!(
validate_settlement(
&request,
journal,
&baseline,
&response,
&claims.context.challenge,
&claims
),
Err(IcSnapshotUploadSettlementError::NotNewCandidate)
));
}
let claims = claims(&request, &baseline, &response, applied(&[9; 256]));
assert_eq!(
validate_settlement(
&request,
journal,
&baseline,
&response,
&claims.context.challenge,
&claims
)
.unwrap()
.snapshot()
.unwrap()
.id(),
&[9; 256]
);
});
}
#[test]
fn malformed_or_lost_observation_cannot_become_settled_uncertainty() {
with_observation(|plan, upload, list, journal| {
let request =
IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
let bytes = encode_fixture(Vec::<Snapshot>::new()).unwrap();
let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
let valid = response(&request, vec![]);
let claims = claims(&request, &baseline, &valid, unresolved());
let before = journal.digest();
let empty = AttemptJournalRecord::new(journal.authority().clone());
assert!(matches!(
validate_settlement(
&request,
&empty,
&baseline,
&valid,
&claims.context.challenge,
&claims
),
Err(IcSnapshotUploadSettlementError::Observation(
IcObservationAssociationError::Reservation(
ObservationReservationError::MutationMismatch
)
))
));
for raw in [
vec![],
b"lost reply".to_vec(),
encode_fixture(vec![snapshot(&[1]), snapshot(&[1])]).unwrap(),
] {
let mut input = observation_input(&request);
input.reply = raw;
let malformed = IcObservationResponse::new(input).unwrap();
assert!(matches!(
validate_settlement(
&request,
journal,
&baseline,
&malformed,
&claims.context.challenge,
&claims
),
Err(IcSnapshotUploadSettlementError::Observation(_))
));
assert_eq!(journal.digest(), before);
assert_eq!(journal.view().pending_observation, Some(2));
}
});
}
#[test]
fn allocation_claim_diagnostics_redact_raw_identifiers() {
let attribution = applied(b"secret-destination-identity");
let debug = format!("{attribution:?}");
assert!(debug.contains("snapshot_id_bytes: 27"));
assert!(!debug.contains("secret-destination-identity"));
assert!(!debug.contains("115, 101, 99"));
}