//! HS publish wait and CBT floor — 180s fail-closed cuts a working HsDir upload. use std::os::unix::fs::PermissionsExt; use std::time::Duration; use onionwire::hs; use tor_hsservice::status::State; #[test] fn publish_wait_covers_hsdir_retries() { assert!( hs::PUBLISH_WAIT >= Duration::from_secs(360), "180s cuts a working HsDir publish while status is still Bootstrapping" ); } #[test] fn cbt_min_timeout_floor_is_at_least_10s() { // Drive the comparison through a runtime value: asserting on the constant // directly is folded away by the compiler and clippy rejects it under // -D warnings (clippy::assertions_on_constants). let floor_ms = hs::CBT_MIN_TIMEOUT_MS; assert!(floor_ms >= 10_000, "learned CBT ~1s kills HsDir circuits"); } #[test] fn cbt_initial_timeout_matches_the_floor() { // Consensus cbtinitialtimeout can be ~2s; 4-hop vanguard HS circuits // need the same floor as cbtmintimeout before the estimator has samples. let initial = hs::CBT_INITIAL_TIMEOUT_MS; let floor = hs::CBT_MIN_TIMEOUT_MS; assert!(initial >= floor, "initial CBT below min floor"); assert!(initial >= 10_000, "initial CBT too low for HsDir circuits"); } #[test] fn no_preemptive_exit_ports() { // Default predicted 80/443 circuits compete with IPT + HsDir builds. // OnionWire never exits; keep the predicted list empty. assert!( hs::PREEMPTIVE_PREDICTED_PORTS.is_empty(), "preemptive exit circuits starve HS publish" ); } #[test] fn arti_status_running_is_ready_without_probe() { assert!(hs::hs_is_ready(State::Running, false)); assert!(hs::hs_is_ready(State::DegradedReachable, false)); } #[test] fn bootstrapping_is_not_ready_until_a_probe_connects() { // Combined status stays Bootstrapping through Arti's 5 min HsDir upload // round even after some descriptors are already fetchable. assert!(!hs::hs_is_ready(State::Bootstrapping, false)); assert!(hs::hs_is_ready(State::Bootstrapping, true)); } #[test] fn degraded_unreachable_is_ready_only_if_probe_connects() { assert!(!hs::hs_is_ready(State::DegradedUnreachable, false)); assert!(hs::hs_is_ready(State::DegradedUnreachable, true)); } #[test] fn broken_or_shutdown_never_ready() { assert!(!hs::hs_is_ready(State::Broken, true)); assert!(!hs::hs_is_ready(State::Shutdown, true)); } #[test] fn hs_log_label_is_not_the_full_v3_onion() { // Public v3 address; checksum is valid so HsId::from_str works. let onion = "facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion"; let label = hs::log_label(onion); assert_ne!(label, onion, "status/probe logs must not use the locator"); assert!( !label.contains("facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd"), "log label leaked the onion body: {label}" ); assert!( label.contains('…') || label.contains("[scrubbed]"), "expected safelog redaction, got {label}" ); } #[test] fn client_config_mkdirs_state_and_cache_0700() { let root = tempfile::tempdir().expect("tempdir"); let state = root.path().join("arti"); let cache = root.path().join("cache"); let _cfg = hs::client_config(&state, &cache); let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; assert_eq!(mode(&state), 0o700); assert_eq!(mode(&cache), 0o700); }