use super::*;
use crate::path::secret::map::Entry;
use std::{
net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6},
sync::Arc,
time::SystemTime,
};
fn roundtrip(peers: &[SocketAddr]) -> (SystemTime, Vec<SocketAddr>) {
let entries: Vec<Arc<Entry>> = peers.iter().map(|peer| Entry::fake(*peer, None)).collect();
roundtrip_with(&entries, Epoch(0), |s| s)
}
fn roundtrip_with(
entries: &[Arc<Entry>],
current_epoch: Epoch,
configure: impl FnOnce(SerializerBuilder) -> SerializerBuilder,
) -> (SystemTime, Vec<SocketAddr>) {
let weak: Vec<Weak<Entry>> = entries.iter().map(Arc::downgrade).collect();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets");
let serializer = configure(Serializer::builder(&path)).build().unwrap();
serializer.serialize(&weak, current_epoch).unwrap();
let entries = deserialize(serializer.path()).unwrap();
let started_at = entries.started_at;
let decoded = entries.map(|e| e.unwrap().peer).collect();
(started_at, decoded)
}
#[test]
fn roundtrip_ipv4() {
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 0, 2, 1), 4433));
let (_, decoded) = roundtrip(&[peer]);
assert_eq!(decoded, vec![peer]);
}
#[test]
fn roundtrip_ipv6_minimal() {
let peer = SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 4433, 0, 0));
let (_, decoded) = roundtrip(&[peer]);
assert_eq!(decoded, vec![peer]);
}
#[test]
fn roundtrip_ipv6_full() {
let peer = SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1),
4433,
7,
42,
));
let (_, decoded) = roundtrip(&[peer]);
assert_eq!(decoded, vec![peer]);
}
#[test]
fn roundtrip_multiple() {
let peers = vec![
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1)),
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 2, 0, 0)),
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 3, 1, 2)),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 4), 4)),
];
let (_, decoded) = roundtrip(&peers);
assert_eq!(decoded, peers);
}
#[test]
fn roundtrip_empty() {
let (_, decoded) = roundtrip(&[]);
assert!(decoded.is_empty());
}
#[test]
fn started_at_is_recent() {
let before = SystemTime::now();
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 4433));
let (started_at, _) = roundtrip(&[peer]);
let after = SystemTime::now();
let one_sec = Duration::from_secs(1);
assert!(started_at + one_sec >= before);
assert!(started_at <= after + one_sec);
}
const EPOCH: Duration = Duration::from_secs(60);
#[test]
fn max_idle_filters_stale_entries() {
let recent_peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1));
let stale_peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 2), 2));
let recent = Entry::fake(recent_peer, None);
let stale = Entry::fake(stale_peer, None);
recent.set_accessed_addr(Epoch(10));
stale.set_accessed_addr(Epoch(4));
let (_, decoded) = roundtrip_with(&[recent, stale], Epoch(12), |s| s.with_max_idle(5 * EPOCH));
assert_eq!(decoded, vec![recent_peer]);
}
#[test]
fn max_idle_boundary_is_inclusive() {
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1));
let entry = Entry::fake(peer, None);
entry.set_accessed_addr(Epoch(7));
let (_, decoded) = roundtrip_with(&[entry], Epoch(10), |s| s.with_max_idle(3 * EPOCH));
assert_eq!(decoded, vec![peer]);
}
#[test]
fn max_idle_drops_never_accessed_entries() {
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1));
let entry = Entry::fake(peer, None);
let (_, decoded) = roundtrip_with(&[entry], Epoch(5), |s| s.with_max_idle(EPOCH));
assert!(decoded.is_empty());
}
#[test]
fn idle_window_wider_than_epoch_keeps_everything() {
let peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1));
let entry = Entry::fake(peer, None);
entry.set_accessed_addr(Epoch(2));
let (_, decoded) = roundtrip_with(&[entry], Epoch(3), |s| s.with_max_idle(100 * EPOCH));
assert_eq!(decoded, vec![peer]);
}
#[test]
fn with_max_idle_rounds_to_nearest_epoch() {
let kept_peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 1));
let dropped_peer = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 2), 2));
let kept = Entry::fake(kept_peer, None);
let dropped = Entry::fake(dropped_peer, None);
kept.set_accessed_addr(Epoch(3));
dropped.set_accessed_addr(Epoch(2));
let (_, decoded) = roundtrip_with(&[kept, dropped], Epoch(5), |s| {
s.with_max_idle(EPOCH + EPOCH / 2)
});
assert_eq!(decoded, vec![kept_peer]);
}
#[test]
fn max_size_stops_adding_entries() {
let peers: Vec<SocketAddr> = (0..100)
.map(|i| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, i as u8), i)))
.collect();
let entries: Vec<Arc<Entry>> = peers.iter().map(|peer| Entry::fake(*peer, None)).collect();
let weak: Vec<Weak<Entry>> = entries.iter().map(Arc::downgrade).collect();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets");
let serializer = Serializer::builder(&path).build().unwrap();
let prefix = (HEADER.len() + VERSION.len() + 8) as u64;
serializer
.serialize_with_max_size(&weak, Epoch(0), prefix + 20)
.unwrap();
let decoded: Vec<SocketAddr> = deserialize(serializer.path())
.unwrap()
.map(|e| e.unwrap().peer)
.collect();
assert!(!decoded.is_empty());
assert!(decoded.len() < peers.len());
assert_eq!(decoded, peers[..decoded.len()]);
}
#[test]
fn build_rejects_missing_directory() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("missing").join("secrets");
let err = match Serializer::builder(&path).build() {
Ok(_) => panic!("expected build to reject a missing destination directory"),
Err(err) => err,
};
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn build_accepts_existing_directory() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets");
assert!(Serializer::builder(&path).build().is_ok());
}
#[test]
fn rejects_bad_header() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets");
std::fs::write(&path, b"not the right header at all").unwrap();
let err = match deserialize(&path) {
Ok(_) => panic!("expected deserialize to reject a bad header"),
Err(err) => err,
};
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn rejects_out_of_range_timestamp() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secrets");
let mut bytes = Vec::new();
bytes.extend_from_slice(HEADER.as_bytes());
bytes.extend_from_slice(VERSION);
bytes.extend_from_slice(&u64::MAX.to_le_bytes());
std::fs::write(&path, &bytes).unwrap();
let err = match deserialize(&path) {
Ok(_) => panic!("expected deserialize to reject an out-of-range timestamp"),
Err(err) => err,
};
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}