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

424 lines
14 KiB
Rust

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