Release v2.0.0
CI / Windows baseline (push) Canceled after 0s

This commit is contained in:
2026-09-10 20:59:52 +03:00
parent 9c987df6e9
commit efda8eb98f
142 changed files with 68308 additions and 9333 deletions
+69
View File
@@ -14,6 +14,8 @@ use proxywarden_lib::models::{
Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target, TargetInput,
TargetKind,
};
#[cfg(windows)]
use proxywarden_lib::safe_fs;
use proxywarden_lib::storage::JsonStorage;
use std::{cell::Cell, fs, path::Path};
@@ -44,6 +46,9 @@ fn external_apply_commits_one_source_state_without_service_control() {
target.id == "main-proxy" && target.host == "proxy.example.test" && target.port == 1080
}));
assert!(Path::new(&result.generated_config_path).exists());
#[cfg(windows)]
safe_fs::verify_path_protected_for_owner_admin_system(Path::new(&result.generated_config_path))
.expect("generated config keeps restricted ACL");
}
#[test]
@@ -169,6 +174,15 @@ fn helper_failure_rolls_back_source_and_generated_artifact() {
fs::read(&generated_path).expect("generated after"),
b"old-generated"
);
#[cfg(windows)]
{
safe_fs::verify_path_protected_for_owner_admin_system(&generated_path)
.expect("rollback keeps generated config restricted");
assert!(
!safe_fs::backup_path(&generated_path).exists(),
"rollback restores prior absence of backup"
);
}
}
#[test]
@@ -213,6 +227,7 @@ fn local_apply_with_missing_running_service_stops_at_preflight() {
let error = run_apply(
&fixture.storage,
ApplyConfigurationInput {
expected_revision: None,
route_mode: ApplyRouteMode::LocalSingbox,
profile: profile_input(),
external_target: None,
@@ -230,8 +245,61 @@ fn local_apply_with_missing_running_service_stops_at_preflight() {
);
}
#[test]
fn editing_shared_target_preserves_the_other_profile_and_target() {
let fixture = ApplyFixture::new("shared-target");
fixture.seed_old_state();
let before_profiles = fixture.storage.read_profiles().unwrap();
let before_targets = fixture.storage.read_targets().unwrap();
let mut input = external_input();
input.disable_other_profiles = false;
input.external_target.as_mut().unwrap().id = Some("legacy-target".into());
input.profile.protocols = vec!["TCP".into()];
input.profile.items = vec![ProfileItemInput {
item_type: "folder".into(),
value: r"C:\Games".into(),
recursive: Some(false),
}];
run_apply(&fixture.storage, input, &RecordingHelper::success()).unwrap();
let profiles = fixture.storage.read_profiles().unwrap();
let targets = fixture.storage.read_targets().unwrap();
assert_eq!(profiles[0], before_profiles[0]);
assert_eq!(targets[0], before_targets[0]);
let edited = profiles.iter().find(|p| p.id == "main-profile").unwrap();
assert_ne!(edited.target_id, "legacy-target");
assert_eq!(edited.protocols, vec![Protocol::Tcp]);
assert!(!edited.items[0].recursive);
assert_eq!(
targets
.iter()
.find(|t| t.id == edited.target_id)
.unwrap()
.host,
"proxy.example.test"
);
}
#[test]
fn clearing_last_profile_requires_explicit_stop_and_does_not_call_helper() {
let fixture = ApplyFixture::new("clear-running");
fixture.seed_old_state();
let before = fixture.storage.read_profiles().unwrap();
let mut input = external_input();
input.profile.id = Some("legacy".into());
input.profile.target_id = "legacy-target".into();
input.profile.enabled = false;
input.profile.items.clear();
input.disable_other_profiles = false;
let helper = RecordingHelper::success();
let error = run_apply(&fixture.storage, input, &helper).unwrap_err();
assert_eq!(error.code(), "stop_before_clearing_route");
assert_eq!(helper.calls.get(), 0);
assert_eq!(fixture.storage.read_profiles().unwrap(), before);
}
fn external_input() -> ApplyConfigurationInput {
ApplyConfigurationInput {
expected_revision: None,
route_mode: ApplyRouteMode::External,
profile: profile_input(),
external_target: Some(TargetInput {
@@ -299,6 +367,7 @@ fn test_proxyfier() -> DetectedProxyfier {
running: true,
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("running".to_string()),
version: Some("2.2.1.0".to_string()),
}
}
+81
View File
@@ -0,0 +1,81 @@
use std::path::PathBuf;
#[cfg(windows)]
use proxywarden_lib::process::AuthenticodePublisher;
use proxywarden_lib::process::{verify_authenticode, AuthenticodeError};
#[cfg(windows)]
#[test]
fn bundled_windows_packet_filter_has_expected_trusted_publisher() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("bundled/components/windows-packet-filter/Windows.Packet.Filter.3.6.2.1.x64.msi");
let verification = verify_authenticode(path).expect("bundled MSI should be verifiable");
assert!(verification.is_trusted);
assert_eq!(
verification.publisher,
Some(AuthenticodePublisher {
common_name: "The Anti-Cloud Corporation".to_owned(),
organization: "The Anti-Cloud Corporation".to_owned(),
})
);
assert_eq!(verification.status_code, 0);
}
#[cfg(windows)]
#[test]
fn unsigned_regular_file_is_not_trusted() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let verification = verify_authenticode(path).expect("regular file should be inspectable");
assert!(!verification.is_trusted);
assert_eq!(verification.publisher, None);
assert_ne!(verification.status_code, 0);
}
#[cfg(windows)]
#[test]
fn reparse_target_is_rejected_when_symlink_creation_is_available() {
use std::{fs, os::windows::fs::symlink_file};
let root = std::env::temp_dir().join(format!(
"proxywarden-authenticode-test-{}",
uuid::Uuid::new_v4().simple()
));
fs::create_dir(&root).expect("test root should be creatable");
let link = root.join("linked-target.exe");
let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
if let Err(error) = symlink_file(target, &link) {
fs::remove_dir(&root).expect("test root should be removable");
if error.raw_os_error() == Some(1314) {
eprintln!("skipping reparse probe because this process lacks symlink privilege");
return;
}
panic!("test symlink creation failed: {error}");
}
let result = verify_authenticode(&link);
fs::remove_file(&link).expect("test symlink should be removable");
fs::remove_dir(&root).expect("test root should be removable");
assert_eq!(result, Err(AuthenticodeError::UnsafeTarget));
}
#[test]
fn missing_target_fails_closed() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("missing-signature-target.exe");
#[cfg(windows)]
assert_eq!(
verify_authenticode(path),
Err(AuthenticodeError::InvalidTarget)
);
#[cfg(not(windows))]
assert_eq!(
verify_authenticode(path),
Err(AuthenticodeError::UnsupportedPlatform)
);
}
+129 -414
View File
@@ -1,26 +1,24 @@
use proxywarden_lib::adapters::proxifyre::ProxiFyreAdapter;
use proxywarden_lib::commands::{
self, apply_profiles_with_services, apply_profiles_with_services_and_detection, build_status,
read_saved_state_with_proxifyre_config, resolve_component_statuses, resolve_preview,
save_profile_to_storage, save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper,
HelperApplyRequest, HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper,
TargetInputDto,
read_saved_state, resolve_component_statuses, resolve_preview, save_profile_to_storage,
save_target_to_storage, Clock, CommandError, DetectedProxyApplyHelper, HelperApplyRequest,
HelperApplyResult, ProfileInputDto, ProfileItemInputDto, ProxyApplyHelper, TargetInputDto,
};
use proxywarden_lib::component_detection::{
DetectedProxyfier, ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry,
};
use proxywarden_lib::models::{
self, ComponentId, ComponentState, ComponentStatus, Profile, ProfileItem, ProfileItemType,
Protocol, ProxyProtocol, Target, TargetKind,
self, ComponentId, ComponentState, Profile, ProfileItem, ProfileItemType, Protocol,
ProxyProtocol, Target, TargetKind,
};
use proxywarden_lib::proxifyre_ownership::ManagedProxiFyreOwnership;
#[cfg(windows)]
use proxywarden_lib::safe_fs;
use proxywarden_lib::storage::JsonStorage;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command as ProcessCommand;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
@@ -75,7 +73,7 @@ fn save_commands_normalize_and_persist_profile_and_target() {
}
#[test]
fn saved_state_bootstraps_from_existing_proxifyre_app_config() {
fn saved_state_read_never_opportunistically_imports_proxifyre_config() {
let root = test_root("proxifyre-config-import");
let storage = JsonStorage::new(root.clone());
let install_dir = root.join("ProxiFyre");
@@ -96,29 +94,19 @@ fn saved_state_bootstraps_from_existing_proxifyre_app_config() {
}"#,
)
.expect("write proxifyre config");
let source_before = fs::read(&config_path).expect("read proxifyre config before normal read");
let state = read_saved_state_with_proxifyre_config(&storage, Some(&config_path))
.expect("state should import proxifyre app config");
let state =
read_saved_state(&storage).expect("normal read should ignore legacy runtime config");
assert_eq!(state.profiles.len(), 1);
assert_eq!(state.targets.len(), 1);
assert_eq!(state.profiles[0].id, "main-profile");
assert_eq!(state.profiles[0].target_id, "main-proxy");
assert_eq!(state.profiles[0].items.len(), 2);
assert!(state.profiles.is_empty());
assert!(state.targets.is_empty());
assert!(!storage.paths().profiles_file.exists());
assert!(!storage.paths().targets_file.exists());
assert_eq!(
state.profiles[0].items[0].item_type,
ProfileItemType::Process
fs::read(&config_path).expect("read proxifyre config after normal read"),
source_before
);
assert_eq!(state.profiles[0].items[0].value, "Discord");
assert_eq!(state.profiles[0].items[1].item_type, ProfileItemType::Exe);
assert_eq!(state.profiles[0].items[1].value, r"C:\Games\Launcher.exe");
assert_eq!(state.targets[0].id, "main-proxy");
assert_eq!(state.targets[0].host, "127.0.0.1");
assert_eq!(state.targets[0].port, 1090);
let persisted = storage.read_profiles().expect("read persisted profiles");
assert_eq!(persisted.len(), 1);
assert_eq!(persisted[0].items.len(), 2);
cleanup(&root);
}
@@ -152,8 +140,7 @@ fn saved_state_keeps_existing_proxywarden_profiles_over_proxifyre_config() {
.write_targets(&[external_socks5_target()])
.expect("write targets");
let state = read_saved_state_with_proxifyre_config(&storage, Some(&config_path))
.expect("state should keep proxywarden storage");
let state = read_saved_state(&storage).expect("state should keep proxywarden storage");
assert_eq!(state.profiles.len(), 1);
assert_eq!(state.profiles[0].id, "discord");
@@ -218,318 +205,6 @@ fn ping_proxy_target_reports_open_tcp_endpoint() {
assert!(result.probes.is_empty());
}
#[test]
#[cfg(windows)]
fn proxifyre_install_script_parses_as_powershell() {
let root = test_root("proxifyre-install-script");
fs::create_dir_all(&root).expect("test root should be created");
let script = commands::wrap_elevated_package_script(
&commands::install_proxifyre_script(&root.join("proxifyre-app-config.json")),
&root.join("install.log"),
);
let script_path = root.join("install.ps1");
let mut script_bytes = vec![0xEF, 0xBB, 0xBF];
script_bytes.extend_from_slice(script.as_bytes());
fs::write(&script_path, script_bytes).expect("script should be written");
let escaped_path = script_path.display().to_string().replace('\'', "''");
let parser = format!(
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
);
let output = ProcessCommand::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
.output()
.expect("powershell parser should run");
assert!(
output.status.success(),
"install script should parse\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
cleanup(&root);
}
#[test]
fn proxifyre_install_script_uses_resilient_download_helpers() {
let root = test_root("proxifyre-install-script-downloads");
let script = commands::install_proxifyre_script(&root.join("proxifyre-app-config.json"));
assert!(script.contains("function Get-SafeUriForLog([string]$uri)"));
assert!(script.contains("function Invoke-ReleaseApi([string]$uri, [string]$label)"));
assert!(script.contains("function Resolve-ReleaseAsset("));
assert!(script.contains("function Get-PinnedWindowsPacketFilterAsset([string]$arch)"));
assert!(script.contains("function Get-PinnedProxiFyreAsset([string]$arch)"));
assert!(
script.contains("function Invoke-Download([string]$uri, [string]$path, [string]$label)")
);
assert!(script.contains("foreach ($attempt in 1..3)"));
assert!(script.contains("Invoke-WebClientDownload $uri $partialPath"));
assert!(script.contains("Invoke-CurlDownload $uri $partialPath"));
assert!(script.contains("--user-agent 'proxywarden' --output $partialPath --url $uri"));
assert!(script.contains("Move-Item -LiteralPath $partialPath -Destination $path -Force"));
assert!(script.contains("function Get-BundledAsset([string]$pattern, [string]$label)"));
assert!(script.contains("function Verify-BundledAssetHash([string]$path, [string]$label)"));
assert!(script
.contains("Invoke-Download $vcRedistUrl $vcRedistPath 'Microsoft Visual C++ Runtime'"));
assert!(script
.contains("Resolve-ReleaseAsset $ndisapiReleaseApi $ndisPattern 'Windows Packet Filter'"));
assert!(script.contains(
"Invoke-Download $ndisAsset.browser_download_url $ndisPath 'Windows Packet Filter'"
));
assert!(
script.contains("Resolve-ReleaseAsset $proxifyreReleaseApi $proxifyrePattern 'ProxiFyre'")
);
assert!(script.contains(
"Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath 'ProxiFyre'"
));
assert!(script.contains("github.com/wiresock/ndisapi/releases/download"));
assert!(script.contains("github.com/wiresock/proxifyre/releases/download"));
assert!(script.contains(
"[IO.File]::WriteAllText($markerPath, $markerJson, [Text.UTF8Encoding]::new($false))"
));
assert!(!script
.contains("ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $markerPath -Encoding UTF8"));
let packet_filter_step = script
.find("Write-ProxyWardenProgress 'install' 'packet-filter'")
.expect("packet filter install step should be present");
let vc_runtime_step = script
.find("Write-ProxyWardenProgress 'install' 'vc-runtime'")
.expect("runtime install step should be present");
let proxifyre_step = script
.find("Write-ProxyWardenProgress 'install' 'proxifyre'")
.expect("proxifyre install step should be present");
assert!(packet_filter_step < vc_runtime_step);
assert!(vc_runtime_step < proxifyre_step);
cleanup(&root);
}
#[test]
fn proxifyre_install_script_prefers_bundled_assets_before_downloads() {
let root = test_root("proxifyre-install-script-bundled-assets");
let bundle_dir = root.join("bundle");
let script = commands::install_proxifyre_script_with_bundle(
&root.join("proxifyre-app-config.json"),
Some(&bundle_dir),
);
assert!(script.contains(&format!(
"$bundledAssetDir = '{}'",
bundle_dir.display().to_string().replace('\'', "''")
)));
assert!(script.contains("$script:bundledAssetDir = [string]$bundledAssetDir"));
assert!(script.contains("function Get-BundledAssetDir"));
assert!(script.contains("$manifestPath = [IO.Path]::Combine($assetDir, 'manifest.json')"));
assert!(script.contains("$script:bundledAssetManifest = Get-BundledAssetManifest"));
assert!(script.contains("Copy-BundledAsset $bundledNdisPath $ndisPath"));
assert!(script.contains("Copy-BundledAsset $bundledVcPath $vcRedistPath"));
assert!(script.contains("Copy-BundledAsset $bundledProxiFyrePath $proxifyreZipPath"));
let bundled_ndis = script
.find("Get-BundledAsset $ndisPattern 'Windows Packet Filter'")
.expect("ndis bundle check should be present");
let online_ndis = script
.find("Resolve-ReleaseAsset $ndisapiReleaseApi $ndisPattern 'Windows Packet Filter'")
.expect("ndis online fallback should be present");
assert!(bundled_ndis < online_ndis);
let bundled_proxifyre = script
.find("Get-BundledAsset $proxifyrePattern 'ProxiFyre'")
.expect("proxifyre bundle check should be present");
let online_proxifyre = script
.find("Resolve-ReleaseAsset $proxifyreReleaseApi $proxifyrePattern 'ProxiFyre'")
.expect("proxifyre online fallback should be present");
assert!(bundled_proxifyre < online_proxifyre);
cleanup(&root);
}
#[test]
fn proxifyre_firewall_script_scopes_rules_to_managed_executable() {
let executable =
PathBuf::from(r"C:\Program Files\Proxy'Warden\components\ProxiFyre\ProxiFyre.exe");
let script = commands::configure_proxifyre_firewall_script(&executable);
assert!(script.contains(
"$exePath = 'C:\\Program Files\\Proxy''Warden\\components\\ProxiFyre\\ProxiFyre.exe'"
));
assert!(script.contains("ProxyWarden.ProxiFyre.Inbound"));
assert!(script.contains("ProxyWarden.ProxiFyre.Outbound"));
assert!(script.contains("-Program $exePath"));
assert!(script.contains("Get-NetFirewallRule -Name $rule.Name"));
assert!(!script.contains("Get-NetFirewallRule -DisplayName"));
}
#[test]
#[cfg(windows)]
fn proxifyre_firewall_script_parses_as_powershell() {
let root = test_root("proxifyre-firewall-script");
fs::create_dir_all(&root).expect("test root should be created");
let script = commands::configure_proxifyre_firewall_script(
&root.join("ProxiFyre").join("ProxiFyre.exe"),
);
let script_path = root.join("firewall.ps1");
fs::write(&script_path, script).expect("script should be written");
let escaped_path = script_path.display().to_string().replace('\'', "''");
let parser = format!(
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
);
let output = ProcessCommand::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
.output()
.expect("powershell parser should run");
assert!(
output.status.success(),
"firewall script should parse\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
cleanup(&root);
}
#[test]
#[cfg(windows)]
fn proxifyre_uninstall_script_parses_as_powershell() {
let root = test_root("proxifyre-uninstall-script");
fs::create_dir_all(&root).expect("test root should be created");
let detected = DetectedProxyfier {
engine: ProxyfierEngine::ProxiFyre,
name: "ProxiFyre".to_string(),
install_dir: root.join("ProxiFyre"),
executable_path: root.join("ProxiFyre").join("ProxiFyre.exe"),
config_path: Some(root.join("ProxiFyre").join("app-config.json")),
running: false,
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("stopped".to_string()),
};
let script = commands::wrap_elevated_package_script(
&commands::uninstall_proxifyre_script(Some(&detected), &managed_ownership(true)),
&root.join("uninstall.log"),
);
let script_path = root.join("uninstall.ps1");
let mut script_bytes = vec![0xEF, 0xBB, 0xBF];
script_bytes.extend_from_slice(script.as_bytes());
fs::write(&script_path, script_bytes).expect("script should be written");
let escaped_path = script_path.display().to_string().replace('\'', "''");
let parser = format!(
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
);
let output = ProcessCommand::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
.output()
.expect("powershell parser should run");
assert!(
output.status.success(),
"uninstall script should parse\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
cleanup(&root);
}
#[test]
fn proxifyre_uninstall_script_removes_packet_filter_after_proxifyre() {
let detected = DetectedProxyfier {
engine: ProxyfierEngine::ProxiFyre,
name: "ProxiFyre".to_string(),
install_dir: PathBuf::from(r"C:\Tools\ProxiFyre"),
executable_path: PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"),
config_path: Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")),
running: true,
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("running".to_string()),
};
let script = commands::uninstall_proxifyre_script(Some(&detected), &managed_ownership(true));
assert!(script.contains("function Find-ManagedProxiFyreService"));
assert!(script.contains("Get-CimInstance Win32_Service"));
assert!(script.contains("[StringComparison]::OrdinalIgnoreCase"));
assert!(!script.contains("function Find-ProxiFyreService"));
assert!(!script.contains("Where-Object { $_.Name -match 'ProxiFyre|Proxifyre'"));
assert!(script.contains("function Resolve-MsiProductCode($program, [string]$label)"));
assert!(script.contains("Отказываюсь запускать произвольный UninstallString"));
assert!(script.contains("Start-Process -FilePath 'msiexec.exe'"));
assert!(script.contains("ArgumentList @('/x', $productCode, '/qn', '/norestart'"));
assert!(script.contains("Uninstall-MsiProgram $packetFilter 'Windows Packet Filter'"));
assert!(script.contains("ProxyWarden.ProxiFyre.Inbound"));
assert!(script.contains("ProxyWarden.ProxiFyre.Outbound"));
let proxifyre_step = script
.find("Write-ProxyWardenProgress 'uninstall' 'proxifyre'")
.expect("proxifyre uninstall step should be present");
let packet_filter_step = script
.find("Write-ProxyWardenProgress 'uninstall' 'packet-filter'")
.expect("packet filter uninstall step should be present");
assert!(proxifyre_step < packet_filter_step);
}
#[test]
fn proxywarden_uninstall_hook_removes_only_managed_firewall_rules() {
let script = include_str!("../bundled/cleanup/uninstall-managed-components.ps1");
assert!(script.contains("ProxyWarden.ProxiFyre.Inbound"));
assert!(script.contains("ProxyWarden.ProxiFyre.Outbound"));
assert!(script.contains("Get-NetFirewallRule -Name $name"));
assert!(!script.contains("Get-NetFirewallRule -DisplayName"));
}
#[test]
fn proxifyre_uninstall_script_leaves_shared_packet_filter_installed() {
let detected = DetectedProxyfier {
engine: ProxyfierEngine::ProxiFyre,
name: "ProxiFyre".to_string(),
install_dir: PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre"),
executable_path: PathBuf::from(
r"C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe",
),
config_path: None,
running: false,
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("stopped".to_string()),
};
let script = commands::uninstall_proxifyre_script(Some(&detected), &managed_ownership(false));
assert!(script.contains("$removePacketFilter = $false"));
assert!(script.contains("if ($removePacketFilter)"));
assert!(script.contains("Windows Packet Filter оставлен"));
assert!(!script.contains("Get-Process -Name 'ProxiFyre'"));
}
#[test]
fn singbox_runner_preserves_installer_args_with_spaces() {
let script = commands::singbox_installer_runner_script(
Path::new(r"C:\ProgramData\ProxyWarden\state\install-singbox.ps1"),
Path::new(r"C:\ProgramData\ProxyWarden\state\install.log"),
&[
"-InstallRoot".to_string(),
r"C:\Program Files\ProxyWarden\components\sing-box".to_string(),
"-ServiceName".to_string(),
"ProxyWardenSingBox".to_string(),
"-Uninstall".to_string(),
],
);
assert!(script.contains(
"$installerArgs = @('-InstallRoot', 'C:\\Program Files\\ProxyWarden\\components\\sing-box'"
));
assert!(script.contains(
"& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs"
));
assert!(
!script.contains("Start-Process -FilePath 'powershell.exe' -ArgumentList $argumentList")
);
}
#[test]
fn apply_generates_derived_config_and_records_activity_with_mock_helper() {
let root = test_root("apply");
@@ -540,11 +215,6 @@ fn apply_generates_derived_config_and_records_activity_with_mock_helper() {
storage
.write_targets(&[external_socks5_target()])
.expect("write targets");
write_json(
&storage.paths().components_file,
&[proxyfier_running(), singbox_missing()],
);
let response = apply_profiles_with_services(
&storage,
&ProxiFyreAdapter::default(),
@@ -566,6 +236,9 @@ fn apply_generates_derived_config_and_records_activity_with_mock_helper() {
assert!(generated_contents.contains("\"appNames\""));
assert!(generated_contents.contains("Discord"));
assert!(generated_path.ends_with("proxifyre-app-config.json"));
#[cfg(windows)]
safe_fs::verify_path_protected_for_owner_admin_system(&generated_path)
.expect("generated ProxiFyre config keeps restricted ACL");
assert_eq!(activity.len(), 1);
assert_eq!(activity[0].at, "2026-07-03T00:00:00Z");
assert_eq!(activity[0].title, "Конфиг ProxiFyre создан");
@@ -583,8 +256,6 @@ fn apply_blocks_local_singbox_target_when_component_is_missing() {
storage
.write_targets(&[local_singbox_target()])
.expect("write targets");
write_json(&storage.paths().components_file, &[singbox_missing()]);
let error = apply_profiles_with_services_and_detection(
&storage,
&ProxiFyreAdapter::default(),
@@ -607,7 +278,6 @@ fn apply_blocks_local_singbox_target_when_component_is_missing() {
#[test]
fn component_status_merges_detected_existing_proxifyre() {
let components = resolve_component_statuses(
Vec::new(),
Some(DetectedProxyfier {
engine: ProxyfierEngine::ProxiFyre,
name: "ProxiFyre".to_string(),
@@ -617,6 +287,7 @@ fn component_status_merges_detected_existing_proxifyre() {
running: true,
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("running".to_string()),
version: Some("2.2.1.0".to_string()),
}),
None,
);
@@ -633,8 +304,8 @@ fn component_status_merges_detected_existing_proxifyre() {
}
#[test]
fn component_status_does_not_keep_stale_installed_state_when_detection_is_missing() {
let components = resolve_component_statuses(vec![proxyfier_running()], None, None);
fn component_status_reports_missing_when_detection_is_missing() {
let components = resolve_component_statuses(None, None);
let proxyfier = components
.iter()
.find(|component| component.id == ComponentId::Proxyfier)
@@ -647,18 +318,37 @@ fn component_status_does_not_keep_stale_installed_state_when_detection_is_missin
}
#[test]
fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
fn managed_current_apply_stages_generated_config_without_writing_sealed_runtime_snapshot() {
let root = test_root("detected-proxifyre");
let install_dir = root.join("ProxiFyre");
fs::create_dir_all(&install_dir).expect("install dir");
fs::write(install_dir.join("ProxiFyre.exe"), "mock exe").expect("mock exe");
fs::write(install_dir.join("app-config.json"), "{}").expect("existing config");
fs::write(
install_dir.join("proxywarden-component.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService",
"installRoot": install_dir.display().to_string(),
"packetFilterInstalledByProxyWarden": false
}))
.expect("marker JSON"),
)
.expect("managed marker");
let generated_config = root.join("generated").join("proxifyre-app-config.json");
let host = DetectionHost::new()
.with_registry("ProxiFyre", &install_dir)
.with_path(&install_dir)
.with_path(&install_dir.join("ProxiFyre.exe"));
let helper = DetectedProxyApplyHelper::from(host);
.with_path(&install_dir.join("ProxiFyre.exe"))
.with_service_path(
"ProxiFyreService",
&format!(
r#""{}" --service"#,
install_dir.join("ProxiFyre.exe").display()
),
);
let helper = DetectedProxyApplyHelper::with_current_root(host, install_dir.clone());
let result = helper
.apply_proxy_config(HelperApplyRequest {
@@ -668,17 +358,61 @@ fn detected_proxy_apply_helper_writes_proxifyre_app_config() {
})
.expect("detected helper should apply");
let applied =
fs::read_to_string(install_dir.join("app-config.json")).expect("read applied app-config");
let backup =
fs::read_to_string(install_dir.join("app-config.json.bak")).expect("read backup config");
assert!(result.success);
assert!(result.changed);
assert_eq!(result.action, "proxifyre.apply-detected-config");
assert_eq!(applied, r#"{"proxies":[]}"#);
assert_eq!(backup, "{}");
assert!(install_dir.join("app-config.json.bak").exists());
assert_eq!(result.action, "proxifyre.stage-managed-config");
assert_eq!(
fs::read_to_string(install_dir.join("app-config.json"))
.expect("read unchanged runtime snapshot"),
"{}"
);
assert!(!install_dir.join("app-config.json.bak").exists());
assert!(result.message.contains("следующем явном запуске"));
cleanup(&root);
}
#[test]
fn detected_proxy_apply_helper_does_not_write_for_foreign_service_collision() {
let root = test_root("detected-proxifyre-foreign-service");
let install_dir = root.join("ProxiFyre");
let config_path = install_dir.join("app-config.json");
fs::create_dir_all(&install_dir).expect("install dir");
fs::write(install_dir.join("ProxiFyre.exe"), "mock exe").expect("mock exe");
fs::write(&config_path, "original").expect("existing config");
fs::write(
install_dir.join("proxywarden-component.json"),
serde_json::to_vec_pretty(&serde_json::json!({
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService",
"installRoot": install_dir.display().to_string(),
"packetFilterInstalledByProxyWarden": false
}))
.expect("marker JSON"),
)
.expect("managed marker");
let generated_config = root.join("generated").join("proxifyre-app-config.json");
let host = DetectionHost::new()
.with_path(&install_dir)
.with_path(&install_dir.join("ProxiFyre.exe"))
.with_service_path(
"ProxiFyreService",
r#""C:\Foreign\ProxiFyre.exe" --service"#,
);
let helper = DetectedProxyApplyHelper::with_current_root(host, install_dir.clone());
let error = helper
.apply_proxy_config(HelperApplyRequest {
adapter_id: "proxifyre",
config_path: &generated_config,
config_contents: r#"{"proxies":[]}"#,
})
.expect_err("foreign service collision must fail before config write");
assert_eq!(error.code, "ownership_mismatch");
assert_eq!(fs::read_to_string(&config_path).unwrap(), "original");
assert!(!install_dir.join("app-config.json.bak").exists());
cleanup(&root);
}
@@ -746,6 +480,7 @@ impl Clock for FixedClock {
struct DetectionHost {
paths: HashSet<String>,
registry: Vec<RegistryInstallEntry>,
service_paths: HashMap<String, String>,
}
impl DetectionHost {
@@ -766,6 +501,12 @@ impl DetectionHost {
});
self
}
fn with_service_path(mut self, service_name: &str, path_name: &str) -> Self {
self.service_paths
.insert(service_name.to_ascii_lowercase(), path_name.to_string());
self
}
}
impl ProxyfierDetectionHost for DetectionHost {
@@ -782,12 +523,33 @@ impl ProxyfierDetectionHost for DetectionHost {
}
fn service_status(&self, _service_name: &str) -> Option<String> {
None
self.service_paths
.contains_key(&_service_name.to_ascii_lowercase())
.then(|| "stopped".to_string())
}
fn service_info(
&self,
service_name: &str,
) -> Option<proxywarden_lib::component_detection::DetectedService> {
self.service_paths
.get(&service_name.to_ascii_lowercase())
.map(
|path_name| proxywarden_lib::component_detection::DetectedService {
name: service_name.to_string(),
status: "stopped".to_string(),
path_name: Some(path_name.clone()),
},
)
}
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
self.registry.clone()
}
fn read_text(&self, path: &Path) -> Option<String> {
fs::read_to_string(path).ok()
}
}
fn normalize_path(path: &Path) -> String {
@@ -810,14 +572,6 @@ fn cleanup(root: &Path) {
let _ = fs::remove_dir_all(root);
}
fn write_json<T: serde::Serialize + ?Sized>(path: &Path, value: &T) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create json parent dir");
}
let contents = serde_json::to_vec_pretty(value).expect("serialize json");
fs::write(path, contents).expect("write json");
}
fn discord_profile(target_id: &str) -> Profile {
Profile {
id: "discord".to_string(),
@@ -856,42 +610,3 @@ fn local_singbox_target() -> Target {
requires_component: Some(ComponentId::Singbox),
}
}
fn proxyfier_running() -> ComponentStatus {
ComponentStatus {
id: ComponentId::Proxyfier,
name: "ProxiFyre".to_string(),
state: ComponentState::Running,
installed: true,
running: true,
version: Some("2.2.1".to_string()),
path: Some(r"C:\Tools\ProxiFyre".to_string()),
service_name: Some("ProxiFyreService".to_string()),
service_status: Some("running".to_string()),
problems: Vec::new(),
actions: vec!["Restart".to_string()],
}
}
fn singbox_missing() -> ComponentStatus {
ComponentStatus {
id: ComponentId::Singbox,
name: "Локальный sing-box".to_string(),
state: ComponentState::Missing,
installed: false,
running: false,
version: None,
path: None,
service_name: Some("ProxyWardenSingBox".to_string()),
service_status: None,
problems: vec!["Локальный sing-box не установлен".to_string()],
actions: vec!["Установить локальный sing-box".to_string()],
}
}
fn managed_ownership(remove_packet_filter: bool) -> ManagedProxiFyreOwnership {
ManagedProxiFyreOwnership {
service_name: "ProxiFyreService".to_string(),
remove_packet_filter,
}
}
+640
View File
@@ -0,0 +1,640 @@
use proxywarden_lib::component_catalog::{
parse_bundled_catalog_if_present, parse_catalog, validate_bundle, AssetArch, ComponentId,
TargetArch,
};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command;
use uuid::Uuid;
#[test]
fn parses_exact_x64_catalog_and_all_trust_policy_variants() {
let catalog = parse_value(&valid_catalog()).expect("valid catalog must parse");
assert_eq!(catalog.target_arch, TargetArch::X64);
assert_eq!(catalog.components.len(), 5);
assert_eq!(
catalog
.components
.iter()
.find(|component| component.id == ComponentId::Winsw)
.expect("WinSW entry")
.asset_arch,
AssetArch::Anycpu
);
}
#[test]
fn rejects_unknown_schema_arch_fields_duplicates_and_incomplete_set() {
assert_rejected(mutate(|catalog| catalog["schemaVersion"] = json!(2)));
assert_rejected(mutate(|catalog| catalog["targetArch"] = json!("arm64")));
assert_rejected(mutate(|catalog| catalog["unexpected"] = json!(true)));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["unexpected"] = json!(true);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["sourceUrl"] = json!(
"https://github.com/attacker/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi"
);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["sourceUrl"] =
json!("https://attacker.example/vc_redist.x64.exe");
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] =
json!(["attacker.example"]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["license"]["unexpected"] = json!(true);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["unexpected"] = json!(true);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["id"] = json!("proxifyre");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["installRole"] = json!("proxifyre-runtime");
}));
assert_rejected(mutate(|catalog| {
catalog["components"]
.as_array_mut()
.expect("components array")
.pop();
}));
}
#[test]
fn rejects_wrong_component_role_or_architecture() {
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["installRole"] = json!("packet-filter-driver");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["assetArch"] = json!("anycpu");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "winsw")["assetArch"] = json!("x64");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "winsw")["effectiveTarget"] = json!("anycpu");
}));
}
#[test]
fn rejects_unsafe_paths_hash_size_license_version_and_source() {
for invalid_path in [
"../asset.zip",
"proxifyre/../asset.zip",
"proxifyre\\asset.zip",
"/proxifyre/asset.zip",
"proxifyre/CON.zip",
"other/asset.zip",
] {
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["assetPath"] = json!(invalid_path);
}));
}
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["license"]["path"] = json!("../LICENSE.txt");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["license"]["path"] =
json!("proxifyre/WPF-LICENSE.txt");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["license"]["path"] =
json!("proxifyre/LICENSE.txt");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["license"]["id"] = json!("GPL 3");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["sha256"] = json!("A".repeat(64));
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["sha256"] = json!("a".repeat(63));
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["size"] = json!(0);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["version"] = json!("2.4.0-beta.1");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "winsw")["productVersion"] = json!("2.12.0-rc.1");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["sourceUrl"] = json!(
"http://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip"
);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["sourceUrl"] = json!(
"https://user:secret@github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip"
);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["sourceUrl"] =
json!("https://github.com/wiresock/proxifyre/releases/download/v2.4.0/wrong.zip");
}));
}
#[test]
fn rejects_invalid_trust_policies() {
for id in ["proxifyre", "windows-packet-filter", "sing-box"] {
assert_rejected(mutate(|catalog| {
component_mut(catalog, id)["updateTrustPolicy"] = json!({
"type": "bundledOnlyNoIndependentProof",
"reason": "Wrong policy for this component."
});
}));
}
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"] = json!({
"type": "bundledOnlyNoIndependentProof",
"reason": "Wrong policy for this component."
});
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "winsw")["updateTrustPolicy"] = json!({
"type": "githubReleaseDigest",
"repository": "winsw/winsw",
"tagPattern": "v*",
"assetPattern": "WinSW.NET461.exe",
"requireStable": true
});
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["requireStable"] = json!(false);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] =
json!("attacker/proxifyre");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v**");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] =
json!([]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] = json!([]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] = json!([" "]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] =
json!("other.exe");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "winsw")["updateTrustPolicy"]["type"] = json!("unknownPolicy");
}));
}
#[test]
fn rejects_component_policy_allowlist_expansion() {
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["repository"] =
json!("Wiresock/proxifyre");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["tagPattern"] = json!("v2.*");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["assetPattern"] = json!("*");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "proxifyre")["updateTrustPolicy"]["authenticodePublishers"] =
Value::Null;
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"]["assetPattern"] =
json!("Windows.Packet.Filter.*");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "windows-packet-filter")["updateTrustPolicy"]
["authenticodePublishers"] =
json!(["The Anti-Cloud Corporation", "Unexpected Publisher"]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "sing-box")["updateTrustPolicy"]["repository"] =
json!("sagernet/sing-box");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "sing-box")["updateTrustPolicy"]["authenticodePublishers"] =
json!(["Unexpected Publisher"]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["allowedSourceHosts"] =
json!(["aka.ms", "attacker.example"]);
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["assetPattern"] = json!("*");
}));
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["updateTrustPolicy"]["publishers"] =
json!(["Microsoft Corporation", "Unexpected Publisher"]);
}));
}
#[test]
fn rejects_wrong_component_license_ids() {
for (id, wrong_license) in [
("proxifyre", "MIT"),
("windows-packet-filter", "GPL-3.0-only"),
("vc-runtime", "LicenseRef-Microsoft-VCRedist"),
("sing-box", "GPL-3.0-or-later"),
("winsw", "AGPL-3.0-only"),
] {
assert_rejected(mutate(|catalog| {
component_mut(catalog, id)["license"]["id"] = json!(wrong_license);
}));
}
}
#[test]
fn rejects_unpinned_or_wrong_vc_runtime_source() {
for source in [
"https://aka.ms/vs/17/release/vc_redist.x64.exe",
"https://aka.ms/vs/18/release/vc_redist.x64.exe",
"https://aka.ms/vs/18/release/14.50.35719/VC_redist.x64.exe",
] {
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["sourceUrl"] = json!(source);
}));
}
assert_rejected(mutate(|catalog| {
component_mut(catalog, "vc-runtime")["version"] = json!("14.50.35719.0");
}));
}
#[test]
fn validates_exact_bundle_contents_hashes_sizes_and_licenses() {
let bundle = TestBundle::new();
let catalog = validate_bundle(bundle.path()).expect("complete bundle must validate");
assert_eq!(catalog.components.len(), 5);
}
#[test]
fn rejects_missing_extra_or_changed_package_assets() {
let missing = TestBundle::new();
fs::remove_file(missing.path().join(asset_path("proxifyre"))).expect("remove fixture asset");
assert!(validate_bundle(missing.path()).is_err());
let extra = TestBundle::new();
fs::write(extra.path().join("unexpected.bin"), b"extra").expect("write extra file");
assert!(validate_bundle(extra.path()).is_err());
let changed = TestBundle::new();
let path = changed.path().join(asset_path("proxifyre"));
let original = fs::read(&path).expect("read fixture asset");
fs::write(&path, vec![b'x'; original.len()]).expect("change fixture asset");
assert!(validate_bundle(changed.path()).is_err());
let wrong_size = TestBundle::new();
let mut catalog: Value = serde_json::from_slice(
&fs::read(wrong_size.path().join("catalog.json")).expect("read fixture catalog"),
)
.expect("parse fixture catalog");
component_mut(&mut catalog, "proxifyre")["size"] = json!(999);
write_catalog(wrong_size.path(), &catalog);
assert!(validate_bundle(wrong_size.path()).is_err());
}
#[test]
fn rejects_missing_or_empty_license_copy() {
let missing = TestBundle::new();
fs::remove_file(missing.path().join("proxifyre/LICENSE.txt")).expect("remove fixture license");
assert!(validate_bundle(missing.path()).is_err());
let empty = TestBundle::new();
fs::write(empty.path().join("proxifyre/LICENSE.txt"), b"").expect("empty fixture license");
assert!(validate_bundle(empty.path()).is_err());
}
#[test]
fn optional_bundle_parse_is_none_only_when_catalog_is_absent() {
let absent = TempDirectory::new();
assert!(parse_bundled_catalog_if_present(absent.path())
.expect("absent catalog is allowed")
.is_none());
let present = TestBundle::new();
assert!(parse_bundled_catalog_if_present(present.path())
.expect("present catalog must validate")
.is_some());
}
#[test]
fn production_bundle_validates_when_catalog_exists() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("bundled")
.join("components");
let catalog = validate_bundle(&root).expect("production component bundle must validate");
assert_eq!(catalog.components.len(), 5);
}
#[cfg(windows)]
#[test]
fn rejects_reparse_bundle_root_and_nested_directory() {
let target = TestBundle::new();
let junctions = TempDirectory::new();
let root_junction = junctions.path().join("bundle-root-junction");
let root_guard = create_junction(&root_junction, target.path());
assert!(validate_bundle(&root_junction).is_err());
drop(root_guard);
let nested = TestBundle::new();
let proxifyre_target = junctions.path().join("proxifyre-target");
fs::rename(nested.path().join("proxifyre"), &proxifyre_target)
.expect("move fixture component behind a junction");
let nested_guard = create_junction(&nested.path().join("proxifyre"), &proxifyre_target);
assert!(validate_bundle(nested.path()).is_err());
drop(nested_guard);
}
fn valid_catalog() -> Value {
json!({
"schemaVersion": 1,
"targetArch": "x64",
"components": [
component(
"proxifyre",
"2.4.0",
"ProxiFyre-v2.4.0-x64-signed.zip",
"x64",
"https://github.com/wiresock/proxifyre/releases/download/v2.4.0/ProxiFyre-v2.4.0-x64-signed.zip",
json!({
"type": "githubReleaseDigest",
"repository": "wiresock/proxifyre",
"tagPattern": "v*",
"assetPattern": "ProxiFyre-v*-x64-signed.zip",
"requireStable": true,
"authenticodePublishers": ["The Anti-Cloud Corporation"]
})
),
component(
"windows-packet-filter",
"3.6.2",
"Windows.Packet.Filter.3.6.2.1.x64.msi",
"x64",
"https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi",
json!({
"type": "githubReleaseDigest",
"repository": "wiresock/ndisapi",
"tagPattern": "v*",
"assetPattern": "Windows.Packet.Filter.*.x64.msi",
"requireStable": true,
"authenticodePublishers": ["The Anti-Cloud Corporation"]
})
),
component(
"vc-runtime",
"14.51.36247.0",
"VC_redist.x64.exe",
"x64",
"https://aka.ms/vs/18/release/14.51.36247/VC_redist.x64.exe",
json!({
"type": "buildTimeOnlyAuthenticode",
"allowedSourceHosts": ["aka.ms"],
"assetPattern": "VC_redist.x64.exe",
"publishers": ["Microsoft Corporation"]
})
),
component(
"sing-box",
"1.13.19",
"sing-box-1.13.19-windows-amd64.zip",
"x64",
"https://github.com/SagerNet/sing-box/releases/download/v1.13.19/sing-box-1.13.19-windows-amd64.zip",
json!({
"type": "githubReleaseDigest",
"repository": "SagerNet/sing-box",
"tagPattern": "v*",
"assetPattern": "sing-box-*-windows-amd64.zip",
"requireStable": true
})
),
component(
"winsw",
"2.12.0",
"WinSW.NET461.exe",
"anycpu",
"https://github.com/winsw/winsw/releases/download/v2.12.0/WinSW.NET461.exe",
json!({
"type": "bundledOnlyNoIndependentProof",
"reason": "Upstream provides no independent digest or Authenticode proof for this asset."
})
)
]
})
}
fn component(
id: &str,
version: &str,
asset_name: &str,
asset_arch: &str,
source_url: &str,
update_trust_policy: Value,
) -> Value {
let bytes = asset_bytes(id);
let (license_id, install_role) = match id {
"proxifyre" => ("AGPL-3.0-only", "proxifyre-runtime"),
"windows-packet-filter" => ("MIT", "packet-filter-driver"),
"vc-runtime" => (
"LicenseRef-Microsoft-Visual-Cpp-v14-Redistributable-2026",
"vc-runtime-prerequisite",
),
"sing-box" => ("LicenseRef-Sing-Box-Project", "sing-box-runtime"),
"winsw" => ("MIT", "sing-box-service-wrapper"),
_ => panic!("unknown fixture component"),
};
json!({
"id": id,
"version": version,
"fileVersion": if id == "windows-packet-filter" { "3.6.2.1" } else { version },
"productVersion": match id {
"windows-packet-filter" => "3.6.2.1",
"winsw" => "2.12.0+eef5c6a",
_ => version
},
"assetPath": format!("{id}/{asset_name}"),
"assetArch": asset_arch,
"effectiveTarget": "x64",
"sha256": sha256(bytes),
"size": bytes.len(),
"sourceUrl": source_url,
"license": {
"id": license_id,
"path": format!("{id}/LICENSE.txt")
},
"installRole": install_role,
"updateTrustPolicy": update_trust_policy
})
}
fn asset_bytes(id: &str) -> &'static [u8] {
match id {
"proxifyre" => b"fixture-proxifyre-asset",
"windows-packet-filter" => b"fixture-packet-filter-asset",
"vc-runtime" => b"fixture-vc-runtime-asset",
"sing-box" => b"fixture-sing-box-asset",
"winsw" => b"fixture-winsw-asset",
_ => panic!("unknown fixture component"),
}
}
fn asset_path(id: &str) -> String {
valid_catalog()["components"]
.as_array()
.expect("components array")
.iter()
.find(|component| component["id"] == id)
.expect("fixture component")["assetPath"]
.as_str()
.expect("asset path")
.to_string()
}
fn sha256(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
fn mutate(change: impl FnOnce(&mut Value)) -> Value {
let mut catalog = valid_catalog();
change(&mut catalog);
catalog
}
fn component_mut<'a>(catalog: &'a mut Value, id: &str) -> &'a mut Value {
catalog["components"]
.as_array_mut()
.expect("components array")
.iter_mut()
.find(|component| component["id"] == id)
.expect("fixture component")
}
fn parse_value(
value: &Value,
) -> Result<
proxywarden_lib::component_catalog::ComponentCatalog,
proxywarden_lib::component_catalog::ComponentCatalogError,
> {
parse_catalog(&serde_json::to_vec(value).expect("serialize fixture catalog"))
}
fn assert_rejected(value: Value) {
assert!(
parse_value(&value).is_err(),
"catalog unexpectedly passed: {value}"
);
}
fn write_catalog(root: &Path, catalog: &Value) {
fs::write(
root.join("catalog.json"),
serde_json::to_vec_pretty(catalog).expect("serialize fixture catalog"),
)
.expect("write fixture catalog");
}
struct TestBundle {
directory: TempDirectory,
}
impl TestBundle {
fn new() -> Self {
let directory = TempDirectory::new();
let catalog = valid_catalog();
for component in catalog["components"].as_array().expect("components array") {
let id = component["id"].as_str().expect("component id");
let asset_path = component["assetPath"].as_str().expect("asset path");
let license_path = component["license"]["path"].as_str().expect("license path");
fs::create_dir_all(
directory
.path()
.join(asset_path)
.parent()
.expect("asset parent"),
)
.expect("create component directory");
fs::write(directory.path().join(asset_path), asset_bytes(id))
.expect("write fixture asset");
fs::write(
directory.path().join(license_path),
format!("License fixture for {id}\n"),
)
.expect("write fixture license");
}
write_catalog(directory.path(), &catalog);
Self { directory }
}
fn path(&self) -> &Path {
self.directory.path()
}
}
struct TempDirectory {
path: PathBuf,
}
impl TempDirectory {
fn new() -> Self {
let path = std::env::temp_dir().join(format!(
"proxywarden-component-catalog-test-{}",
Uuid::new_v4()
));
fs::create_dir_all(&path).expect("create temporary test directory");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TempDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
#[cfg(windows)]
struct JunctionGuard {
path: PathBuf,
}
#[cfg(windows)]
impl Drop for JunctionGuard {
fn drop(&mut self) {
let _ = fs::remove_dir(&self.path);
}
}
#[cfg(windows)]
fn create_junction(path: &Path, target: &Path) -> JunctionGuard {
let output = Command::new("cmd")
.args(["/d", "/c", "mklink", "/J"])
.arg(path)
.arg(target)
.output()
.expect("run mklink for reparse-point fixture");
assert!(
output.status.success(),
"mklink failed: {}",
String::from_utf8_lossy(&output.stderr)
);
JunctionGuard {
path: path.to_path_buf(),
}
}
File diff suppressed because it is too large Load Diff
+456 -42
View File
@@ -1,7 +1,15 @@
#[cfg(windows)]
use proxywarden_lib::component_detection::SystemProxyfierDetectionHost;
use proxywarden_lib::component_detection::{
detect_proxyfier_install_with_host, detect_singbox_install_with_host,
proxyfier_component_from_detection, singbox_component_from_detection, ProxyfierDetectionHost,
ProxyfierEngine, RegistryInstallEntry,
has_additional_matching_legacy_proxifyre_service_with_host, inventory_proxyfier_with_host,
inventory_singbox_with_host, matches_legacy_proxifyre_2_2_1_manifest,
proxyfier_component_from_detection, proxyfier_component_from_inventory,
service_executable_from_path_name, singbox_component_from_detection, LegacyPackageFileIdentity,
ProxyfierDetectionHost, ProxyfierEngine, RegistryInstallEntry, LEGACY_PROXIFYRE_2_2_1_MANIFEST,
};
use proxywarden_lib::component_inventory::{
BinaryIdentityEvidence, ComponentClassification, OWNERSHIP_MISMATCH,
};
use proxywarden_lib::models::ComponentState;
use std::{
@@ -18,7 +26,9 @@ fn detects_existing_proxifyre_from_registry_install_location() {
.with_service_path(
"ProxiFyreService",
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
);
)
.with_known_binary(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
.with_version(r"C:\Tools\ProxiFyre\ProxiFyre.exe", "2.2.1.0");
let detected = detect_proxyfier_install_with_host(&host)
.expect("existing ProxiFyre install should be detected");
@@ -32,6 +42,7 @@ fn detects_existing_proxifyre_from_registry_install_location() {
assert!(detected.running);
assert_eq!(detected.service_name, Some("ProxiFyreService".to_string()));
assert_eq!(detected.service_status, Some("running".to_string()));
assert_eq!(detected.version, Some("2.2.1.0".to_string()));
let component = proxyfier_component_from_detection(Some(&detected));
assert_eq!(component.state, ComponentState::Running);
@@ -40,9 +51,72 @@ fn detects_existing_proxifyre_from_registry_install_location() {
assert_eq!(component.path, Some(r"C:\Tools\ProxiFyre".to_string()));
assert_eq!(component.service_name, Some("ProxiFyreService".to_string()));
assert_eq!(component.service_status, Some("running".to_string()));
assert_eq!(component.version, Some("2.2.1.0".to_string()));
assert!(component.problems.is_empty());
}
#[test]
fn detects_current_proxifyre_only_with_strong_marker_and_exact_service_path() {
let root = r"C:\Program Files\ProxyWarden\components\ProxiFyre";
let executable = r"C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe";
let marker = serde_json::json!({
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService",
"installRoot": root,
"packetFilterInstalledByProxyWarden": false
})
.to_string();
let host = MockHost::new()
.with_path(root)
.with_path(executable)
.with_text(
r"C:\Program Files\ProxyWarden\components\ProxiFyre\proxywarden-component.json",
&marker,
)
.with_stopped_service_path(
"ProxiFyreService",
r#""C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe" --service"#,
)
.with_version(executable, "2.4.0.0");
let detected = detect_proxyfier_install_with_host(&host).expect("managed current ProxiFyre");
assert_eq!(detected.install_dir, PathBuf::from(root));
assert_eq!(detected.version, Some("2.4.0.0".to_string()));
}
#[test]
fn current_proxifyre_with_foreign_same_name_service_is_ownership_mismatch() {
let root = r"C:\Program Files\ProxyWarden\components\ProxiFyre";
let executable = r"C:\Program Files\ProxyWarden\components\ProxiFyre\ProxiFyre.exe";
let marker = serde_json::json!({
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService",
"installRoot": root,
"packetFilterInstalledByProxyWarden": false
})
.to_string();
let host = MockHost::new()
.with_path(root)
.with_path(executable)
.with_text(
r"C:\Program Files\ProxyWarden\components\ProxiFyre\proxywarden-component.json",
&marker,
)
.with_service_path(
"ProxiFyreService",
r#""C:\Foreign\ProxiFyre.exe" --service"#,
);
let inventory = inventory_proxyfier_with_host(&host);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
assert_eq!(
inventory.selected_candidate().unwrap().issues[0].code,
OWNERSHIP_MISMATCH
);
}
#[test]
fn ignores_empty_common_proxifyre_folder_without_executable() {
let host = MockHost::new().with_path(r"C:\Tools\ProxiFyre");
@@ -65,19 +139,14 @@ fn ignores_plain_proxifier_install() {
}
#[test]
fn env_override_can_point_to_portable_proxifyre_install() {
fn env_override_does_not_make_portable_proxifyre_managed() {
let host = MockHost::new()
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"D:\Portable\ProxiFyre")
.with_path(r"D:\Portable\ProxiFyre\ProxiFyre.exe");
let detected = detect_proxyfier_install_with_host(&host)
.expect("env override should be checked before common paths");
assert_eq!(detected.engine, ProxyfierEngine::ProxiFyre);
assert_eq!(
detected.executable_path,
PathBuf::from(r"D:\Portable\ProxiFyre\ProxiFyre.exe")
);
assert!(detect_proxyfier_install_with_host(&host).is_none());
let inventory = inventory_proxyfier_with_host(&host);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
}
#[test]
@@ -88,7 +157,8 @@ fn reports_stopped_proxifyre_service_when_executable_exists() {
.with_stopped_service_path(
"ProxiFyreService",
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
);
)
.with_known_binary(r"C:\Tools\ProxiFyre\ProxiFyre.exe");
let detected =
detect_proxyfier_install_with_host(&host).expect("proxifyre executable should be detected");
@@ -102,6 +172,45 @@ fn reports_stopped_proxifyre_service_when_executable_exists() {
assert!(component.problems.is_empty());
}
#[test]
fn primary_and_alias_same_root_remain_discoverable_but_cutover_is_ambiguous() {
let root = Path::new(r"C:\Tools\ProxiFyre");
let executable = root.join("ProxiFyre.exe");
let host = MockHost::new()
.with_path(executable.to_str().expect("fixture path"))
.with_service_path(
"ProxiFyreService",
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService"#,
)
.with_service_path(
"ProxiFyre",
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
)
.with_known_binary(executable.to_str().expect("fixture path"));
assert_eq!(
inventory_proxyfier_with_host(&host).classification(),
ComponentClassification::ManagedLegacy,
"Task 5 discovery/Start/Stop classification stays unchanged"
);
assert!(has_additional_matching_legacy_proxifyre_service_with_host(
&host, root
));
}
#[test]
fn any_present_alias_service_makes_the_strict_service_set_ambiguous() {
let root = Path::new(r"C:\Tools\ProxiFyre");
for host in [
MockHost::new().with_service("ProxiFyre"),
MockHost::new().with_service_path("ProxiFyre", r#""C:\Foreign\ProxiFyre.exe" --service"#),
] {
assert!(has_additional_matching_legacy_proxifyre_service_with_host(
&host, root
));
}
}
#[test]
fn missing_proxyfier_returns_install_action_status() {
let component = proxyfier_component_from_detection(None);
@@ -121,11 +230,15 @@ fn ignores_known_service_name_when_path_points_to_foreign_binary() {
r#""C:\Foreign\ProxiFyre.exe" --service"#,
);
let detected =
detect_proxyfier_install_with_host(&host).expect("executable should still be detected");
assert!(!detected.running);
assert_eq!(detected.service_status, None);
assert!(detect_proxyfier_install_with_host(&host).is_none());
let inventory = inventory_proxyfier_with_host(&host);
let candidate = inventory.selected_candidate().expect("foreign collision");
assert_eq!(candidate.classification, ComponentClassification::Foreign);
assert_eq!(candidate.issues[0].code, OWNERSHIP_MISMATCH);
let component = proxyfier_component_from_inventory(&inventory);
assert_eq!(component.state, ComponentState::Error);
assert!(!component.running);
assert!(component.actions.is_empty());
}
#[test]
@@ -135,18 +248,32 @@ fn ignores_known_service_name_without_path_metadata() {
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
.with_service("ProxiFyreService");
let detected =
detect_proxyfier_install_with_host(&host).expect("executable should still be detected");
assert!(!detected.running);
assert_eq!(detected.service_status, None);
assert!(detect_proxyfier_install_with_host(&host).is_none());
let inventory = inventory_proxyfier_with_host(&host);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
assert_eq!(
inventory.selected_candidate().unwrap().issues[0].code,
OWNERSHIP_MISMATCH
);
}
#[test]
fn detects_running_local_singbox_from_default_install_root_and_service() {
let host = MockHost::new()
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
.with_service("ProxyWardenSingBox");
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
.with_text(
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
winsw_xml(),
)
.with_service_path(
"ProxyWardenSingBox",
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
)
.with_version(
r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe",
"1.11.0.0",
);
let detected =
detect_singbox_install_with_host(&host).expect("existing sing-box should be detected");
@@ -157,6 +284,7 @@ fn detects_running_local_singbox_from_default_install_root_and_service() {
);
assert_eq!(detected.service_name, "ProxyWardenSingBox");
assert!(detected.running);
assert_eq!(detected.version, Some("1.11.0.0".to_string()));
let component = singbox_component_from_detection(Some(&detected));
assert_eq!(component.state, ComponentState::Running);
@@ -170,30 +298,99 @@ fn detects_running_local_singbox_from_default_install_root_and_service() {
}
#[test]
fn detects_stopped_local_singbox_from_env_override() {
fn current_singbox_with_foreign_same_name_service_is_ownership_mismatch() {
let host = MockHost::new()
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
.with_text(
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
winsw_xml(),
)
.with_service_path(
"ProxyWardenSingBox",
r#""C:\Foreign\ProxyWardenSingBox.exe""#,
);
let inventory = inventory_singbox_with_host(&host);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
assert_eq!(
inventory.selected_candidate().unwrap().issues[0].code,
OWNERSHIP_MISMATCH
);
}
#[test]
fn winsw_identity_cannot_be_spoofed_by_comments_or_unrelated_nodes() {
let spoofed_xml = r#"<service>
<!-- <id>ProxyWardenSingBox</id> -->
<!-- <executable>%BASE%\sing-box.exe</executable> -->
<metadata><arguments>run -c "%BASE%\config.json"</arguments></metadata>
<id>ForeignService</id>
<executable>C:\Foreign\sing-box.exe</executable>
<arguments>run -c "C:\Foreign\config.json"</arguments>
</service>"#;
let host = MockHost::new()
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
.with_text(
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
spoofed_xml,
)
.with_service_path(
"ProxyWardenSingBox",
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
);
let inventory = inventory_singbox_with_host(&host);
assert_eq!(
inventory.classification(),
ComponentClassification::Incomplete
);
assert!(detect_singbox_install_with_host(&host).is_none());
}
#[test]
fn winsw_identity_rejects_duplicate_dtd_cdata_second_root_and_oversized_xml() {
let oversized = format!(
"<service><id>ProxyWardenSingBox</id><executable>%BASE%\\sing-box.exe</executable><arguments>run -c \"%BASE%\\config.json\"</arguments><description>{}</description></service>",
"x".repeat(65 * 1024)
);
let invalid_xml = vec![
r#"<service><id>ProxyWardenSingBox</id><id>ProxyWardenSingBox</id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
r#"<!DOCTYPE service [<!ENTITY owned "ProxyWardenSingBox">]><service><id>&owned;</id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
r#"<service><id><![CDATA[ProxyWardenSingBox]]></id><executable>%BASE%\sing-box.exe</executable><arguments>run -c "%BASE%\config.json"</arguments></service>"#.to_string(),
format!("{}<service></service>", winsw_xml()),
oversized,
];
for xml in invalid_xml {
let host = current_singbox_host_with_xml(&xml);
assert!(
detect_singbox_install_with_host(&host).is_none(),
"unsafe WinSW XML was accepted"
);
}
}
#[test]
fn winsw_identity_accepts_xml_declaration_bom_and_current_extra_nodes() {
let xml = "\u{feff}<?xml version=\"1.0\" encoding=\"utf-8\"?><service><id>ProxyWardenSingBox</id><name>ProxyWarden Local sing-box</name><executable>%BASE%\\sing-box.exe</executable><arguments>run -c \"%BASE%\\config.json\"</arguments><log mode=\"roll-by-size\"><keepFiles>4</keepFiles></log><onfailure action=\"restart\" /></service>".to_string();
let host = current_singbox_host_with_xml(&xml);
assert!(detect_singbox_install_with_host(&host).is_some());
}
#[test]
fn portable_singbox_env_override_remains_foreign() {
let host = MockHost::new()
.with_env("PROXYWARDEN_SINGBOX_ROOT", r"D:\Portable\sing-box")
.with_path(r"D:\Portable\sing-box\sing-box.exe");
let detected = detect_singbox_install_with_host(&host).expect("env override should be checked");
let component = singbox_component_from_detection(Some(&detected));
assert!(detect_singbox_install_with_host(&host).is_none());
assert_eq!(
detected.executable_path,
PathBuf::from(r"D:\Portable\sing-box\sing-box.exe")
inventory_singbox_with_host(&host).classification(),
ComponentClassification::Foreign
);
assert_eq!(component.state, ComponentState::Stopped);
assert!(component.installed);
assert!(!component.running);
assert_eq!(
component.service_name,
Some("ProxyWardenSingBox".to_string())
);
assert_eq!(component.service_status, Some("stopped".to_string()));
assert!(component
.problems
.iter()
.any(|problem| problem.contains("остановлена")));
}
#[test]
@@ -207,6 +404,153 @@ fn missing_local_singbox_returns_optional_install_action_status() {
assert!(component.problems.is_empty());
}
#[test]
fn parses_service_pathname_without_accepting_malformed_quotes() {
assert_eq!(
service_executable_from_path_name(
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe" install"#,
),
Some(PathBuf::from(
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe"
))
);
assert_eq!(
service_executable_from_path_name(r"C:\Tools\ProxiFyre\ProxiFyre.exe --service"),
Some(PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"))
);
assert!(service_executable_from_path_name(r#""C:\Broken\ProxiFyre.exe --service"#).is_none());
assert!(service_executable_from_path_name(" ").is_none());
}
#[test]
fn frozen_proxifyre_manifest_matches_all_ten_files_and_nothing_less() {
let observed = LEGACY_PROXIFYRE_2_2_1_MANIFEST
.iter()
.rev()
.map(|file| LegacyPackageFileIdentity {
relative_path: PathBuf::from(file.relative_path.to_ascii_uppercase()),
size: file.size,
sha256: file.sha256.to_ascii_uppercase(),
})
.collect::<Vec<_>>();
assert!(matches_legacy_proxifyre_2_2_1_manifest(&observed));
let mut missing = observed.clone();
missing.pop();
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&missing));
let mut wrong_size = observed.clone();
wrong_size[0].size += 1;
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&wrong_size));
let mut wrong_hash = observed.clone();
wrong_hash[0].sha256 = "0".repeat(64);
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&wrong_hash));
let mut extra = observed.clone();
extra.push(LegacyPackageFileIdentity {
relative_path: PathBuf::from("unexpected.dll"),
size: 1,
sha256: "0".repeat(64),
});
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&extra));
let mut duplicate = observed;
duplicate[0] = duplicate[1].clone();
assert!(!matches_legacy_proxifyre_2_2_1_manifest(&duplicate));
}
#[test]
#[cfg(windows)]
fn reads_windows_pe_file_version_without_executing_binary() {
let windows_dir = std::env::var("WINDIR").expect("WINDIR on Windows");
let notepad = PathBuf::from(windows_dir)
.join("System32")
.join("notepad.exe");
let version = SystemProxyfierDetectionHost
.file_version(&notepad)
.expect("notepad PE version");
assert_eq!(version.split('.').count(), 4);
assert!(version
.split('.')
.all(|segment| segment.parse::<u32>().is_ok()));
}
#[test]
fn production_component_detection_has_only_native_windows_owners() {
let source = include_str!("../src/component_detection.rs");
let source_lower = source.to_ascii_lowercase();
for forbidden in [
"command_no_window(",
"get-process",
"get-service",
"get-ciminstance",
"\"powershell\"",
"std::process::command",
"extern \"system\"",
"#[link(",
] {
assert!(
!source_lower.contains(forbidden),
"production detection still contains shell boundary: {forbidden}"
);
}
for native_owner in [
"CreateToolhelp32Snapshot",
"OpenSCManagerW",
"QueryServiceStatusEx",
"QueryServiceConfigW",
"winreg::",
] {
assert!(
source.contains(native_owner),
"native detection owner is missing: {native_owner}"
);
}
}
#[test]
#[cfg(windows)]
fn native_process_inventory_finds_the_running_test_binary() {
let executable_name = std::env::current_exe()
.expect("current test executable")
.file_name()
.expect("current test executable file name")
.to_string_lossy()
.into_owned();
assert!(SystemProxyfierDetectionHost.process_running(&executable_name));
assert!(SystemProxyfierDetectionHost.process_running(&executable_name.to_ascii_uppercase()));
assert!(SystemProxyfierDetectionHost.process_running(executable_name.trim_end_matches(".exe")));
}
#[test]
#[cfg(windows)]
fn native_service_inventory_reads_status_and_path_from_scm() {
let service = SystemProxyfierDetectionHost
.service_info("EventLog")
.expect("Windows EventLog service should be queryable without elevation");
assert_eq!(service.name, "EventLog");
assert!(matches!(
service.status.as_str(),
"stopped"
| "start pending"
| "stop pending"
| "running"
| "continue pending"
| "pause pending"
| "paused"
| "unknown"
));
assert!(service
.path_name
.as_deref()
.is_some_and(|path| !path.trim().is_empty()));
}
#[derive(Default)]
struct MockHost {
env: HashMap<String, String>,
@@ -214,6 +558,9 @@ struct MockHost {
processes: HashSet<String>,
services: HashMap<String, String>,
service_paths: HashMap<String, String>,
texts: HashMap<String, String>,
known_binaries: HashSet<String>,
versions: HashMap<String, String>,
registry: Vec<RegistryInstallEntry>,
}
@@ -267,6 +614,24 @@ impl MockHost {
});
self
}
fn with_text(mut self, path: &str, contents: &str) -> Self {
self.paths.insert(normalize_path(path));
self.texts
.insert(normalize_path(path), contents.to_string());
self
}
fn with_known_binary(mut self, path: &str) -> Self {
self.known_binaries.insert(normalize_path(path));
self
}
fn with_version(mut self, path: &str, version: &str) -> Self {
self.versions
.insert(normalize_path(path), version.to_string());
self
}
}
impl ProxyfierDetectionHost for MockHost {
@@ -306,8 +671,57 @@ impl ProxyfierDetectionHost for MockHost {
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
self.registry.clone()
}
fn read_text(&self, path: &Path) -> Option<String> {
self.texts
.get(&normalize_path(&path.display().to_string()))
.cloned()
}
fn file_version(&self, path: &Path) -> Option<String> {
self.versions
.get(&normalize_path(&path.display().to_string()))
.cloned()
}
fn binary_identity(
&self,
_component_id: &proxywarden_lib::models::ComponentId,
path: &Path,
) -> BinaryIdentityEvidence {
if self
.known_binaries
.contains(&normalize_path(&path.display().to_string()))
{
BinaryIdentityEvidence::KnownPackage
} else {
BinaryIdentityEvidence::Unknown
}
}
}
fn normalize_path(path: &str) -> String {
path.replace('/', "\\").to_ascii_lowercase()
}
fn winsw_xml() -> &'static str {
r#"<service>
<id>ProxyWardenSingBox</id>
<executable>%BASE%\sing-box.exe</executable>
<arguments>run -c "%BASE%\config.json"</arguments>
</service>"#
}
fn current_singbox_host_with_xml(xml: &str) -> MockHost {
MockHost::new()
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe")
.with_text(
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.xml",
xml,
)
.with_service_path(
"ProxyWardenSingBox",
r#""C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe""#,
)
}
@@ -0,0 +1,666 @@
use proxywarden_lib::component_detection::{
proxyfier_component_from_inventory, singbox_component_from_inventory,
};
use proxywarden_lib::component_inventory::{
authorize_component_action, classify_component_candidates,
component_inventory_fingerprint_for_cutover, legacy_proxifyre_topshelf_path_matches,
prove_legacy_cutover, BinaryIdentityEvidence, CandidateRole, ComponentCandidateProbe,
ComponentClassification, InventoryAction, InventoryIssue, LegacyCutoverEvidence,
LegacyCutoverProof, LegacyProxifyreScmProfile, MarkerEvidence, ServiceEvidence,
AMBIGUOUS_LEGACY, MANUAL_MIGRATION_REQUIRED, OWNERSHIP_MISMATCH,
};
use proxywarden_lib::component_status::resolve_component_statuses_with_inventories;
use proxywarden_lib::models::{ComponentId, ComponentState};
use std::path::{Path, PathBuf};
#[test]
fn managed_current_requires_marker_files_and_exact_service_path() {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let inventory = classify_component_candidates(
ComponentId::Proxyfier,
vec![probe(
ComponentId::Proxyfier,
CandidateRole::Current,
&root,
true,
MarkerEvidence::Valid,
BinaryIdentityEvidence::Unknown,
Some(service(&root.join("ProxiFyre.exe"), true)),
)],
);
assert_eq!(
inventory.classification(),
ComponentClassification::ManagedCurrent
);
assert_eq!(
inventory
.selected_candidate()
.expect("selected current")
.binary_version,
Some("2.4.0.0".to_string())
);
}
#[test]
fn same_service_name_with_foreign_path_is_ownership_mismatch() {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let inventory = classify_component_candidates(
ComponentId::Proxyfier,
vec![probe(
ComponentId::Proxyfier,
CandidateRole::Current,
&root,
true,
MarkerEvidence::Valid,
BinaryIdentityEvidence::KnownPackage,
Some(service(
PathBuf::from(r"C:\Foreign\ProxiFyre.exe").as_path(),
false,
)),
)],
);
let candidate = inventory
.selected_candidate()
.expect("foreign current candidate");
assert_eq!(candidate.classification, ComponentClassification::Foreign);
assert_eq!(candidate.issues[0].code, OWNERSHIP_MISMATCH);
}
#[test]
fn tools_proxifyre_is_legacy_and_never_current() {
let root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = classify_component_candidates(
ComponentId::Proxyfier,
vec![probe(
ComponentId::Proxyfier,
CandidateRole::Legacy,
&root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::KnownPackage,
Some(service(&root.join("ProxiFyre.exe"), true)),
)],
);
assert_eq!(
inventory.classification(),
ComponentClassification::ManagedLegacy
);
let component = proxyfier_component_from_inventory(&inventory);
assert_eq!(component.actions, vec!["Перенести ProxiFyre"]);
assert!(component
.problems
.iter()
.any(|problem| problem.contains("явного переноса")));
}
#[test]
fn bare_singbox_root_stays_foreign_without_complete_identity() {
let root = PathBuf::from(r"C:\Program Files\sing-box");
let mut candidate = probe(
ComponentId::Singbox,
CandidateRole::ForeignByDefault,
&root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::Unknown,
Some(service(&root.join("ProxyWardenSingBox.exe"), true)),
);
candidate.legacy_identity_complete = false;
let inventory = classify_component_candidates(ComponentId::Singbox, vec![candidate]);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
}
#[test]
fn current_root_without_required_marker_is_incomplete() {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let inventory = classify_component_candidates(
ComponentId::Proxyfier,
vec![probe(
ComponentId::Proxyfier,
CandidateRole::Current,
&root,
true,
MarkerEvidence::Missing,
BinaryIdentityEvidence::KnownPackage,
Some(service(&root.join("ProxiFyre.exe"), true)),
)],
);
assert_eq!(
inventory.classification(),
ComponentClassification::Incomplete
);
}
#[test]
fn startup_status_preserves_foreign_and_incomplete_inventory_errors() {
let proxyfier_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let foreign_proxyfier = classify_component_candidates(
ComponentId::Proxyfier,
vec![probe(
ComponentId::Proxyfier,
CandidateRole::Current,
&proxyfier_root,
true,
MarkerEvidence::Valid,
BinaryIdentityEvidence::KnownPackage,
Some(service(
PathBuf::from(r"C:\Foreign\ProxiFyre.exe").as_path(),
false,
)),
)],
);
let singbox_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box");
let mut incomplete_singbox_probe = probe(
ComponentId::Singbox,
CandidateRole::Current,
&singbox_root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::KnownPackage,
Some(service(&singbox_root.join("ProxyWardenSingBox.exe"), true)),
);
incomplete_singbox_probe
.missing_files
.push(singbox_root.join("ProxyWardenSingBox.xml"));
let incomplete_singbox =
classify_component_candidates(ComponentId::Singbox, vec![incomplete_singbox_probe]);
let statuses =
resolve_component_statuses_with_inventories(&foreign_proxyfier, &incomplete_singbox);
let proxyfier = statuses
.iter()
.find(|status| status.id == ComponentId::Proxyfier)
.expect("ProxiFyre status");
let singbox = statuses
.iter()
.find(|status| status.id == ComponentId::Singbox)
.expect("sing-box status");
assert_eq!(proxyfier.state, ComponentState::Error);
assert_eq!(singbox.state, ComponentState::Error);
assert!(!proxyfier.problems.is_empty());
assert!(!singbox.problems.is_empty());
}
#[test]
fn current_candidate_wins_but_legacy_remains_visible() {
let current_root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
let legacy_root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = classify_component_candidates(
ComponentId::Proxyfier,
vec![
probe(
ComponentId::Proxyfier,
CandidateRole::Legacy,
&legacy_root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::KnownPackage,
Some(service(&legacy_root.join("ProxiFyre.exe"), true)),
),
probe(
ComponentId::Proxyfier,
CandidateRole::Current,
&current_root,
true,
MarkerEvidence::Valid,
BinaryIdentityEvidence::KnownPackage,
Some(service(&current_root.join("ProxiFyre.exe"), true)),
),
],
);
assert_eq!(inventory.candidates.len(), 2);
assert_eq!(
inventory.classification(),
ComponentClassification::ManagedCurrent
);
assert_eq!(
inventory
.selected_candidate()
.expect("selected current")
.root,
current_root
);
}
#[test]
fn multiple_managed_legacy_candidates_block_selection() {
let roots = [
PathBuf::from(r"C:\Tools\ProxiFyre"),
PathBuf::from(r"C:\Program Files\ProxiFyre"),
];
let probes = roots
.iter()
.map(|root| {
probe(
ComponentId::Proxyfier,
CandidateRole::Legacy,
root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::KnownPackage,
Some(service(&root.join("ProxiFyre.exe"), true)),
)
})
.collect();
let inventory = classify_component_candidates(ComponentId::Proxyfier, probes);
assert!(inventory.selected_candidate().is_none());
assert_eq!(inventory.issues[0].code, AMBIGUOUS_LEGACY);
}
#[test]
fn reparse_point_is_never_managed() {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box");
let mut candidate = probe(
ComponentId::Singbox,
CandidateRole::Current,
&root,
false,
MarkerEvidence::NotRequired,
BinaryIdentityEvidence::KnownPackage,
Some(service(&root.join("ProxyWardenSingBox.exe"), true)),
);
candidate.has_reparse_point = true;
let inventory = classify_component_candidates(ComponentId::Singbox, vec![candidate]);
assert_eq!(inventory.classification(), ComponentClassification::Foreign);
assert_eq!(
inventory.selected_candidate().unwrap().issues[0].code,
OWNERSHIP_MISMATCH
);
}
#[test]
fn exact_frozen_proxifyre_identity_is_the_only_automatic_cutover() {
let root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = legacy_inventory(
ComponentId::Proxyfier,
&root,
"ProxiFyreService",
&topshelf_path(&root),
"2.2.1.0",
);
let proof = prove_legacy_cutover(&inventory, &exact_cutover_evidence())
.expect("exact identity must produce an opaque proof");
assert_eq!(proof.fingerprint().len(), 64);
}
#[test]
fn discovery_evidence_never_substitutes_for_cutover_identity() {
let auto_root = PathBuf::from(r"C:\Tools\ProxiFyre");
let cases = [
legacy_inventory(
ComponentId::Proxyfier,
Path::new(r"C:\Program Files\ProxiFyre"),
"ProxiFyreService",
&topshelf_path(Path::new(r"C:\Program Files\ProxiFyre")),
"2.2.1.0",
),
legacy_inventory(
ComponentId::Proxyfier,
&auto_root,
"ProxiFyre",
&topshelf_path(&auto_root),
"2.2.1.0",
),
legacy_inventory(
ComponentId::Proxyfier,
&auto_root,
"ProxiFyreService",
&format!(
r#""{}" --service"#,
auto_root.join("ProxiFyre.exe").display()
),
"2.2.1.0",
),
legacy_inventory(
ComponentId::Proxyfier,
&auto_root,
"ProxiFyreService",
&topshelf_path(&auto_root),
"2.4.0.0",
),
];
for inventory in cases {
assert_eq!(
inventory.classification(),
ComponentClassification::ManagedLegacy
);
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &exact_cutover_evidence()));
}
let inventory = legacy_inventory(
ComponentId::Proxyfier,
&auto_root,
"ProxiFyreService",
&topshelf_path(&auto_root),
"2.2.1.0",
);
let mut bad_manifest = exact_cutover_evidence();
bad_manifest.proxifyre_manifest_matches = false;
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_manifest));
let mut bad_snapshot_fingerprint = exact_cutover_evidence();
bad_snapshot_fingerprint
.proxifyre_scm_snapshot_fingerprint
.clear();
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_snapshot_fingerprint));
let mut bad_profile = exact_cutover_evidence();
bad_profile
.proxifyre_scm_profile
.as_mut()
.expect("profile")
.delayed_auto_start = true;
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &bad_profile));
let mut extra_candidate = inventory.clone();
extra_candidate
.candidates
.push(extra_candidate.candidates[0].clone());
assert_manual_without_mutation(prove_legacy_cutover(
&extra_candidate,
&exact_cutover_evidence(),
));
let mut alias_collision = exact_cutover_evidence();
alias_collision.additional_matching_service = true;
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &alias_collision));
}
#[test]
fn legacy_singbox_is_always_manual_and_has_zero_mutation_authority() {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\sing-box");
let inventory = legacy_inventory(
ComponentId::Singbox,
&root,
"ProxyWardenSingBox",
&format!(r#""{}""#, root.join("ProxyWardenSingBox.exe").display()),
"1.13.19",
);
assert_eq!(
inventory.classification(),
ComponentClassification::ManagedLegacy
);
let component = singbox_component_from_inventory(&inventory);
assert!(component.actions.is_empty());
assert!(component
.problems
.iter()
.any(|problem| problem.contains("ручного переноса")));
assert_manual_without_mutation(prove_legacy_cutover(&inventory, &exact_cutover_evidence()));
}
#[test]
fn generic_inventory_authorization_never_grants_cutover() {
let root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = legacy_inventory(
ComponentId::Proxyfier,
&root,
"ProxiFyreService",
&topshelf_path(&root),
"2.2.1.0",
);
let error = authorize_component_action(&inventory, InventoryAction::Cutover)
.expect_err("generic lifecycle authorization must not grant cutover");
assert_eq!(error.code, "legacy_cutover_required");
let mut current = inventory.clone();
current.candidates[0].classification = ComponentClassification::ManagedCurrent;
let missing =
proxywarden_lib::component_inventory::ComponentInventory::missing(ComponentId::Proxyfier);
for inventory in [&current, &missing] {
assert!(authorize_component_action(inventory, InventoryAction::Cutover).is_err());
}
prove_legacy_cutover(&inventory, &exact_cutover_evidence())
.expect("strict gate remains the only proof constructor");
}
#[test]
fn generic_inventory_authorization_never_writes_legacy_runtime_config() {
let root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = legacy_inventory(
ComponentId::Proxyfier,
&root,
"ProxiFyreService",
&topshelf_path(&root),
"2.2.1.0",
);
for action in [
InventoryAction::Apply,
InventoryAction::Start,
InventoryAction::Stop,
] {
let error = authorize_component_action(&inventory, action)
.expect_err("legacy runtime actions require explicit cutover");
assert_eq!(error.code, "legacy_cutover_required");
}
}
#[test]
fn cutover_inventory_fingerprint_is_stable_and_binds_live_service_state() {
let root = PathBuf::from(r"C:\Tools\ProxiFyre");
let inventory = legacy_inventory(
ComponentId::Proxyfier,
&root,
"ProxiFyreService",
&topshelf_path(&root),
"2.2.1.0",
);
let first = component_inventory_fingerprint_for_cutover(&inventory);
assert_eq!(
first,
component_inventory_fingerprint_for_cutover(&inventory)
);
let mut changed = inventory.clone();
changed.candidates[0]
.service
.as_mut()
.expect("service")
.status = "running".to_string();
assert_ne!(first, component_inventory_fingerprint_for_cutover(&changed));
}
#[test]
fn frozen_scm_profile_rejects_each_unsafe_or_unknown_field() {
let mutations: [fn(&mut LegacyProxifyreScmProfile); 15] = [
|profile| profile.service_type = 0x20,
|profile| profile.start_type = 3,
|profile| profile.error_control = 0,
|profile| profile.account_name = "NetworkService".to_string(),
|profile| profile.display_name = "ProxiFyre".to_string(),
|profile| profile.description.clear(),
|profile| profile.dependencies.push("Tcpip".to_string()),
|profile| profile.load_order_group = Some("Network".to_string()),
|profile| profile.has_failure_actions = true,
|profile| profile.failure_actions_on_non_crash = true,
|profile| profile.delayed_auto_start = true,
|profile| profile.sid_type = 1,
|profile| {
profile
.required_privileges
.push("SeDebugPrivilege".to_string())
},
|profile| profile.has_triggers = true,
|profile| profile.untrusted_mutation_rights = true,
];
assert!(exact_scm_profile().matches_frozen_2_2_1_profile());
for mutate in mutations {
let mut profile = exact_scm_profile();
mutate(&mut profile);
assert!(!profile.matches_frozen_2_2_1_profile());
}
}
#[test]
fn topshelf_cutover_path_is_token_exact_and_pair_order_independent() {
let executable = Path::new(r"C:\Tools\ProxiFyre\ProxiFyre.exe");
for path_name in [
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename "ProxiFyreService""#,
r#"C:\Tools\ProxiFyre\ProxiFyre.exe -servicename ProxiFyreService -displayname "ProxiFyre Service""#,
] {
assert!(legacy_proxifyre_topshelf_path_matches(
path_name, executable
));
}
for path_name in [
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" --service"#,
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService --run"#,
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "Foreign" -servicename ProxiFyreService"#,
r#""C:\Tools\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyre"#,
r#""C:\Tools\ProxiFyre\ProxiFyre.exe -displayname "ProxiFyre Service" -servicename ProxiFyreService"#,
] {
assert!(!legacy_proxifyre_topshelf_path_matches(
path_name, executable
));
}
assert!(!legacy_proxifyre_topshelf_path_matches(
r#""C:\Program Files\ProxiFyre\ProxiFyre.exe" -displayname "ProxiFyre Service" -servicename ProxiFyreService"#,
Path::new(r"C:\Program Files\ProxiFyre\ProxiFyre.exe"),
));
}
fn assert_manual_without_mutation(result: Result<LegacyCutoverProof, InventoryIssue>) {
assert_eq!(
result
.expect_err("manual identity must not yield a proof")
.code,
MANUAL_MIGRATION_REQUIRED
);
}
fn exact_cutover_evidence() -> LegacyCutoverEvidence {
LegacyCutoverEvidence {
proxifyre_manifest_matches: true,
proxifyre_scm_profile: Some(exact_scm_profile()),
proxifyre_scm_snapshot_fingerprint: "9".repeat(64),
additional_matching_service: false,
}
}
fn exact_scm_profile() -> LegacyProxifyreScmProfile {
LegacyProxifyreScmProfile {
service_type: 0x10,
start_type: 2,
error_control: 1,
account_name: "LocalSystem".to_string(),
display_name: "ProxiFyre Service".to_string(),
description: "ProxiFyre - SOCKS5 ProxiFyre Service".to_string(),
dependencies: Vec::new(),
load_order_group: None,
has_failure_actions: false,
failure_actions_on_non_crash: false,
delayed_auto_start: false,
sid_type: 0,
required_privileges: Vec::new(),
has_triggers: false,
untrusted_mutation_rights: false,
}
}
fn legacy_inventory(
component_id: ComponentId,
root: &Path,
service_name: &str,
path_name: &str,
version: &str,
) -> proxywarden_lib::component_inventory::ComponentInventory {
let executable_name = match component_id {
ComponentId::Proxyfier => "ProxiFyre.exe",
ComponentId::Singbox => "sing-box.exe",
ComponentId::ControlApp => "ProxyWarden.exe",
};
let service_executable = match component_id {
ComponentId::Singbox => root.join("ProxyWardenSingBox.exe"),
ComponentId::Proxyfier | ComponentId::ControlApp => root.join(executable_name),
};
classify_component_candidates(
component_id.clone(),
vec![ComponentCandidateProbe {
component_id,
role: CandidateRole::Legacy,
root: root.to_path_buf(),
root_exists: true,
has_reparse_point: false,
executable_path: Some(root.join(executable_name)),
missing_files: Vec::new(),
marker: MarkerEvidence::NotRequired,
marker_required: false,
binary_identity: BinaryIdentityEvidence::KnownPackage,
binary_version: Some(version.to_string()),
service: Some(ServiceEvidence {
name: service_name.to_string(),
status: "stopped".to_string(),
path_name: Some(path_name.to_string()),
executable_path: Some(service_executable),
path_matches_candidate: true,
binary_version: Some(version.to_string()),
}),
service_required: true,
legacy_identity_complete: true,
}],
)
}
fn topshelf_path(root: &Path) -> String {
format!(
r#""{}" -displayname "ProxiFyre Service" -servicename "ProxiFyreService""#,
root.join("ProxiFyre.exe").display()
)
}
fn probe(
component_id: ComponentId,
role: CandidateRole,
root: &Path,
marker_required: bool,
marker: MarkerEvidence,
binary_identity: BinaryIdentityEvidence,
service: Option<ServiceEvidence>,
) -> ComponentCandidateProbe {
let executable_name = match component_id {
ComponentId::Proxyfier => "ProxiFyre.exe",
ComponentId::Singbox => "sing-box.exe",
ComponentId::ControlApp => "ProxyWarden.exe",
};
ComponentCandidateProbe {
component_id,
role,
root: root.to_path_buf(),
root_exists: true,
has_reparse_point: false,
executable_path: Some(root.join(executable_name)),
missing_files: Vec::new(),
marker,
marker_required,
binary_identity,
binary_version: Some("2.4.0.0".to_string()),
service,
service_required: true,
legacy_identity_complete: true,
}
}
fn service(executable: &std::path::Path, matches: bool) -> ServiceEvidence {
ServiceEvidence {
name: "ProxiFyreService".to_string(),
status: "stopped".to_string(),
path_name: Some(format!(r#""{}" --service"#, executable.display())),
executable_path: Some(executable.to_path_buf()),
path_matches_candidate: matches,
binary_version: Some("2.4.0.0".to_string()),
}
}
File diff suppressed because it is too large Load Diff
+423
View File
@@ -0,0 +1,423 @@
use proxywarden_lib::component_catalog::{ComponentId, ComponentPackage, UpdateTrustPolicy};
use proxywarden_lib::component_packages::{
ComponentPackageService, ComponentUpdateObservation, ComponentUpdatesState,
GithubReleaseDigestProof, PackageCacheManifest, PackageSource, TrustedGithubReleaseObservation,
COMPONENT_UPDATES_STATE_SCHEMA_VERSION, PACKAGE_CACHE_MANIFEST_FILENAME,
PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
};
use proxywarden_lib::safe_fs::{ensure_no_reparse_ancestors, protect_path_for_owner_admin_system};
use proxywarden_lib::storage::StoragePaths;
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use uuid::Uuid;
#[test]
fn offline_selection_uses_the_real_bundled_package_without_a_cache() {
let packages = TestDirectory::new();
let paths = StoragePaths::new(packages.path().join("missing-storage"));
let service = ComponentPackageService::open(bundled_root(), &paths)
.expect("open local component package service");
let selected = service
.select_verified(ComponentId::Proxifyre)
.expect("select bundled package offline");
assert_eq!(selected.source, PackageSource::Bundled);
assert_eq!(selected.version, "2.4.0");
assert_eq!(
selected.asset_path,
bundled_root()
.join("proxifyre")
.join("ProxiFyre-v2.4.0-x64-signed.zip")
);
}
#[test]
fn verified_newer_cache_wins_with_numeric_version_ordering() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (expected, manifest) =
write_verified_cache(&packages.packages_path(), &component, "1.100.0", |_| {});
write_trusted_state(&packages.state_path(), &manifest);
let selected = service
.select_verified(ComponentId::SingBox)
.expect("select newest verified cache");
assert_eq!(selected.source, PackageSource::Cache);
assert_eq!(selected.version, "1.100.0");
assert_eq!(selected.package_root, expected);
}
#[test]
fn same_verified_cache_does_not_replace_the_bundle() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (_, manifest) =
write_verified_cache(&packages.packages_path(), &component, "1.13.19", |_| {});
write_trusted_state(&packages.state_path(), &manifest);
assert_bundled(&service);
}
#[test]
fn older_verified_cache_does_not_replace_the_bundle() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (_, manifest) =
write_verified_cache(&packages.packages_path(), &component, "1.13.18", |_| {});
write_trusted_state(&packages.state_path(), &manifest);
assert_bundled(&service);
}
#[test]
fn corrupt_cache_does_not_break_bundled_fallback() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (version_root, manifest) =
write_verified_cache(&packages.packages_path(), &component, "1.14.0", |_| {});
write_trusted_state(&packages.state_path(), &manifest);
fs::write(
version_root.join(PACKAGE_CACHE_MANIFEST_FILENAME),
b"{not-json",
)
.expect("write corrupt manifest");
assert_bundled(&service);
}
#[test]
fn cache_from_the_wrong_repository_is_rejected() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (_, manifest) = write_verified_cache(
&packages.packages_path(),
&component,
"1.14.0",
|manifest| {
manifest.independent_proof.repository = "attacker/sing-box".to_string();
},
);
write_trusted_state(&packages.state_path(), &manifest);
assert_bundled(&service);
}
#[test]
fn handwritten_far_future_cache_without_trusted_state_is_rejected() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
write_verified_cache(&packages.packages_path(), &component, "999.0.0", |_| {});
assert_bundled(&service);
}
#[test]
fn matching_manifest_and_state_with_inherited_acl_are_rejected() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (_, manifest) = write_cache(
&packages.packages_path(),
&component,
"1.14.0",
|_| {},
false,
);
write_state(&packages.state_path(), &manifest, false);
assert_bundled(&service);
}
#[test]
fn cache_with_an_extra_file_is_rejected() {
let packages = TestDirectory::new();
let service = open_service(&packages);
let component = sing_box_component(&service);
let (version_root, manifest) =
write_verified_cache(&packages.packages_path(), &component, "1.14.0", |_| {});
write_trusted_state(&packages.state_path(), &manifest);
fs::write(version_root.join("unexpected.txt"), b"not part of package")
.expect("write unexpected cache file");
assert_bundled(&service);
}
#[cfg(windows)]
#[test]
fn reparse_point_cache_root_is_rejected() {
let workspace = TestDirectory::new();
let target = workspace.path().join("junction-target");
fs::create_dir_all(&target).expect("create junction target");
let storage_paths = workspace.storage_paths();
let service = ComponentPackageService::open(bundled_root(), &storage_paths)
.expect("open service before creating junction");
let component = sing_box_component(&service);
let (_, manifest) = write_verified_cache(&target, &component, "1.14.0", |_| {});
write_trusted_state(&storage_paths.component_updates_file, &manifest);
let junction = storage_paths.packages_dir.clone();
let _junction_guard = create_junction(&junction, &target, workspace.path());
assert_bundled(&service);
}
fn bundled_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("bundled")
.join("components")
}
fn open_service(packages: &TestDirectory) -> ComponentPackageService {
ComponentPackageService::open(bundled_root(), &packages.storage_paths())
.expect("production bundle must open")
}
fn sing_box_component(service: &ComponentPackageService) -> ComponentPackage {
service
.catalog()
.components
.iter()
.find(|component| component.id == ComponentId::SingBox)
.expect("production sing-box component")
.clone()
}
fn assert_bundled(service: &ComponentPackageService) {
let selected = service
.select_verified(ComponentId::SingBox)
.expect("fall back to bundled package");
assert_eq!(selected.source, PackageSource::Bundled);
assert_eq!(selected.version, "1.13.19");
}
fn cache_version_root(packages_root: &Path, version: &str) -> PathBuf {
packages_root
.join(ComponentId::SingBox.as_str())
.join(version)
}
fn write_verified_cache(
packages_root: &Path,
component: &ComponentPackage,
version: &str,
mutate: impl FnOnce(&mut PackageCacheManifest),
) -> (PathBuf, PackageCacheManifest) {
write_cache(packages_root, component, version, mutate, true)
}
fn write_cache(
packages_root: &Path,
component: &ComponentPackage,
version: &str,
mutate: impl FnOnce(&mut PackageCacheManifest),
protect: bool,
) -> (PathBuf, PackageCacheManifest) {
let version_root = cache_version_root(packages_root, version);
fs::create_dir_all(&version_root).expect("create cache version directory");
let asset_name = format!("sing-box-{version}-windows-amd64.zip");
let asset_bytes = format!("verified sing-box package {version}").into_bytes();
let sha256 = format!("{:x}", Sha256::digest(&asset_bytes));
let repository = match &component.update_trust_policy {
UpdateTrustPolicy::GithubReleaseDigest {
repository,
authenticode_publishers,
..
} => {
assert!(
authenticode_publishers.is_none(),
"sing-box cache must not claim Authenticode evidence"
);
repository.clone()
}
_ => panic!("sing-box must use GitHub release digest trust"),
};
let mut manifest = PackageCacheManifest {
schema_version: PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
component_id: component.id,
version: version.to_string(),
asset_name: asset_name.clone(),
sha256: sha256.clone(),
size: asset_bytes.len() as u64,
independent_proof: GithubReleaseDigestProof {
repository,
release_id: 1,
asset_id: 1,
stable_tag: format!("v{version}"),
asset_name: asset_name.clone(),
size: asset_bytes.len() as u64,
sha256_from_api: sha256,
verified_signatures: Vec::new(),
},
};
mutate(&mut manifest);
let asset_path = version_root.join(&asset_name);
fs::write(&asset_path, asset_bytes).expect("write cached package asset");
let manifest_path = version_root.join(PACKAGE_CACHE_MANIFEST_FILENAME);
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest).expect("serialize cache manifest"),
)
.expect("write cache manifest");
if protect {
let component_root = packages_root.join(component.id.as_str());
for path in [
packages_root,
component_root.as_path(),
version_root.as_path(),
asset_path.as_path(),
manifest_path.as_path(),
] {
protect_path_for_owner_admin_system(path).expect("protect trusted cache path");
}
}
(version_root, manifest)
}
fn write_trusted_state(state_path: &Path, manifest: &PackageCacheManifest) {
write_state(state_path, manifest, true);
}
fn write_state(state_path: &Path, manifest: &PackageCacheManifest, protect: bool) {
let proof = &manifest.independent_proof;
let checked_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock must be after Unix epoch")
.as_secs();
let state = ComponentUpdatesState {
schema_version: COMPONENT_UPDATES_STATE_SCHEMA_VERSION,
observations: vec![ComponentUpdateObservation {
component_id: manifest.component_id,
checked_at_unix,
latest_known_version: manifest.version.clone(),
trusted_releases: vec![TrustedGithubReleaseObservation {
repository: proof.repository.clone(),
release_id: proof.release_id,
asset_id: proof.asset_id,
stable_tag: proof.stable_tag.clone(),
asset_name: proof.asset_name.clone(),
size: proof.size,
sha256_from_api: proof.sha256_from_api.clone(),
}],
}],
};
let state_parent = state_path.parent().expect("state path has parent");
fs::create_dir_all(state_parent).expect("create state directory");
fs::write(
state_path,
serde_json::to_vec_pretty(&state).expect("serialize trusted update state"),
)
.expect("write trusted update state");
if protect {
protect_path_for_owner_admin_system(state_parent).expect("protect state parent");
protect_path_for_owner_admin_system(state_path).expect("protect trusted update state");
}
}
struct TestDirectory {
path: PathBuf,
target_root: PathBuf,
}
impl TestDirectory {
fn new() -> Self {
let target_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("target");
fs::create_dir_all(&target_root).expect("create Cargo target directory");
let target_root =
fs::canonicalize(target_root).expect("canonicalize Cargo target directory");
let path = target_root.join(format!("component-package-tests-{}", Uuid::new_v4()));
fs::create_dir(&path).expect("create isolated component package test directory");
Self { path, target_root }
}
fn path(&self) -> &Path {
&self.path
}
fn storage_paths(&self) -> StoragePaths {
StoragePaths::new(&self.path)
}
fn packages_path(&self) -> PathBuf {
self.storage_paths().packages_dir
}
fn state_path(&self) -> PathBuf {
self.storage_paths().component_updates_file
}
}
impl Drop for TestDirectory {
fn drop(&mut self) {
let has_exact_parent = self.path.parent() == Some(self.target_root.as_path());
let has_test_name = self
.path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.strip_prefix("component-package-tests-")
.is_some_and(|id| Uuid::parse_str(id).is_ok())
});
if has_exact_parent
&& has_test_name
&& self.path.is_absolute()
&& ensure_no_reparse_ancestors(&self.path).is_ok()
{
let _ = fs::remove_dir_all(&self.path);
}
}
}
#[cfg(windows)]
struct JunctionGuard {
path: PathBuf,
expected_parent: PathBuf,
}
#[cfg(windows)]
impl Drop for JunctionGuard {
fn drop(&mut self) {
if self.path.parent() == Some(self.expected_parent.as_path())
&& self.path.file_name().is_some_and(|name| name == "packages")
{
let _ = fs::remove_dir(&self.path);
}
}
}
#[cfg(windows)]
fn create_junction(path: &Path, target: &Path, expected_parent: &Path) -> JunctionGuard {
assert_eq!(path.parent(), Some(expected_parent));
assert_eq!(
path.file_name().and_then(|name| name.to_str()),
Some("packages")
);
let output = Command::new("cmd")
.args(["/d", "/c", "mklink", "/J"])
.arg(path)
.arg(target)
.output()
.expect("run mklink for reparse-point fixture");
assert!(
output.status.success(),
"mklink failed: {}",
String::from_utf8_lossy(&output.stderr)
);
JunctionGuard {
path: path.to_path_buf(),
expected_parent: expected_parent.to_path_buf(),
}
}
@@ -0,0 +1,177 @@
use proxywarden_lib::{
configuration_transaction::{read_guard, revision_locked, ConfigurationTransaction},
storage::JsonStorage,
};
use std::{fs, path::PathBuf};
struct Fixture {
root: PathBuf,
storage: JsonStorage,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!("pw-transaction-{}", uuid::Uuid::new_v4()));
let storage = JsonStorage::new(&root);
storage.write_profiles(&[]).unwrap();
storage.write_targets(&[]).unwrap();
Self { root, storage }
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[test]
fn interrupted_commit_restores_primary_and_backup_before_next_read() {
let fixture = Fixture::new();
let path = &fixture.storage.paths().profiles_file;
let before = fs::read(path).unwrap();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
fixture.storage.write_profiles(&[]).unwrap();
fs::write(path, b"half-written").unwrap();
transaction.abort().unwrap();
assert_eq!(fs::read(path).unwrap(), before);
assert!(!proxywarden_lib::safe_fs::backup_path(path).exists());
let _guard = read_guard(&fixture.storage).unwrap();
assert!(fixture.storage.read_profiles().unwrap().is_empty());
}
#[test]
fn shared_lock_rejects_second_writer_and_reader() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
assert!(ConfigurationTransaction::begin(&fixture.storage, None).is_err());
assert!(read_guard(&fixture.storage).is_err());
transaction.commit().unwrap();
assert!(read_guard(&fixture.storage).is_ok());
}
#[test]
fn committed_revision_rejects_delayed_result_even_when_values_are_identical() {
let fixture = Fixture::new();
let revision = {
let _guard = read_guard(&fixture.storage).unwrap();
revision_locked(&fixture.storage).unwrap()
};
ConfigurationTransaction::begin(&fixture.storage, Some(&revision))
.unwrap()
.commit()
.unwrap();
assert!(ConfigurationTransaction::begin(&fixture.storage, Some(&revision)).is_err());
}
#[test]
fn damaged_snapshot_blocks_all_restoration_and_next_writer() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
let path = &fixture.storage.paths().profiles_file;
fs::write(path, b"new-state").unwrap();
fs::write(
fixture
.storage
.paths()
.migrations_dir
.join("configuration-before-2.json"),
b"damaged",
)
.unwrap();
assert!(transaction.abort().is_err());
assert_eq!(
fs::read(path).unwrap(),
b"new-state",
"validate all snapshots before restoring any"
);
assert!(read_guard(&fixture.storage).is_err());
assert!(ConfigurationTransaction::begin(&fixture.storage, None).is_err());
}
#[test]
fn successful_commit_removes_sensitive_fixed_snapshots() {
let fixture = Fixture::new();
ConfigurationTransaction::begin(&fixture.storage, None)
.unwrap()
.commit()
.unwrap();
for item in fs::read_dir(&fixture.storage.paths().migrations_dir).unwrap() {
let name = item.unwrap().file_name().to_string_lossy().to_string();
assert!(
!name.starts_with("configuration-before-") && !name.starts_with("configuration-commit")
);
}
}
#[test]
fn transaction_child() {
let Some(root) = std::env::var_os("PW_TEST_TRANSACTION_ROOT") else {
return;
};
let storage = JsonStorage::new(PathBuf::from(root));
let Ok(_transaction) = ConfigurationTransaction::begin(&storage, None) else {
std::process::exit(2);
};
fs::write(&storage.paths().profiles_file, b"interrupted-child-write").unwrap();
// Deliberately bypass Drop, as a terminated application does.
std::process::exit(0);
}
#[test]
fn committed_marker_survives_partial_snapshot_cleanup_without_rollback() {
let fixture = Fixture::new();
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
fs::write(&fixture.storage.paths().profiles_file, b"committed-state").unwrap();
// Model death after publishing the terminal marker and removing one snapshot.
let journal = fixture
.storage
.paths()
.migrations_dir
.join("configuration-commit.json");
let mut intent: serde_json::Value =
serde_json::from_slice(&fs::read(&journal).unwrap()).unwrap();
intent["committed"] = serde_json::Value::Bool(true);
fs::write(&journal, serde_json::to_vec(&intent).unwrap()).unwrap();
fs::remove_file(
fixture
.storage
.paths()
.migrations_dir
.join("configuration-before-0.json"),
)
.unwrap();
drop(transaction);
let _guard = read_guard(&fixture.storage).unwrap();
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
b"committed-state"
);
assert!(!journal.exists());
}
#[test]
fn process_death_is_recovered_before_normal_read_and_lock_excludes_other_processes() {
let fixture = Fixture::new();
let before = fs::read(&fixture.storage.paths().profiles_file).unwrap();
let launch = || {
std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", "transaction_child"])
.env("PW_TEST_TRANSACTION_ROOT", &fixture.root)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap()
};
let transaction = ConfigurationTransaction::begin(&fixture.storage, None).unwrap();
assert_eq!(launch().code(), Some(2));
transaction.abort().unwrap();
assert!(launch().success());
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
b"interrupted-child-write"
);
let _guard = read_guard(&fixture.storage).unwrap();
assert_eq!(
fs::read(&fixture.storage.paths().profiles_file).unwrap(),
before
);
}
+938
View File
@@ -0,0 +1,938 @@
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()
}
+445
View File
@@ -0,0 +1,445 @@
{
"schemaVersion": 1,
"historyEvidence": [
{
"commit": "c5120669d2b86f417f6dbd8fc7e01eeafbcea3ab",
"proves": "split storage and the generated ProxiFyre config shape"
},
{
"commit": "e745633d91880b2f795fee2496d7fb4c35c54a38",
"proves": "the later opportunistic generated-config bootstrap"
},
{
"commit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"proves": "the historical component candidate roots"
},
{
"commit": "dbba3806ccdc8c4792d59add359c7ade7fff1176",
"proves": "the current app-adjacent component layout and strong ProxiFyre marker"
}
],
"startup": {
"canonicalSplitSourceFiles": [
"config/profiles.json",
"config/targets.json",
"config/components.json",
"config/local-singbox.json"
],
"generatedRecoverySource": "generated/proxifyre-app-config.json",
"rules": {
"currentMetadata": "no_op",
"anySplitSourceExists": "adopt_split_without_generated_import",
"allSplitSourcesAbsentAndOneSupportedGeneratedConfig": "import_once",
"partialOrCorruptSplit": "recover_or_warn_without_generated_merge"
}
},
"fixtures": [
{
"id": "proxifyre-generated",
"status": "supported",
"files": [
"proxifyre-generated/app-config.json"
]
},
{
"id": "proxifyre-unsupported",
"status": "unsupported_preserve_original",
"files": [
"proxifyre-unsupported/app-config.json"
]
},
{
"id": "proxifyre-real-sanitized",
"status": "supported_structure_preserving_sanitized_sample",
"files": [
"proxifyre-real-sanitized/app-config.json"
],
"provenance": {
"capturedAt": "2026-08-17",
"source": "C:\\ProgramData\\ProxyWarden\\generated\\proxifyre-app-config.json on a pre-1.2 local installation",
"sourceSha256": "078C3E74B96D26DF155229B2F6FD380F762550B0A3F863CE2E913F5B6493F710",
"preserved": "root/proxy key sets, proxy count, app count and app value categories, protocol values, default flags and endpoint shape",
"replaced": "all app names, filesystem paths, hostnames and ports"
}
},
{
"id": "marker-formats",
"status": "schema_evidence_only",
"files": [
"markers/install-proxyfier.marker.json",
"markers/proxywarden-component.json"
]
},
{
"id": "pre-1.2-split",
"status": "adopt_without_generated_import",
"files": [
"pre-1.2-split/config/profiles.json",
"pre-1.2-split/config/targets.json",
"pre-1.2-split/config/components.json",
"pre-1.2-split/config/local-singbox.json"
]
}
],
"proxifyreFieldMatrix": [
{
"id": "log-level-info",
"jsonPath": "$.logLevel == Info",
"outcome": "derived",
"destination": "generator default Info",
"coverage": "proxifyre-generated"
},
{
"id": "log-level-other",
"jsonPath": "$.logLevel != Info",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "bypass-lan-true",
"jsonPath": "$.bypassLan == true",
"outcome": "derived",
"destination": "generator default true",
"coverage": "proxifyre-generated"
},
{
"id": "bypass-lan-other",
"jsonPath": "$.bypassLan != true",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "proxies",
"jsonPath": "$.proxies[*]",
"outcome": "derived",
"destination": "one enabled profile and target pair per entry",
"coverage": "proxifyre-generated"
},
{
"id": "app-names",
"jsonPath": "$.proxies[*].appNames[*]",
"outcome": "canonical",
"destination": "profiles[*].items; folder recursive is derived because the generated format cannot represent it",
"coverage": "proxifyre-generated"
},
{
"id": "plain-endpoint",
"jsonPath": "$.proxies[*].socks5ProxyEndpoint plain host:port",
"outcome": "canonical",
"destination": "targets[*].host and targets[*].port",
"coverage": "proxifyre-generated"
},
{
"id": "endpoint-scheme-or-userinfo",
"jsonPath": "$.proxies[*].socks5ProxyEndpoint with scheme, userinfo, missing port, or port 0",
"outcome": "unsupported",
"coverage": "inline-uri-userinfo"
},
{
"id": "protocol-tcp",
"jsonPath": "$.proxies[*].supportedProtocols[*] == TCP",
"outcome": "canonical",
"destination": "profiles[*].protocols TCP",
"coverage": "proxifyre-generated"
},
{
"id": "protocol-udp",
"jsonPath": "$.proxies[*].supportedProtocols[*] == UDP",
"outcome": "canonical",
"destination": "profiles[*].protocols UDP",
"coverage": "proxifyre-generated"
},
{
"id": "protocol-other-or-empty",
"jsonPath": "$.proxies[*].supportedProtocols empty or value other than TCP/UDP",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "credentials-userinfo",
"jsonPath": "$.proxies[*].username/password/userinfo",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "tls",
"jsonPath": "$.proxies[*].tls",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "address-family",
"jsonPath": "$.proxies[*].addressFamily",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "unknown-root-key",
"jsonPath": "$.* unknown root key",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
},
{
"id": "unknown-proxy-key",
"jsonPath": "$.proxies[*].* unknown proxy key",
"outcome": "unsupported",
"coverage": "proxifyre-unsupported"
}
],
"components": {
"proxifyre": {
"managedCurrentRootTemplate": "{controlAppDir}\\components\\ProxiFyre",
"confirmedManagedLegacyDefaultRoots": [
"C:\\Tools\\ProxiFyre"
],
"legacyCandidates": [
{
"path": "C:\\Tools\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/commands.rs"
},
{
"path": "%ProgramFiles%\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles(x86)%\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%LOCALAPPDATA%\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles%\\ProxyWarden\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "dbba3806ccdc8c4792d59add359c7ade7fff1176",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles(x86)%\\ProxyWarden\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "dbba3806ccdc8c4792d59add359c7ade7fff1176",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%LOCALAPPDATA%\\ProxyWarden\\ProxiFyre",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "dbba3806ccdc8c4792d59add359c7ade7fff1176",
"evidenceFile": "src-tauri/src/component_detection.rs"
}
],
"managedLegacyClassificationRequires": [
"exact allowlisted root",
"ProxiFyre.exe exists at that root",
"service PathName points to that exact executable",
"binary matches a known bundled package identity"
],
"service": {
"primaryName": "ProxiFyreService",
"discoveryOnlyAliases": [
"ProxiFyre"
],
"discoveryOnlyPathNameTemplate": "\"{root}\\ProxiFyre.exe\" --service",
"autoCutoverPathNameTemplate": "\"{root}\\ProxiFyre.exe\" -displayname \"ProxiFyre Service\" -servicename \"ProxiFyreService\""
},
"autoCutover": {
"decision": "automatic_proxifyre_2_2_1",
"root": "C:\\Tools\\ProxiFyre",
"versionValues": [
"2.2.1",
"2.2.1.0"
],
"packageManifest": [
{
"relativePath": "Newtonsoft.Json.dll",
"size": 711952,
"sha256": "e1e27af7b07eeedf5ce71a9255f0422816a6fc5849a483c6714e1b472044fa9d"
},
{
"relativePath": "Newtonsoft.Json.xml",
"size": 713541,
"sha256": "79ee87d4ede8783461de05b93379d576f6e8575d4ab49359f15897a854b643c4"
},
{
"relativePath": "NLog.config",
"size": 382,
"sha256": "06b8e52be9385e4e6a2f042f0d7ca3dd0b043378b455535299846b02fd19250d"
},
{
"relativePath": "NLog.dll",
"size": 940032,
"sha256": "4b1d3cf9f1f3c4a6ead141243069162172e9ef48ba1a9bf4f7ccd618b8194b5c"
},
{
"relativePath": "NLog.xml",
"size": 1608606,
"sha256": "6871374d682e75aff17de2a8626a75e9c75409516f5e7527e9d159c1de6831bb"
},
{
"relativePath": "ProxiFyre.exe",
"size": 35960,
"sha256": "2a60a76480715fca52185163d7ac6d850d4b0abe4079b7d461d7d0fcb3f02d93"
},
{
"relativePath": "ProxiFyre.exe.config",
"size": 177,
"sha256": "8403846edd2ee98fd53b351dbf8773951c8e30f4b04dd53676a7e7dfbd8930b0"
},
{
"relativePath": "socksify.dll",
"size": 1309184,
"sha256": "940b22ae8e97ff575317cc4a6c20467ed2ff760d01c7b056d13fcb20e7043cbd"
},
{
"relativePath": "Topshelf.dll",
"size": 190464,
"sha256": "bd70a5832124e36840452ff46e442efa0a09a4ceba842aea72c79b2d322d7fe8"
},
{
"relativePath": "Topshelf.xml",
"size": 80754,
"sha256": "3b2228b3333c4fd86e29020bc2d77a5260dbf03e911829d6226498ade53c2790"
}
],
"scmProfile": {
"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
},
"allOtherDiscoveryCandidates": {
"decision": "manual_migration_required",
"mutationPlan": []
}
},
"markers": {
"managedLegacyRoot": "none",
"weakStandaloneScriptHint": {
"file": "install-proxyfier.marker.json",
"fields": [
"component",
"packagePath",
"serviceName",
"installedAt"
],
"ownershipProof": false
},
"managedCurrent": {
"file": "proxywarden-component.json",
"requiredValues": {
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService"
},
"rootField": "installRoot",
"packetFilterOwnershipField": "packetFilterInstalledByProxyWarden"
}
}
},
"singbox": {
"managedCurrentRootTemplate": "{controlAppDir}\\components\\sing-box",
"confirmedManagedLegacyDefaultRoots": [
"C:\\Program Files\\ProxyWarden\\sing-box"
],
"legacyCandidates": [
{
"path": "C:\\Tools\\ProxyWarden\\sing-box",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles%\\ProxyWarden\\sing-box",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles(x86)%\\ProxyWarden\\sing-box",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%LOCALAPPDATA%\\ProxyWarden\\sing-box",
"classificationBeforeIdentity": "candidate",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
}
],
"managedLegacyClassificationRequires": [
"exact allowlisted root",
"sing-box.exe and ProxyWardenSingBox.exe exist at that root",
"service PathName points to that exact wrapper",
"ProxyWardenSingBox.xml has matching id, executable, and config arguments",
"binaries match known bundled package identities"
],
"foreignByDefaultCandidates": [
{
"path": "%ProgramFiles%\\sing-box",
"classificationBeforeIdentity": "foreign",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%ProgramFiles(x86)%\\sing-box",
"classificationBeforeIdentity": "foreign",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
},
{
"path": "%LOCALAPPDATA%\\sing-box",
"classificationBeforeIdentity": "foreign",
"evidenceCommit": "9fd0a8c0b9e8472aa8e9a983e6c07780f5236c4a",
"evidenceFile": "src-tauri/src/component_detection.rs"
}
],
"service": {
"primaryName": "ProxyWardenSingBox",
"pathNameTemplate": "\"{root}\\ProxyWardenSingBox.exe\"",
"identityFiles": [
"ProxyWardenSingBox.exe",
"ProxyWardenSingBox.xml",
"sing-box.exe"
]
},
"markers": {
"managedLegacyRoot": "none"
},
"autoCutover": {
"decision": "manual_migration_required",
"reason": "historical installer downloaded moving latest sing-box and WinSW-x64 without a frozen inner identity",
"mutationPlan": []
}
}
},
"collisionPolicy": {
"currentAndLegacy": "current_wins_orphan_untouched_manual",
"multipleLegacyCandidates": "block_without_mutation",
"sameServiceNameForeignPath": "ownership_mismatch_without_mutation"
},
"runningStatePolicy": {
"running": "restore_running_after_success_or_rollback",
"stopped": "keep_stopped_after_success_or_rollback",
"pendingOrUnknown": "block_without_mutation"
}
}
@@ -0,0 +1,6 @@
{
"component": "proxyfier",
"packagePath": "C:\\Fixture\\Packages\\proxifyre-package.zip",
"serviceName": "ProxiFyreService",
"installedAt": "2026-01-01T00:00:00Z"
}
@@ -0,0 +1,7 @@
{
"manager": "ProxyWarden",
"component": "proxifyre",
"serviceName": "ProxiFyreService",
"installRoot": "C:\\Fixture\\ProxyWarden\\components\\ProxiFyre",
"packetFilterInstalledByProxyWarden": false
}
@@ -0,0 +1,24 @@
[
{
"id": "proxyfier",
"name": "ProxiFyre",
"state": "installed",
"installed": true,
"running": false,
"version": null,
"path": "C:\\Tools\\ProxiFyre\\ProxiFyre.exe",
"problems": [],
"actions": []
},
{
"id": "singbox",
"name": "Local sing-box",
"state": "missing",
"installed": false,
"running": false,
"version": null,
"path": null,
"problems": [],
"actions": []
}
]
@@ -0,0 +1,9 @@
{
"subscription_url": "https://subscription.example.test/redacted",
"selected_server_tag": "fixture-server",
"listen_host": "127.0.0.1",
"listen_port": 1080,
"service_name": "ProxyWardenSingBox",
"install_root": "C:\\Program Files\\ProxyWarden\\sing-box",
"updated_at": null
}
@@ -0,0 +1,29 @@
[
{
"id": "fixture-profile",
"name": "Fixture profile",
"enabled": true,
"target_id": "fixture-target",
"protocols": [
"TCP",
"UDP"
],
"items": [
{
"type": "process",
"value": "FixtureProcess",
"recursive": false
},
{
"type": "exe",
"value": "C:\\Fixture\\Apps\\fixture.exe",
"recursive": false
},
{
"type": "folder",
"value": "C:\\Fixture\\Games",
"recursive": true
}
]
}
]
@@ -0,0 +1,11 @@
[
{
"id": "fixture-target",
"name": "Fixture target",
"kind": "external",
"protocol": "socks5",
"host": "proxy.example.test",
"port": 1080,
"requires_component": null
}
]
@@ -0,0 +1,18 @@
{
"logLevel": "Info",
"bypassLan": true,
"proxies": [
{
"appNames": [
"FixtureProcess",
"C:\\Fixture\\Apps\\fixture.exe",
"C:\\Fixture\\Games"
],
"socks5ProxyEndpoint": "proxy.example.test:1080",
"supportedProtocols": [
"TCP",
"UDP"
]
}
]
}
@@ -0,0 +1,23 @@
{
"logLevel": "Info",
"bypassLan": true,
"proxies": [
{
"appNames": [
"FixtureProcess1",
"FixtureProcess2",
"FixtureProcess3",
"FixtureProcess4",
"FixtureProcess5",
"C:\\Fixture\\Folder1",
"C:\\Fixture\\Folder2",
"C:\\Fixture\\Folder3"
],
"socks5ProxyEndpoint": "proxy.example.test:1080",
"supportedProtocols": [
"TCP",
"UDP"
]
}
]
}
@@ -0,0 +1,25 @@
{
"logLevel": "Debug",
"bypassLan": false,
"customRootField": "REDACTED",
"proxies": [
{
"appNames": [
"FixtureProcess"
],
"socks5ProxyEndpoint": "proxy.example.test:1080",
"supportedProtocols": [
"TCP",
"ICMP"
],
"username": "__REDACTED_USERNAME__",
"password": "__REDACTED_PASSWORD__",
"tls": {
"enabled": true,
"serverName": "tls.example.test"
},
"addressFamily": "IPv4",
"customProxyField": "REDACTED"
}
]
}
-134
View File
@@ -1,134 +0,0 @@
use proxywarden_lib::helper::{
helper_action_requires_elevation, install_request, parse_helper_response,
proxifyre_apply_request, service_request, HelperAction, HelperCommandOutput,
HelperCommandRunner, HelperCommandSpec, HelperError, HelperResponse, StructuredHelper,
};
use proxywarden_lib::models::ComponentId;
use serde_json::json;
use std::cell::RefCell;
use std::path::PathBuf;
#[test]
fn structured_helper_serializes_request_and_parses_json_response() {
let runner = MockRunner {
output: HelperCommandOutput {
status_code: 0,
stdout: serde_json::to_string(&HelperResponse {
success: true,
action: HelperAction::ProxyfierApply,
changed: true,
message: "Applied".to_string(),
details: json!({ "serviceName": "ProxiFyreService" }),
})
.expect("response json"),
stderr: String::new(),
},
seen: RefCell::new(Vec::new()),
};
let helper = StructuredHelper::new("proxywarden-helper.exe", runner);
let response = helper
.execute(&proxifyre_apply_request(
r"C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json",
"ProxiFyreService",
))
.expect("helper response");
assert!(response.success);
assert_eq!(response.action, HelperAction::ProxyfierApply);
assert_eq!(response.details["serviceName"], "ProxiFyreService");
}
#[test]
fn helper_runner_receives_json_stdin_and_elevation_flag() {
let runner = MockRunner {
output: HelperCommandOutput {
status_code: 0,
stdout: r#"{"success":true,"action":"service.restart","changed":true,"message":"Restarted","details":{}}"#.to_string(),
stderr: String::new(),
},
seen: RefCell::new(Vec::new()),
};
let helper = StructuredHelper::new("proxywarden-helper.exe", runner);
let request = service_request(ComponentId::Proxyfier, HelperAction::ServiceRestart);
let _ = helper.execute(&request).expect("helper response");
let seen = helper.runner().seen.borrow();
let spec = seen.first().expect("runner should be called");
let stdin: serde_json::Value = serde_json::from_str(&spec.stdin).expect("stdin json");
assert_eq!(spec.program, PathBuf::from("proxywarden-helper.exe"));
assert_eq!(spec.args, vec!["--json"]);
assert!(spec.requires_elevation);
assert_eq!(stdin["action"], "service.restart");
assert_eq!(stdin["component"], "proxyfier");
}
#[test]
fn install_requests_are_explicit_component_actions() {
let control = install_request(ComponentId::ControlApp);
let proxyfier = install_request(ComponentId::Proxyfier);
let singbox = install_request(ComponentId::Singbox);
assert_eq!(control.action, HelperAction::InstallControlApp);
assert_eq!(proxyfier.action, HelperAction::InstallProxyfier);
assert_eq!(singbox.action, HelperAction::InstallSingbox);
assert!(helper_action_requires_elevation(&proxyfier.action));
}
#[test]
fn apply_request_does_not_encode_installer_action() {
let request = proxifyre_apply_request(
r"C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json",
"ProxiFyreService",
);
assert_eq!(request.action, HelperAction::ProxyfierApply);
assert_eq!(request.component, Some(ComponentId::Proxyfier));
assert_eq!(
request.payload["configPath"],
r"C:\ProgramData\ProxyWarden\generated\proxifyre-app-config.json"
);
}
#[test]
fn non_json_helper_stdout_is_rejected() {
let error = parse_helper_response("Proxyfier restarted successfully")
.expect_err("raw stdout should not be accepted");
assert_eq!(error.code, "helper_response_decode");
}
#[test]
fn failed_helper_exit_is_structured_error() {
let runner = MockRunner {
output: HelperCommandOutput {
status_code: 5,
stdout: String::new(),
stderr: "Access denied".to_string(),
},
seen: RefCell::new(Vec::new()),
};
let helper = StructuredHelper::new("proxywarden-helper.exe", runner);
let error = helper
.execute(&service_request(
ComponentId::Proxyfier,
HelperAction::ServiceRestart,
))
.expect_err("failed exit should become helper error");
assert_eq!(error.code, "helper_exit");
assert!(error.message.contains("Access denied"));
}
struct MockRunner {
output: HelperCommandOutput,
seen: RefCell<Vec<HelperCommandSpec>>,
}
impl HelperCommandRunner for MockRunner {
fn run(&self, spec: &HelperCommandSpec) -> Result<HelperCommandOutput, HelperError> {
self.seen.borrow_mut().push(spec.clone());
Ok(self.output.clone())
}
}
@@ -0,0 +1,136 @@
use proxywarden_lib::command_dto::CommandError;
use proxywarden_lib::component_inventory::{
classify_component_candidates, BinaryIdentityEvidence, CandidateRole, ComponentCandidateProbe,
InventoryAction, MarkerEvidence, ServiceEvidence, OWNERSHIP_MISMATCH,
};
use proxywarden_lib::models::ComponentId;
use proxywarden_lib::proxifyre_runtime::run_proxifyre_lifecycle_entrypoint;
use proxywarden_lib::singbox_runtime::{
run_singbox_config_check_entrypoint, run_singbox_lifecycle_entrypoint,
};
use std::cell::Cell;
use std::path::PathBuf;
#[test]
fn foreign_component_blocks_real_proxifyre_lifecycle_runners_before_call() {
let inventory = foreign_inventory();
for action in [
InventoryAction::Install,
InventoryAction::Apply,
InventoryAction::CheckBinary,
InventoryAction::Start,
InventoryAction::Stop,
InventoryAction::ConfigureFirewall,
InventoryAction::Update,
InventoryAction::Uninstall,
] {
let calls = Cell::new(0_u32);
let result = run_proxifyre_lifecycle_entrypoint(&inventory, action, |_| {
calls.set(calls.get() + 1);
Ok::<_, CommandError>(())
});
assert_eq!(calls.get(), 0, "runner was called for {action:?}");
assert_eq!(result.unwrap_err().code, OWNERSHIP_MISMATCH);
}
}
#[test]
fn incomplete_component_blocks_real_singbox_process_service_and_delete_runners() {
let inventory = incomplete_inventory();
for action in [
InventoryAction::Install,
InventoryAction::Apply,
InventoryAction::CheckBinary,
InventoryAction::Start,
InventoryAction::Stop,
InventoryAction::Uninstall,
] {
let calls = Cell::new(0_u32);
let result = run_singbox_lifecycle_entrypoint(&inventory, action, |_| {
calls.set(calls.get() + 1);
Ok::<_, CommandError>(())
});
assert_eq!(calls.get(), 0, "runner was called for {action:?}");
assert!(result.is_err());
}
let process_calls = Cell::new(0_u32);
let result = run_singbox_config_check_entrypoint(&inventory, |_| {
process_calls.set(process_calls.get() + 1);
Ok::<_, CommandError>(())
});
assert!(result.is_err());
assert_eq!(process_calls.get(), 0, "sing-box checker was called");
}
#[test]
fn missing_component_allows_install_runner_only() {
let inventory = classify_component_candidates(ComponentId::Singbox, Vec::new());
let calls = Cell::new(0_u32);
run_singbox_lifecycle_entrypoint(&inventory, InventoryAction::Install, |candidate| {
assert!(candidate.is_none());
calls.set(calls.get() + 1);
Ok::<_, CommandError>(())
})
.expect("missing component should permit explicit install");
assert_eq!(calls.get(), 1);
}
fn foreign_inventory() -> proxywarden_lib::component_inventory::ComponentInventory {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\ProxiFyre");
classify_component_candidates(
ComponentId::Proxyfier,
vec![ComponentCandidateProbe {
component_id: ComponentId::Proxyfier,
role: CandidateRole::Current,
root: root.clone(),
root_exists: true,
has_reparse_point: false,
executable_path: Some(root.join("ProxiFyre.exe")),
missing_files: Vec::new(),
marker: MarkerEvidence::Valid,
marker_required: true,
binary_identity: BinaryIdentityEvidence::KnownPackage,
binary_version: Some("2.4.0.0".to_string()),
service: Some(ServiceEvidence {
name: "ProxiFyreService".to_string(),
status: "running".to_string(),
path_name: Some(r#""C:\Foreign\ProxiFyre.exe" --service"#.to_string()),
executable_path: Some(PathBuf::from(r"C:\Foreign\ProxiFyre.exe")),
path_matches_candidate: false,
binary_version: None,
}),
service_required: true,
legacy_identity_complete: false,
}],
)
}
fn incomplete_inventory() -> proxywarden_lib::component_inventory::ComponentInventory {
let root = PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box");
classify_component_candidates(
ComponentId::Singbox,
vec![ComponentCandidateProbe {
component_id: ComponentId::Singbox,
role: CandidateRole::Current,
root: root.clone(),
root_exists: true,
has_reparse_point: false,
executable_path: Some(root.join("sing-box.exe")),
missing_files: vec![root.join("ProxyWardenSingBox.exe")],
marker: MarkerEvidence::NotRequired,
marker_required: false,
binary_identity: BinaryIdentityEvidence::Unknown,
binary_version: None,
service: None,
service_required: true,
legacy_identity_complete: false,
}],
)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,346 @@
use proxywarden_lib::component_catalog::ComponentId;
use proxywarden_lib::component_packages::{
ComponentPackageService, ComponentPackagesError, GithubReleaseDigestProof,
NativePrivilegedBundleVerifier, PackageSignatureVerifier, PackageSource,
PrivilegedBundleVerificationError, PrivilegedBundleVerifier, PrivilegedCachedUpdatePlan,
SignaturePublisher, SignatureVerifierError, UpdateRequestKind, UpdateTransport,
UpdateTransportError, UpdateTransportRequest, UpdateTransportResponse,
};
use proxywarden_lib::safe_fs::protect_path_for_owner_admin_system;
use proxywarden_lib::storage::StoragePaths;
use serde_json::json;
use sha2::{Digest, Sha256};
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use uuid::Uuid;
#[test]
fn privileged_fresh_install_ignores_forged_newer_cache_and_has_no_transport() {
let workspace = TestWorkspace::new();
let paths = workspace.storage_paths();
let forged = paths
.packages_dir
.join(ComponentId::SingBox.as_str())
.join("999.0.0");
fs::create_dir_all(&forged).expect("create forged cache");
fs::write(forged.join("forged.zip"), b"not an official package").expect("write forged cache");
let service =
ComponentPackageService::open(bundled_root(), &paths).expect("open component packages");
let verifier = ExplicitTestBundleVerifier::default();
let lease = service
.lease_bundled_for_privileged_install(ComponentId::SingBox, &verifier)
.expect("lease immutable bundled sing-box");
assert_eq!(lease.proof().source, PackageSource::Bundled);
assert_eq!(lease.proof().version, "1.13.19");
assert_eq!(lease.proof().independent_proof, None);
assert_eq!(verifier.calls.load(Ordering::SeqCst), 1);
assert!(lease.asset_path().starts_with(bundled_root()));
// There is intentionally no transport argument on the fresh-install API.
}
#[test]
fn native_bundle_verifier_rejects_user_owned_or_noncanonical_bundle() {
let workspace = TestWorkspace::new();
let service = ComponentPackageService::open(bundled_root(), &workspace.storage_paths())
.expect("open component packages");
assert!(matches!(
service.lease_bundled_for_privileged_install(
ComponentId::SingBox,
&NativePrivilegedBundleVerifier
),
Err(ComponentPackagesError::UntrustedBundleRoot)
));
}
#[test]
fn privileged_update_requires_the_exact_live_latest_proof_before_staging() {
let workspace = TestWorkspace::new();
let service = ComponentPackageService::open(bundled_root(), &workspace.storage_paths())
.expect("open component packages");
let plan = sing_box_plan(b"cached sing-box update");
let mut wrong_latest = plan.clone();
wrong_latest.independent_proof.release_id += 1;
let transport = MetadataTransport::success(metadata_for(&wrong_latest));
assert!(matches!(
service.lease_cached_update_for_privileged_install(
&plan,
workspace.path(),
&transport,
&NoopVerifier,
),
Err(ComponentPackagesError::InvalidPrivilegedUpdatePlan)
));
assert_eq!(transport.calls.load(Ordering::SeqCst), 1);
assert_no_privileged_staging(workspace.path());
let timeout = MetadataTransport::failure();
assert!(matches!(
service.lease_cached_update_for_privileged_install(
&plan,
workspace.path(),
&timeout,
&NoopVerifier,
),
Err(ComponentPackagesError::Transport)
));
assert_eq!(timeout.calls.load(Ordering::SeqCst), 1);
assert_no_privileged_staging(workspace.path());
}
#[cfg(windows)]
#[test]
fn tampered_cache_fails_before_any_admin_staging_is_created() {
let workspace = TestWorkspace::new();
let paths = workspace.storage_paths();
let service =
ComponentPackageService::open(bundled_root(), &paths).expect("open component packages");
let plan = sing_box_plan(b"official update bytes");
write_cache(&paths, &plan, b"tampered cache bytes");
let transport = MetadataTransport::success(metadata_for(&plan));
assert!(matches!(
service.lease_cached_update_for_privileged_install(
&plan,
workspace.path(),
&transport,
&NoopVerifier,
),
Err(ComponentPackagesError::DigestMismatch)
));
assert_eq!(transport.calls.load(Ordering::SeqCst), 1);
assert_no_privileged_staging(workspace.path());
}
#[test]
fn winsw_and_vc_runtime_make_zero_privileged_update_transport_calls() {
let workspace = TestWorkspace::new();
let service = ComponentPackageService::open(bundled_root(), &workspace.storage_paths())
.expect("open component packages");
for component_id in [ComponentId::Winsw, ComponentId::VcRuntime] {
let transport = MetadataTransport::failure();
let mut plan = sing_box_plan(b"unused");
plan.component_id = component_id;
assert!(matches!(
service.lease_cached_update_for_privileged_install(
&plan,
workspace.path(),
&transport,
&NoopVerifier,
),
Err(ComponentPackagesError::NoTrustedUpdate)
));
assert_eq!(transport.calls.load(Ordering::SeqCst), 0);
}
}
#[derive(Default)]
struct ExplicitTestBundleVerifier {
calls: AtomicUsize,
}
struct NoopVerifier;
impl PackageSignatureVerifier for NoopVerifier {
fn verify(&self, _path: &Path) -> Result<SignaturePublisher, SignatureVerifierError> {
Err(SignatureVerifierError)
}
}
impl PrivilegedBundleVerifier for ExplicitTestBundleVerifier {
fn verify(
&self,
bundled_root: &Path,
catalog_path: &Path,
asset_path: &Path,
) -> Result<(), PrivilegedBundleVerificationError> {
self.calls.fetch_add(1, Ordering::SeqCst);
if catalog_path != bundled_root.join("catalog.json")
|| !asset_path.starts_with(bundled_root)
{
return Err(PrivilegedBundleVerificationError);
}
Ok(())
}
}
struct MetadataTransport {
response: Result<Vec<u8>, UpdateTransportError>,
calls: AtomicUsize,
}
impl MetadataTransport {
fn success(response: Vec<u8>) -> Self {
Self {
response: Ok(response),
calls: AtomicUsize::new(0),
}
}
fn failure() -> Self {
Self {
response: Err(UpdateTransportError::RequestFailed),
calls: AtomicUsize::new(0),
}
}
}
impl UpdateTransport for MetadataTransport {
fn get(
&self,
request: &UpdateTransportRequest,
) -> Result<UpdateTransportResponse, UpdateTransportError> {
self.calls.fetch_add(1, Ordering::SeqCst);
assert_eq!(request.kind, UpdateRequestKind::GithubReleaseMetadata);
assert_eq!(
request.url,
"https://api.github.com/repos/SagerNet/sing-box/releases/latest"
);
let body = self.response.clone()?;
Ok(UpdateTransportResponse {
status: 200,
location: None,
content_length: Some(body.len() as u64),
body: Box::new(Cursor::new(body)),
})
}
}
fn sing_box_plan(bytes: &[u8]) -> PrivilegedCachedUpdatePlan {
let version = "1.14.0";
let asset_name = format!("sing-box-{version}-windows-amd64.zip");
let sha256 = format!("{:x}", Sha256::digest(bytes));
PrivilegedCachedUpdatePlan {
component_id: ComponentId::SingBox,
version: version.to_string(),
independent_proof: GithubReleaseDigestProof {
repository: "SagerNet/sing-box".to_string(),
release_id: 700,
asset_id: 701,
stable_tag: format!("v{version}"),
asset_name,
size: bytes.len() as u64,
sha256_from_api: sha256,
verified_signatures: Vec::new(),
},
}
}
fn metadata_for(plan: &PrivilegedCachedUpdatePlan) -> Vec<u8> {
let proof = &plan.independent_proof;
serde_json::to_vec(&json!({
"id": proof.release_id,
"tag_name": proof.stable_tag,
"draft": false,
"prerelease": false,
"assets": [{
"id": proof.asset_id,
"name": proof.asset_name,
"size": proof.size,
"digest": format!("sha256:{}", proof.sha256_from_api),
"browser_download_url": format!(
"https://github.com/{}/releases/download/{}/{}",
proof.repository, proof.stable_tag, proof.asset_name
)
}]
}))
.expect("serialize GitHub metadata")
}
#[cfg(windows)]
fn write_cache(paths: &StoragePaths, plan: &PrivilegedCachedUpdatePlan, bytes: &[u8]) {
use proxywarden_lib::component_packages::{
PackageCacheManifest, PACKAGE_CACHE_MANIFEST_FILENAME,
PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
};
let component_root = paths.packages_dir.join(plan.component_id.as_str());
let version_root = component_root.join(&plan.version);
fs::create_dir_all(&version_root).expect("create cache directory");
let asset_path = version_root.join(&plan.independent_proof.asset_name);
fs::write(&asset_path, bytes).expect("write cache bytes");
let manifest_path = version_root.join(PACKAGE_CACHE_MANIFEST_FILENAME);
let manifest = PackageCacheManifest {
schema_version: PACKAGE_CACHE_MANIFEST_SCHEMA_VERSION,
component_id: plan.component_id,
version: plan.version.clone(),
asset_name: plan.independent_proof.asset_name.clone(),
sha256: plan.independent_proof.sha256_from_api.clone(),
size: plan.independent_proof.size,
independent_proof: plan.independent_proof.clone(),
};
fs::write(
&manifest_path,
serde_json::to_vec_pretty(&manifest).expect("serialize cache manifest"),
)
.expect("write cache manifest");
for path in [
paths.packages_dir.as_path(),
component_root.as_path(),
version_root.as_path(),
manifest_path.as_path(),
asset_path.as_path(),
] {
protect_path_for_owner_admin_system(path).expect("protect cache path");
}
}
fn assert_no_privileged_staging(parent: &Path) {
let staging = fs::read_dir(parent)
.expect("read staging parent")
.filter_map(Result::ok)
.filter_map(|entry| entry.file_name().into_string().ok())
.filter(|name| name.starts_with(".package-"))
.collect::<Vec<_>>();
assert!(
staging.is_empty(),
"unexpected staging entries: {staging:?}"
);
}
fn bundled_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("bundled")
.join("components")
}
struct TestWorkspace {
path: PathBuf,
}
impl TestWorkspace {
fn new() -> Self {
let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("target");
fs::create_dir_all(&target).expect("create Cargo target directory");
let path = target.join(format!("privileged-package-trust-{}", Uuid::new_v4()));
fs::create_dir(&path).expect("create test workspace");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
fn storage_paths(&self) -> StoragePaths {
StoragePaths::new(&self.path)
}
}
impl Drop for TestWorkspace {
fn drop(&mut self) {
if self
.path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("privileged-package-trust-"))
{
let _ = fs::remove_dir_all(&self.path);
}
}
}
File diff suppressed because it is too large Load Diff
+43
View File
@@ -86,6 +86,49 @@ fn skips_singbox_check_when_binary_path_is_not_supplied() {
assert!(checker.calls.borrow().is_empty());
}
#[test]
fn same_endpoint_uses_exact_outbound_id_and_never_falls_back_from_missing_id() {
let parsed = proxywarden_lib::subscription::parse_subscription_body(r#"{"outbounds":[
{"type":"vless","tag":"same%20label","server":"edge.example.test","server_port":443,"uuid":"11111111-1111-1111-1111-111111111111"},
{"type":"vless","tag":"same%20label","server":"edge.example.test","server_port":443,"uuid":"22222222-2222-2222-2222-222222222222"}
]}"#).unwrap();
let mut cache = SubscriptionCache {
config: parsed.config,
servers: parsed.servers,
user_info: Default::default(),
fetched_at: "fixture".into(),
};
let mut config = local_singbox_config("same label");
config.selected_server_id = Some(cache.servers[1].id.clone());
cache.normalize_percent_encoded_tags();
let adapter = SingBoxAdapter::default();
let generated = adapter
.generate_config(
SingBoxGenerationRequest::new(&config, &cache, None),
&RecordingChecker::ok("fixture"),
)
.unwrap();
let value: serde_json::Value = serde_json::from_str(&generated.contents).unwrap();
assert_eq!(
value["outbounds"][0]["uuid"],
"22222222-2222-2222-2222-222222222222"
);
config.selected_server_id = Some("pw-missing".into());
assert!(adapter
.generate_config(
SingBoxGenerationRequest::new(&config, &cache, None),
&RecordingChecker::ok("fixture")
)
.is_err());
config.selected_server_id = None;
assert!(adapter
.generate_config(
SingBoxGenerationRequest::new(&config, &cache, None),
&RecordingChecker::ok("fixture")
)
.is_err());
}
#[test]
fn blocks_config_when_server_is_not_selected() {
let adapter = SingBoxAdapter::default();
+112
View File
@@ -11,6 +11,8 @@ use proxywarden_lib::models::{
ActivityLevel, ComponentId, LocalSingBoxConfig, ProxyProtocol, SubscriptionCache,
SubscriptionServer, TargetKind,
};
#[cfg(windows)]
use proxywarden_lib::safe_fs;
use proxywarden_lib::storage::JsonStorage;
use proxywarden_lib::subscription;
use serde_json::{json, Map};
@@ -342,6 +344,11 @@ fn generate_writes_config_and_local_singbox_target() {
assert_eq!(target.port, 1080);
assert_eq!(target.requires_component, Some(ComponentId::Singbox));
assert_eq!(activity[0].title, "Конфиг Local sing-box создан");
#[cfg(windows)]
safe_fs::verify_path_protected_for_owner_admin_system(Path::new(
&response.generated_config_path,
))
.expect("generated sing-box config keeps restricted ACL");
cleanup(&root);
}
@@ -526,6 +533,111 @@ fn sample_cache() -> SubscriptionCache {
}
}
#[test]
fn failed_candidate_fetch_keeps_the_previous_url_cache_and_selection() {
struct FailedFetcher;
impl SubscriptionFetcher for FailedFetcher {
fn fetch_subscription(
&self,
_: &str,
_: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
Err(subscription::SubscriptionError {
message: "offline".to_string(),
})
}
}
let root = test_root("candidate-failure");
let storage = JsonStorage::new(&root);
save_singbox_subscription_to_storage(
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: "https://old.example.test/token".into(),
},
&FixedClock,
)
.unwrap();
storage
.write_singbox_subscription_cache(&sample_cache())
.unwrap();
let before = fs::read(&storage.paths().local_singbox_file).unwrap();
let cache_before = fs::read(&storage.paths().singbox_subscription_cache_file).unwrap();
assert!(
proxywarden_lib::singbox_subscription::fetch_singbox_subscription_candidate(
&storage,
Some("https://new.example.test/token"),
&FailedFetcher,
&FixedClock
)
.is_err()
);
assert_eq!(
fs::read(&storage.paths().local_singbox_file).unwrap(),
before
);
assert_eq!(
fs::read(&storage.paths().singbox_subscription_cache_file).unwrap(),
cache_before
);
cleanup(&root);
}
#[test]
fn fetch_finishing_after_forget_cannot_resurrect_subscription_or_backup() {
struct ForgetDuringFetch<'a>(&'a JsonStorage);
impl SubscriptionFetcher for ForgetDuringFetch<'_> {
fn fetch_subscription(
&self,
_: &str,
_: &subscription::SubscriptionFetchIdentity,
) -> Result<SubscriptionCache, subscription::SubscriptionError> {
forget_singbox_subscription_in_storage(self.0, &FixedClock).unwrap();
Ok(sample_cache())
}
}
let root = test_root("forget-during-fetch");
let storage = JsonStorage::new(&root);
save_singbox_subscription_to_storage(
&storage,
SaveSingBoxSubscriptionInputDto {
subscription_url: "https://old.example.test/token".into(),
},
&FixedClock,
)
.unwrap();
storage
.write_singbox_subscription_cache(&sample_cache())
.unwrap();
storage
.write_singbox_subscription_cache(&sample_cache())
.unwrap();
let error = fetch_singbox_subscription_with_fetcher(
&storage,
&ForgetDuringFetch(&storage),
&FixedClock,
)
.unwrap_err();
assert_eq!(error.code, "configuration_changed");
assert!(storage
.read_local_singbox_config()
.unwrap()
.subscription_url
.is_none());
assert!(storage.read_singbox_subscription_cache().unwrap().is_none());
assert!(!proxywarden_lib::safe_fs::backup_path(&storage.paths().local_singbox_file).exists());
assert!(!proxywarden_lib::safe_fs::backup_path(
&storage.paths().singbox_subscription_cache_file
)
.exists());
fs::remove_file(&storage.paths().local_singbox_file).unwrap();
assert!(storage
.read_local_singbox_config()
.unwrap()
.subscription_url
.is_none());
cleanup(&root);
}
fn sample_cache_with_flag_tag() -> SubscriptionCache {
SubscriptionCache {
config: json!({
+15 -84
View File
@@ -1,11 +1,8 @@
use proxywarden_lib::component_detection::DetectedSingBox;
use proxywarden_lib::singbox_service::{
build_singbox_setup_status, ensure_safe_singbox_install_dir, parse_service_command_output,
service_control_script, SingBoxServiceAction,
build_singbox_setup_status, singbox_service_xml, SINGBOX_SERVICE_LOG_DIR,
};
use std::path::{Path, PathBuf};
#[cfg(windows)]
use std::process::Command as ProcessCommand;
use std::path::PathBuf;
#[test]
fn setup_status_reports_missing_items_when_singbox_is_absent() {
@@ -28,90 +25,22 @@ fn setup_status_reports_ready_when_binary_wrapper_and_service_exist() {
assert!(status.ready);
assert_eq!(status.missing_count, 0);
assert!(status.items.iter().all(|item| item.installed));
assert_eq!(status.items[0].version, Some("1.11.0.0".to_string()));
assert_eq!(status.items[1].version, Some("3.0.0.0".to_string()));
assert_eq!(status.items[2].version, None);
}
#[test]
fn parses_last_json_service_command_output_line() {
let output = br#"
noise
{"success":true,"code":"started","serviceName":"ProxyWardenSingBox","status":"Running","processId":42}
"#;
let parsed = parse_service_command_output(output).expect("service json should parse");
fn winsw_disables_logs_and_targets_fixed_app_root_log_directory() {
let xml = singbox_service_xml();
assert!(parsed.success);
assert_eq!(parsed.code, "started");
assert_eq!(parsed.service_name, Some("ProxyWardenSingBox".to_string()));
assert_eq!(parsed.status, Some("Running".to_string()));
assert_eq!(parsed.process_id, Some(42));
}
#[test]
fn safe_install_dir_allows_only_proxywarden_singbox_folder() {
assert!(ensure_safe_singbox_install_dir(Path::new(
r"C:\Program Files\ProxyWarden\components\sing-box"
))
.is_ok());
assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Windows")).is_err());
assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Program Files\sing-box")).is_err());
}
#[test]
fn service_control_script_targets_named_service_and_action() {
let script = service_control_script(
SingBoxServiceAction::Start,
"ProxyWardenSingBox",
None,
None,
);
assert!(script.contains("$serviceName = 'ProxyWardenSingBox'"));
assert!(script.contains("$action = 'start'"));
assert!(script.contains("ConvertTo-Json -Compress"));
}
#[test]
fn service_control_script_syncs_generated_config_before_start() {
let source = Path::new(r"C:\ProgramData\ProxyWarden\generated\sing-box-config.json");
let target = Path::new(r"C:\Program Files\ProxyWarden\components\sing-box\config.json");
let script = service_control_script(
SingBoxServiceAction::Start,
"ProxyWardenSingBox",
Some(source),
Some(target),
);
assert!(script.contains(
"$configSource = 'C:\\ProgramData\\ProxyWarden\\generated\\sing-box-config.json'"
));
assert!(script.contains(
"$configTarget = 'C:\\Program Files\\ProxyWarden\\components\\sing-box\\config.json'"
));
assert!(script.contains("Copy-Item -LiteralPath $configSource"));
assert!(script.contains("'config_sync_failed'"));
}
#[test]
#[cfg(windows)]
fn install_singbox_script_parses_as_powershell() {
let script_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("scripts")
.join("install-singbox.ps1");
let escaped_path = script_path.display().to_string().replace('\'', "''");
let parser = format!(
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
);
let output = ProcessCommand::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
.output()
.expect("powershell parser should run");
assert!(
output.status.success(),
"install-singbox.ps1 should parse\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
assert!(xml.contains("<log mode=\"none\"/>"));
assert!(xml.contains(&format!("<logpath>{SINGBOX_SERVICE_LOG_DIR}</logpath>")));
assert_eq!(
SINGBOX_SERVICE_LOG_DIR,
r"%BASE%\..\..\.proxywarden-service-logs\sing-box"
);
assert!(!xml.contains("ProgramData"));
}
fn detected_singbox(binary_exists: bool, wrapper_exists: bool, running: bool) -> DetectedSingBox {
@@ -127,5 +56,7 @@ fn detected_singbox(binary_exists: bool, wrapper_exists: bool, running: bool) ->
wrapper_exists,
running,
service_name: "ProxyWardenSingBox".to_string(),
version: Some("1.11.0.0".to_string()),
wrapper_version: Some("3.0.0.0".to_string()),
}
}
+31 -35
View File
@@ -1,7 +1,6 @@
use proxywarden_lib::models::{
ActivityEntry, ActivityLevel, ComponentId, ComponentState, ComponentStatus, LocalSingBoxConfig,
Profile, ProfileItem, ProfileItemType, Protocol, ProxyProtocol, SubscriptionCache,
SubscriptionServer, Target, TargetKind,
ActivityEntry, ActivityLevel, LocalSingBoxConfig, Profile, ProfileItem, ProfileItemType,
Protocol, ProxyProtocol, SubscriptionCache, SubscriptionServer, Target, TargetKind,
};
use proxywarden_lib::storage::{backup_path, default_config_root, JsonStorage, StoragePaths};
use std::fs;
@@ -17,13 +16,12 @@ fn storage_defaults_to_programdata_root() {
}
#[test]
fn roundtrips_profiles_targets_components_and_activity() {
fn roundtrips_profiles_targets_and_activity() {
let root = test_root("roundtrip");
let storage = JsonStorage::new(root.clone());
let profiles = vec![sample_profile("discord")];
let targets = vec![sample_target("home-gateway")];
let components = vec![sample_component()];
let activity = vec![sample_activity(
"created",
"2026-01-01T10:00:00Z",
@@ -32,15 +30,10 @@ fn roundtrips_profiles_targets_components_and_activity() {
storage.write_profiles(&profiles).expect("write profiles");
storage.write_targets(&targets).expect("write targets");
write_json(&storage.paths().components_file, &components);
write_json(&storage.paths().activity_file, &activity);
assert_eq!(storage.read_profiles().expect("read profiles"), profiles);
assert_eq!(storage.read_targets().expect("read targets"), targets);
assert_eq!(
storage.read_components().expect("read components"),
components
);
assert_eq!(storage.read_activity().expect("read activity"), activity);
cleanup(&root);
@@ -86,6 +79,9 @@ fn roundtrips_local_singbox_config_and_subscription_cache() {
config.subscription_display_url(),
Some("https://sub.example.test/...".to_string())
);
let persisted = fs::read_to_string(&storage.paths().local_singbox_file)
.expect("read persisted local sing-box config");
assert!(!persisted.contains("install_root"));
cleanup(&root);
}
@@ -155,13 +151,13 @@ fn reads_percent_encoded_singbox_tags_as_utf8() {
assert_eq!(config.selected_server_tag, Some(decoded_tag.to_string()));
assert_eq!(cache.servers[0].tag, decoded_tag);
assert_eq!(cache.config["outbounds"][0]["tag"], decoded_tag);
assert_eq!(cache.config["outbounds"][0]["tag"], encoded_tag);
cleanup(&root);
}
#[test]
fn invalid_subscription_cache_without_backup_returns_error_and_moves_corrupt_file() {
fn invalid_subscription_cache_without_backup_preserves_corruption_across_repeated_reads() {
let root = test_root("invalid-subscription-cache");
let storage = JsonStorage::new(root.clone());
fs::create_dir_all(&storage.paths().state_dir).expect("create state dir");
@@ -176,16 +172,14 @@ fn invalid_subscription_cache_without_backup_returns_error_and_moves_corrupt_fil
.expect_err("invalid cache should not silently fallback");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(!storage.paths().singbox_subscription_cache_file.exists());
assert!(has_corrupt_sibling(
&storage.paths().singbox_subscription_cache_file
));
assert!(storage.paths().singbox_subscription_cache_file.exists());
assert!(storage.read_singbox_subscription_cache().is_err());
cleanup(&root);
}
#[test]
fn invalid_json_without_backup_returns_error_and_moves_corrupt_file() {
fn invalid_json_without_backup_preserves_corruption_across_repeated_reads() {
let root = test_root("invalid-json-no-backup");
let storage = JsonStorage::new(root.clone());
fs::create_dir_all(&storage.paths().config_dir).expect("create config dir");
@@ -196,8 +190,8 @@ fn invalid_json_without_backup_returns_error_and_moves_corrupt_file() {
.expect_err("invalid profiles should not silently fallback");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(!storage.paths().profiles_file.exists());
assert!(has_corrupt_sibling(&storage.paths().profiles_file));
assert!(storage.paths().profiles_file.exists());
assert!(storage.read_profiles().is_err());
cleanup(&root);
}
@@ -308,6 +302,24 @@ fn cleanup(root: &Path) {
let _ = fs::remove_dir_all(root);
}
#[test]
fn missing_primary_restores_valid_backup_but_never_defaults_over_invalid_backup() {
let root = test_root("missing-source-backup");
let storage = JsonStorage::new(root.clone());
let saved = vec![sample_profile("preserved")];
storage.write_profiles(&saved).unwrap();
storage.write_profiles(&[]).unwrap();
fs::remove_file(&storage.paths().profiles_file).unwrap();
assert_eq!(storage.read_profiles().unwrap(), saved);
assert_eq!(storage.read_profiles().unwrap(), saved);
fs::remove_file(&storage.paths().profiles_file).unwrap();
fs::write(backup_path(&storage.paths().profiles_file), "{broken").unwrap();
assert!(storage.read_profiles().is_err());
assert!(storage.read_profiles().is_err());
assert!(!storage.paths().profiles_file.exists());
cleanup(&root);
}
fn write_json<T: serde::Serialize + ?Sized>(path: &Path, value: &T) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create json parent dir");
@@ -363,22 +375,6 @@ fn sample_target(id: &str) -> Target {
}
}
fn sample_component() -> ComponentStatus {
ComponentStatus {
id: ComponentId::Proxyfier,
name: "ProxiFyre".to_string(),
state: ComponentState::Missing,
installed: false,
running: false,
version: None,
path: None,
service_name: Some("ProxiFyreService".to_string()),
service_status: None,
problems: vec!["ProxiFyre не установлен".to_string()],
actions: vec!["Установить ProxiFyre".to_string()],
}
}
fn sample_subscription_cache() -> SubscriptionCache {
SubscriptionCache {
config: serde_json::json!({