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

641 lines
22 KiB
Rust

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