mod funding;
mod intents;
mod pool;
mod provisioning;
mod receipts;
use super::*;
use candid::{CandidType, Principal};
use canic_core::{
dto::{
error::Error,
pool::{
CanisterPoolAsset, CanisterPoolAssetOrigin, CanisterPoolAssetStatus,
CanisterPoolResponse,
},
},
ids::{CanonicalNetworkId, SubnetId},
};
use ic_agent::identity::BasicIdentity;
use ic_testkit::pocket_ic::{CanisterSettings, PocketIcBuilder};
use sha2_host::{Digest, Sha256};
use std::{fs, process::Command, time::SystemTime};
#[derive(CandidType)]
enum RegistryReply {
Registry(Box<FleetRegistry>),
}
#[derive(CandidType)]
enum PoolReply {
Pool(Box<CanisterPoolResponse>),
}
fn wire_fixture() -> Vec<u8> {
let directory = crate::test_support::temp_dir("release-inventory-wire");
fs::create_dir_all(&directory).unwrap();
let source = directory.join("fixture.rs");
let output = directory.join("fixture.wasm");
fs::write(&source, include_str!("../fixture/mod.rs")).unwrap();
let result = Command::new("rustc")
.args([
"--edition=2024",
"--crate-type=cdylib",
"--target=wasm32-unknown-unknown",
"-O",
])
.arg(&source)
.arg("-o")
.arg(&output)
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let bytes = fs::read(output).unwrap();
fs::remove_dir_all(directory).unwrap();
bytes
}
fn pool(review: &FleetReleaseReviewRecord, registry: &FleetRegistry) -> CanisterPoolResponse {
let mut entries = review.sources[2..]
.iter()
.map(|source| CanisterPoolAsset {
canister_id: source.binding.canister_id,
creation_receipt: None,
cycles: Cycles::new(1_000),
origin: CanisterPoolAssetOrigin::Imported,
status: if matches!(source.role, FleetReleaseRole::Store { .. }) {
CanisterPoolAssetStatus::Store
} else {
CanisterPoolAssetStatus::PendingReset
},
added_at_ns: 1,
updated_at_ns: 1,
})
.collect::<Vec<_>>();
entries.sort_by_key(|entry| entry.canister_id);
CanisterPoolResponse {
config: registry.fleet_subnet_roots[0].limits.canister_pool.clone(),
tracked: 2,
store: 1,
store_deletion_pending: 0,
pooled: 1,
workload: 0,
surplus: 1,
ready: 0,
pending_reset: 1,
claimed: 0,
recycling: 0,
handing_off: 0,
failed: 0,
completed_handoffs: 0,
pending_creation: None,
pending_handoff: None,
entries,
next_start_after: None,
}
}
#[test]
#[ignore = "the workspace runner supplies one shared PocketIC server and serial execution"]
#[expect(
clippy::too_many_lines,
reason = "one signed-query journey covers exact closure and independent refusal boundaries"
)]
fn governed_pocketic_release_inventory_binds_complete_ownership() {
let mut pic = crate::test_support::start_pocket_ic(
PocketIcBuilder::new()
.with_nns_subnet()
.with_application_subnet(),
);
pic.set_time(SystemTime::now().into());
let agent = Agent::builder()
.with_url(pic.make_live(None))
.with_identity(BasicIdentity::from_raw_key(&[19; 32]))
.with_max_response_body_size(inventory::RESPONSE_BYTES)
.build()
.unwrap();
agent.set_root_key(pic.root_key().unwrap());
let operator = agent.get_principal().unwrap();
let ids = (0..4)
.map(|_| pic.create_canister_with_settings(Some(operator), None))
.collect::<Vec<_>>();
let (mut review, mut registry) = fixture();
review.authority.operator = operator;
review.authority.coordinator = ids[0];
review.authority.network_root_key_sha256 = Sha256::digest(agent.read_root_key()).into();
review.authority.fleet.fleet.canonical_network_id =
CanonicalNetworkId::from_der_root_trust_anchor(&agent.read_root_key()).unwrap();
let wasm = wire_fixture();
for (source, id) in review.sources.iter_mut().zip(&ids) {
source.binding.canister_id = *id;
source.binding.subnet = SubnetId::from_principal(pic.get_subnet(*id).unwrap());
source.binding.controllers = vec![operator];
source.binding.module_sha256 = Some(Sha256::digest(&wasm).into());
match &mut source.role {
FleetReleaseRole::Store { root } | FleetReleaseRole::Child { root } => *root = ids[1],
_ => {}
}
}
registry.authority.binding.fleet = review.authority.fleet.clone();
registry.authority.binding.coordinator = ids[0];
registry.authority.binding.coordinator_subnet = review.sources[0].binding.subnet;
registry.authority.binding.recovery_controllers = vec![operator];
registry.fleet_subnet_roots[0].fleet_subnet_root = ids[1];
registry.fleet_subnet_roots[0].placement_subnet = review.sources[1].binding.subnet;
let registry_reply = candid::encode_one(Ok::<_, Error>(RegistryReply::Registry(Box::new(
registry.clone(),
))))
.unwrap();
let pool_reply = candid::encode_one(Ok::<_, Error>(PoolReply::Pool(Box::new(pool(
&review, ®istry,
)))))
.unwrap();
for (index, id) in ids.iter().enumerate() {
let reply = if index == 0 {
registry_reply.clone()
} else {
pool_reply.clone()
};
pic.install_canister(*id, wasm.clone(), reply, Some(operator));
}
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let collect = |review: &FleetReleaseReviewRecord, registry: &FleetRegistry| {
runtime.block_on(collect_with_agent(&agent, review, registry))
};
assert_eq!(
collect(&review, ®istry).unwrap().children,
expected_ownership(&review).unwrap()
);
let mut omitted = review.clone();
omitted.sources.pop();
assert!(matches!(
collect(&omitted, ®istry),
Err(ReleaseInventoryError::Evidence(
FleetReleaseError::Inventory
))
));
let mut wrong_registry = registry.clone();
wrong_registry.revision += 1;
assert!(matches!(
collect(&review, &wrong_registry),
Err(ReleaseInventoryError::Evidence(
FleetReleaseError::Authority
))
));
let mut wrong_signer = review.clone();
wrong_signer.authority.operator = Principal::anonymous();
assert!(matches!(
collect(&wrong_signer, ®istry),
Err(ReleaseInventoryError::Authentication(
super::super::super::observation::ReleaseObservationError::Authority
))
));
funding::assert_census(&pic, &agent, &runtime, &review, ®istry);
pool::assert_census(&pic, &agent, &runtime, &review, ®istry);
provisioning::assert_census(&pic, &agent, &runtime, &review, ®istry);
receipts::assert_census(&pic, &agent, &runtime, &review, ®istry);
intents::assert_census(&pic, &agent, &runtime, &review, ®istry);
pic.update_call(ids[1], operator, "replace", b"DIDL".to_vec())
.unwrap();
assert!(matches!(
collect(&review, ®istry),
Err(ReleaseInventoryError::Query(
CapacityImportJournalError::InventoryObservation { .. }
))
));
pic.update_call(ids[1], operator, "replace", pool_reply)
.unwrap();
pic.update_canister_settings(
ids[3],
Some(operator),
CanisterSettings {
controllers: Some(vec![operator, Principal::anonymous()]),
..CanisterSettings::default()
},
)
.unwrap();
assert!(matches!(
collect(&review, ®istry),
Err(ReleaseInventoryError::Evidence(
FleetReleaseError::Custody { .. }
))
));
}