use crate::test_support::temp_dir;
use crate::{
HostCacheError,
cache_file::CacheFileError,
sns::report::tests::{fixtures::*, *},
};
use std::fs;
#[test]
fn sns_neurons_refresh_writes_complete_cache_and_cached_sort_uses_it() {
let root = temp_dir("ic-query-sns-neurons-refresh");
let request = sns_neurons_refresh_request(&root, None);
let refresh = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
.expect("refresh neurons");
let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
let attempt_path = sns_neurons_refresh_attempt_path(&root, MAINNET_NETWORK, ROOT_A);
let lock_path = sns_neurons_refresh_lock_path(&root, MAINNET_NETWORK, ROOT_A);
assert!(cache_path.is_file());
assert!(attempt_path.is_file());
assert!(!lock_path.exists());
assert!(refresh.complete);
assert_eq!(refresh.id, 1);
assert_eq!(refresh.page_count, 3);
assert_eq!(refresh.neuron_count, 3);
let cache: serde_json::Value =
serde_json::from_slice(&fs::read(&cache_path).expect("read cache")).expect("parse cache");
assert_eq!(cache["schema_version"], 1);
assert_eq!(cache["domain"], "sns");
assert_eq!(cache["entity"], ROOT_A);
assert_eq!(cache["collection"], "neurons");
assert_eq!(cache["scope"], "full");
assert_eq!(cache["id"], 1);
assert_eq!(cache["completeness"]["status"], "api_exhausted");
assert_eq!(cache["neurons"].as_array().expect("cache neurons").len(), 3);
assert!(cache.get("metadata").is_none());
assert!(cache.get("data").is_none());
let mut cached_request = neurons_request("1");
cached_request.cache_root = Some(root.clone());
cached_request.sort = SnsNeuronsSort::Stake;
cached_request.limit = 2;
cached_request.verbose = true;
let report = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
.expect("cached neurons report");
assert_eq!(report.data_source.as_str(), "cache");
assert_eq!(report.sort, "stake");
assert_eq!(report.total_neuron_count, 3);
assert_eq!(report.neuron_count, 2);
assert_eq!(report.requested_limit, 2);
assert!(report.verbose);
assert_eq!(report.owner_principal_id, None);
assert_eq!(report.cache_complete, Some(true));
let report_json = serde_json::to_value(&report).expect("serialize cached report");
for field in [
"network",
"sns_wasm_canister_id",
"fetched_at",
"source_endpoint",
"fetched_by",
"id",
"name",
"root_canister_id",
"governance_canister_id",
] {
assert_eq!(report_json[field], cache[field], "cached {field}");
}
assert_eq!(report.neurons[0].neuron_id, "03".repeat(32));
assert_eq!(report.neurons[0].cached_neuron_stake_e8s, 50);
assert_eq!(report.neurons[1].neuron_id, "02".repeat(32));
assert_eq!(report.neurons[1].cached_neuron_stake_e8s, 30);
let attempt: serde_json::Value =
serde_json::from_slice(&fs::read(attempt_path).expect("read attempt"))
.expect("parse attempt");
assert_eq!(attempt["status"], "complete");
assert_eq!(attempt["pages_fetched"], 3);
assert_eq!(attempt["rows_fetched"], 3);
assert_eq!(attempt["last_cursor"], "03".repeat(32));
assert_eq!(attempt["id"], refresh.id);
assert_eq!(attempt["root_canister_id"], ROOT_A);
assert!(attempt.get("metadata").is_none());
let _ = fs::remove_dir_all(root);
}
struct ExclusiveNeuronPagesSource;
delegate_sns_discovery!(ExclusiveNeuronPagesSource);
impl SnsNeuronsSource for ExclusiveNeuronPagesSource {
fn fetch_sns_neurons(
&self,
_request: &SnsSourceRequest,
_sns: &MainnetSns,
_limit: u32,
_owner_principal_id: Option<&str>,
) -> Result<MainnetSnsNeurons, SnsHostError> {
unreachable!("refresh uses pages")
}
fn fetch_sns_neuron_page(
&self,
_request: &SnsSourceRequest,
_sns: &MainnetSns,
limit: u32,
start_page_at: Option<&SnsNeuronId>,
owner_principal_id: Option<&str>,
) -> Result<MainnetSnsNeuronPage, SnsHostError> {
assert_eq!(limit, 2);
assert!(owner_principal_id.is_none());
let ids = match start_page_at.map(|cursor| cursor.id.as_slice()) {
None => vec![1, 2],
Some(cursor) if cursor == [2; 32] => vec![3, 4],
Some(cursor) if cursor == [4; 32] => vec![],
other => panic!("unexpected cursor {other:?}"),
};
Ok(MainnetSnsNeuronPage {
last_cursor: ids.last().map(|id| SnsNeuronId { id: vec![*id; 32] }),
neurons: ids
.into_iter()
.map(|id| {
let mut row = fixture_sns_neuron().detail.neuron;
row.neuron_id = format!("{id:02x}").repeat(32);
row
})
.collect(),
})
}
}
#[test]
fn sns_neurons_refresh_accepts_exclusive_pages_and_empty_terminal_page() {
let root = temp_dir("ic-query-sns-neurons-exclusive-pages");
let request = sns_neurons_refresh_request(&root, None);
let refresh = refresh_sns_neurons_cache_with_source(&request, &ExclusiveNeuronPagesSource)
.expect("exclusive pages exhaust successfully");
assert_eq!(refresh.page_count, 3);
assert_eq!(refresh.neuron_count, 4);
let cache: serde_json::Value = serde_json::from_slice(
&fs::read(sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A)).unwrap(),
)
.unwrap();
let ids: Vec<_> = cache["neurons"]
.as_array()
.unwrap()
.iter()
.map(|row| row["neuron_id"].as_str().unwrap())
.collect();
assert_eq!(
ids,
(1..=4)
.map(|id| format!("{id:02x}").repeat(32))
.collect::<Vec<_>>()
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn sns_neurons_refresh_failure_preserves_existing_complete_cache() {
let root = temp_dir("ic-query-sns-neurons-refresh-preserves-cache");
let complete_request = sns_neurons_refresh_request(&root, None);
refresh_sns_neurons_cache_with_source(&complete_request, &PagedFixtureSnsNeuronsSource)
.expect("complete refresh");
let failed_request = sns_neurons_refresh_request(&root, Some(1));
refresh_sns_neurons_cache_with_source(&failed_request, &PagedFixtureSnsNeuronsSource)
.expect_err("incomplete refresh");
let mut cached_request = neurons_request("1");
cached_request.cache_root = Some(root.clone());
cached_request.sort = SnsNeuronsSort::Stake;
let report = build_sns_neurons_report_with_source(&cached_request, &NoLiveSnsNeuronsSource)
.expect("previous complete cache remains usable");
assert_eq!(report.data_source.as_str(), "cache");
assert_eq!(report.total_neuron_count, 3);
assert_eq!(report.neurons[0].neuron_id, "03".repeat(32));
let attempt_path = sns_neurons_refresh_attempt_path(&root, MAINNET_NETWORK, ROOT_A);
let attempt: serde_json::Value =
serde_json::from_slice(&fs::read(attempt_path).expect("read attempt"))
.expect("parse attempt");
assert_eq!(attempt["status"], "failed");
let _ = fs::remove_dir_all(root);
}
#[test]
fn sns_neurons_refresh_rejects_stale_lock_without_publishing() {
let root = temp_dir("ic-query-sns-neurons-stale-lock");
let request = sns_neurons_refresh_request(&root, None);
let cache_path = sns_neurons_cache_path(&root, MAINNET_NETWORK, ROOT_A);
let lock_path = sns_neurons_refresh_lock_path(&root, MAINNET_NETWORK, ROOT_A);
crate::cache_file::write_managed_text_atomically(
&root,
&lock_path,
&serde_json::to_string_pretty(&serde_json::json!({
"schema_version": 1,
"network": MAINNET_NETWORK,
"pid": 999_999,
"started_at_unix_ms": 1,
"stale_after_seconds": DEFAULT_SNS_NEURONS_REFRESH_LOCK_STALE_SECONDS,
"target_path": cache_path.display().to_string(),
}))
.expect("serialize stale lock"),
)
.expect("write stale lock");
let err = refresh_sns_neurons_cache_with_source(&request, &PagedFixtureSnsNeuronsSource)
.expect_err("stale lock requires manual cleanup");
assert!(matches!(
err,
SnsHostError::Cache(HostCacheError::Operation {
source: CacheFileError::StaleRefreshLock { .. },
..
})
));
assert!(!cache_path.is_file());
assert!(lock_path.exists());
let _ = fs::remove_dir_all(root);
}