Skip to content
Merged
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
25 changes: 25 additions & 0 deletions crates/socket-patch-cli/tests/common/fs_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! Recursive directory copy for test fixtures, reached as
//! `common::fs_tree::copy_tree`. Shared modules whose includers don't all
//! declare `mod common` pull this file in on its own with
//! `#[path = "../common/fs_tree.rs"] mod fs_tree;`.

#![allow(dead_code)]

use std::path::Path;

/// Copy `src` into `dst`, creating `dst`. A symlink to a file is copied as
/// the file; a symlink to a directory panics rather than being followed,
/// so a link cycle in an installed tree can't recurse forever. Panics if
/// `src` is not a readable directory.
pub fn copy_tree(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let to = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_tree(&entry.path(), &to);
} else {
std::fs::copy(entry.path(), &to).unwrap();
}
}
}
8 changes: 8 additions & 0 deletions crates/socket-patch-cli/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ pub mod oracle;

pub use oracle::{binary, git_sha256};

/// Recursive fixture copy. Shared modules whose includers don't all
/// declare `mod common` pull it in with
/// `#[path = "../common/fs_tree.rs"] mod fs_tree;`.
pub mod fs_tree;

/// The mock public proxy the hermetic `scan` suites answer with.
pub mod scan_proxy;

// ── Binary discovery + invocation ─────────────────────────────────────

/// Quick check whether `cmd` is on PATH. Used to soft-skip
Expand Down
34 changes: 34 additions & 0 deletions crates/socket-patch-cli/tests/common/scan_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! The mock Socket public proxy the hermetic `scan` suites (`e2e_cargo`,
//! `e2e_composer`, `e2e_maven`, `e2e_nuget`) pin with `--proxy-url`.

use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

/// Start a mock Socket public proxy answering the scan's `POST /patch/batch`
/// with an empty (no-patch) result, so no scan ever leaves localhost.
pub async fn start_proxy() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/patch/batch"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
server
}

/// Hermeticity guard: every scan in a test must have routed its patch lookup
/// through the in-test proxy. Fewer recorded requests than scans means at
/// least one binary invocation talked to the live API (or skipped the lookup
/// outright) despite the pinning.
pub async fn assert_proxy_served_scans(server: &MockServer, scans: usize) {
let requests = server.received_requests().await.unwrap_or_default();
assert!(
requests.len() >= scans,
"expected all {scans} scan invocations to hit the in-test proxy; \
recorded only {} request(s)",
requests.len()
);
}
19 changes: 5 additions & 14 deletions crates/socket-patch-cli/tests/composer_e2e_common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@

#![allow(dead_code)]

#[allow(clippy::duplicate_mod)]
#[path = "../common/fs_tree.rs"]
mod fs_tree;

use std::path::Path;
use std::process::{Command, Output};

Expand Down Expand Up @@ -320,27 +324,14 @@ pub fn installed_packages(proj: &Path) -> Vec<serde_json::Value> {
.clone()
}

pub fn copy_dir_recursive(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let to = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir_recursive(&entry.path(), &to);
} else {
std::fs::copy(entry.path(), &to).unwrap();
}
}
}

