#[path = "../src/helper.rs"] mod helper; #[path = "../src/models.rs"] mod models; use helper::{ helper_action_requires_elevation, install_request, parse_helper_response, proxifyre_apply_request, service_request, HelperAction, HelperCommandOutput, HelperCommandRunner, HelperCommandSpec, HelperError, HelperResponse, StructuredHelper, }; use 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>, } impl HelperCommandRunner for MockRunner { fn run(&self, spec: &HelperCommandSpec) -> Result { self.seen.borrow_mut().push(spec.clone()); Ok(self.output.clone()) } }