Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/socket-patch-cli/tests/common/cache_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,15 @@ pub fn scrub_ambient_bun_env(cmd: &mut Command) {
scrub_parent_vars(cmd, parent_var_names(), is_ambient_bun_var);
}

/// [`scrub_ambient_bun_env`] minus `SOCKET_*`, for a `socket-patch` child
/// built with `hermetic::command`: the builder already handled `SOCKET_*`,
/// and removing them again would undo the opt-outs it forces on.
pub fn scrub_ambient_bun_tool_env(cmd: &mut Command) {
scrub_parent_vars(cmd, parent_var_names(), |name| {
!name.starts_with("SOCKET_") && is_ambient_bun_var(name)
});
}

/// The names of the PARENT environment, read once.
fn parent_var_names() -> Vec<OsString> {
std::env::vars_os().map(|(k, _)| k).collect()
Expand Down
60 changes: 43 additions & 17 deletions crates/socket-patch-cli/tests/common/hermetic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ const HOSTILE_SEEDS: &[(&str, &str)] = &[
("SOCKET_UPDATE_STATE_DIR", "/nonexistent"),
];

/// The opt-outs [`command`] forces on in every child: the config layer
/// (a developer's real `socket login` must never authenticate a test
/// child), the passive update notifier (no child may fetch release metadata
/// from real GitHub; the PTY suites hand the binary a real terminal, so the
/// stderr-TTY guard doesn't hold there) and telemetry (an unauthenticated
/// child would POST to the real public proxy; the workspace `[env]` default
/// can be overridden by the developer's shell). Suites that clear the
/// environment re-apply these after the clear. Notifier and telemetry
/// suites opt back in via caller env, which lands last.
pub const FORCED_OPT_OUTS: &[(&str, &str)] = &[
("SOCKET_NO_CONFIG", "1"),
("SOCKET_NO_UPDATE_CHECK", "1"),
("SOCKET_TELEMETRY_DISABLED", "1"),
];

/// A `Command` for `bin` with the hermetic `SOCKET_*` environment: the
/// hostile seeds scrubbed, every flag-bound `SOCKET_*` and every other
/// ambient `SOCKET_*` removed (removing `SOCKET_API_TOKEN` also forces the
Expand Down Expand Up @@ -75,23 +90,12 @@ pub fn command(bin: &Path) -> Command {
}
cmd.env_remove("SOCKET_API_TOKEN");
scrub_socket_vars(&mut cmd);
// Belt-and-braces on top of the `.cargo/config.toml` `[env]` default:
// a developer's real `socket login` (the socket-cli config.json token
// fallback) must never authenticate a test child — it would flip every
// "no token → public proxy" assertion onto the authed path.
cmd.env("SOCKET_NO_CONFIG", "1");
// Same posture for the passive update notifier: no test child may ever
// fetch release metadata from real GitHub. The stderr-TTY guard covers
// piped children, but the PTY suites hand the binary a real terminal —
// this force-set is the layer that holds there. Notifier tests opt back
// in via caller env (which lands last).
cmd.env("SOCKET_NO_UPDATE_CHECK", "1");
// And for telemetry: an unauthenticated child POSTs its events to the
// real public proxy unless a suite points it at a mock. The workspace
// `[env]` default can be overridden by the developer's shell, so force
// it here too. Telemetry suites opt back in via caller env with
// `SOCKET_TELEMETRY_DISABLED=0` and a wiremock endpoint.
cmd.env("SOCKET_TELEMETRY_DISABLED", "1");
// Belt-and-braces on top of the `.cargo/config.toml` `[env]` defaults
// (see `FORCED_OPT_OUTS`): a developer's real `socket login` would flip
// every "no token → public proxy" assertion onto the authed path.
// Telemetry suites opt back in with `SOCKET_TELEMETRY_DISABLED=0` and a
// wiremock endpoint.
cmd.envs(FORCED_OPT_OUTS.iter().copied());
cmd
}

Expand Down Expand Up @@ -192,6 +196,13 @@ pub enum Extra {
/// `BUNDLE_APP_CONFIG` and then scrubbed; run it before the suite sets
/// its own `BUNDLE_*` values.
Bundle,
/// Every `PDM_*` var plus `UV_PROJECT_ENVIRONMENT`: PDM reads any
/// setting from `PDM_<NAME>`, and its uv backend follows
/// `UV_PROJECT_ENVIRONMENT` to a different venv.
Pdm,
/// Every `POETRY_*` var: Poetry reads any config key from
/// `POETRY_<KEY>` (`POETRY_VIRTUALENVS_PATH`, repository URLs, ...).
Poetry,
}

