Files
ProxyWarden/src-tauri/tests/fixture_contract_tests.rs
T
dokril efda8eb98f
CI / Windows baseline (push) Canceled after 0s
Release v2.0.0
2026-09-10 20:59:52 +03:00

939 lines
31 KiB
Rust

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<Path>) -> 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<Profile> =
serde_json::from_value(read_json(root.join("pre-1.2-split/config/profiles.json")))
.expect("expected profiles");
let expected_targets: Vec<Target> =
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<Profile> =
serde_json::from_value(read_json(root.join("pre-1.2-split/config/profiles.json")))
.expect("legacy profiles fixture");
let targets: Vec<Target> =
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<ComponentStatus> =
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<Profile>, Vec<Target>), &'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::<u16>()
.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<PathBuf>) {
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()
}