use proxywarden_lib::adapters::proxifyre::ProxiFyreAdapter; use proxywarden_lib::adapters::proxy_router::ProxyRouterRequest; use proxywarden_lib::component_detection::LEGACY_PROXIFYRE_2_2_1_MANIFEST; use proxywarden_lib::models::{ ComponentStatus, LocalSingBoxConfig, Profile, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, Target, TargetKind, }; use serde_json::Value; use std::collections::BTreeSet; use std::fs; use std::path::{Component, Path, PathBuf}; use url::Url; fn fixture_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/legacy") } fn read_json(path: impl AsRef) -> Value { let path = path.as_ref(); let contents = fs::read_to_string(path) .unwrap_or_else(|error| panic!("failed to read fixture {}: {error}", path.display())); serde_json::from_str(&contents) .unwrap_or_else(|error| panic!("invalid fixture JSON {}: {error}", path.display())) } fn contract() -> Value { read_json(fixture_root().join("contract.json")) } fn fixture_case<'a>(contract: &'a Value, id: &str) -> &'a Value { contract["fixtures"] .as_array() .expect("fixtures array") .iter() .find(|case| case["id"] == id) .unwrap_or_else(|| panic!("missing fixture case {id}")) } #[test] fn fixture_inventory_references_existing_parseable_json() { let contract = contract(); assert_eq!(contract["schemaVersion"], 1); let mut case_ids = BTreeSet::new(); for case in contract["fixtures"].as_array().expect("fixtures array") { let id = case["id"].as_str().expect("fixture id"); assert!(case_ids.insert(id), "duplicate fixture id {id}"); for relative in case["files"].as_array().expect("fixture files") { let relative = relative.as_str().expect("relative fixture path"); let path = Path::new(relative); assert!( !path.is_absolute(), "fixture path must be relative: {relative}" ); assert!( !path.components().any(|part| part == Component::ParentDir), "fixture path must not escape its root: {relative}" ); let full_path = fixture_root().join(path); assert!( full_path.is_file(), "missing fixture: {}", full_path.display() ); read_json(full_path); } } assert_eq!( case_ids, BTreeSet::from([ "marker-formats", "pre-1.2-split", "proxifyre-generated", "proxifyre-real-sanitized", "proxifyre-unsupported" ]) ); } #[test] fn fixture_values_are_sanitized_but_sensitive_key_names_are_preserved() { let root = fixture_root(); let mut json_paths = Vec::new(); collect_json_files(&root, &mut json_paths); assert!(!json_paths.is_empty(), "legacy fixture inventory is empty"); for path in json_paths { let value = read_json(&path); assert_sanitized(&value, "$", None) .unwrap_or_else(|error| panic!("{}: {error}", path.display())); } let unsupported = read_json(root.join("proxifyre-unsupported/app-config.json")); let proxy = &unsupported["proxies"][0]; assert!(proxy.get("username").is_some()); assert!(proxy.get("password").is_some()); } #[test] fn sanitizer_rejects_non_redacted_sensitive_values_and_uri_userinfo() { for value in [ serde_json::json!({"password": "not-a-secret-fixture"}), serde_json::json!({"password": "__REDACTED_REAL_SECRET__"}), serde_json::json!( "https://fixture-user:fixture-password@subscription.example.test/redacted" ), serde_json::json!("fixture-user@proxy.example.test:1080"), ] { let error = assert_sanitized(&value, "$", None).expect_err("value must be rejected"); assert!(error.starts_with('$')); assert!(!error.contains("not-a-secret-fixture")); assert!(!error.contains("fixture-password")); } } #[test] fn real_sanitized_sample_preserves_shape_and_records_provenance() { let contract = contract(); let case = fixture_case(&contract, "proxifyre-real-sanitized"); let provenance = &case["provenance"]; let source_hash = provenance["sourceSha256"] .as_str() .expect("real sample source hash"); assert_eq!(source_hash.len(), 64); assert!(source_hash .chars() .all(|character| character.is_ascii_hexdigit())); assert!(provenance["source"] .as_str() .is_some_and(|source| source.contains("pre-1.2 local installation"))); let sample = read_json(fixture_root().join("proxifyre-real-sanitized/app-config.json")); assert_eq!( object_keys(&sample), BTreeSet::from(["bypassLan", "logLevel", "proxies"]) ); assert_eq!(sample["logLevel"], "Info"); assert_eq!(sample["bypassLan"], true); assert_eq!( sample["proxies"].as_array().expect("sample proxies").len(), 1 ); let proxy = &sample["proxies"][0]; assert_eq!( object_keys(proxy), BTreeSet::from(["appNames", "socks5ProxyEndpoint", "supportedProtocols"]) ); let app_names = proxy["appNames"].as_array().expect("sample app names"); assert_eq!(app_names.len(), 8); assert_eq!( app_names .iter() .filter_map(Value::as_str) .filter(|name| name.contains('\\')) .count(), 3 ); assert_eq!( proxy["supportedProtocols"], serde_json::json!(["TCP", "UDP"]) ); } #[test] fn marker_fixtures_freeze_weak_and_strong_schemas() { let root = fixture_root().join("markers"); let weak = read_json(root.join("install-proxyfier.marker.json")); assert_eq!( object_keys(&weak), BTreeSet::from(["component", "installedAt", "packagePath", "serviceName"]) ); assert_eq!(weak["component"], "proxyfier"); assert_eq!(weak["serviceName"], "ProxiFyreService"); let strong = read_json(root.join("proxywarden-component.json")); assert_eq!( object_keys(&strong), BTreeSet::from([ "component", "installRoot", "manager", "packetFilterInstalledByProxyWarden", "serviceName", ]) ); assert_eq!(strong["manager"], "ProxyWarden"); assert_eq!(strong["component"], "proxifyre"); assert_eq!(strong["serviceName"], "ProxiFyreService"); } #[test] fn legacy_generated_fixture_maps_to_canonical_state_and_regenerates() { let root = fixture_root(); let source_path = root.join("proxifyre-generated/app-config.json"); let source_before = fs::read(&source_path).expect("legacy generated fixture bytes"); let source = read_json(&source_path); let (profiles, targets) = strict_import_generated_proxifyre(&source) .expect("historically generated config must be strictly importable"); let expected_profiles: Vec = serde_json::from_value(read_json(root.join("pre-1.2-split/config/profiles.json"))) .expect("expected profiles"); let expected_targets: Vec = serde_json::from_value(read_json(root.join("pre-1.2-split/config/targets.json"))) .expect("expected targets"); assert_eq!(profiles, expected_profiles); assert_eq!(targets, expected_targets); let regenerated = ProxiFyreAdapter::default() .generate_proxifyre_config(ProxyRouterRequest::new(&profiles, &targets, &[])) .expect("canonical state must regenerate"); assert_eq!( serde_json::to_value(regenerated).expect("regenerated JSON"), source ); assert_eq!( fs::read(&source_path).expect("legacy source after import attempt"), source_before, "fixture importer must not mutate its legacy source" ); } #[test] fn every_unsupported_legacy_variant_fails_closed_without_mutating_source() { let root = fixture_root(); let supported = read_json(root.join("proxifyre-generated/app-config.json")); let unsupported_path = root.join("proxifyre-unsupported/app-config.json"); let unsupported_before = fs::read(&unsupported_path).expect("unsupported source bytes"); let unsupported = read_json(&unsupported_path); assert_eq!( object_keys(&unsupported), BTreeSet::from(["bypassLan", "customRootField", "logLevel", "proxies"]) ); assert_eq!( object_keys(&unsupported["proxies"][0]), BTreeSet::from([ "addressFamily", "appNames", "customProxyField", "password", "socks5ProxyEndpoint", "supportedProtocols", "tls", "username", ]) ); assert!(strict_import_generated_proxifyre(&unsupported).is_err()); let variants = unsupported_variants(&supported); for (label, variant) in variants { assert!( strict_import_generated_proxifyre(&variant).is_err(), "unsupported variant was accepted: {label}" ); } assert_eq!( fs::read(&unsupported_path).expect("unsupported source after validation"), unsupported_before, "validation must preserve unsupported legacy source bytes" ); } #[test] fn generated_plain_socks5_fixture_roundtrips_semantically() { let root = fixture_root(); let profiles: Vec = serde_json::from_value(read_json(root.join("pre-1.2-split/config/profiles.json"))) .expect("legacy profiles fixture"); let targets: Vec = serde_json::from_value(read_json(root.join("pre-1.2-split/config/targets.json"))) .expect("legacy targets fixture"); let expected = read_json(root.join("proxifyre-generated/app-config.json")); let generated = ProxiFyreAdapter::default() .generate_proxifyre_config(ProxyRouterRequest::new(&profiles, &targets, &[])) .expect("supported fixture must generate"); let actual = serde_json::to_value(generated).expect("generated config JSON"); assert_eq!(actual, expected); } #[test] fn pre_1_2_split_fixture_still_deserializes_with_current_defaults() { let root = fixture_root().join("pre-1.2-split/config"); let components: Vec = serde_json::from_value(read_json(root.join("components.json"))) .expect("legacy components fixture"); let local_singbox: LocalSingBoxConfig = serde_json::from_value(read_json(root.join("local-singbox.json"))) .expect("legacy local sing-box fixture"); assert!(components.iter().all(|component| { component.service_name.is_none() && component.service_status.is_none() })); assert!(local_singbox.device_hwid.is_none()); assert!(local_singbox.selected_server_id.is_none()); assert_eq!( local_singbox.install_root, r"C:\Program Files\ProxyWarden\sing-box" ); } #[test] fn field_matrix_is_total_and_fail_closed() { let contract = contract(); let matrix = contract["proxifyreFieldMatrix"] .as_array() .expect("field matrix"); let expected_ids = BTreeSet::from([ "address-family", "app-names", "bypass-lan-other", "bypass-lan-true", "credentials-userinfo", "endpoint-scheme-or-userinfo", "log-level-info", "log-level-other", "plain-endpoint", "protocol-other-or-empty", "protocol-tcp", "protocol-udp", "proxies", "tls", "unknown-proxy-key", "unknown-root-key", ]); let fixture_ids: BTreeSet<&str> = contract["fixtures"] .as_array() .expect("fixtures array") .iter() .filter_map(|case| case["id"].as_str()) .collect(); let mut actual_ids = BTreeSet::new(); for rule in matrix { let id = rule["id"].as_str().expect("matrix rule id"); assert!(actual_ids.insert(id), "duplicate matrix rule {id}"); let outcome = rule["outcome"].as_str().expect("matrix outcome"); assert!( matches!(outcome, "canonical" | "derived" | "unsupported"), "invalid matrix outcome for {id}: {outcome}" ); if outcome != "unsupported" { assert!( rule["destination"] .as_str() .is_some_and(|value| !value.is_empty()), "supported rule {id} must identify its destination" ); } let coverage = rule["coverage"].as_str().expect("matrix coverage"); assert!( fixture_ids.contains(coverage) || coverage.starts_with("inline-"), "matrix rule {id} has unknown coverage {coverage}" ); } assert_eq!(actual_ids, expected_ids); assert_eq!( fixture_case(&contract, "proxifyre-unsupported")["status"], "unsupported_preserve_original" ); } #[test] fn split_source_precedence_roots_services_collisions_and_state_are_frozen() { let contract = contract(); let split_sources: BTreeSet<&str> = contract["startup"]["canonicalSplitSourceFiles"] .as_array() .expect("split sources") .iter() .filter_map(Value::as_str) .collect(); assert_eq!( split_sources, BTreeSet::from([ "config/components.json", "config/local-singbox.json", "config/profiles.json", "config/targets.json", ]) ); assert_eq!( contract["startup"]["rules"]["anySplitSourceExists"], "adopt_split_without_generated_import" ); assert_eq!( strings_at( &contract, "/components/proxifyre/confirmedManagedLegacyDefaultRoots" ), BTreeSet::from([r"C:\Tools\ProxiFyre"]) ); assert_eq!( strings_at( &contract, "/components/singbox/confirmedManagedLegacyDefaultRoots" ), BTreeSet::from([r"C:\Program Files\ProxyWarden\sing-box"]) ); assert_eq!( candidate_paths_at(&contract, "/components/proxifyre/legacyCandidates"), BTreeSet::from([ r"%LOCALAPPDATA%\ProxiFyre", r"%LOCALAPPDATA%\ProxyWarden\ProxiFyre", r"%ProgramFiles(x86)%\ProxiFyre", r"%ProgramFiles(x86)%\ProxyWarden\ProxiFyre", r"%ProgramFiles%\ProxiFyre", r"%ProgramFiles%\ProxyWarden\ProxiFyre", r"C:\Tools\ProxiFyre", ]) ); assert_eq!( candidate_paths_at(&contract, "/components/singbox/legacyCandidates"), BTreeSet::from([ r"%LOCALAPPDATA%\ProxyWarden\sing-box", r"%ProgramFiles(x86)%\ProxyWarden\sing-box", r"%ProgramFiles%\ProxyWarden\sing-box", r"C:\Tools\ProxyWarden\sing-box", ]) ); assert_eq!( contract["components"]["proxifyre"]["service"]["primaryName"], "ProxiFyreService" ); assert_eq!( contract["components"]["proxifyre"]["service"]["discoveryOnlyPathNameTemplate"], r#""{root}\ProxiFyre.exe" --service"# ); assert_eq!( strings_at( &contract, "/components/proxifyre/service/discoveryOnlyAliases" ), BTreeSet::from(["ProxiFyre"]) ); assert_eq!( contract["components"]["proxifyre"]["service"]["autoCutoverPathNameTemplate"], r#""{root}\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename "ProxiFyreService""# ); assert_eq!( contract["components"]["singbox"]["service"]["primaryName"], "ProxyWardenSingBox" ); assert_eq!( contract["components"]["singbox"]["service"]["pathNameTemplate"], r#""{root}\ProxyWardenSingBox.exe""# ); assert_eq!( contract["components"]["proxifyre"]["markers"]["managedLegacyRoot"], "none" ); assert_eq!( contract["components"]["proxifyre"]["markers"]["weakStandaloneScriptHint"] ["ownershipProof"], false ); assert_eq!( contract["components"]["proxifyre"]["markers"]["managedCurrent"]["requiredValues"] ["manager"], "ProxyWarden" ); assert_eq!( contract["components"]["proxifyre"]["markers"]["managedCurrent"]["requiredValues"] ["serviceName"], "ProxiFyreService" ); assert_eq!( contract["components"]["singbox"]["markers"]["managedLegacyRoot"], "none" ); assert!(strings_at( &contract, "/components/proxifyre/managedLegacyClassificationRequires" ) .contains("service PathName points to that exact executable")); assert!(strings_at( &contract, "/components/singbox/managedLegacyClassificationRequires" ) .contains("service PathName points to that exact wrapper")); assert_eq!( contract["components"]["proxifyre"]["autoCutover"]["root"], r"C:\Tools\ProxiFyre" ); assert_eq!( contract["components"]["proxifyre"]["autoCutover"]["allOtherDiscoveryCandidates"] ["decision"], "manual_migration_required" ); assert_eq!( contract["components"]["proxifyre"]["autoCutover"]["allOtherDiscoveryCandidates"] ["mutationPlan"], serde_json::json!([]) ); assert_eq!( contract["components"]["singbox"]["autoCutover"]["decision"], "manual_migration_required" ); assert_eq!( contract["components"]["singbox"]["autoCutover"]["mutationPlan"], serde_json::json!([]) ); let frozen_manifest = contract["components"]["proxifyre"]["autoCutover"]["packageManifest"] .as_array() .expect("frozen ProxiFyre package manifest"); assert_eq!(frozen_manifest.len(), LEGACY_PROXIFYRE_2_2_1_MANIFEST.len()); for expected in LEGACY_PROXIFYRE_2_2_1_MANIFEST { let actual = frozen_manifest .iter() .find(|file| file["relativePath"] == expected.relative_path) .unwrap_or_else(|| panic!("missing frozen package file {}", expected.relative_path)); assert_eq!(actual["size"], expected.size); assert_eq!(actual["sha256"], expected.sha256); } assert_eq!( contract["components"]["proxifyre"]["autoCutover"]["scmProfile"], serde_json::json!({ "serviceType": "win32_own_process", "startType": "auto_start", "errorControl": "normal", "account": "LocalSystem", "displayName": "ProxiFyre Service", "description": "ProxiFyre - SOCKS5 ProxiFyre Service", "dependencies": [], "loadOrderGroup": null, "failureActions": [], "failureActionsOnNonCrash": false, "delayedAutoStart": false, "sidType": "none", "requiredPrivileges": [], "triggers": [], "untrustedMutationRights": false }) ); assert_eq!( candidate_paths_at(&contract, "/components/singbox/foreignByDefaultCandidates"), BTreeSet::from([ r"%LOCALAPPDATA%\sing-box", r"%ProgramFiles(x86)%\sing-box", r"%ProgramFiles%\sing-box", ]) ); let history_commits: BTreeSet<&str> = contract["historyEvidence"] .as_array() .expect("history evidence") .iter() .filter_map(|item| item["commit"].as_str()) .collect(); for pointer in [ "/components/proxifyre/legacyCandidates", "/components/singbox/legacyCandidates", "/components/singbox/foreignByDefaultCandidates", ] { for candidate in contract .pointer(pointer) .expect("candidate list") .as_array() .expect("candidate array") { let commit = candidate["evidenceCommit"] .as_str() .expect("candidate evidence commit"); assert!( history_commits.contains(commit), "candidate evidence commit is absent from historyEvidence: {commit}" ); assert!( candidate["evidenceFile"] .as_str() .is_some_and(|path| path.starts_with("src-tauri/src/")), "candidate must name its historical source file" ); } } assert_eq!( contract["collisionPolicy"]["currentAndLegacy"], "current_wins_orphan_untouched_manual" ); assert_eq!( contract["collisionPolicy"]["sameServiceNameForeignPath"], "ownership_mismatch_without_mutation" ); assert_eq!( contract["runningStatePolicy"]["running"], "restore_running_after_success_or_rollback" ); assert_eq!( contract["runningStatePolicy"]["stopped"], "keep_stopped_after_success_or_rollback" ); assert_eq!( contract["runningStatePolicy"]["pendingOrUnknown"], "block_without_mutation" ); } fn strict_import_generated_proxifyre( value: &Value, ) -> Result<(Vec, Vec), &'static str> { if object_keys(value) != BTreeSet::from(["bypassLan", "logLevel", "proxies"]) { return Err("unsupported root fields"); } if value["logLevel"] != "Info" { return Err("unsupported log level"); } if value["bypassLan"] != true { return Err("unsupported bypassLan"); } let proxies = value["proxies"] .as_array() .ok_or("proxies must be an array")?; if proxies.is_empty() { return Err("generated config contains no recoverable proxies"); } let mut profiles = Vec::with_capacity(proxies.len()); let mut targets = Vec::with_capacity(proxies.len()); for (index, proxy) in proxies.iter().enumerate() { if object_keys(proxy) != BTreeSet::from(["appNames", "socks5ProxyEndpoint", "supportedProtocols"]) { return Err("unsupported proxy fields"); } let app_names = proxy["appNames"] .as_array() .ok_or("appNames must be an array")?; if app_names.is_empty() { return Err("appNames must not be empty"); } let mut items = Vec::with_capacity(app_names.len()); for app_name in app_names { let app_name = app_name.as_str().ok_or("app name must be a string")?; if app_name.trim().is_empty() { return Err("app name must not be empty"); } let is_path = app_name.contains(['\\', '/']); let (item_type, recursive) = if is_path && app_name.to_ascii_lowercase().ends_with(".exe") { (ProfileItemType::Exe, false) } else if is_path { (ProfileItemType::Folder, true) } else { (ProfileItemType::Process, false) }; items.push(ProfileItem { item_type, value: app_name.to_string(), recursive, }); } let endpoint = proxy["socks5ProxyEndpoint"] .as_str() .ok_or("endpoint must be a string")?; let (host, port) = strict_plain_endpoint(endpoint)?; let protocol_values = proxy["supportedProtocols"] .as_array() .ok_or("supportedProtocols must be an array")?; if protocol_values.is_empty() { return Err("supportedProtocols must not be empty"); } let mut protocols = Vec::with_capacity(protocol_values.len()); for protocol in protocol_values { let protocol = match protocol.as_str() { Some("TCP") => Protocol::Tcp, Some("UDP") => Protocol::Udp, _ => return Err("unsupported protocol"), }; if protocols.contains(&protocol) { return Err("duplicate protocol"); } protocols.push(protocol); } let ordinal = index + 1; let profile_id = if proxies.len() == 1 { "fixture-profile".to_string() } else { format!("legacy-proxifyre-profile-{ordinal}") }; let target_id = if proxies.len() == 1 { "fixture-target".to_string() } else { format!("legacy-proxifyre-target-{ordinal}") }; profiles.push(Profile { id: profile_id, name: if proxies.len() == 1 { "Fixture profile".to_string() } else { format!("Legacy ProxiFyre profile {ordinal}") }, enabled: true, target_id: target_id.clone(), protocols, items, }); targets.push(Target { id: target_id, name: if proxies.len() == 1 { "Fixture target".to_string() } else { format!("Legacy ProxiFyre target {ordinal}") }, kind: TargetKind::External, protocol: ProxyProtocol::Socks5, host, port, requires_component: None, }); } Ok((profiles, targets)) } fn strict_plain_endpoint(endpoint: &str) -> Result<(String, u16), &'static str> { if endpoint.contains(['/', '@']) || endpoint.matches(':').count() != 1 { return Err("endpoint must be plain host:port"); } let (host, port) = endpoint .rsplit_once(':') .ok_or("endpoint must include a port")?; if host.trim().is_empty() || host.chars().any(char::is_whitespace) { return Err("endpoint host is invalid"); } let port = port .parse::() .map_err(|_| "endpoint port is invalid")?; if port == 0 { return Err("endpoint port must not be zero"); } Ok((host.to_string(), port)) } fn unsupported_variants(supported: &Value) -> Vec<(&'static str, Value)> { let mut variants = Vec::new(); let mut add = |label, mutate: fn(&mut Value)| { let mut value = supported.clone(); mutate(&mut value); variants.push((label, value)); }; add("non-default logLevel", |value| { value["logLevel"] = "Debug".into() }); add("non-default bypassLan", |value| { value["bypassLan"] = false.into() }); add("empty proxies", |value| { value["proxies"] = serde_json::json!([]) }); add("empty appNames", |value| { value["proxies"][0]["appNames"] = serde_json::json!([]) }); add("scheme endpoint", |value| { value["proxies"][0]["socks5ProxyEndpoint"] = "socks5://proxy.example.test:1080".into() }); add("userinfo endpoint", |value| { value["proxies"][0]["socks5ProxyEndpoint"] = "fixture-user@proxy.example.test:1080".into() }); add("missing endpoint port", |value| { value["proxies"][0]["socks5ProxyEndpoint"] = "proxy.example.test".into() }); add("zero endpoint port", |value| { value["proxies"][0]["socks5ProxyEndpoint"] = "proxy.example.test:0".into() }); add("empty protocols", |value| { value["proxies"][0]["supportedProtocols"] = serde_json::json!([]) }); add("unknown protocol", |value| { value["proxies"][0]["supportedProtocols"] = serde_json::json!(["TCP", "ICMP"]) }); add("username", |value| { value["proxies"][0]["username"] = "__REDACTED_USERNAME__".into() }); add("password", |value| { value["proxies"][0]["password"] = "__REDACTED_PASSWORD__".into() }); add("userinfo field", |value| { value["proxies"][0]["userinfo"] = "__REDACTED_USERINFO__".into() }); add("tls", |value| { value["proxies"][0]["tls"] = serde_json::json!({"enabled": true}) }); add("address family", |value| { value["proxies"][0]["addressFamily"] = "IPv4".into() }); add("unknown root key", |value| { value["customRootField"] = "REDACTED".into() }); add("unknown proxy key", |value| { value["proxies"][0]["customProxyField"] = "REDACTED".into() }); variants } fn object_keys(value: &Value) -> BTreeSet<&str> { value .as_object() .map(|object| object.keys().map(String::as_str).collect()) .unwrap_or_default() } fn collect_json_files(directory: &Path, output: &mut Vec) { let mut entries: Vec<_> = fs::read_dir(directory) .unwrap_or_else(|error| panic!("failed to read {}: {error}", directory.display())) .map(|entry| entry.expect("fixture directory entry").path()) .collect(); entries.sort(); for path in entries { if path.is_dir() { collect_json_files(&path, output); } else if path.extension().and_then(|value| value.to_str()) == Some("json") { output.push(path); } } } fn assert_sanitized(value: &Value, path: &str, key: Option<&str>) -> Result<(), String> { if key.is_some_and(is_sensitive_key) && !is_safe_sensitive_value(value) { return Err(format!("{path}: sensitive fixture value is not redacted")); } match value { Value::Object(object) => { for (child_key, child_value) in object { assert_sanitized(child_value, &format!("{path}.{child_key}"), Some(child_key))?; } } Value::Array(array) => { for (index, child) in array.iter().enumerate() { assert_sanitized(child, &format!("{path}[{index}]"), key)?; } } Value::String(text) => assert_safe_string(text, path)?, Value::Null | Value::Bool(_) | Value::Number(_) => {} } Ok(()) } fn is_sensitive_key(key: &str) -> bool { matches!( key.to_ascii_lowercase().replace(['_', '-'], "").as_str(), "username" | "password" | "token" | "secret" | "subscriptionurl" | "userinfo" | "authorization" ) } fn is_safe_sensitive_value(value: &Value) -> bool { match value { Value::Null => true, Value::String(text) if matches!( text.as_str(), "__REDACTED_USERNAME__" | "__REDACTED_PASSWORD__" | "__REDACTED_USERINFO__" | "__REDACTED_TOKEN__" ) => { true } Value::String(text) => synthetic_url_is_safe(text), _ => false, } } fn assert_safe_string(text: &str, path: &str) -> Result<(), String> { if text.contains("://") && !synthetic_url_is_safe(text) { return Err(format!("{path}: fixture URL is not safely synthetic")); } if text.contains('@') { return Err(format!( "{path}: fixture endpoint must not contain userinfo" )); } Ok(()) } fn synthetic_url_is_safe(text: &str) -> bool { let Ok(url) = Url::parse(text) else { return false; }; let synthetic_host = url .host_str() .is_some_and(|host| host == "example.test" || host.ends_with(".example.test")); synthetic_host && url.username().is_empty() && url.password().is_none() && url.query().is_none() && url.fragment().is_none() } fn strings_at<'a>(value: &'a Value, pointer: &str) -> BTreeSet<&'a str> { value .pointer(pointer) .unwrap_or_else(|| panic!("missing contract pointer {pointer}")) .as_array() .unwrap_or_else(|| panic!("contract pointer is not an array: {pointer}")) .iter() .filter_map(Value::as_str) .collect() } fn candidate_paths_at<'a>(value: &'a Value, pointer: &str) -> BTreeSet<&'a str> { value .pointer(pointer) .unwrap_or_else(|| panic!("missing contract pointer {pointer}")) .as_array() .unwrap_or_else(|| panic!("contract pointer is not an array: {pointer}")) .iter() .filter_map(|candidate| candidate["path"].as_str()) .collect() }