/// A fresh checkout of `proj` at `dst`: ONLY the committable files
/// (composer.json, composer.lock, `.socket/`) — never `vendor/`.
pub fn fresh_checkout(proj: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
std::fs::copy(proj.join("composer.json"), dst.join("composer.json")).unwrap();
std::fs::copy(proj.join("composer.lock"), dst.join("composer.lock")).unwrap();
if proj.join(".socket").is_dir() {
copy_dir_recursive(&proj.join(".socket"), &dst.join(".socket"));
fs_tree::copy_tree(&proj.join(".socket"), &dst.join(".socket"));
}
assert!(
!dst.join("vendor").exists(),
Expand Down
5 changes: 4 additions & 1 deletion crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@

#![cfg(feature = "docker-e2e")]

#[allow(clippy::duplicate_mod)]
#[path = "common/fs_tree.rs"]
mod fs_tree;
#[path = "common/cache_env.rs"]
mod cache_env;
#[path = "composer_e2e_common/mod.rs"]
Expand Down Expand Up @@ -437,7 +440,7 @@ fn assert_manifestless_vex_from_host(host_dir: &std::path::Path) {
let purl = format!("pkg:composer/psr/log@{psr_ver}");
let copy = tempfile::tempdir().expect("tempdir");
let fresh = copy.path().join("fresh");
composer_e2e_common::copy_dir_recursive(&host_dir.join("fresh"), &fresh);
fs_tree::copy_tree(&host_dir.join("fresh"), &fresh);
let patched = std::fs::read(fresh.join("vendor/psr/log/src/LoggerInterface.php"))
.expect("the container-installed patched file");
let vulns: &[(&str, &[&str])] = &[(GHSA, &["CVE-2024-66666"])];
Expand Down
33 changes: 1 addition & 32 deletions crates/socket-patch-cli/tests/e2e_cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,12 @@ use common::{binary, git_sha256, hermetic};
use std::path::{Path, PathBuf};
use std::process::Output;

use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::scan_proxy::{assert_proxy_served_scans, start_proxy};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Start a mock Socket public proxy answering the scan's `POST /patch/batch`
/// with an empty (no-patch) result, so no scan in this file ever leaves
/// localhost.
async fn start_proxy() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/patch/batch"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
server
}

/// 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`.
///
Expand Down Expand Up @@ -105,20 +88,6 @@ async fn scan_json(cwd: &Path, proxy_url: &str) -> serde_json::Value {
value
}

/// Hermeticity guard: every scan in a test must have routed its patch lookup
/// through the in-test proxy. Fewer recorded requests than scans means at
/// least one binary invocation talked to the live API (or skipped the lookup
/// outright) despite the pinning.
async fn assert_proxy_served_scans(server: &MockServer, scans: usize) {
let requests = server.received_requests().await.unwrap_or_default();
assert!(
requests.len() >= scans,
"expected all {scans} scan invocations to hit the in-test proxy; \
recorded only {} request(s)",
requests.len()
);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand Down
33 changes: 1 addition & 32 deletions crates/socket-patch-cli/tests/e2e_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,12 @@ use common::{binary, hermetic};

use std::process::Output;

use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::scan_proxy::{assert_proxy_served_scans, start_proxy};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Start a mock Socket public proxy answering the scan's `POST /patch/batch`
/// with an empty (no-patch) result, so no scan in this file ever leaves
/// localhost. Same shape as the e2e_nuget/e2e_gem harnesses.
async fn start_proxy() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/patch/batch"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
server
}

/// 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`. The child gets the hermetic `SOCKET_*` environment (no
Expand Down Expand Up @@ -72,20 +55,6 @@ async fn run(args: &[&str], cwd: &std::path::Path, proxy_url: &str) -> Output {
.expect("socket-patch subprocess task panicked")
}

/// Hermeticity guard: every scan in a test must have routed its patch lookup
/// through the in-test proxy. Fewer recorded requests than scans means a
/// binary invocation talked to the live API (or skipped the lookup) despite
/// the pinning.
async fn assert_proxy_served_scans(server: &MockServer, scans: usize) {
let requests = server.received_requests().await.unwrap_or_default();
assert!(
requests.len() >= scans,
"expected all {scans} scan invocations to hit the in-test proxy; \
recorded only {} request(s)",
requests.len()
);
}

/// Run `socket-patch scan --json ...`, assert the process succeeded, and
/// return the parsed JSON envelope from stdout.
///
Expand Down
33 changes: 1 addition & 32 deletions crates/socket-patch-cli/tests/e2e_maven.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,12 @@ use common::hermetic;
use std::path::Path;
use std::process::Output;

use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::scan_proxy::{assert_proxy_served_scans, start_proxy};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Start a mock Socket public proxy answering the scan's `POST /patch/batch`
/// with an empty (no-patch) result, so no scan in this file ever leaves
/// localhost.
async fn start_proxy() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/patch/batch"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
server
}

/// 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`.
///
Expand Down Expand Up @@ -84,20 +67,6 @@ async fn run(args: &[&str], cwd: &Path, m2_repo: &Path, proxy_url: &str) -> Outp
.expect("socket-patch subprocess task panicked")
}

