mod common; use std::collections::VecDeque; use std::net::{Ipv4Addr, Ipv6Addr}; use std::sync::Arc; use common::test_helper::{ as_route_origin, as_v4_route_origin, indent_block, payloads_to_string, serial_result_detail_to_string, snapshot_hashes_to_string, test_report, v4_origin, v6_origin, }; use rpki::rtr::cache::{Delta, RtrCacheBuilder, SerialResult, Snapshot}; use rpki::rtr::payload::{Payload, Timing}; use rpki::rtr::store_db::RtrStore; fn delta_to_string(delta: &Delta) -> String { format!( "serial: {}\nannounced:\n{}withdrawn:\n{}", delta.serial(), indent_block(&payloads_to_string(delta.announced()), 2), indent_block(&payloads_to_string(delta.withdrawn()), 2), ) } fn deltas_window_to_string(deltas: &VecDeque>) -> String { if deltas.is_empty() { return " \n".to_string(); } let mut out = String::new(); for (idx, delta) in deltas.iter().enumerate() { out.push_str(&format!("delta[{}]:\n", idx)); out.push_str(&indent_block(&delta_to_string(delta), 2)); } out } fn get_deltas_since_input_to_string( cache_session_id: u16, cache_serial: u32, client_session: u16, client_serial: u32, ) -> String { format!( "cache.session_id: {}\ncache.serial: {}\nclient_session: {}\nclient_serial: {}\n", cache_session_id, cache_serial, client_session, client_serial ) } fn snapshot_hashes_and_sorted_view_to_string(snapshot: &Snapshot) -> String { let payloads = snapshot.payloads_for_rtr(); format!( "hashes:\n{}sorted payloads_for_rtr:\n{}", indent_block(&snapshot_hashes_to_string(snapshot), 2), indent_block(&payloads_to_string(&payloads), 2), ) } #[test] fn snapshot_hash_is_stable_for_same_content_with_different_input_order() { let a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let c = v6_origin( Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32, 48, 64498, ); let s1_input = vec![ Payload::RouteOrigin(a.clone()), Payload::RouteOrigin(b.clone()), Payload::RouteOrigin(c.clone()), ]; let s2_input = vec![ Payload::RouteOrigin(c), Payload::RouteOrigin(a), Payload::RouteOrigin(b), ]; let s1 = Snapshot::from_payloads(s1_input.clone()); let s2 = Snapshot::from_payloads(s2_input.clone()); let input = format!( "s1 原始输入 payloads:\n{}\ns2 原始输入 payloads:\n{}", indent_block(&payloads_to_string(&s1_input), 2), indent_block(&payloads_to_string(&s2_input), 2), ); let output = format!( "s1:\n{}\ns2:\n{}\n结论:\n same_content: {}\n same_origins: {}\n snapshot_hash 相同: {}\n origins_hash 相同: {}\n", indent_block(&snapshot_hashes_and_sorted_view_to_string(&s1), 2), indent_block(&snapshot_hashes_and_sorted_view_to_string(&s2), 2), s1.same_content(&s2), s1.same_origins(&s2), s1.snapshot_hash() == s2.snapshot_hash(), s1.origins_hash() == s2.origins_hash(), ); test_report( "snapshot_hash_is_stable_for_same_content_with_different_input_order", "验证相同语义内容即使原始输入顺序不同,Snapshot 的 hash 仍然稳定一致。", &input, &output, ); assert!(s1.same_content(&s2)); assert!(s1.same_origins(&s2)); assert_eq!(s1.snapshot_hash(), s2.snapshot_hash()); assert_eq!(s1.origins_hash(), s2.origins_hash()); } #[test] fn snapshot_diff_reports_announced_and_withdrawn_correctly() { let old_a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let old_b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let new_c = v6_origin( Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 0), 48, 48, 64499, ); let old_input = vec![ Payload::RouteOrigin(old_a.clone()), Payload::RouteOrigin(old_b.clone()), ]; let new_input = vec![ Payload::RouteOrigin(old_b), Payload::RouteOrigin(new_c.clone()), ]; let old_snapshot = Snapshot::from_payloads(old_input.clone()); let new_snapshot = Snapshot::from_payloads(new_input.clone()); let (announced, withdrawn) = old_snapshot.diff(&new_snapshot); let input = format!( "old_snapshot 原始输入:\n{}\nnew_snapshot 原始输入:\n{}", indent_block(&payloads_to_string(&old_input), 2), indent_block(&payloads_to_string(&new_input), 2), ); let output = format!( "announced:\n{}withdrawn:\n{}", indent_block(&payloads_to_string(&announced), 2), indent_block(&payloads_to_string(&withdrawn), 2), ); test_report( "snapshot_diff_reports_announced_and_withdrawn_correctly", "验证 diff() 能正确找出 announced 和 withdrawn 的 payload。", &input, &output, ); assert_eq!(announced.len(), 1); assert_eq!(withdrawn.len(), 1); match &announced[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &new_c), _ => panic!("expected announced RouteOrigin"), } match &withdrawn[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &old_a), _ => panic!("expected withdrawn RouteOrigin"), } } #[test] fn snapshot_payloads_for_rtr_sorts_ipv4_before_ipv6_and_ipv4_announcements_descending() { let v4_low = v4_origin(192, 0, 2, 0, 24, 24, 64496); let v4_high = v4_origin(198, 51, 100, 0, 24, 24, 64497); let v6 = v6_origin( Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0), 32, 48, 64498, ); let input_payloads = vec![ Payload::RouteOrigin(v6.clone()), Payload::RouteOrigin(v4_low.clone()), Payload::RouteOrigin(v4_high.clone()), ]; let snapshot = Snapshot::from_payloads(input_payloads.clone()); let output_payloads = snapshot.payloads_for_rtr(); let input = format!( "原始输入 payloads(构造 Snapshot 前):\n{}", indent_block(&payloads_to_string(&input_payloads), 2), ); let output = format!( "排序后 payloads_for_rtr:\n{}", indent_block(&payloads_to_string(&output_payloads), 2), ); test_report( "snapshot_payloads_for_rtr_sorts_ipv4_before_ipv6_and_ipv4_announcements_descending", "验证 Snapshot::payloads_for_rtr() 会按 RTR 规则排序:IPv4 在 IPv6 前,且 IPv4 announcement 按地址降序。", &input, &output, ); assert_eq!(output_payloads.len(), 3); let first = as_v4_route_origin(&output_payloads[0]); let second = as_v4_route_origin(&output_payloads[1]); assert_eq!( first.prefix().address.to_ipv4(), Some(Ipv4Addr::new(198, 51, 100, 0)) ); assert_eq!( second.prefix().address.to_ipv4(), Some(Ipv4Addr::new(192, 0, 2, 0)) ); let third = as_route_origin(&output_payloads[2]); assert!(third.prefix().address.is_ipv6()); assert_eq!( third.prefix().address.to_ipv6(), Some(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)) ); } #[test] fn delta_new_sorts_announced_descending_and_withdrawn_ascending() { let announced_low = v4_origin(192, 0, 2, 0, 24, 24, 64496); let announced_high = v4_origin(198, 51, 100, 0, 24, 24, 64497); let withdrawn_high = v4_origin(203, 0, 113, 0, 24, 24, 64501); let withdrawn_low = v4_origin(10, 0, 0, 0, 24, 24, 64500); let input_announced = vec![ Payload::RouteOrigin(announced_low), Payload::RouteOrigin(announced_high), ]; let input_withdrawn = vec![ Payload::RouteOrigin(withdrawn_high), Payload::RouteOrigin(withdrawn_low), ]; let delta = Delta::new(101, input_announced.clone(), input_withdrawn.clone()); let input = format!( "announced(构造前):\n{}withdrawn(构造前):\n{}", indent_block(&payloads_to_string(&input_announced), 2), indent_block(&payloads_to_string(&input_withdrawn), 2), ); let output = indent_block(&delta_to_string(&delta), 2); test_report( "delta_new_sorts_announced_descending_and_withdrawn_ascending", "验证 Delta::new() 会自动排序:announced 按 RTR announcement 规则,withdrawn 按 RTR withdrawal 规则。", &input, &output, ); assert_eq!(delta.serial(), 101); assert_eq!(delta.announced().len(), 2); assert_eq!(delta.withdrawn().len(), 2); let a0 = as_v4_route_origin(&delta.announced()[0]); let a1 = as_v4_route_origin(&delta.announced()[1]); assert_eq!( a0.prefix().address.to_ipv4(), Some(Ipv4Addr::new(198, 51, 100, 0)) ); assert_eq!( a1.prefix().address.to_ipv4(), Some(Ipv4Addr::new(192, 0, 2, 0)) ); let w0 = as_v4_route_origin(&delta.withdrawn()[0]); let w1 = as_v4_route_origin(&delta.withdrawn()[1]); assert_eq!(w0.prefix().address.to_ipv4(), Some(Ipv4Addr::new(10, 0, 0, 0))); assert_eq!( w1.prefix().address.to_ipv4(), Some(Ipv4Addr::new(203, 0, 113, 0)) ); } #[test] fn get_deltas_since_returns_up_to_date_when_client_serial_matches_current() { let cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .build(); let result = cache.get_deltas_since(42, 100); let input = get_deltas_since_input_to_string(cache.session_id(), cache.serial(), 42, 100); let output = serial_result_detail_to_string(&result); test_report( "get_deltas_since_returns_up_to_date_when_client_serial_matches_current", "验证当客户端 serial 与缓存当前 serial 相同,返回 UpToDate。", &input, &output, ); match result { SerialResult::UpToDate => {} _ => panic!("expected UpToDate"), } } #[test] fn get_deltas_since_returns_reset_required_on_session_mismatch() { let cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .build(); let result = cache.get_deltas_since(999, 100); let input = get_deltas_since_input_to_string(cache.session_id(), cache.serial(), 999, 100); let output = serial_result_detail_to_string(&result); test_report( "get_deltas_since_returns_reset_required_on_session_mismatch", "验证当客户端 session_id 与缓存 session_id 不一致时,返回 ResetRequired。", &input, &output, ); match result { SerialResult::ResetRequired => {} _ => panic!("expected ResetRequired"), } } #[test] fn get_deltas_since_returns_reset_required_when_client_serial_is_too_old() { let d1 = Arc::new(Delta::new( 101, vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))], vec![], )); let d2 = Arc::new(Delta::new( 102, vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))], vec![], )); let mut deltas = VecDeque::new(); deltas.push_back(d1); deltas.push_back(d2); let cache = RtrCacheBuilder::new() .session_id(42) .serial(102) .timing(Timing::default()) .deltas(deltas.clone()) .build(); let result = cache.get_deltas_since(42, 99); let input = format!( "{}delta_window:\n{}", get_deltas_since_input_to_string(cache.session_id(), cache.serial(), 42, 99), indent_block(&deltas_window_to_string(&deltas), 2), ); let output = serial_result_detail_to_string(&result); test_report( "get_deltas_since_returns_reset_required_when_client_serial_is_too_old", "验证当客户端 serial 太旧,已超出 delta window 覆盖范围时,返回 ResetRequired。", &input, &output, ); match result { SerialResult::ResetRequired => {} _ => panic!("expected ResetRequired"), } } #[test] fn get_deltas_since_returns_applicable_deltas_in_order() { let d1 = Arc::new(Delta::new( 101, vec![Payload::RouteOrigin(v4_origin(192, 0, 2, 0, 24, 24, 64496))], vec![], )); let d2 = Arc::new(Delta::new( 102, vec![Payload::RouteOrigin(v4_origin(198, 51, 100, 0, 24, 24, 64497))], vec![], )); let d3 = Arc::new(Delta::new( 103, vec![Payload::RouteOrigin(v4_origin(203, 0, 113, 0, 24, 24, 64498))], vec![], )); let mut deltas = VecDeque::new(); deltas.push_back(d1); deltas.push_back(d2); deltas.push_back(d3); let cache = RtrCacheBuilder::new() .session_id(42) .serial(103) .timing(Timing::default()) .deltas(deltas.clone()) .build(); let result = cache.get_deltas_since(42, 101); let input = format!( "{}delta_window:\n{}", get_deltas_since_input_to_string(cache.session_id(), cache.serial(), 42, 101), indent_block(&deltas_window_to_string(&deltas), 2), ); let output = serial_result_detail_to_string(&result); test_report( "get_deltas_since_returns_applicable_deltas_in_order", "验证当客户端 serial 在 delta window 内时,返回正确且有序的 deltas。", &input, &output, ); match result { SerialResult::Deltas(result) => { assert_eq!(result.len(), 2); assert_eq!(result[0].serial(), 102); assert_eq!(result[1].serial(), 103); } _ => panic!("expected Deltas"), } } #[test] fn get_deltas_since_returns_reset_required_when_client_serial_is_in_future() { let cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .build(); let result = cache.get_deltas_since(42, 101); let input = get_deltas_since_input_to_string(cache.session_id(), cache.serial(), 42, 101); let output = serial_result_detail_to_string(&result); test_report( "get_deltas_since_returns_reset_required_when_client_serial_is_in_future", "验证当客户端 serial 比缓存当前 serial 还大时,返回 ResetRequired。", &input, &output, ); match result { SerialResult::ResetRequired => {} _ => panic!("expected ResetRequired"), } } #[tokio::test] async fn update_no_change_keeps_serial_and_produces_no_delta() { let old_a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let old_b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let old_input = vec![ Payload::RouteOrigin(old_a.clone()), Payload::RouteOrigin(old_b.clone()), ]; let snapshot = Snapshot::from_payloads(old_input.clone()); let mut cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .snapshot(snapshot.clone()) .build(); let dir = tempfile::tempdir().unwrap(); let store = RtrStore::open(dir.path()).unwrap(); let new_payloads = vec![ Payload::RouteOrigin(old_b), Payload::RouteOrigin(old_a), ]; cache.update(new_payloads.clone(), &store).unwrap(); let current_snapshot = cache.snapshot(); let result = cache.get_deltas_since(42, 100); let input = format!( "old_snapshot 原始输入:\n{}new_payloads 原始输入:\n{}", indent_block(&payloads_to_string(&old_input), 2), indent_block(&payloads_to_string(&new_payloads), 2), ); let output = format!( "cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(42, 100):\n{}", cache.serial(), indent_block(&snapshot_hashes_and_sorted_view_to_string(¤t_snapshot), 2), indent_block(&serial_result_detail_to_string(&result), 2), ); test_report( "update_no_change_keeps_serial_and_produces_no_delta", "验证 update() 在新旧内容完全相同时:serial 不变、snapshot 不变、不会产生新的 delta。", &input, &output, ); assert_eq!(cache.serial(), 100); assert!(cache.snapshot().same_content(&snapshot)); match result { SerialResult::UpToDate => {} _ => panic!("expected UpToDate"), } } #[tokio::test] async fn update_add_only_increments_serial_and_generates_announced_delta() { let old_a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let new_b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let old_input = vec![Payload::RouteOrigin(old_a.clone())]; let old_snapshot = Snapshot::from_payloads(old_input.clone()); let mut cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .snapshot(old_snapshot.clone()) .build(); let dir = tempfile::tempdir().unwrap(); let store = RtrStore::open(dir.path()).unwrap(); let new_payloads = vec![ Payload::RouteOrigin(old_a.clone()), Payload::RouteOrigin(new_b.clone()), ]; cache.update(new_payloads.clone(), &store).unwrap(); let current_snapshot = cache.snapshot(); let result = cache.get_deltas_since(42, 100); let input = format!( "old_snapshot 原始输入:\n{}new_payloads 原始输入:\n{}", indent_block(&payloads_to_string(&old_input), 2), indent_block(&payloads_to_string(&new_payloads), 2), ); let output = format!( "cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(42, 100):\n{}", cache.serial(), indent_block(&snapshot_hashes_and_sorted_view_to_string(¤t_snapshot), 2), indent_block(&serial_result_detail_to_string(&result), 2), ); test_report( "update_add_only_increments_serial_and_generates_announced_delta", "验证 update() 在只新增 payload 时:serial + 1,delta 中只有 announced,withdrawn 为空。", &input, &output, ); assert_eq!(cache.serial(), 101); match result { SerialResult::Deltas(deltas) => { assert_eq!(deltas.len(), 1); let delta = &deltas[0]; assert_eq!(delta.serial(), 101); assert_eq!(delta.announced().len(), 1); assert_eq!(delta.withdrawn().len(), 0); match &delta.announced()[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &new_b), _ => panic!("expected announced RouteOrigin"), } } _ => panic!("expected Deltas"), } } #[tokio::test] async fn update_remove_only_increments_serial_and_generates_withdrawn_delta() { let old_a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let old_b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let old_input = vec![ Payload::RouteOrigin(old_a.clone()), Payload::RouteOrigin(old_b.clone()), ]; let old_snapshot = Snapshot::from_payloads(old_input.clone()); let mut cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .snapshot(old_snapshot.clone()) .build(); let dir = tempfile::tempdir().unwrap(); let store = RtrStore::open(dir.path()).unwrap(); let new_payloads = vec![Payload::RouteOrigin(old_b.clone())]; cache.update(new_payloads.clone(), &store).unwrap(); let current_snapshot = cache.snapshot(); let result = cache.get_deltas_since(42, 100); let input = format!( "old_snapshot 原始输入:\n{}new_payloads 原始输入:\n{}", indent_block(&payloads_to_string(&old_input), 2), indent_block(&payloads_to_string(&new_payloads), 2), ); let output = format!( "cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(42, 100):\n{}", cache.serial(), indent_block(&snapshot_hashes_and_sorted_view_to_string(¤t_snapshot), 2), indent_block(&serial_result_detail_to_string(&result), 2), ); test_report( "update_remove_only_increments_serial_and_generates_withdrawn_delta", "验证 update() 在只删除 payload 时:serial + 1,delta 中只有 withdrawn,announced 为空。", &input, &output, ); assert_eq!(cache.serial(), 101); match result { SerialResult::Deltas(deltas) => { assert_eq!(deltas.len(), 1); let delta = &deltas[0]; assert_eq!(delta.serial(), 101); assert_eq!(delta.announced().len(), 0); assert_eq!(delta.withdrawn().len(), 1); match &delta.withdrawn()[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &old_a), _ => panic!("expected withdrawn RouteOrigin"), } } _ => panic!("expected Deltas"), } } #[tokio::test] async fn update_add_and_remove_increments_serial_and_generates_both_sides() { let old_a = v4_origin(192, 0, 2, 0, 24, 24, 64496); let old_b = v4_origin(198, 51, 100, 0, 24, 24, 64497); let new_c = v6_origin( Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 0), 48, 48, 64499, ); let old_input = vec![ Payload::RouteOrigin(old_a.clone()), Payload::RouteOrigin(old_b.clone()), ]; let old_snapshot = Snapshot::from_payloads(old_input.clone()); let mut cache = RtrCacheBuilder::new() .session_id(42) .serial(100) .timing(Timing::default()) .snapshot(old_snapshot.clone()) .build(); let dir = tempfile::tempdir().unwrap(); let store = RtrStore::open(dir.path()).unwrap(); let new_payloads = vec![ Payload::RouteOrigin(old_b.clone()), Payload::RouteOrigin(new_c.clone()), ]; cache.update(new_payloads.clone(), &store).unwrap(); let current_snapshot = cache.snapshot(); let result = cache.get_deltas_since(42, 100); let input = format!( "old_snapshot 原始输入:\n{}new_payloads 原始输入:\n{}", indent_block(&payloads_to_string(&old_input), 2), indent_block(&payloads_to_string(&new_payloads), 2), ); let output = format!( "cache.serial_after_update: {}\ncurrent_snapshot:\n{}get_deltas_since(42, 100):\n{}", cache.serial(), indent_block(&snapshot_hashes_and_sorted_view_to_string(¤t_snapshot), 2), indent_block(&serial_result_detail_to_string(&result), 2), ); test_report( "update_add_and_remove_increments_serial_and_generates_both_sides", "验证 update() 在同时新增和删除 payload 时:serial + 1,delta 中 announced 和 withdrawn 都正确。", &input, &output, ); assert_eq!(cache.serial(), 101); match result { SerialResult::Deltas(deltas) => { assert_eq!(deltas.len(), 1); let delta = &deltas[0]; assert_eq!(delta.serial(), 101); assert_eq!(delta.announced().len(), 1); assert_eq!(delta.withdrawn().len(), 1); match &delta.announced()[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &new_c), _ => panic!("expected announced RouteOrigin"), } match &delta.withdrawn()[0] { Payload::RouteOrigin(ro) => assert_eq!(ro, &old_a), _ => panic!("expected withdrawn RouteOrigin"), } } _ => panic!("expected Deltas"), } }