/// Apply each of `extras` to `cmd` (see [`Extra`]). Run it before
Expand Down Expand Up @@ -247,6 +258,21 @@ pub fn scrub_extra(cmd: &mut Command, extras: &[Extra]) {
}
cmd.env_remove("BUNDLE_APP_CONFIG");
}
Extra::Pdm => {
for (key, _) in std::env::vars_os() {
if key.to_string_lossy().starts_with("PDM_") {
cmd.env_remove(&key);
}
}
cmd.env_remove("UV_PROJECT_ENVIRONMENT");
}
Extra::Poetry => {
for (key, _) in std::env::vars_os() {
if key.to_string_lossy().starts_with("POETRY_") {
cmd.env_remove(&key);
}
}
}
}
}
}
14 changes: 9 additions & 5 deletions crates/socket-patch-cli/tests/hosted_memory_common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ use tokio_util::sync::CancellationToken;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate};

#[path = "../common/hermetic.rs"]
pub mod hermetic;

pub const ORG: &str = "test-org";

pub fn fixtures_root() -> PathBuf {
Expand Down Expand Up @@ -405,8 +408,12 @@ fn disk_command(
// The checkout is its own repo: no socket.yml above the temp dir applies.
std::fs::create_dir_all(checkout.path().join(".git")).unwrap();
let cwd = checkout.path().join(cwd_rel);
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch"));
// A cleared environment: the disk scan must see none of the host's tool
// state either, so only the vars set below reach the child, including
// the hermetic builder's forced opt-outs, re-applied after the clear.
let mut cmd = hermetic::binary_command();
cmd.env_clear();
cmd.envs(hermetic::FORCED_OPT_OUTS.iter().copied());
for keep in [
"SYSTEMROOT",
"SystemRoot",
Expand All @@ -431,10 +438,7 @@ fn disk_command(
.env("GOPATH", empty("go"))
.env("GOMODCACHE", empty("gomodcache"))
.env("GEM_HOME", empty("gem"))
.env("XDG_CONFIG_HOME", empty("xdg"))
.env("SOCKET_NO_CONFIG", "1")
.env("SOCKET_TELEMETRY_DISABLED", "1")
.env("SOCKET_NO_UPDATE_CHECK", "1");
.env("XDG_CONFIG_HOME", empty("xdg"));
cmd.args([
"scan",
"--mode",
Expand Down
9 changes: 2 additions & 7 deletions crates/socket-patch-cli/tests/hosted_memory_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ async fn hosted_bundle_command_prints_the_engine_result() {
.map(|(k, v)| (k, String::from_utf8(v).unwrap()))
.collect();
let bundle = serde_json::json!({ "files": files, "presentOnly": [".pnp.loader.mjs"] });
let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch"))
let mut child = hermetic::binary_command()
.args([
"hosted-bundle",
"--org",
Expand All @@ -818,8 +818,6 @@ async fn hosted_bundle_command_prints_the_engine_result() {
"--api-url",
&server.uri(),
])
.env("SOCKET_NO_CONFIG", "1")
.env("SOCKET_TELEMETRY_DISABLED", "1")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
Expand Down Expand Up @@ -852,11 +850,8 @@ async fn hosted_bundle_command_prints_the_engine_result() {
.unwrap()
.starts_with(env!("CARGO_PKG_VERSION")));

let missing = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch"))
let missing = hermetic::binary_command()
.args(["hosted-bundle"])
.env_remove("SOCKET_API_TOKEN")
.env_remove("SOCKET_ORG_SLUG")
.env("SOCKET_NO_CONFIG", "1")
.stdin(std::process::Stdio::null())
.output()
.unwrap();
Expand Down
18 changes: 7 additions & 11 deletions crates/socket-patch-cli/tests/hosted_superseding_pypi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@

use std::io::Write as _;
use std::path::Path;
use std::process::Command;

use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};

#[path = "common/hermetic.rs"]
mod hermetic;

const ORG: &str = "test-org";
const PURL: &str = "pkg:pypi/six@1.16.0";
const WHEEL: &str = "six-1.16.0-py2.py3-none-any.whl";
Expand Down Expand Up @@ -110,13 +112,13 @@ async fn offer(server: &MockServer, uuid: &str, grant: &str, generation: u8) ->
hosted_url
}

/// `scan --mode hosted --json` with every ambient `SOCKET_*` var scrubbed.
/// `scan --mode hosted --json` in the hermetic environment.
/// `VIRTUAL_ENV` points at the fixture venv ([`project`]), which keeps the
/// installed-tree probes off the host's Python.
fn hosted_scan(root: &Path, server: &MockServer) -> (i32, Value) {
let venv = root.join("../venv");
let uri = server.uri();
let mut cmd = Command::new(env!("CARGO_BIN_EXE_socket-patch"));
let mut cmd = hermetic::binary_command();
cmd.args([
"scan",
"--mode",
Expand All @@ -136,14 +138,8 @@ fn hosted_scan(root: &Path, server: &MockServer) -> (i32, Value) {
])
.arg("--cwd")
.arg(root)
.current_dir(root);
for (key, _) in std::env::vars() {
if key.starts_with("SOCKET_") {
cmd.env_remove(key);
}
}
cmd.env("SOCKET_TELEMETRY_DISABLED", "1")
.env("VIRTUAL_ENV", &venv);
.current_dir(root)
.env("VIRTUAL_ENV", &venv);
let out = cmd.output().expect("spawn socket-patch");
let stdout = String::from_utf8_lossy(&out.stdout);
let env = serde_json::from_str(stdout.trim()).unwrap_or_else(|e| {
Expand Down
46 changes: 7 additions & 39 deletions crates/socket-patch-cli/tests/in_process_redirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
//! This is the CLI counterpart of the depscan-side install-verify e2e; the
//! rewriter bytes themselves are pinned by the shared golden fixtures.

#[path = "common/hermetic.rs"]
mod hermetic;
#[path = "common/rollback_json.rs"]
mod rollback_json;

Expand Down Expand Up @@ -2273,27 +2275,12 @@ async fn native_binary_no_matching_version_preserves_exact_bytes() {
.exists());
}

/// A `socket-patch` Command with the ambient `SOCKET_*` env surface scrubbed,
/// for the subprocess tests below: the binary binds a wide clap env surface
/// (SOCKET_DRY_RUN, SOCKET_OFFLINE, SOCKET_ECOSYSTEMS, SOCKET_PROXY_URL, ...),
/// and an ambient value silently changes what these tests exercise — ambient
/// `SOCKET_DRY_RUN=true` turns the rewrite into a no-op and every on-disk
/// oracle red. Seed-then-scrub (the `common/mod.rs` pattern): the hostile
/// seeds never reach the child because `env_remove` clears them too, but if a
/// scrub line is ever dropped the seed turns the suite red immediately.
/// Telemetry opt-outs are deliberately kept so an opted-out dev stays opted
/// out. The in-process tests above don't need this — their literal `ScanArgs`
/// bypass clap's env bindings entirely.
/// A `socket-patch` Command in the hermetic environment
/// (`tests/common/hermetic.rs`), for the subprocess tests below. The
/// in-process tests above don't need it: their literal `ScanArgs` bypass
/// clap's env bindings entirely.
fn scrubbed_cli() -> std::process::Command {
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch"));
cmd.env("SOCKET_DRY_RUN", "true")
.env("SOCKET_OFFLINE", "true")
.env("SOCKET_ECOSYSTEMS", "cargo")
.env("SOCKET_MANIFEST_PATH", "/nonexistent/manifest.json")
.env_remove("SOCKET_DRY_RUN")
.env_remove("SOCKET_OFFLINE")
.env_remove("SOCKET_ECOSYSTEMS")
.env_remove("SOCKET_MANIFEST_PATH");
let mut cmd = hermetic::binary_command();
// A registry exported by npm (`npm_config_registry`) or Bun would
// steer the Bun restores off the fixtures' registry.
for key in [
Expand All @@ -2303,25 +2290,6 @@ fn scrubbed_cli() -> std::process::Command {
] {
cmd.env_remove(key);
}
for (key, _) in std::env::vars_os() {
let name = key.to_string_lossy();
if name.starts_with("SOCKET_") && !name.contains("TELEMETRY") && name != "SOCKET_NO_CONFIG"
{
cmd.env_remove(&key);
}
}
// In-process tests in this binary `std::env::set_var` these via
// `apply_env_toggles`; one set by a parallel test between the scan
// above and the spawn would be inherited, so remove them
// unconditionally (see in_process_vendor.rs `run_cli`).
for key in [
"SOCKET_OFFLINE",
"SOCKET_DEBUG",
"SOCKET_API_URL",
"SOCKET_PROXY_URL",
] {
cmd.env_remove(key);
}
cmd
}

Expand Down
27 changes: 12 additions & 15 deletions crates/socket-patch-cli/tests/in_process_redirect_pdm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use socket_patch_core::hash::git_sha256::compute_git_sha256_from_bytes;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};

#[path = "common/hermetic.rs"]
mod hermetic;
#[path = "vex_e2e_common/mod.rs"]
mod vex_e2e_common;
use vex_e2e_common::{
Expand Down Expand Up @@ -667,26 +669,21 @@ async fn assert_relock_roundtrip(lock: &str, relocked: &str) {
);
}

/// Spawn `scan --mode hosted --json` on `root` with a scrubbed environment
/// (no ambient `SOCKET_*`, `PDM_*` or `VIRTUAL_ENV`).
/// Spawn `scan --mode hosted --json` on `root` in the hermetic environment,
/// with no ambient `PDM_*`, `VIRTUAL_ENV` or `UV_PROJECT_ENVIRONMENT` either.
async fn scan_json(
root: &Path,
server: &MockServer,
extra: &[&str],
) -> (Option<i32>, serde_json::Value) {
let mut cmd = tokio::process::Command::new(binary());
for (key, _) in std::env::vars_os() {
let name = key.to_string_lossy();
if name.starts_with("SOCKET_")
|| name.starts_with("PDM_")
|| name == "VIRTUAL_ENV"
|| name == "UV_PROJECT_ENVIRONMENT"
{
cmd.env_remove(key);
}
}
cmd.env("SOCKET_TELEMETRY_DISABLED", "1")
.args(["scan", "--mode", "hosted", "--yes", "--json", "--cwd"])
let mut std_cmd = hermetic::binary_command();
hermetic::scrub_extra(&mut std_cmd, &[hermetic::Extra::Venv, hermetic::Extra::Pdm]);
// The #[serial] in-process tests here `set_var` this for their own
// mock; a non-serial scan building its command meanwhile would inherit
// it, so remove it by name rather than only via the builder's sweep.
std_cmd.env_remove("SOCKET_PYPI_JSON_API");
let mut cmd = tokio::process::Command::from(std_cmd);
cmd.args(["scan", "--mode", "hosted", "--yes", "--json", "--cwd"])
.arg(root)
.args([
"--api-url",
Expand Down
34 changes: 7 additions & 27 deletions crates/socket-patch-cli/tests/in_process_redirect_pnpm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use socket_patch_cli::commands::rollback::{self, RollbackArgs};
use socket_patch_cli::commands::scan::{run, ScanArgs, ScanMode};
use std::path::Path;

#[path = "common/hermetic.rs"]
mod hermetic;
#[path = "vex_e2e_common/mod.rs"]
mod vex_e2e_common;
use wiremock::matchers::{method, path, path_regex};
Expand Down Expand Up @@ -1031,34 +1033,12 @@ async fn hosted_pnpm_manifestless_vex_from_lockfile_legacy_ledger_and_api() {
}
}

/// The CLI binary with ambient SOCKET_* env scrubbed (same hermeticity rule
/// as `in_process_redirect.rs`'s `scrubbed_cli`; duplicated because test
/// binaries cannot share helpers). Subprocess (not in-process `run`) so the
/// `--json` stdout envelope can be read back — the diagnostics under test
/// ARE the envelope's `redirect.warnings`.
/// The CLI binary in the hermetic environment (`tests/common/hermetic.rs`).
/// Subprocess (not in-process `run`) so the `--json` stdout envelope can be
/// read back — the diagnostics under test ARE the envelope's
/// `redirect.warnings`.
fn scrubbed_cli() -> std::process::Command {
let cmd = std::process::Command::new(env!("CARGO_BIN_EXE_socket-patch"));
let mut cmd = cmd;
for (key, _) in std::env::vars_os() {
let name = key.to_string_lossy();
if name.starts_with("SOCKET_") && !name.contains("TELEMETRY") && name != "SOCKET_NO_CONFIG"
{
cmd.env_remove(&key);
}
}
// In-process tests in this binary `std::env::set_var` these via
// `apply_env_toggles`; one set by a parallel test between the scan
// above and the spawn would be inherited, so remove them
// unconditionally (see in_process_vendor.rs `run_cli`).
for key in [
"SOCKET_OFFLINE",
"SOCKET_DEBUG",
"SOCKET_API_URL",
"SOCKET_PROXY_URL",
] {
cmd.env_remove(key);
}
cmd
hermetic::binary_command()
}

/// Run `scan --mode hosted --json` as a subprocess against `cwd` and parse
Expand Down
Loading
Loading