rpki/src/cli/tests.rs

1934 lines
63 KiB
Rust

use super::*;
use crate::memory_telemetry::{
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
};
use crate::storage::{
RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown,
VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown,
VcirStorageEntrySummary, VcirStorageSummary,
};
#[test]
fn parse_help_returns_usage() {
let argv = vec!["rpki".to_string(), "--help".to_string()];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("Usage:"), "{err}");
assert!(err.contains("--db"), "{err}");
assert!(err.contains("--rsync-mirror-root"), "{err}");
assert!(err.contains("--rsync-scope"), "{err}");
assert!(err.contains("--parallel-phase2-object-workers"), "{err}");
assert!(err.contains("--memory-trim-after-validation"), "{err}");
assert!(err.contains("--enable-roa-validation-cache"), "{err}");
assert!(!err.contains("--parallel-phase1"), "{err}");
assert!(!err.contains("--parallel-phase2 "), "{err}");
}
#[test]
fn parse_rejects_unknown_argument() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--nope".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("unknown argument"), "{err}");
}
#[test]
fn parse_rejects_both_tal_url_and_tal_path() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(
err.contains("one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs"),
"{err}"
);
}
#[test]
fn parse_rejects_invalid_max_depth() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--max-depth".to_string(),
"nope".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("invalid --max-depth"), "{err}");
}
#[test]
fn parse_accepts_ccr_out_path() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
"--ccr-out".to_string(),
"out/example.ccr".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.ccr_out_path.as_deref(),
Some(std::path::Path::new("out/example.ccr"))
);
}
#[test]
fn parse_accepts_memory_trim_after_validation() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--memory-trim-after-validation".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.memory_trim_after_validation);
}
#[test]
fn parse_disables_memory_trim_after_validation_by_default() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(!args.memory_trim_after_validation);
}
#[test]
fn parse_accepts_enable_roa_validation_cache() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--enable-roa-validation-cache".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.enable_roa_validation_cache);
}
#[test]
fn parse_accepts_enable_transport_request_prefetch() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--enable-transport-request-prefetch".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.enable_transport_request_prefetch);
}
#[test]
fn parse_disables_roa_validation_cache_by_default() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(!args.enable_roa_validation_cache);
assert!(!args.enable_transport_request_prefetch);
}
#[test]
fn parse_accepts_analysis_out_and_implies_analyze() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--analysis-out".to_string(),
"run/analyze".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.analyze);
assert_eq!(
args.analysis_out_path.as_deref(),
Some(std::path::Path::new("run/analyze"))
);
}
#[test]
fn parse_accepts_report_json_compact_when_report_json_is_set() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--report-json".to_string(),
"out/report.json".to_string(),
"--report-json-compact".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.report_json_path.as_deref(),
Some(std::path::Path::new("out/report.json"))
);
assert!(args.report_json_compact);
}
#[test]
fn parse_accepts_rsync_scope_policy() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--rsync-scope".to_string(),
"module-root".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::ModuleRoot);
}
#[test]
fn parse_accepts_host_rsync_scope_policy() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--rsync-scope".to_string(),
"host".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::Host);
}
#[test]
fn parse_rejects_invalid_rsync_scope_policy() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--rsync-scope".to_string(),
"wide".to_string(),
];
let err = parse_args(&argv).expect_err("invalid rsync scope should fail");
assert!(err.contains("invalid --rsync-scope"), "{err}");
}
#[test]
fn parse_accepts_strict_policy_list() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--strict".to_string(),
"name,cms-der".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.strict_policy,
Some(StrictPolicy {
name: true,
cms_der: true,
signed_attrs: false,
})
);
}
#[test]
fn parse_accepts_strict_without_value_as_all() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--strict".to_string(),
"--report-json".to_string(),
"out/report.json".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(args.strict_policy, Some(StrictPolicy::all()));
}
#[test]
fn parse_rejects_unknown_strict_policy() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--strict=unknown".to_string(),
];
let err = parse_args(&argv).expect_err("unknown strict policy should fail");
assert!(err.contains("unknown strict policy"), "{err}");
}
#[test]
fn effective_cir_tal_uris_filters_skipped_multi_tal_inputs() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"afrinic.tal".to_string(),
"--ta-path".to_string(),
"afrinic.cer".to_string(),
"--tal-path".to_string(),
"apnic.tal".to_string(),
"--ta-path".to_string(),
"apnic.cer".to_string(),
"--tal-path".to_string(),
"arin.tal".to_string(),
"--ta-path".to_string(),
"arin.cer".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out.cir".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/afrinic.cer".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/apnic.cer".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/arin.cer".to_string(),
];
let args = parse_args(&argv).expect("parse args");
let mut shared = synthetic_post_validation_shared();
shared.discoveries = vec![shared.discovery.clone(), shared.discovery.clone()].into();
shared.successful_tal_inputs =
vec![args.tal_inputs[0].clone(), args.tal_inputs[2].clone()].into();
let effective = effective_cir_tal_uris_for_discoveries(
&args,
&shared,
resolve_cir_export_tal_uris(&args).expect("resolve cir tal uris"),
)
.expect("map effective cir tal uris");
assert_eq!(
effective,
vec![
"https://example.test/afrinic.cer".to_string(),
"https://example.test/arin.cer".to_string(),
]
);
}
#[test]
fn parse_rejects_report_json_compact_without_report_json() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--report-json-compact".to_string(),
];
let err = parse_args(&argv).expect_err("compact flag without report path should fail");
assert!(
err.contains("--report-json-compact requires --report-json"),
"{err}"
);
}
#[test]
fn parse_accepts_skip_report_build_without_report_json() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--ccr-out".to_string(),
"out/result.ccr".to_string(),
"--skip-report-build".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.skip_report_build);
assert_eq!(
args.ccr_out_path.as_deref(),
Some(std::path::Path::new("out/result.ccr"))
);
}
#[test]
fn parse_accepts_skip_vcir_persist() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--skip-vcir-persist".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.skip_vcir_persist);
}
#[test]
fn parse_rejects_skip_report_build_with_report_json() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--report-json".to_string(),
"out/report.json".to_string(),
"--skip-report-build".to_string(),
];
let err = parse_args(&argv).expect_err("skip report build with report path should fail");
assert!(
err.contains("--skip-report-build cannot be combined with --report-json"),
"{err}"
);
}
#[test]
fn parse_accepts_direct_compare_view_csv_outputs() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--vrps-csv-out".to_string(),
"out/vrps.csv".to_string(),
"--vaps-csv-out".to_string(),
"out/vaps.csv".to_string(),
"--compare-view-trust-anchor".to_string(),
"unknown".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.vrps_csv_out_path.as_deref(),
Some(std::path::Path::new("out/vrps.csv"))
);
assert_eq!(
args.vaps_csv_out_path.as_deref(),
Some(std::path::Path::new("out/vaps.csv"))
);
assert_eq!(args.compare_view_trust_anchor.as_deref(), Some("unknown"));
}
#[test]
fn parse_rejects_partial_direct_compare_view_csv_outputs() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--vrps-csv-out".to_string(),
"out/vrps.csv".to_string(),
];
let err = parse_args(&argv).expect_err("partial direct compare view output should fail");
assert!(
err.contains("--vrps-csv-out and --vaps-csv-out must be provided together"),
"{err}"
);
}
#[test]
fn parse_accepts_external_raw_store_db() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--raw-store-db".to_string(),
"raw-store.db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.raw_store_db.as_deref(),
Some(std::path::Path::new("raw-store.db"))
);
}
#[test]
fn parse_accepts_external_repo_bytes_db() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--repo-bytes-db".to_string(),
"repo-bytes.db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.repo_bytes_db.as_deref(),
Some(std::path::Path::new("repo-bytes.db"))
);
}
#[test]
fn parse_accepts_cir_enable_with_raw_store_backend() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--raw-store-db".to_string(),
"raw-store.db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/root.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.cir_enabled);
assert_eq!(
args.raw_store_db.as_deref(),
Some(std::path::Path::new("raw-store.db"))
);
assert_eq!(args.cir_static_root, None);
}
#[test]
fn parse_accepts_cir_enable_with_required_paths_and_tal_override() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/root.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert!(args.cir_enabled);
assert_eq!(
args.cir_out_path.as_deref(),
Some(std::path::Path::new("out/example.cir"))
);
assert_eq!(
args.cir_tal_uri.as_deref(),
Some("https://example.test/root.tal")
);
assert_eq!(
args.cir_tal_uris,
vec!["https://example.test/root.tal".to_string()]
);
}
#[test]
fn parse_rejects_deprecated_cir_static_root() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
"--cir-static-root".to_string(),
"out/static".to_string(),
];
let err = parse_args(&argv).expect_err("cir-static-root should be rejected");
assert!(err.contains("no longer supported"), "{err}");
}
#[test]
fn parse_accepts_default_parallel_config_and_phase2_overrides() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-object-workers".to_string(),
"3".to_string(),
"--parallel-phase2-worker-queue-capacity".to_string(),
"17".to_string(),
"--parallel-phase2-ready-batch-size".to_string(),
"31".to_string(),
"--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(),
"43".to_string(),
"--parallel-phase2-result-drain-batch-size".to_string(),
"37".to_string(),
"--parallel-phase2-finalize-batch-size".to_string(),
"41".to_string(),
"--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(),
"47".to_string(),
"--parallel-phase2-finalize-queue-capacity".to_string(),
"8192".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(args.parallel_phase2_config.object_workers, 3);
assert_eq!(args.parallel_phase2_config.worker_queue_capacity, 17);
assert_eq!(args.parallel_phase2_config.ready_batch_size, 31);
assert_eq!(
args.parallel_phase2_config.ready_batch_wall_time_budget_ms,
43
);
assert_eq!(
args.parallel_phase2_config.object_result_drain_batch_size,
37
);
assert_eq!(
args.parallel_phase2_config
.publication_point_finalize_batch_size,
41
);
assert_eq!(
args.parallel_phase2_config
.publication_point_finalize_wall_time_budget_ms,
47
);
assert_eq!(
args.parallel_phase2_config
.publication_point_finalize_queue_capacity,
8192
);
assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default());
}
#[test]
fn parse_rejects_zero_phase2_ready_batch_size() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-ready-batch-size".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero ready batch must fail");
assert!(err.contains("--parallel-phase2-ready-batch-size"), "{err}");
}
#[test]
fn parse_rejects_zero_phase2_result_drain_batch_size() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-result-drain-batch-size".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero result drain batch must fail");
assert!(
err.contains("--parallel-phase2-result-drain-batch-size"),
"{err}"
);
}
#[test]
fn parse_rejects_zero_phase2_ready_batch_wall_time_budget_ms() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero ready time budget must fail");
assert!(
err.contains("--parallel-phase2-ready-batch-wall-time-budget-ms"),
"{err}"
);
}
#[test]
fn parse_rejects_zero_phase2_finalize_batch_size() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-finalize-batch-size".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero finalize batch must fail");
assert!(
err.contains("--parallel-phase2-finalize-batch-size"),
"{err}"
);
}
#[test]
fn parse_rejects_zero_phase2_finalize_batch_wall_time_budget_ms() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero finalize time budget must fail");
assert!(
err.contains("--parallel-phase2-finalize-batch-wall-time-budget-ms"),
"{err}"
);
}
#[test]
fn parse_rejects_zero_phase2_finalize_queue_capacity() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2-finalize-queue-capacity".to_string(),
"0".to_string(),
];
let err = parse_args(&argv).expect_err("zero finalize queue capacity must fail");
assert!(
err.contains("--parallel-phase2-finalize-queue-capacity"),
"{err}"
);
}
#[test]
fn parse_rejects_removed_parallel_enable_flags() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase1".to_string(),
];
let err = parse_args(&argv).expect_err("removed phase flag should fail");
assert!(err.contains("unknown argument: --parallel-phase1"), "{err}");
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--parallel-phase2".to_string(),
];
let err = parse_args(&argv).expect_err("removed phase flag should fail");
assert!(err.contains("unknown argument: --parallel-phase2"), "{err}");
}
#[test]
fn parse_accepts_multi_tal_cir_overrides_in_file_mode() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"apnic.tal".to_string(),
"--ta-path".to_string(),
"apnic.cer".to_string(),
"--tal-path".to_string(),
"arin.tal".to_string(),
"--ta-path".to_string(),
"arin.cer".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/apnic.tal".to_string(),
"--cir-tal-uri".to_string(),
"https://example.test/arin.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.cir_tal_uris,
vec![
"https://example.test/apnic.tal".to_string(),
"https://example.test/arin.tal".to_string()
]
);
}
#[test]
fn parse_rejects_incomplete_or_invalid_cir_flags() {
let argv_missing = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--cir-enable".to_string(),
];
let err = parse_args(&argv_missing).unwrap_err();
assert!(err.contains("--cir-enable requires --cir-out"), "{err}");
let argv_needs_enable = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/root.tal".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
];
let err = parse_args(&argv_needs_enable).unwrap_err();
assert!(err.contains("require --cir-enable"), "{err}");
let argv_offline_missing_uri = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"x.tal".to_string(),
"--ta-path".to_string(),
"x.cer".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
"--cir-enable".to_string(),
"--cir-out".to_string(),
"out/example.cir".to_string(),
];
let err = parse_args(&argv_offline_missing_uri).unwrap_err();
assert!(err.contains("requires --cir-tal-uri"), "{err}");
}
#[test]
fn parse_rejects_invalid_validation_time() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--validation-time".to_string(),
"not-a-time".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("invalid --validation-time"), "{err}");
}
#[test]
fn parse_rejects_invalid_max_instances() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--max-instances".to_string(),
"nope".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("invalid --max-instances"), "{err}");
}
#[test]
fn parse_rejects_missing_value_for_db() {
let argv = vec!["rpki".to_string(), "--db".to_string()];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("--db requires a value"), "{err}");
}
#[test]
fn parse_rejects_missing_value_for_tal_url() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("--tal-url requires a value"), "{err}");
}
#[test]
fn parse_rejects_missing_db() {
let argv = vec!["rpki".to_string(), "--tal-url".to_string(), "x".to_string()];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("--db is required"), "{err}");
}
#[test]
fn parse_rejects_missing_tal_mode() {
let argv = vec!["rpki".to_string(), "--db".to_string(), "db".to_string()];
let err = parse_args(&argv).unwrap_err();
assert!(
err.contains("--tal-url") || err.contains("--tal-path"),
"{err}"
);
}
#[test]
fn parse_accepts_tal_url_mode() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_url.as_deref(), Some("https://example.test/x.tal"));
assert_eq!(
args.tal_urls,
vec!["https://example.test/x.tal".to_string()]
);
assert!(args.tal_path.is_none());
assert!(args.ta_path.is_none());
assert_eq!(args.tal_inputs.len(), 1);
assert_eq!(args.tal_inputs[0].tal_id, "x");
assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default());
assert_eq!(args.parallel_phase2_config, ParallelPhase2Config::default());
}
#[test]
fn parse_accepts_multi_tal_without_parallel_flags() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/arin.tal".to_string(),
"--tal-url".to_string(),
"https://example.test/apnic.tal".to_string(),
"--tal-url".to_string(),
"https://example.test/ripe.tal".to_string(),
"--parallel-max-repo-sync-workers-global".to_string(),
"8".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_urls.len(), 3);
assert_eq!(args.tal_inputs.len(), 3);
assert_eq!(args.tal_inputs[0].tal_id, "arin");
assert_eq!(args.tal_inputs[1].tal_id, "apnic");
assert_eq!(args.tal_inputs[2].tal_id, "ripe");
assert_eq!(args.parallel_phase1_config.max_repo_sync_workers_global, 8);
}
#[test]
fn parse_accepts_multi_tal_urls_by_default() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/arin.tal".to_string(),
"--tal-url".to_string(),
"https://example.test/apnic.tal".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_urls.len(), 2);
assert_eq!(args.tal_inputs.len(), 2);
}
#[test]
fn parse_accepts_offline_mode_requires_ta() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--max-depth".to_string(),
"0".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_paths, vec![PathBuf::from("a.tal")]);
assert_eq!(args.ta_paths, vec![PathBuf::from("ta.cer")]);
assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal")));
assert_eq!(args.ta_path.as_deref(), Some(Path::new("ta.cer")));
assert_eq!(args.max_depth, Some(0));
}
#[test]
fn parse_accepts_multiple_tal_path_pairs_by_default() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"apnic.tal".to_string(),
"--ta-path".to_string(),
"apnic-ta.cer".to_string(),
"--tal-path".to_string(),
"arin.tal".to_string(),
"--ta-path".to_string(),
"arin-ta.cer".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_paths.len(), 2);
assert_eq!(args.ta_paths.len(), 2);
assert_eq!(args.tal_inputs.len(), 2);
}
#[test]
fn parse_rejects_mixed_tal_url_and_tal_path_modes() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/arin.tal".to_string(),
"--tal-path".to_string(),
"apnic.tal".to_string(),
"--ta-path".to_string(),
"apnic-ta.cer".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(
err.contains(
"must specify either one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs"
),
"{err}"
);
}
#[test]
fn parse_rejects_mismatched_tal_path_and_ta_path_counts() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"apnic.tal".to_string(),
"--tal-path".to_string(),
"arin.tal".to_string(),
"--ta-path".to_string(),
"apnic-ta.cer".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(
err.contains("--tal-path and --ta-path counts must match"),
"{err}"
);
}
#[test]
fn parse_accepts_tal_path_without_ta_when_disable_rrdp_is_set() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--disable-rrdp".to_string(),
"--rsync-command".to_string(),
"/tmp/fake-rsync".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal")));
assert!(args.ta_path.is_none());
assert!(args.disable_rrdp);
assert_eq!(
args.rsync_command.as_deref(),
Some(Path::new("/tmp/fake-rsync"))
);
}
#[test]
fn parse_accepts_multiple_tal_paths_without_ta_when_disable_rrdp() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--tal-path".to_string(),
"b.tal".to_string(),
"--disable-rrdp".to_string(),
"--rsync-command".to_string(),
"/tmp/fake-rsync".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert_eq!(
args.tal_paths,
vec![PathBuf::from("a.tal"), PathBuf::from("b.tal")]
);
assert!(args.ta_paths.is_empty());
assert_eq!(args.tal_inputs.len(), 2);
assert!(args.disable_rrdp);
}
#[test]
fn parse_accepts_payload_delta_replay_mode_with_offline_tal_and_ta() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-base-archive".to_string(),
"base-archive".to_string(),
"--payload-base-locks".to_string(),
"base-locks.json".to_string(),
"--payload-delta-archive".to_string(),
"delta-archive".to_string(),
"--payload-delta-locks".to_string(),
"delta-locks.json".to_string(),
];
let args = parse_args(&argv).expect("parse delta replay mode");
assert_eq!(
args.payload_base_archive.as_deref(),
Some(Path::new("base-archive"))
);
assert_eq!(
args.payload_base_locks.as_deref(),
Some(Path::new("base-locks.json"))
);
assert_eq!(
args.payload_delta_archive.as_deref(),
Some(Path::new("delta-archive"))
);
assert_eq!(
args.payload_delta_locks.as_deref(),
Some(Path::new("delta-locks.json"))
);
}
#[test]
fn parse_rejects_partial_payload_delta_arguments_and_mutual_exclusion() {
let argv_partial = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-base-archive".to_string(),
"base-archive".to_string(),
];
let err = parse_args(&argv_partial).unwrap_err();
assert!(err.contains("must be provided together"), "{err}");
let argv_both = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-replay-archive".to_string(),
"archive".to_string(),
"--payload-replay-locks".to_string(),
"locks.json".to_string(),
"--payload-base-archive".to_string(),
"base-archive".to_string(),
"--payload-base-locks".to_string(),
"base-locks.json".to_string(),
"--payload-delta-archive".to_string(),
"delta-archive".to_string(),
"--payload-delta-locks".to_string(),
"delta-locks.json".to_string(),
];
let err = parse_args(&argv_both).unwrap_err();
assert!(err.contains("mutually exclusive"), "{err}");
}
#[test]
fn parse_rejects_payload_delta_with_tal_url_or_rsync_local_dir() {
let argv_url = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--payload-base-archive".to_string(),
"base-archive".to_string(),
"--payload-base-locks".to_string(),
"base-locks.json".to_string(),
"--payload-delta-archive".to_string(),
"delta-archive".to_string(),
"--payload-delta-locks".to_string(),
"delta-locks.json".to_string(),
];
let err = parse_args(&argv_url).unwrap_err();
assert!(err.contains("--tal-url is not supported"), "{err}");
let argv_rsync = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-base-archive".to_string(),
"base-archive".to_string(),
"--payload-base-locks".to_string(),
"base-locks.json".to_string(),
"--payload-delta-archive".to_string(),
"delta-archive".to_string(),
"--payload-delta-locks".to_string(),
"delta-locks.json".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
];
let err = parse_args(&argv_rsync).unwrap_err();
assert!(
err.contains("payload delta replay mode cannot be combined with --rsync-local-dir"),
"{err}"
);
}
#[test]
fn parse_accepts_payload_replay_mode_with_offline_tal_and_ta() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-replay-archive".to_string(),
"archive".to_string(),
"--payload-replay-locks".to_string(),
"locks.json".to_string(),
];
let args = parse_args(&argv).expect("parse replay mode");
assert_eq!(
args.payload_replay_archive.as_deref(),
Some(Path::new("archive"))
);
assert_eq!(
args.payload_replay_locks.as_deref(),
Some(Path::new("locks.json"))
);
}
#[test]
fn parse_rejects_partial_payload_replay_arguments() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-replay-archive".to_string(),
"archive".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("must be provided together"), "{err}");
}
#[test]
fn parse_rejects_payload_replay_with_tal_url_or_rsync_local_dir() {
let argv_url = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--payload-replay-archive".to_string(),
"archive".to_string(),
"--payload-replay-locks".to_string(),
"locks.json".to_string(),
];
let err = parse_args(&argv_url).unwrap_err();
assert!(err.contains("--tal-url is not supported"), "{err}");
let argv_rsync = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"a.tal".to_string(),
"--ta-path".to_string(),
"ta.cer".to_string(),
"--payload-replay-archive".to_string(),
"archive".to_string(),
"--payload-replay-locks".to_string(),
"locks.json".to_string(),
"--rsync-local-dir".to_string(),
"repo".to_string(),
];
let err = parse_args(&argv_rsync).unwrap_err();
assert!(
err.contains("cannot be combined with --rsync-local-dir"),
"{err}"
);
}
#[test]
fn parse_accepts_validation_time_rfc3339() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--validation-time".to_string(),
"2026-01-01T00:00:00Z".to_string(),
];
let args = parse_args(&argv).expect("parse");
assert!(args.validation_time.is_some());
}
#[test]
fn parse_rejects_removed_revalidate_only_flag() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
"--revalidate-only".to_string(),
];
let err = parse_args(&argv).unwrap_err();
assert!(err.contains("unknown argument: --revalidate-only"), "{err}");
}
#[test]
fn read_policy_accepts_valid_toml() {
let dir = tempfile::tempdir().expect("tmpdir");
let p = dir.path().join("policy.toml");
std::fs::write(
&p,
"signed_object_failure_policy = \"drop_publication_point\"\n",
)
.expect("write policy");
let policy = read_policy(Some(&p)).expect("parse policy");
assert_eq!(
policy.signed_object_failure_policy,
crate::policy::SignedObjectFailurePolicy::DropPublicationPoint
);
assert_eq!(policy.strict, StrictPolicy::default());
}
#[test]
fn read_policy_accepts_strict_table() {
let dir = tempfile::tempdir().expect("tmpdir");
let p = dir.path().join("policy.toml");
std::fs::write(
&p,
r#"
[strict]
name = true
cms_der = true
"#,
)
.expect("write policy");
let policy = read_policy(Some(&p)).expect("parse policy");
assert_eq!(
policy.strict,
StrictPolicy {
name: true,
cms_der: true,
signed_attrs: false,
}
);
}
#[test]
fn read_policy_reports_missing_file() {
let dir = tempfile::tempdir().expect("tmpdir");
let p = dir.path().join("missing.toml");
let err = read_policy(Some(&p)).unwrap_err();
assert!(err.contains("read policy file failed"), "{err}");
}
fn synthetic_post_validation_shared() -> PostValidationShared {
let tal_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
)
.expect("read tal fixture");
let ta_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"),
)
.expect("read ta fixture");
let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der(
&tal_bytes, &ta_der, None,
)
.expect("discover root");
let tree = crate::validation::tree::TreeRunOutput {
instances_processed: 1,
instances_failed: 0,
warnings: vec![
crate::report::Warning::new("synthetic warning")
.with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
.with_context("rsync://example.test/repo/pp/"),
],
vrps: vec![
crate::validation::objects::Vrp {
asn: 64496,
prefix: crate::data_model::roa::IpPrefix {
afi: crate::data_model::roa::RoaAfi::Ipv4,
prefix_len: 24,
addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length: 24,
},
crate::validation::objects::Vrp {
asn: 64497,
prefix: crate::data_model::roa::IpPrefix {
afi: crate::data_model::roa::RoaAfi::Ipv6,
prefix_len: 48,
addr: [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length: 64,
},
],
aspas: vec![crate::validation::objects::AspaAttestation {
customer_as_id: 64496,
provider_as_ids: vec![64497, 64498],
}],
router_keys: Vec::new(),
};
let mut pp1 = crate::audit::PublicationPointAudit::default();
pp1.source = "fresh".to_string();
pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
pp1.manifest_rsync_uri = "rsync://example.test/repo/pp1/manifest.mft".to_string();
pp1.objects.push(crate::audit::ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/pp1/a.roa".to_string(),
sha256_hex: "11".repeat(32),
kind: crate::audit::AuditObjectKind::Roa,
result: crate::audit::AuditObjectResult::Ok,
detail: None,
});
let mut pp2 = crate::audit::PublicationPointAudit::default();
pp2.source = "fresh".to_string();
pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
let mut pp3 = crate::audit::PublicationPointAudit::default();
pp3.source = "fresh".to_string();
pp3.rrdp_notification_uri = Some("https://example.test/n2.xml".to_string());
let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points: vec![pp1, pp2, pp3],
roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(),
downloads: Vec::new(),
download_stats: crate::audit::AuditDownloadStats::default(),
current_repo_objects: Vec::new(),
ccr_accumulator: None,
cir_input: crate::cir::CirInputSnapshot::default(),
};
PostValidationShared::from_run_output(out)
}
fn sample_cli_ccr_accumulator() -> CcrAccumulator {
let tal_bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
)
.expect("read tal fixture");
let ta_der = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"),
)
.expect("read ta fixture");
let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der(
&tal_bytes, &ta_der, None,
)
.expect("discover root");
let mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]);
let projection = crate::storage::VcirCcrManifestProjection {
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
manifest_sha256: vec![0x44; 32],
manifest_size: 2048,
manifest_ee_aki: vec![0x55; 20],
manifest_number_be: vec![1],
manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
time::OffsetDateTime::now_utc(),
),
manifest_sia_locations_der: vec![vec![
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
b'r', b's', b'y', b'n', b'c',
]],
subordinate_skis: vec![vec![0x33; 20]],
};
accumulator
.append_manifest_projection(&projection)
.expect("append manifest projection");
accumulator
}
#[test]
fn build_report_and_helpers_work_on_synthetic_output() {
let shared = synthetic_post_validation_shared();
let policy = Policy::default();
let validation_time = time::OffsetDateTime::now_utc();
let report = build_report(&policy, validation_time, &shared);
assert_eq!(unique_rrdp_repos(&report), 2);
assert_eq!(report.vrps.len(), 2);
assert_eq!(report.aspas.len(), 1);
print_summary(&report);
}
#[test]
fn run_report_task_and_stage_timing_work() {
let shared = synthetic_post_validation_shared();
let policy = Policy::default();
let validation_time = time::OffsetDateTime::now_utc();
let dir = tempfile::tempdir().expect("tmpdir");
let report_path = dir.path().join("report.json");
let report_output = run_report_task(
&policy,
validation_time,
&shared,
Some(&report_path),
ReportJsonFormat::Compact,
)
.expect("run report task");
assert!(report_output.report_write_ms.is_some());
let report_json = std::fs::read_to_string(&report_path).expect("read report json");
assert!(!report_json.contains('\n'), "{report_json}");
let report: serde_json::Value =
serde_json::from_str(&report_json).expect("parse compact report json");
assert_eq!(report["vrps"].as_array().unwrap().len(), 2);
assert_eq!(report["aspas"].as_array().unwrap().len(), 1);
assert_eq!(report["queryAudit"]["status"].as_str(), Some("complete"));
assert!(report["queryAudit"]["eventsCount"].as_u64().unwrap() > 0);
let events_path = dir.path().join(
report["queryAudit"]["eventsPath"]
.as_str()
.expect("events path"),
);
let events = std::fs::read_to_string(events_path).expect("read validation events");
assert!(
events
.lines()
.any(|line| line.contains("\"eventType\":\"object\""))
);
let stage_timing = RunStageTiming {
validation_ms: 1,
enable_roa_validation_cache: false,
enable_transport_request_prefetch: false,
report_build_ms: report_output.report_build_ms,
report_write_ms: report_output.report_write_ms,
ccr_build_ms: Some(2),
ccr_build_breakdown: None,
ccr_write_ms: Some(3),
compare_view_build_ms: Some(4),
compare_view_write_ms: Some(5),
cir_build_cir_ms: Some(6),
cir_write_cir_ms: Some(7),
cir_total_ms: Some(8),
total_ms: 9,
publication_points: shared.publication_points.len(),
repo_sync_ms_total: 10,
publication_point_repo_sync_ms_total: 11,
download_event_count: 12,
rrdp_download_ms_total: 13,
rsync_download_ms_total: 14,
download_bytes_total: 15,
roa_validation_cache: crate::validation::objects::RoaValidationCacheStats::default(),
analysis_counts: std::collections::HashMap::new(),
vcir_storage_summary_ms: Some(16),
vcir_storage: Some(VcirStorageSummary {
entry_count: 2,
vcir_value_bytes: 100,
vcir_value_bytes_max: 60,
vcir_value_bytes_max_manifest_rsync_uri: Some(
"rsync://example.test/repo/max.mft".to_string(),
),
core_fields: VcirCoreFieldSizeBreakdown {
manifest_rsync_uri_bytes: 10,
..VcirCoreFieldSizeBreakdown::default()
},
ccr_projection: VcirCcrProjectionSizeBreakdown {
manifest_sha256_bytes: 32,
..VcirCcrProjectionSizeBreakdown::default()
},
child_resources: VcirChildResourceSizeBreakdown {
effective_ip_resource_cbor_bytes: 12,
effective_as_resource_cbor_bytes: 6,
},
field_sizes: VcirFieldSizeBreakdown {
local_output_count: 1,
local_output_payload_json_bytes: 70,
local_output_payload_typed_body_bytes: 20,
..VcirFieldSizeBreakdown::default()
},
local_output_old_projection_bytes: 80,
local_output_typed_projection_bytes: 30,
local_output_projection_saved_bytes: 50,
top_entries_by_vcir_value_bytes: vec![VcirStorageEntrySummary {
manifest_rsync_uri: "rsync://example.test/repo/max.mft".to_string(),
vcir_value_bytes: 60,
local_vrp_count: 1,
local_aspa_count: 0,
local_router_key_count: 0,
accepted_object_count: 1,
rejected_object_count: 0,
child_count: 0,
core_fields: VcirCoreFieldSizeBreakdown::default(),
ccr_projection: VcirCcrProjectionSizeBreakdown::default(),
child_resources: VcirChildResourceSizeBreakdown::default(),
field_sizes: VcirFieldSizeBreakdown::default(),
local_output_old_projection_bytes: 1,
local_output_typed_projection_bytes: 1,
local_output_projection_saved_bytes: 0,
}],
}),
memory_telemetry: None,
};
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
let stage_timing_json =
std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing");
assert!(stage_timing_json.contains("\"validation_ms\""));
assert!(stage_timing_json.contains("\"ccr_build_ms\""));
assert!(stage_timing_json.contains("\"vcir_storage\""));
assert!(stage_timing_json.contains("\"local_output_projection_saved_bytes\""));
let ccr_path = dir.path().join("result.ccr");
write_stage_timing(Some(&ccr_path), &stage_timing).expect("write stage timing via ccr path");
assert!(
dir.path().join("stage-timing.json").exists(),
"stage timing should use parent directory of the anchor path"
);
let skipped = ReportTaskOutput::skipped();
assert_eq!(skipped.report_build_ms, 0);
assert!(skipped.report_write_ms.is_none());
}
#[test]
fn stage_timing_serializes_memory_telemetry() {
let dir = tempfile::tempdir().expect("tmpdir");
let report_path = dir.path().join("report.json");
let stage_timing = RunStageTiming {
validation_ms: 1,
enable_roa_validation_cache: true,
enable_transport_request_prefetch: true,
report_build_ms: 2,
report_write_ms: None,
ccr_build_ms: None,
ccr_build_breakdown: None,
ccr_write_ms: None,
compare_view_build_ms: None,
compare_view_write_ms: None,
cir_build_cir_ms: None,
cir_write_cir_ms: None,
cir_total_ms: None,
total_ms: 3,
publication_points: 4,
repo_sync_ms_total: 5,
publication_point_repo_sync_ms_total: 6,
download_event_count: 7,
rrdp_download_ms_total: 8,
rsync_download_ms_total: 9,
download_bytes_total: 10,
roa_validation_cache: crate::validation::objects::RoaValidationCacheStats {
hit_roas: 2,
..crate::validation::objects::RoaValidationCacheStats::default()
},
analysis_counts: std::collections::HashMap::from([(
"roa_validation_cache_hit_roas".to_string(),
2,
)]),
vcir_storage_summary_ms: None,
vcir_storage: None,
memory_telemetry: Some(MemoryTelemetrySummary {
checkpoints: vec![MemoryTelemetryCheckpoint {
label: "after_validation".to_string(),
elapsed_ms: 11,
process: ProcessMemorySnapshot {
label: "after_validation".to_string(),
vm_rss_kb: Some(12),
vm_size_kb: None,
vm_data_kb: None,
vm_swap_kb: None,
rss_anon_kb: Some(13),
rss_file_kb: None,
rss_shmem_kb: None,
threads: Some(14),
fd_count: Some(15),
smaps_rollup: None,
smaps_mapping_summary: None,
errors: Vec::new(),
},
rocksdb: RocksDbMemorySnapshot {
databases: Vec::new(),
totals: RocksDbMemoryTotals {
cur_size_all_mem_tables: 16,
size_all_mem_tables: 17,
estimate_table_readers_mem: 18,
block_cache_capacity: 19,
block_cache_usage: 20,
block_cache_pinned_usage: 21,
},
},
}],
object_graph: None,
malloc_trim_probes: Vec::new(),
}),
};
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(dir.path().join("stage-timing.json")).unwrap(),
)
.expect("parse stage timing json");
let checkpoint = &value["memory_telemetry"]["checkpoints"][0];
assert_eq!(checkpoint["label"], "after_validation");
assert_eq!(checkpoint["process"]["vm_rss_kb"], 12);
assert_eq!(
checkpoint["rocksdb"]["totals"]["cur_size_all_mem_tables"],
16
);
assert_eq!(value["analysis_counts"]["roa_validation_cache_hit_roas"], 2);
assert_eq!(value["roa_validation_cache"]["hit_roas"], 2);
assert!(
value["memory_telemetry"]
.as_object()
.expect("memory telemetry object")
.get("malloc_trim_probes")
.is_none()
);
}
#[test]
fn shared_object_graph_estimate_counts_audit_and_outputs() {
let mut shared = synthetic_post_validation_shared();
let mut publication_points = shared
.publication_points
.iter()
.cloned()
.collect::<Vec<_>>();
publication_points[0].rsync_base_uri = "rsync://example.test/repo/".to_string();
publication_points[0].manifest_rsync_uri = "rsync://example.test/repo/a.mft".to_string();
publication_points[0].publication_point_rsync_uri = "rsync://example.test/repo/".to_string();
publication_points[0].objects = vec![crate::audit::ObjectAuditEntry {
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
sha256_hex: "11".repeat(32),
kind: crate::audit::AuditObjectKind::Roa,
result: crate::audit::AuditObjectResult::Ok,
detail: None,
}];
shared.publication_points = publication_points.into();
let graph = estimate_shared_object_graph(&shared);
let publication_points_section = graph
.sections
.iter()
.find(|section| section.name == "publication_points")
.expect("publication points section");
let object_count = publication_points_section
.details
.iter()
.find(|metric| metric.name == "object_audit_entry_count")
.expect("object count metric");
assert_eq!(object_count.value, 1);
assert!(publication_points_section.estimated_bytes > 0);
let vrps_section = graph
.sections
.iter()
.find(|section| section.name == "vrps")
.expect("vrps section");
assert_eq!(vrps_section.item_count, 2);
assert!(graph.total_estimated_bytes >= publication_points_section.estimated_bytes);
}
#[test]
fn run_compare_view_task_writes_csv_from_shared_output() {
let shared = synthetic_post_validation_shared();
let dir = tempfile::tempdir().expect("tmpdir");
let vrps_path = dir.path().join("vrps.csv");
let vaps_path = dir.path().join("vaps.csv");
let output = run_compare_view_task(&shared, Some(&vrps_path), Some(&vaps_path), "unknown")
.expect("write direct compare views");
assert!(output.build_ms.is_some());
assert!(output.write_ms.is_some());
let vrps_csv = std::fs::read_to_string(vrps_path).expect("read vrps csv");
let vaps_csv = std::fs::read_to_string(vaps_path).expect("read vaps csv");
assert!(vrps_csv.contains("ASN,IP Prefix,Max Length,Trust Anchor"));
assert!(vrps_csv.contains("AS64496,192.0.2.0/24,24,unknown"));
assert!(vrps_csv.contains("AS64497,2001:db8::/48,64,unknown"));
assert!(vaps_csv.contains("Customer ASN,Providers,Trust Anchor"));
assert!(vaps_csv.contains("AS64496,AS64497;AS64498,unknown"));
}
#[test]
fn run_ccr_task_uses_accumulator_when_phase2_output_contains_reuse_sources() {
let mut shared = synthetic_post_validation_shared();
shared.ccr_accumulator = Some(sample_cli_ccr_accumulator());
let mut publication_points = shared
.publication_points
.iter()
.cloned()
.collect::<Vec<_>>();
publication_points[1].source = "vcir_current_instance".to_string();
publication_points[2].source = "failed_no_cache".to_string();
shared.publication_points = publication_points.into();
let dir = tempfile::tempdir().expect("tmpdir");
let ccr_path = dir.path().join("result.ccr");
let store = RocksStore::open(&dir.path().join("db")).expect("open empty store");
let output = run_ccr_task(
&store,
&shared,
Some(&ccr_path),
time::OffsetDateTime::now_utc(),
)
.expect("run ccr task");
assert!(output.ccr_build_ms.is_some());
assert!(output.ccr_build_breakdown.is_none());
let der = std::fs::read(&ccr_path).expect("read ccr");
let ci = crate::ccr::decode_content_info(&der).expect("decode ccr");
assert_eq!(
ci.content
.mfts
.as_ref()
.map(|manifest_state| manifest_state.mis.len()),
Some(1)
);
}
#[test]
fn write_json_writes_report() {
let report = AuditReportV2 {
format_version: 2,
meta: AuditRunMeta {
validation_time_rfc3339_utc: "2026-01-01T00:00:00Z".to_string(),
},
policy: Policy::default(),
tree: TreeSummary {
instances_processed: 0,
instances_failed: 0,
warnings: Vec::new(),
},
publication_points: Vec::new(),
vrps: Vec::new(),
aspas: Vec::new(),
downloads: Vec::new(),
download_stats: crate::audit::AuditDownloadStats::default(),
repo_sync_stats: crate::audit::AuditRepoSyncStats::default(),
query_audit: None,
};
let dir = tempfile::tempdir().expect("tmpdir");
let pretty_path = dir.path().join("report-pretty.json");
write_json(&pretty_path, &report, ReportJsonFormat::Pretty).expect("write pretty json");
let pretty = std::fs::read_to_string(&pretty_path).expect("read pretty report");
assert!(pretty.contains("\"format_version\""));
assert!(pretty.contains("\"policy\""));
assert!(pretty.contains("\n \"format_version\""), "{pretty}");
let compact_path = dir.path().join("report-compact.json");
write_json(&compact_path, &report, ReportJsonFormat::Compact).expect("write compact json");
let compact = std::fs::read_to_string(&compact_path).expect("read compact report");
assert!(compact.contains("\"format_version\""));
assert!(compact.contains("\"policy\""));
assert!(!compact.contains('\n'), "{compact}");
}
#[test]
fn build_repo_sync_stats_aggregates_phase_and_terminal_state() {
let mut pp1 = crate::audit::PublicationPointAudit::default();
pp1.repo_sync_phase = Some("rrdp_ok".to_string());
pp1.repo_sync_duration_ms = Some(10);
pp1.repo_terminal_state = "fresh".to_string();
let mut pp2 = crate::audit::PublicationPointAudit::default();
pp2.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string());
pp2.repo_sync_duration_ms = Some(20);
pp2.repo_terminal_state = "failed_no_cache".to_string();
let mut pp3 = crate::audit::PublicationPointAudit::default();
pp3.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string());
pp3.repo_sync_duration_ms = Some(30);
pp3.repo_terminal_state = "failed_no_cache".to_string();
let stats = build_repo_sync_stats(&[pp1, pp2, pp3]);
assert_eq!(stats.publication_points_total, 3);
assert_eq!(stats.by_phase["rrdp_ok"].count, 1);
assert_eq!(stats.by_phase["rrdp_ok"].duration_ms_total, 10);
assert_eq!(stats.by_phase["rrdp_failed_rsync_failed"].count, 2);
assert_eq!(
stats.by_phase["rrdp_failed_rsync_failed"].duration_ms_total,
50
);
assert_eq!(stats.by_terminal_state["fresh"].count, 1);
assert_eq!(stats.by_terminal_state["failed_no_cache"].count, 2);
assert_eq!(
stats.by_terminal_state["failed_no_cache"].duration_ms_total,
50
);
}