use super::*;
use canic_core::dto::{
fixture_provisioning::{
FixtureChunkDescriptor, FixtureChunkUpload, FixtureDescriptor, FixtureSourceStatus,
},
root_store::{RootStoreBootstrapRequest, RootStoreFixture, RootStoreFixturePrepareRequest},
};
use std::fs;
fn paths(label: &str) -> (std::path::PathBuf, EnsurePaths) {
let root = crate::test_support::temp_dir(&format!("plan-content-{label}"));
let paths = EnsurePaths::under(&root, "local", "demo");
fs::create_dir_all(&paths.content).expect("create content store");
(root, paths)
}
#[test]
fn read_object_rejects_invalid_expected_size_before_object_access() {
let (root, paths) = paths("declared-oversize");
let expected = [0_u8; 32];
let error = read_object(
&paths,
&expected,
u64::try_from(canic_core::CANIC_WASM_CHUNK_BYTES).expect("chunk bound") + 1,
)
.expect_err("oversized expected authority must reject");
assert!(matches!(error, EnsureStateError::StoreChunkMismatch { .. }));
fs::remove_dir_all(root).expect("remove temp root");
}
#[test]
fn read_object_rejects_oversized_truncated_and_hash_mismatched_content() {
let cases: &[(&str, &[u8])] = &[
("oversized", b"12345"),
("truncated", b"123"),
("hash-mismatch", b"abcd"),
];
for (label, retained) in cases {
let (root, paths) = paths(label);
let expected_bytes = b"1234";
let expected = wasm_hash(expected_bytes);
fs::write(object_path(&paths, &expected), retained).expect("write retained object");
let error = read_object(&paths, &expected, expected_bytes.len() as u64)
.expect_err("invalid retained object must reject");
assert!(matches!(error, EnsureStateError::StoreChunkMismatch { .. }));
fs::remove_dir_all(root).expect("remove temp root");
}
}
#[cfg(unix)]
#[test]
fn read_object_rejects_a_linked_content_object() {
let (root, paths) = paths("linked");
let bytes = b"1234";
let expected = wasm_hash(bytes);
let outside = root.join("outside");
fs::write(&outside, bytes).expect("write link target");
std::os::unix::fs::symlink(&outside, object_path(&paths, &expected))
.expect("create retained object link");
let error = read_object(&paths, &expected, bytes.len() as u64)
.expect_err("linked retained object must reject");
assert!(matches!(
error,
EnsureStateError::StoreChunkUnavailable { .. }
));
fs::remove_dir_all(root).expect("remove temp root");
}
fn fixture_projection(paths: &EnsurePaths) -> Value {
let bytes = b"fixture rows".to_vec();
let descriptor = FixtureDescriptor {
schema_version: 1,
format_hash: [1; 32],
encoded_length: bytes.len() as u64,
chunks: vec![FixtureChunkDescriptor {
digest: wasm_hash(&bytes).try_into().unwrap(),
length: u32::try_from(bytes.len()).unwrap(),
}],
completion_summary: [3; 32],
};
let content_id =
canic_control_plane::api::fixture_content::FixtureContentApi::content_id(&descriptor)
.unwrap();
let source = RootStoreFixture {
role: canic_core::ids::CanisterRole::new("app"),
content_id,
descriptor,
};
let store = candid::Principal::from_slice(&[7]);
retain_object(paths, &wasm_hash(&bytes), &bytes).unwrap();
let prepare = CurrentFleetProtocolAction::PrepareStoreFixture {
maximum_attempts: 1,
request: RootStoreFixturePrepareRequest {
bootstrap: RootStoreBootstrapRequest {
operation_id: [4; 32],
manifest_payload_size_bytes: 123,
},
role: source.role.clone(),
},
store,
source,
};
let publish = CurrentFleetProtocolAction::PublishStoreFixtureChunk {
maximum_attempts: 1,
expected: FixtureSourceStatus {
content_id,
next_chunk: 1,
chunk_count: 1,
received_bytes: bytes.len() as u64,
complete: true,
},
source_bytes: bytes.len() as u64,
request: FixtureChunkUpload {
content_id,
index: 0,
bytes,
},
};
serde_json::json!({"protocol_actions": [
{"kind":"fleet_protocol", "principal":candid::Principal::from_slice(&[8]).to_text(), "action":prepare},
{"kind":"fleet_protocol", "principal":store.to_text(), "action":publish},
]})
}
#[test]
fn fixture_content_round_trips_without_inline_payloads_and_rejects_substitution() {
let (root, paths) = paths("fixture-publication");
let original = fixture_projection(&paths);
let mut compact = original.clone();
remove_inline_bytes(&mut compact).unwrap();
assert!(!contains_inline_bytes(&compact).unwrap());
let mut hydrated = compact.clone();
hydrate(&paths, &mut hydrated).unwrap();
assert_eq!(hydrated, original);
let mut cases = Vec::new();
let mut wrong = compact.clone();
wrong["protocol_actions"][1]["principal"] =
Value::String(candid::Principal::anonymous().to_text());
cases.push(wrong);
let mut wrong = compact.clone();
wrong["protocol_actions"][0]["action"]["request"]["role"] = Value::String("other".into());
cases.push(wrong);
let mut wrong = compact.clone();
wrong["protocol_actions"].as_array_mut().unwrap().remove(0);
cases.push(wrong);
let mut wrong = compact.clone();
wrong["protocol_actions"][1]["action"]["expected"]["received_bytes"] = Value::from(1);
cases.push(wrong);
let mut wrong = compact.clone();
wrong["protocol_actions"][1]["action"]["source_bytes"] = Value::from(1);
cases.push(wrong);
let other = b"foreign rows";
let hash = wasm_hash(other);
retain_object(&paths, &hash, other).unwrap();
let mut wrong = compact;
wrong["protocol_actions"][1]["action"]["request"]["bytes_sha256"] =
Value::String(hex_bytes(&hash));
wrong["protocol_actions"][1]["action"]["request"]["bytes_size"] = Value::from(other.len());
cases.push(wrong);
for mut wrong in cases {
std::assert_matches!(
hydrate(&paths, &mut wrong),
Err(EnsureStateError::StoreChunkAuthority { .. })
);
}
fs::remove_dir_all(root).unwrap();
}
#[test]
fn fused_chunk_content_rejects_changed_preparation_authority() {
let (root, paths) = paths("fused-chunk");
let bytes = vec![7; 32];
let digest = wasm_hash(&bytes);
retain_object(&paths, &digest, &bytes).unwrap();
let mut projection = serde_json::json!({"protocol_actions": [{
"kind": "fleet_protocol", "action": {
"kind": "publish_store_chunk", "request": {
"template_id": "app", "version": "current", "chunk_index": 0,
"bytes": bytes, "preparation": {
"manifest": null, "template_id": "app", "version": "current",
"payload_hash": digest, "payload_size_bytes": 32,
"chunk_hashes": [digest]
}
}
}
}]});
let original = projection.clone();
remove_inline_bytes(&mut projection).unwrap();
let mut hydrated = projection.clone();
hydrate(&paths, &mut hydrated).unwrap();
assert_eq!(hydrated, original);
for case in 0..4 {
let mut wrong = projection.clone();
let request = &mut wrong["protocol_actions"][0]["action"]["request"];
match case {
0 => request["preparation"]["template_id"] = Value::String("other".into()),
1 => request["chunk_index"] = Value::from(1),
2 => request["preparation"]["chunk_hashes"] = serde_json::json!([vec![8; 32]]),
_ => {
request.as_object_mut().unwrap().remove("preparation");
}
}
std::assert_matches!(
hydrate(&paths, &mut wrong),
Err(EnsureStateError::StoreChunkAuthority { .. })
);
}
fs::remove_dir_all(root).unwrap();
}