use super::fixtures::*;
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn catalog_progress_tracks_live_policy_and_not_cache_hits_or_cache_only() {
let root = temp_dir("ic-query-catalog-progress");
let path = subnet_catalog_path(&root, MAINNET_NETWORK);
let source = FixtureRefreshSource::ok(fixture_catalog());
let mut request = list_request(&root);
let mut events = Vec::new();
for disposition in [
CacheDisposition::RefreshedMissing,
CacheDisposition::RefreshedInvalid,
CacheDisposition::RefreshedStale,
CacheDisposition::ForcedRefresh,
] {
match disposition {
CacheDisposition::RefreshedInvalid => {
crate::cache_file::write_managed_text_atomically(&root, &path, "not-json").unwrap();
}
CacheDisposition::RefreshedStale => {
request.now_unix_secs += 100;
request.read_policy = CatalogReadPolicy::RefreshMissingInvalidOrOlderThan {
source: CatalogSourceSelection::uncertified_query(
DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT,
),
max_age_seconds: 1,
};
}
CacheDisposition::ForcedRefresh => {
request.read_policy = CatalogReadPolicy::ForceRefresh {
source: CatalogSourceSelection::uncertified_query(
DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT,
),
};
}
_ => {}
}
events.clear();
let report = build_subnet_catalog_list_report_with_source_and_progress(
&request,
&source,
&mut |event| events.push(event),
)
.await
.unwrap();
assert_eq!(report.cache_disposition, disposition);
assert_eq!(
events,
vec![crate::QueryProgressEvent::CacheRefresh {
component: "subnet catalog".to_string(),
path: path.clone(),
source_endpoint: DEFAULT_SUBNET_CATALOG_SOURCE_ENDPOINT.to_string(),
}]
);
}
let source_calls = source.call_count();
request.read_policy = CatalogReadPolicy::CacheOnly;
events.clear();
build_subnet_catalog_list_report_with_source_and_progress(&request, &source, &mut |event| {
events.push(event);
})
.await
.unwrap();
request.read_policy = list_request(&root).read_policy;
build_subnet_catalog_list_report_with_source_and_progress(&request, &source, &mut |event| {
events.push(event);
})
.await
.unwrap();
assert_eq!(events, Vec::<crate::QueryProgressEvent>::new());
assert_eq!(source.call_count(), source_calls);
fs::remove_file(&path).unwrap();
request.read_policy = CatalogReadPolicy::CacheOnly;
assert!(
build_subnet_catalog_list_report_with_source_and_progress(
&request,
&source,
&mut |event| events.push(event)
)
.await
.is_err()
);
assert_eq!(events, Vec::<crate::QueryProgressEvent>::new());
assert_eq!(source.call_count(), source_calls);
let _ = fs::remove_dir_all(root);
}
#[tokio::test(flavor = "current_thread")]
async fn catalog_lock_refusal_does_not_announce_a_source_call() {
let root = temp_dir("ic-query-catalog-progress-lock");
let request = list_request(&root);
let refresh = refresh_request(&root);
let lock = subnet_catalog_refresh_lock_path(&root, MAINNET_NETWORK);
let _lock_guard = hold_refresh_lock_for_test(&lock, &refresh);
let source = FixtureRefreshSource::ok(fixture_catalog());
let mut events = Vec::new();
assert!(
build_subnet_catalog_list_report_with_source_and_progress(
&request,
&source,
&mut |event| events.push(event)
)
.await
.is_err()
);
assert_eq!(events, Vec::<crate::QueryProgressEvent>::new());
assert_eq!(source.call_count(), 0);
let _ = fs::remove_dir_all(root);
}
#[tokio::test(flavor = "current_thread")]
async fn list_report_loads_cached_catalog_and_caps_ranges() {
let root = temp_dir("ic-query-subnet-list");
write_catalog(&root, fixture_catalog());
let request = list_request(&root);
let report = build_subnet_catalog_list_report(&request)
.await
.expect("list report");
let _ = fs::remove_dir_all(root);
assert_eq!(report.subnets.len(), 2);
assert_eq!(report.cache_disposition, CacheDisposition::CacheHit);
assert_eq!(
report.metadata.assurance,
CatalogAssurance::UncertifiedQuery
);
assert_eq!(report.subnets[0].range_count, 2);
assert_eq!(report.subnets[0].ranges_shown, 1);
for subnet in &report.subnets {
let _compact_principal = subnet.subnet_principal.chars().take(5).collect::<String>();
}
}
#[tokio::test(flavor = "current_thread")]
async fn list_range_views_preserve_totals_and_per_subnet_pagination() {
let root = temp_dir("ic-query-subnet-range-views");
write_catalog(&root, fixture_catalog());
for (show, offset, limit, shown_a, shown_b) in [
(true, 0, 1, 1, 1),
(true, 1, 1, 1, 0),
(false, 0, 1, 0, 0),
(true, 0, 0, 0, 0),
(true, usize::MAX, usize::MAX, 0, 0),
] {
let mut request = list_request(&root);
request.show_ranges = show;
request.range_offset = offset;
request.range_limit = limit;
let report = build_subnet_catalog_list_report(&request).await.unwrap();
let [a, b] = report.subnets.as_slice() else {
panic!("both catalog Subnets are retained");
};
assert_eq!(a.subnet_principal, SUBNET_A);
assert_eq!(b.subnet_principal, SUBNET_B);
assert_eq!((a.range_count, b.range_count), (2, 1));
assert_eq!((a.ranges_shown, b.ranges_shown), (shown_a, shown_b));
assert_eq!((a.ranges.len(), b.ranges.len()), (shown_a, shown_b));
if offset == 1 {
assert_eq!(a.ranges[0].start_canister_id, CANISTER_A);
}
}
let request = list_request(&root).with_kind(SubnetKind::Application);
let report = build_subnet_catalog_list_report(&request).await.unwrap();
assert_eq!(report.subnets.len(), 1);
assert_eq!(report.subnets[0].subnet_principal, SUBNET_A);
assert_eq!(report.subnets[0].range_count, 2);
let _ = fs::remove_dir_all(root);
}
#[tokio::test(flavor = "current_thread")]
async fn list_report_refreshes_missing_catalog() {
let root = temp_dir("ic-query-subnet-list-refresh");
let mut catalog = fixture_catalog();
catalog.provenance.registry_version = 987_654;
catalog.canonicalize_and_seal().expect("reseal fixture");
let source = FixtureRefreshSource::ok(catalog);
let request = list_request(&root);
let report = build_subnet_catalog_list_report_with_source(&request, &source)
.await
.expect("list report");
let cached =
load_cached_subnet_catalog(&cache_only_load_request(&root)).expect("cached catalog");
let _ = fs::remove_dir_all(root);
assert_eq!(report.metadata.registry_version, 987_654);
assert_eq!(report.cache_disposition, CacheDisposition::RefreshedMissing);
assert_eq!(cached.catalog.provenance().registry_version, 987_654);
}
#[tokio::test(flavor = "current_thread")]
async fn list_report_refreshes_invalid_catalog_but_cache_only_remains_strict() {
let root = temp_dir("ic-query-subnet-list-invalid-refresh");
let path = subnet_catalog_path(&root, MAINNET_NETWORK);
crate::cache_file::write_managed_text_atomically(&root, &path, "not-json")
.expect("write invalid catalog");
let request = list_request(&root);
let error = load_cached_subnet_catalog(&cache_only_load_request(&root))
.expect_err("cache-only load is strict");
assert!(matches!(
error.source,
SubnetCatalogHostError::Catalog(CatalogError::Json(_))
));
let mut catalog = fixture_catalog();
catalog.provenance.registry_version = 987_654;
catalog.canonicalize_and_seal().expect("reseal fixture");
let report =
build_subnet_catalog_list_report_with_source(&request, &FixtureRefreshSource::ok(catalog))
.await
.expect("invalid catalog refreshes");
assert_eq!(report.metadata.registry_version, 987_654);
assert_eq!(report.cache_disposition, CacheDisposition::RefreshedInvalid);
assert_ne!(
fs::read_to_string(path).expect("refreshed catalog"),
"not-json"
);
let _ = fs::remove_dir_all(root);
}
#[tokio::test(flavor = "current_thread")]
async fn failed_invalid_catalog_refresh_preserves_original_file() {
let root = temp_dir("ic-query-subnet-list-invalid-refresh-failure");
let path = subnet_catalog_path(&root, MAINNET_NETWORK);
crate::cache_file::write_managed_text_atomically(&root, &path, "not-json")
.expect("write invalid catalog");
let error = build_subnet_catalog_list_report_with_source(
&list_request(&root),
&FixtureRefreshSource::err(),
)
.await
.expect_err("failed refresh remains visible");
assert!(matches!(
error,
SubnetCatalogHostError::Catalog(CatalogError::EmptySubnets)
));
assert_eq!(
fs::read_to_string(path).expect("preserved invalid catalog"),
"not-json"
);
let _ = fs::remove_dir_all(root);
}