ic-query 0.8.2

Internet Computer query library for NNS, SNS, ICRC, and related public network metadata
Documentation
use super::{acquire::acquire_refresh_lock, model::RefreshLockRequest};
use crate::{cache_file::CacheFileError, test_support::temp_dir};
use serde_json::json;
use std::{
    fs,
    path::{Path, PathBuf},
};

const NETWORK: &str = "ic";
const STALE_AFTER_SECONDS: u64 = 60;

#[test]
fn corrupted_fresh_refresh_lock_requires_manual_cleanup() {
    let fixture = LockFixture::new("ic-query-corrupted-fresh-refresh-lock");
    fixture.write_lock(r#"{"schema_version":1,"started_at_unix_ms":"60"}"#);

    let err = acquire_refresh_lock(fixture.request(60)).expect_err("corrupted lock is rejected");

    assert_parse_refresh_lock_error(err, &fixture.lock_path);
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

#[test]
fn corrupted_stale_refresh_lock_requires_manual_cleanup() {
    let fixture = LockFixture::new("ic-query-corrupted-stale-refresh-lock");
    fixture
        .write_lock(r#"{"schema_version":1,"network":"ic","pid":999999,"started_at_unix_ms":1,"#);

    let err = acquire_refresh_lock(fixture.request(120)).expect_err("corrupted lock is rejected");

    assert_parse_refresh_lock_error(err, &fixture.lock_path);
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

#[test]
fn refresh_lock_rejects_unknown_fields() {
    let fixture = LockFixture::new("ic-query-refresh-lock-unknown-field");
    let mut lock = fixture.valid_lock_value(100_000);
    lock["unexpected"] = json!(true);
    fixture.write_lock_value(&lock);

    let err = acquire_refresh_lock(fixture.request(120)).expect_err("unknown field is rejected");

    assert_parse_refresh_lock_error(err, &fixture.lock_path);
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

#[test]
fn refresh_lock_rejects_mismatched_identity() {
    let fixture = LockFixture::new("ic-query-refresh-lock-wrong-network");
    let mut lock = fixture.valid_lock_value(100_000);
    lock["network"] = json!("local");
    fixture.write_lock_value(&lock);

    let err = acquire_refresh_lock(fixture.request(120)).expect_err("wrong network is rejected");

    assert!(matches!(err, CacheFileError::InvalidRefreshLock { .. }));
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

#[test]
fn stale_valid_refresh_lock_requires_manual_cleanup() {
    let fixture = LockFixture::new("ic-query-stale-valid-refresh-lock");
    fixture.write_valid_lock(1);

    let err = acquire_refresh_lock(fixture.request(120)).expect_err("stale lock is rejected");

    assert!(matches!(err, CacheFileError::StaleRefreshLock { .. }));
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

#[test]
fn active_valid_refresh_lock_is_rejected() {
    let fixture = LockFixture::new("ic-query-active-valid-refresh-lock");
    fixture.write_valid_lock(100_000);

    let err = acquire_refresh_lock(fixture.request(120)).expect_err("active lock is rejected");

    match err {
        CacheFileError::RefreshAlreadyInProgress {
            path,
            started_at_unix_ms,
        } => {
            assert_eq!(path, fixture.lock_path);
            assert_eq!(started_at_unix_ms, 100_000);
        }
        other => panic!("unexpected error: {other:?}"),
    }
    assert!(fixture.lock_path.exists());
    fixture.cleanup();
}

fn assert_parse_refresh_lock_error(err: CacheFileError, lock_path: &Path) {
    let message = err.to_string();
    match err {
        CacheFileError::ParseRefreshLock { path, .. } => assert_eq!(path, lock_path),
        other => panic!("unexpected error: {other:?}"),
    }
    assert!(message.contains("remove the lock manually"));
}

struct LockFixture {
    root: PathBuf,
    lock_path: PathBuf,
    target_path: PathBuf,
}

impl LockFixture {
    fn new(prefix: &str) -> Self {
        let root = temp_dir(prefix);
        let target_path = root.join(".icq").join("test").join("full.json");
        let lock_path = target_path.with_file_name("full.refresh.lock");
        fs::create_dir_all(lock_path.parent().expect("lock parent")).expect("create lock parent");
        Self {
            root,
            lock_path,
            target_path,
        }
    }

    fn request(&self, now_unix_secs: u64) -> RefreshLockRequest<'_> {
        RefreshLockRequest {
            lock_path: &self.lock_path,
            target_path: &self.target_path,
            network: NETWORK,
            now_unix_secs,
            lock_stale_after_seconds: STALE_AFTER_SECONDS,
        }
    }

    fn write_lock(&self, contents: &str) {
        fs::write(&self.lock_path, contents).expect("write lock");
    }

    fn write_valid_lock(&self, started_at_unix_ms: u64) {
        self.write_lock_value(&self.valid_lock_value(started_at_unix_ms));
    }

    fn valid_lock_value(&self, started_at_unix_ms: u64) -> serde_json::Value {
        json!({
            "schema_version": 1,
            "network": NETWORK,
            "pid": 999_999,
            "started_at_unix_ms": started_at_unix_ms,
            "target_path": self.target_path.display().to_string(),
        })
    }

    fn write_lock_value(&self, value: &serde_json::Value) {
        fs::write(
            &self.lock_path,
            serde_json::to_vec_pretty(value).expect("serialize lock"),
        )
        .expect("write lock");
    }

    fn cleanup(self) {
        let _ = fs::remove_dir_all(self.root);
    }
}