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

667 lines
22 KiB
Rust

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()),
}
}