/// Hermeticity guard: every scan in a test must have routed its patch lookup
/// through the in-test proxy. Fewer recorded requests than scans means at
/// least one binary invocation talked to the live API (or skipped the lookup
/// outright) despite the pinning.
async fn assert_proxy_served_scans(server: &MockServer, scans: usize) {
let requests = server.received_requests().await.unwrap_or_default();
assert!(
requests.len() >= scans,
"expected all {scans} scan invocations to hit the in-test proxy; \
recorded only {} request(s)",
requests.len()
);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand Down
33 changes: 1 addition & 32 deletions crates/socket-patch-cli/tests/e2e_nuget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,12 @@ use common::{binary, hermetic};
use std::path::Path;
use std::process::Output;

use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::scan_proxy::{assert_proxy_served_scans, start_proxy};

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Start a mock Socket public proxy answering the scan's `POST /patch/batch`
/// with an empty (no-patch) result, so no scan in this file ever leaves
/// localhost.
async fn start_proxy() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/patch/batch"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
server
}

/// 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`.
///
Expand Down Expand Up @@ -155,20 +138,6 @@ async fn assert_json_scanned(
);
}

/// Hermeticity guard: every scan in a test must have routed its patch lookup
/// through the in-test proxy. Fewer recorded requests than scans means at
/// least one binary invocation talked to the live API (or skipped the lookup
/// outright) despite the pinning.
async fn assert_proxy_served_scans(server: &MockServer, scans: usize) {
let requests = server.received_requests().await.unwrap_or_default();
assert!(
requests.len() >= scans,
"expected all {scans} scan invocations to hit the in-test proxy; \
recorded only {} request(s)",
requests.len()
);
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand Down
21 changes: 6 additions & 15 deletions crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
//! done
//! ```

#[allow(clippy::duplicate_mod)]
#[path = "common/fs_tree.rs"]
mod fs_tree;

#[path = "vex_e2e_common/mod.rs"]
mod vex_e2e_common;

Expand Down Expand Up @@ -308,7 +312,7 @@ fn fresh_checkout(from: &Path, to: &Path) {
}
// v5 hosted mode writes no `.socket/`: the lock pins are its whole state.
if from.join(".socket").is_dir() {
copy_tree(&from.join(".socket"), &to.join(".socket"));
fs_tree::copy_tree(&from.join(".socket"), &to.join(".socket"));
}
strip_manifest(to);
let blobs = to.join(".socket/blobs");
Expand All @@ -317,19 +321,6 @@ fn fresh_checkout(from: &Path, to: &Path) {
}
}

fn copy_tree(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for entry in std::fs::read_dir(from).unwrap() {
let entry = entry.unwrap();
let dst = to.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_tree(&entry.path(), &dst);
} else {
std::fs::copy(entry.path(), dst).unwrap();
}
}
}

/// The patch server's rebuild: the upstream nupkg with `FILE_KEY` replaced
/// and the package signature dropped (its digest no longer matches).
fn patched_nupkg(upstream: &[u8], patched: &[u8]) -> Vec<u8> {
Expand Down Expand Up @@ -1190,7 +1181,7 @@ fn nuget_vendored_solution_member_and_named_locks_restore() {
// Fresh checkout of the committable files, cold cache, each project.
let checkout = sb.dir("solution-checkout");
std::fs::copy(root.join("nuget.config"), checkout.join("nuget.config")).unwrap();
copy_tree(&root.join(".socket"), &checkout.join(".socket"));
fs_tree::copy_tree(&root.join(".socket"), &checkout.join(".socket"));
strip_manifest(&checkout);
let blobs = checkout.join(".socket/blobs");
if blobs.exists() {
Expand Down
Loading
Loading