190 lines
6.2 KiB
Rust
190 lines
6.2 KiB
Rust
|
|
//! Local support-state contracts.
|
||
|
|
//!
|
||
|
|
//! These tests cover state that is not itself gossip: bounded local caches and local peer-
|
||
|
|
//! authorization policy.
|
||
|
|
//!
|
||
|
|
//! Behavioral/correctness guarantees:
|
||
|
|
//! - Local support state is bounded, deterministic, and explicitly invalidated.
|
||
|
|
//! - Cached actor locations never outlive invalidation by actor or host node.
|
||
|
|
//! - Peer authorization policy is deterministic across memory and disk.
|
||
|
|
|
||
|
|
mod location_cache {
|
||
|
|
//! Bounded ActorAddress-to-NodeId cache behavior: hit/miss, LRU refresh, updates, and explicit
|
||
|
|
//! invalidation.
|
||
|
|
|
||
|
|
use distribution::cache::LocationCache;
|
||
|
|
use distribution::types::NodeId;
|
||
|
|
use swactor::actor::ActorAddress;
|
||
|
|
|
||
|
|
fn node(byte: u8) -> NodeId {
|
||
|
|
NodeId([byte; 32])
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn insert_and_get() {
|
||
|
|
let mut cache = LocationCache::new(10);
|
||
|
|
let actor = ActorAddress::new_random();
|
||
|
|
cache.insert(actor, node(1));
|
||
|
|
assert_eq!(cache.get(&actor), Some(node(1)));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn get_missing_returns_none() {
|
||
|
|
let mut cache = LocationCache::new(10);
|
||
|
|
let actor = ActorAddress::new_random();
|
||
|
|
assert_eq!(cache.get(&actor), None);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn invalidate_removes_entry() {
|
||
|
|
let mut cache = LocationCache::new(10);
|
||
|
|
let actor = ActorAddress::new_random();
|
||
|
|
cache.insert(actor, node(1));
|
||
|
|
assert!(cache.invalidate(&actor));
|
||
|
|
assert_eq!(cache.get(&actor), None);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn capacity_evicts_lru() {
|
||
|
|
let mut cache = LocationCache::new(2);
|
||
|
|
let a1 = ActorAddress::new_random();
|
||
|
|
let a2 = ActorAddress::new_random();
|
||
|
|
let a3 = ActorAddress::new_random();
|
||
|
|
|
||
|
|
cache.insert(a1, node(1));
|
||
|
|
cache.insert(a2, node(2));
|
||
|
|
// a1 is LRU, inserting a3 should evict it
|
||
|
|
cache.insert(a3, node(3));
|
||
|
|
|
||
|
|
assert_eq!(cache.len(), 2);
|
||
|
|
assert_eq!(cache.get(&a1), None, "a1 should have been evicted");
|
||
|
|
assert_eq!(cache.get(&a2), Some(node(2)));
|
||
|
|
assert_eq!(cache.get(&a3), Some(node(3)));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn get_refreshes_lru_order() {
|
||
|
|
let mut cache = LocationCache::new(2);
|
||
|
|
let a1 = ActorAddress::new_random();
|
||
|
|
let a2 = ActorAddress::new_random();
|
||
|
|
let a3 = ActorAddress::new_random();
|
||
|
|
|
||
|
|
cache.insert(a1, node(1));
|
||
|
|
cache.insert(a2, node(2));
|
||
|
|
// Touch a1 — now a2 is LRU
|
||
|
|
cache.get(&a1);
|
||
|
|
// Insert a3 — should evict a2 (LRU), not a1
|
||
|
|
cache.insert(a3, node(3));
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
cache.get(&a1),
|
||
|
|
Some(node(1)),
|
||
|
|
"a1 should survive (recently accessed)"
|
||
|
|
);
|
||
|
|
assert_eq!(cache.get(&a2), None, "a2 should have been evicted");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn invalidate_node_removes_all_entries_for_that_node() {
|
||
|
|
let mut cache = LocationCache::new(10);
|
||
|
|
let a1 = ActorAddress::new_random();
|
||
|
|
let a2 = ActorAddress::new_random();
|
||
|
|
let a3 = ActorAddress::new_random();
|
||
|
|
|
||
|
|
cache.insert(a1, node(1));
|
||
|
|
cache.insert(a2, node(1)); // same node
|
||
|
|
cache.insert(a3, node(2)); // different node
|
||
|
|
|
||
|
|
let removed = cache.invalidate_node(&node(1));
|
||
|
|
assert_eq!(removed, 2);
|
||
|
|
assert_eq!(cache.len(), 1);
|
||
|
|
assert_eq!(cache.peek(&a3), Some(node(2)));
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn update_existing_entry() {
|
||
|
|
let mut cache = LocationCache::new(10);
|
||
|
|
let actor = ActorAddress::new_random();
|
||
|
|
cache.insert(actor, node(1));
|
||
|
|
cache.insert(actor, node(2));
|
||
|
|
assert_eq!(cache.get(&actor), Some(node(2)));
|
||
|
|
assert_eq!(cache.len(), 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
mod peer_authorization {
|
||
|
|
//! Peer allow-list behavior: open mode, restrictive file-backed mode, persistence, and
|
||
|
|
//! revocation.
|
||
|
|
|
||
|
|
use distribution::peer_auth::PeerAllowList;
|
||
|
|
use distribution::types::NodeId;
|
||
|
|
use std::fs;
|
||
|
|
|
||
|
|
fn id(byte: u8) -> NodeId {
|
||
|
|
NodeId([byte; 32])
|
||
|
|
}
|
||
|
|
|
||
|
|
fn temp_peer_file(name: &str) -> std::path::PathBuf {
|
||
|
|
let mut path = std::env::temp_dir();
|
||
|
|
path.push(format!(
|
||
|
|
"swactor-dist-peer-auth-{name}-{}-{}.json",
|
||
|
|
std::process::id(),
|
||
|
|
std::thread::current().name().unwrap_or("test")
|
||
|
|
));
|
||
|
|
let _ = fs::remove_file(&path);
|
||
|
|
path
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn open_mode_allows_every_peer_without_persisted_entries() {
|
||
|
|
// Correctness: open mode is an explicit policy state, not an empty restrictive
|
||
|
|
// allow-list. It accepts unknown peers and has no listed peer entries to save.
|
||
|
|
let allow = PeerAllowList::open();
|
||
|
|
|
||
|
|
assert!(allow.is_open());
|
||
|
|
assert!(allow.is_allowed(&id(1)));
|
||
|
|
assert!(allow.is_allowed(&id(2)));
|
||
|
|
assert!(allow.list_peers().is_empty());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn file_backed_allow_list_is_restrictive_and_round_trips() {
|
||
|
|
// Correctness: a missing file creates restrictive mode; only added peers are
|
||
|
|
// allowed, and saving/reloading preserves the exact authorization policy.
|
||
|
|
let path = temp_peer_file("round-trip");
|
||
|
|
let trusted = id(7);
|
||
|
|
let other = id(8);
|
||
|
|
|
||
|
|
let mut allow = PeerAllowList::from_file(&path).unwrap();
|
||
|
|
assert!(!allow.is_open());
|
||
|
|
assert!(!allow.is_allowed(&trusted));
|
||
|
|
|
||
|
|
allow.add_peer(trusted, "trusted-peer".into());
|
||
|
|
assert!(allow.is_allowed(&trusted));
|
||
|
|
assert!(!allow.is_allowed(&other));
|
||
|
|
allow.save().unwrap();
|
||
|
|
|
||
|
|
let reloaded = PeerAllowList::from_file(&path).unwrap();
|
||
|
|
assert!(reloaded.is_allowed(&trusted));
|
||
|
|
assert!(!reloaded.is_allowed(&other));
|
||
|
|
assert_eq!(reloaded.list_peers()[0].label, "trusted-peer");
|
||
|
|
|
||
|
|
let _ = fs::remove_file(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn removing_a_peer_revokes_authorization() {
|
||
|
|
// Correctness: authorization is explicit and reversible; removing a peer from a
|
||
|
|
// restrictive allow-list immediately revokes it without changing open-mode policy.
|
||
|
|
let mut allow = PeerAllowList::from_file(&temp_peer_file("remove")).unwrap();
|
||
|
|
let trusted = id(9);
|
||
|
|
|
||
|
|
allow.add_peer(trusted, "temporary".into());
|
||
|
|
assert!(allow.is_allowed(&trusted));
|
||
|
|
|
||
|
|
allow.remove_peer(&trusted);
|
||
|
|
assert!(!allow.is_allowed(&trusted));
|
||
|
|
assert!(allow.list_peers().is_empty());
|
||
|
|
}
|
||
|
|
}
|