diff --git a/crates/socket-patch-cli/tests/common/hermetic.rs b/crates/socket-patch-cli/tests/common/hermetic.rs index 8f5e78057..0d2eabf68 100644 --- a/crates/socket-patch-cli/tests/common/hermetic.rs +++ b/crates/socket-patch-cli/tests/common/hermetic.rs @@ -209,6 +209,14 @@ pub enum Extra { /// Every `POETRY_*` var: Poetry reads any config key from /// `POETRY_` (`POETRY_VIRTUALENVS_PATH`, repository URLs, ...). Poetry, + /// Every `COMPOSER_*` var, then `COMPOSER_HOME` pinned to a missing + /// directory. The crawler resolves a project's vendor directory from + /// `COMPOSER_VENDOR_DIR`, else the user-level `config.json` in the + /// Composer home, which it finds through `COMPOSER_HOME` or an + /// absolute `XDG_CONFIG_HOME`; any of them moves the crawl off the + /// fixture's `vendor/`. The pin outranks `XDG_CONFIG_HOME` and holds + /// no config. Run it before the suite sets its own `COMPOSER_*` values. + Composer, } /// Apply each of `extras` to `cmd` (see [`Extra`]). Run it before @@ -279,6 +287,14 @@ pub fn scrub_extra(cmd: &mut Command, extras: &[Extra]) { } } } + Extra::Composer => { + for (key, _) in std::env::vars_os() { + if key.to_string_lossy().starts_with("COMPOSER_") { + cmd.env_remove(&key); + } + } + cmd.env("COMPOSER_HOME", "/nonexistent/composer-home"); + } } } } diff --git a/crates/socket-patch-cli/tests/e2e_cargo.rs b/crates/socket-patch-cli/tests/e2e_cargo.rs index 58eb89672..c9b2b00ac 100644 --- a/crates/socket-patch-cli/tests/e2e_cargo.rs +++ b/crates/socket-patch-cli/tests/e2e_cargo.rs @@ -15,10 +15,10 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, hermetic}; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::Output; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -46,27 +46,24 @@ async fn start_proxy() -> MockServer { /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to `proxy_url`. /// -/// `SOCKET_API_TOKEN` is stripped so the binary deterministically takes the -/// public-proxy path (an ambient token would flip it onto the authenticated -/// API, bypassing `--proxy-url`), and every other variable that could -/// redirect the API elsewhere or disable it is scrubbed so an ambient value -/// can't quietly change what the scan reports. +/// The child gets the hermetic `SOCKET_*` environment (no token, so the +/// binary deterministically takes the public-proxy path `--proxy-url` +/// pins). The scans are unscoped, so an activated venv is scrubbed and +/// `HOME` points at a stand-in (`jvm_env::isolate_cli`): the developer's +/// venv, `~/.m2` or user site-packages would add packages to the counts +/// these tests assert. `CARGO_HOME` is the fixture's own `.cargo`. async fn run(args: &[&str], cwd: &Path, proxy_url: &str) -> Output { let mut args: Vec = args.iter().map(|s| s.to_string()).collect(); args.extend(["--proxy-url".to_string(), proxy_url.to_string()]); let cwd = cwd.to_path_buf(); tokio::task::spawn_blocking(move || { let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - Command::new(binary()) - .args(&arg_refs) + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra(&mut cmd, &[hermetic::Extra::Venv]); + common::jvm_env::isolate_cli(&mut cmd); + cmd.args(&arg_refs) .current_dir(&cwd) .env("CARGO_HOME", cwd.join(".cargo")) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") - .env_remove("SOCKET_API_URL") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_PROXY_URL") - .env_remove("SOCKET_BATCH_SIZE") .output() .expect("Failed to run socket-patch binary") }) diff --git a/crates/socket-patch-cli/tests/e2e_composer.rs b/crates/socket-patch-cli/tests/e2e_composer.rs index a5ccc37f1..cb378ffb2 100644 --- a/crates/socket-patch-cli/tests/e2e_composer.rs +++ b/crates/socket-patch-cli/tests/e2e_composer.rs @@ -14,9 +14,9 @@ #[path = "common/mod.rs"] mod common; -use common::binary; +use common::{binary, hermetic}; -use std::process::{Command, Output}; +use std::process::Output; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -43,24 +43,28 @@ async fn start_proxy() -> MockServer { /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to -/// `proxy_url`. `SOCKET_API_TOKEN` is stripped so the binary -/// deterministically takes the public-proxy path, and every other variable -/// that could redirect the API elsewhere or disable it is scrubbed. +/// `proxy_url`. The child gets the hermetic `SOCKET_*` environment (no +/// token, so the binary deterministically takes the public-proxy path +/// `--proxy-url` pins). The scans are unscoped, so an activated venv is +/// scrubbed and `HOME` points at a stand-in (`jvm_env::isolate_cli`): the +/// developer's venv, `~/.m2` or user site-packages would add packages to +/// the counts these tests assert. `Extra::Composer` keeps an ambient +/// `COMPOSER_VENDOR_DIR` or user-level Composer config from moving the +/// vendor directory the crawler reads off the fixture's. async fn run(args: &[&str], cwd: &std::path::Path, proxy_url: &str) -> Output { let mut args: Vec = args.iter().map(|s| s.to_string()).collect(); args.extend(["--proxy-url".to_string(), proxy_url.to_string()]); let cwd = cwd.to_path_buf(); tokio::task::spawn_blocking(move || { let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect(); - Command::new(binary()) - .args(&arg_refs) + let mut cmd = hermetic::command(&binary()); + hermetic::scrub_extra( + &mut cmd, + &[hermetic::Extra::Venv, hermetic::Extra::Composer], + ); + common::jvm_env::isolate_cli(&mut cmd); + cmd.args(&arg_refs) .current_dir(&cwd) - .env_remove("SOCKET_API_TOKEN") - .env_remove("SOCKET_CLI_API_TOKEN") - .env_remove("SOCKET_API_URL") - .env_remove("SOCKET_OFFLINE") - .env_remove("SOCKET_PROXY_URL") - .env_remove("SOCKET_BATCH_SIZE") .output() .expect("Failed to run socket-patch binary") }) diff --git a/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs b/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs index 0051d2bcd..8d1c31d81 100644 --- a/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs +++ b/crates/socket-patch-cli/tests/e2e_composer_version_identity.rs @@ -9,6 +9,8 @@ //! `scan --redirect` repoints the lock entry. Each test runs the built binary //! against a wiremock API that serves only the padded spelling. +#[path = "common/hermetic.rs"] +mod hermetic; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -180,17 +182,19 @@ async fn mount_api(server: &MockServer, prebuilt: bool) { .await; } -/// The built binary with every ambient `SOCKET_*` variable scrubbed and -/// telemetry off, so the assertions reflect only the argv. +/// The built binary in the hermetic `SOCKET_*` environment, so the +/// assertions reflect only the argv. Its scans and `vex` runs crawl every +/// ecosystem, so an activated venv is scrubbed and `HOME` points at a +/// stand-in (`jvm_env::isolate_cli`), and `Extra::Composer` keeps an +/// ambient `COMPOSER_VENDOR_DIR` or user-level Composer config from moving +/// the vendor directory off the fixture's. fn cli() -> Command { - let mut cmd = Command::new(env!("CARGO_BIN_EXE_socket-patch")); - for (key, _) in std::env::vars_os() { - let name = key.to_string_lossy(); - if name.starts_with("SOCKET_") && name != "SOCKET_NO_CONFIG" { - cmd.env_remove(&key); - } - } - cmd.env("SOCKET_TELEMETRY_DISABLED", "1"); + let mut cmd = hermetic::binary_command(); + hermetic::scrub_extra( + &mut cmd, + &[hermetic::Extra::Venv, hermetic::Extra::Composer], + ); + prebuilt_common::jvm_env::isolate_cli(&mut cmd); cmd } diff --git a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs index 55f4bbb1b..e228b5424 100644 --- a/crates/socket-patch-cli/tests/spawn_env_hygiene.rs +++ b/crates/socket-patch-cli/tests/spawn_env_hygiene.rs @@ -30,9 +30,6 @@ use serial_test::serial; /// scrubs by hand. const PENDING_RAW_SPAWNS: &[&str] = &[ "apply/apply_network.rs", - "e2e_cargo.rs", - "e2e_composer.rs", - "e2e_composer_version_identity.rs", "e2e_gem.rs", "e2e_hosted_production.rs", "e2e_maven.rs", @@ -457,6 +454,39 @@ fn bundle_extra_sweeps_every_bundler_override() { ); } +#[test] +#[serial] +fn composer_extra_sweeps_overrides_and_pins_an_empty_home() { + with_ambient( + &[ + ("COMPOSER_VENDOR_DIR", "third"), + ("COMPOSER_HOME", "/ambient/composer"), + ("COMPOSER_CACHE_DIR", "/ambient/composer-cache"), + ("XDG_CONFIG_HOME", "/ambient/config"), + ], + || { + let mut cmd = Command::new("socket-patch"); + hermetic::scrub_extra(&mut cmd, &[Extra::Composer]); + let env = effective_env(&cmd); + assert_eq!(get(&env, "COMPOSER_VENDOR_DIR"), None); + assert_eq!(get(&env, "COMPOSER_CACHE_DIR"), None); + // The pin outranks XDG_CONFIG_HOME in the Composer home lookup. + assert_eq!( + get(&env, "COMPOSER_HOME"), + Some("/nonexistent/composer-home") + ); + assert_eq!(get(&env, "XDG_CONFIG_HOME"), Some("/ambient/config")); + + // A suite's own COMPOSER_HOME, set after the scrub, survives. + cmd.env("COMPOSER_HOME", "/fixture/home"); + assert_eq!( + get(&effective_env(&cmd), "COMPOSER_HOME"), + Some("/fixture/home") + ); + }, + ); +} + #[test] #[serial] fn extra_seeds_are_scrubbed_whatever_the_ambient_value() {