#![cfg(feature = "docker-e2e")]
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use sha2::{Digest, Sha256};
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
const ORG: &str = "test-org";
const PURL: &str = "pkg:npm/minimist@1.2.2";
const UUID: &str = "11111111-1111-4111-8111-111111111111";
const PATCHED_BYTES: &[u8] = b"/* SOCKET-PATCH-E2E-MARKER */\nmodule.exports = function () { return {}; };\n";
fn git_sha256(content: &[u8]) -> String {
let header = format!("blob {}\0", content.len());
let mut hasher = Sha256::new();
hasher.update(header.as_bytes());
hasher.update(content);
hex::encode(hasher.finalize())
}
fn cov_docker_args() -> Vec<String> {
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
return Vec::new();
};
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
return Vec::new();
};
vec![
"-v".into(),
format!("{bin}:/usr/local/bin/socket-patch:ro"),
"-v".into(),
format!("{dir}:/coverage"),
"-e".into(),
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
]
}
fn host_mode() -> bool {
std::env::var("SOCKET_PATCH_TEST_HOST")
.map(|v| v == "1")
.unwrap_or(false)
}
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace root")
.to_path_buf()
}
async fn make_mock_server(after_hash: &str) -> MockServer {
let listener =
std::net::TcpListener::bind("0.0.0.0:0").expect("bind wiremock to 0.0.0.0:0");
let server = MockServer::builder().listener(listener).start().await;
Mock::given(method("POST"))
.and(path(format!("/v0/orgs/{ORG}/patches/batch")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"packages": [{
"purl": PURL,
"patches": [{
"uuid": UUID,
"purl": PURL,
"tier": "free",
"cveIds": ["CVE-2021-44906"],
"ghsaIds": ["GHSA-xvch-5gv4-984h"],
"severity": "high",
"title": "Synthetic prototype pollution patch (e2e fixture)"
}]
}],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path_regex(format!(
"^/v0/orgs/{ORG}/patches/by-package/.+$"
)))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"patches": [{
"uuid": UUID,
"purl": PURL,
"publishedAt": "2024-01-01T00:00:00Z",
"description": "E2E test fixture",
"license": "MIT",
"tier": "free",
"vulnerabilities": {}
}],
"canAccessPaidPatches": false,
})))
.mount(&server)
.await;
use base64::Engine;
let blob_b64 = base64::engine::general_purpose::STANDARD.encode(PATCHED_BYTES);
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG}/patches/view/{UUID}")))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"uuid": UUID,
"purl": PURL,
"publishedAt": "2024-01-01T00:00:00Z",
"files": {
"package/index.js": {
"beforeHash": "0000000000000000000000000000000000000000000000000000000000000000",
"afterHash": after_hash,
"blobContent": blob_b64,
}
},
"vulnerabilities": {},
"description": "E2E test fixture",
"license": "MIT",
"tier": "free",
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/v0/orgs/{ORG}/patches/blob/{after_hash}")))
.respond_with(ResponseTemplate::new(200).set_body_bytes(PATCHED_BYTES.to_vec()))
.mount(&server)
.await;
server
}
fn api_url_for_container(server: &MockServer) -> String {
let port = server.address().port();
format!("http://host.docker.internal:{port}")
}
fn make_container_script(api_url: &str) -> String {
format!(
r#"#!/usr/bin/env bash
set -uo pipefail
COMMON_ARGS=(--api-url '{api_url}' --api-token fake --org {ORG})
# 1. Install the real package via real npm.
mkdir -p /workspace/proj && cd /workspace/proj
echo '{{ "name": "e2e-proj", "version": "0.0.0" }}' > package.json
npm install --silent --no-audit --no-fund minimist@1.2.2
# 2. scan --json: should discover the patch.
echo "===SCAN OUTPUT===" >&2
socket-patch scan --json "${{COMMON_ARGS[@]}}" 2>/tmp/scan.err
SCAN_RC=$?
echo "scan exit=$SCAN_RC" >&2
cat /tmp/scan.err >&2 || true
# 3. scan --sync writes the manifest and applies the patch in one go.
echo "===SCAN/SYNC OUTPUT===" >&2
socket-patch scan --json --sync --yes "${{COMMON_ARGS[@]}}" 2>/tmp/sync.err
SYNC_RC=$?
echo "sync exit=$SYNC_RC" >&2
cat /tmp/sync.err >&2 || true
# 4. scan --sync may end up with "no installed package" (unmatched)
# because the fixture's installed minimist has different bytes than
# our synthetic patch expects. Force-apply via the manifest written
# by scan above.
echo "===APPLY OUTPUT===" >&2
socket-patch apply --json --force --offline 2>/tmp/apply.err
APPLY_RC=$?
echo "apply exit=$APPLY_RC" >&2
cat /tmp/apply.err >&2 || true
echo "===POST-APPLY STATE===" >&2
echo "manifest:" >&2
cat .socket/manifest.json 2>&1 >&2 || echo "no manifest" >&2
echo "blobs:" >&2
ls -la .socket/blobs/ 2>&1 >&2 || echo "no blobs" >&2
echo "first bytes of patched file:" >&2
head -2 node_modules/minimist/index.js >&2 || echo "no file" >&2
# 5. Assert the patched marker is in the on-disk file.
if ! grep -q 'SOCKET-PATCH-E2E-MARKER' node_modules/minimist/index.js; then
echo "FAIL: marker not found in node_modules/minimist/index.js after apply" >&2
exit 1
fi
echo "===PATCH VERIFIED===" >&2
# 6. rollback — the fixture doesn't serve beforeHash blobs, so this
# exercises the dispatch path but exits non-zero on the offline guard.
echo "===ROLLBACK OUTPUT===" >&2
socket-patch rollback --json --offline 2>/tmp/rb.err
RB_RC=$?
echo "rollback exit=$RB_RC" >&2
cat /tmp/rb.err >&2 || true
echo "===E2E PASS==="
exit 0
"#
)
}
fn make_global_script(api_url: &str) -> String {
format!(
r#"#!/usr/bin/env bash
set -uo pipefail
COMMON_ARGS=(--api-url '{api_url}' --api-token fake --org {ORG})
# Global install — populates $(npm root -g)/minimist/.
npm install -g --silent --no-audit --no-fund minimist@1.2.2 > /tmp/install.log 2>&1 || {{
cat /tmp/install.log >&2; exit 1
}}
NPM_GLOBAL_ROOT=$(npm root -g)
GLOBAL_FILE="$NPM_GLOBAL_ROOT/minimist/index.js"
[ -f "$GLOBAL_FILE" ] || {{ echo "FAIL: $GLOBAL_FILE missing" >&2; ls "$NPM_GLOBAL_ROOT" >&2 || true; exit 1; }}
echo "Global-installed at: $GLOBAL_FILE" >&2
# scan + apply run from an empty workspace; --global tells the crawler
# to look at $(npm root -g) instead of cwd-relative node_modules.
mkdir -p /workspace/proj && cd /workspace/proj
socket-patch scan --json --sync --yes --global "${{COMMON_ARGS[@]}}" \
--ecosystems npm 2>/tmp/sync.err
cat /tmp/sync.err >&2
socket-patch apply --json --force --offline --global --ecosystems npm 2>/tmp/apply.err
cat /tmp/apply.err >&2
if ! grep -q 'SOCKET-PATCH-E2E-MARKER' "$GLOBAL_FILE"; then
echo "FAIL: marker not in $GLOBAL_FILE" >&2
head -3 "$GLOBAL_FILE" >&2
exit 1
fi
echo "===PATCH VERIFIED===" >&2
echo "===E2E PASS==="
exit 0
"#
)
}
fn make_bun_script(api_url: &str) -> String {
format!(
r#"#!/usr/bin/env bash
set -uo pipefail
COMMON_ARGS=(--api-url '{api_url}' --api-token fake --org {ORG})
# 1. Pre-warm bun's cache (~/.bun/install/cache/) by installing the
# target package in a throwaway project first. Guarantees the
# cache contains minimist before the test install, so the test
# install can hard-link from it.
mkdir -p /tmp/prewarm && cd /tmp/prewarm
echo '{{"name":"prewarm","version":"0.0.0"}}' > package.json
bun install --silent --no-summary minimist@1.2.2 >/dev/null 2>&1 || true
# 2. Real install into the test project. By default bun's Linux
# backend hard-links from ~/.bun/install/cache/ into node_modules.
mkdir -p /workspace/proj && cd /workspace/proj
echo '{{"name":"e2e-proj","version":"0.0.0"}}' > package.json
bun install --silent --no-summary minimist@1.2.2
# 3. Locate the installed file and record inode + nlink.
TARGET=node_modules/minimist/index.js
TARGET_INODE_BEFORE=$(stat -c %i "$TARGET")
TARGET_NLINK_BEFORE=$(stat -c %h "$TARGET")
echo "bun target inode_before=$TARGET_INODE_BEFORE nlink_before=$TARGET_NLINK_BEFORE" >&2
# Locate the cache twin via inode if nlink > 1.
CACHE_TWIN=""
CACHE_HASH_BEFORE=""
if [ "$TARGET_NLINK_BEFORE" -gt 1 ]; then
CACHE_TWIN=$(find /root/.bun/install/cache -inum "$TARGET_INODE_BEFORE" 2>/dev/null | head -1 || true)
if [ -n "$CACHE_TWIN" ] && [ -f "$CACHE_TWIN" ]; then
CACHE_HASH_BEFORE=$(sha256sum "$CACHE_TWIN" | cut -d' ' -f1)
echo "bun cache twin: $CACHE_TWIN hash=$CACHE_HASH_BEFORE" >&2
fi
fi
# 4. scan --sync.
socket-patch scan --json --sync --yes "${{COMMON_ARGS[@]}}" 2>/tmp/sync.err
echo "sync exit=$?" >&2
cat /tmp/sync.err >&2 || true
# 5. apply --force --offline.
socket-patch apply --json --force --offline 2>/tmp/apply.err
echo "apply exit=$?" >&2
cat /tmp/apply.err >&2 || true
# 6. Marker must be in the on-disk file.
if ! grep -q 'SOCKET-PATCH-E2E-MARKER' "$TARGET"; then
echo "FAIL: marker not in $TARGET" >&2
head -3 "$TARGET" >&2
exit 1
fi
# 7. If the install hard-linked from cache, the apply must have
# isolated the venv copy via CoW. The cache twin's bytes must be
# unchanged.
if [ "$TARGET_NLINK_BEFORE" -gt 1 ] && [ -n "$CACHE_TWIN" ] && [ -f "$CACHE_TWIN" ]; then
CACHE_HASH_AFTER=$(sha256sum "$CACHE_TWIN" | cut -d' ' -f1)
if [ "$CACHE_HASH_AFTER" != "$CACHE_HASH_BEFORE" ]; then
echo "FAIL: bun cache content CORRUPTED — CoW didn't isolate the venv copy!" >&2
echo " before=$CACHE_HASH_BEFORE" >&2
echo " after =$CACHE_HASH_AFTER" >&2
echo " path =$CACHE_TWIN" >&2
head -3 "$CACHE_TWIN" >&2
exit 1
fi
if grep -q 'SOCKET-PATCH-E2E-MARKER' "$CACHE_TWIN"; then
echo "FAIL: bun cache twin contains the marker — patch leaked into ~/.bun/install/cache/" >&2
exit 1
fi
echo "bun cache integrity PRESERVED: $CACHE_TWIN unchanged" >&2
else
echo "(bun did not hard-link in this environment; CoW path was a no-op)" >&2
fi
echo "===PATCH VERIFIED===" >&2
echo "===E2E PASS==="
exit 0
"#
)
}
fn run_in_container(script: &str) -> std::process::Output {
let mut cmd = Command::new("docker");
cmd.args([
"run",
"--rm",
"--add-host=host.docker.internal:host-gateway",
"-i",
])
.args(cov_docker_args())
.args(["socket-patch-test-npm:latest", "bash", "-c", script]);
cmd.output().expect("docker run failed to spawn")
}
fn run_on_host(script: &str) -> std::process::Output {
let tmp = tempfile::tempdir().expect("tempdir");
let script_path = tmp.path().join("run.sh");
let mut f = std::fs::File::create(&script_path).unwrap();
f.write_all(script.as_bytes()).unwrap();
drop(f);
let host_proj = tmp.path().join("proj");
let host_script = script
.replace("/workspace/proj", host_proj.to_str().unwrap())
.replace("node_modules/minimist/index.js", "node_modules/minimist/index.js");
Command::new("bash")
.arg("-c")
.arg(host_script)
.output()
.expect("bash failed to spawn")
}
#[must_use]
fn skip_if_no_docker_image() -> bool {
let Ok(out) = Command::new("docker")
.args(["image", "inspect", "socket-patch-test-npm:latest"])
.output()
else {
eprintln!("skipping: `docker` not on PATH (set SOCKET_PATCH_TEST_HOST=1 to run on the host)");
return true;
};
if !out.status.success() {
eprintln!("skipping: docker image `socket-patch-test-npm:latest` not present");
return true;
}
false
}
#[tokio::test]
async fn npm_install_scan_apply_rollback_cycle() {
let after_hash = git_sha256(PATCHED_BYTES);
let server = make_mock_server(&after_hash).await;
let output = if host_mode() {
let api = format!("http://127.0.0.1:{}", server.address().port());
run_on_host(&make_container_script(&api))
} else {
if skip_if_no_docker_image() {
return;
}
let api = api_url_for_container(&server);
run_in_container(&make_container_script(&api))
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"container script failed:\nstdout=\n{stdout}\nstderr=\n{stderr}"
);
assert!(
stderr.contains("===PATCH VERIFIED==="),
"expected post-apply marker grep to succeed (===PATCH VERIFIED=== in stderr).\nstdout=\n{stdout}\nstderr=\n{stderr}"
);
assert!(
stdout.contains("===E2E PASS==="),
"PASS marker missing from stdout:\n{stdout}\nstderr:\n{stderr}"
);
let _ = workspace_root();
let received = server.received_requests().await.unwrap_or_default();
assert!(
received
.iter()
.any(|r| r.url.path().contains("/patches/batch")),
"scan should have called /patches/batch; received={received:#?}"
);
}
#[tokio::test]
async fn npm_global_install_full_apply_chain() {
let after_hash = git_sha256(PATCHED_BYTES);
let server = make_mock_server(&after_hash).await;
if host_mode() {
return;
}
if skip_if_no_docker_image() {
return;
}
let api = api_url_for_container(&server);
let out = run_in_container(&make_global_script(&api));
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"npm global apply failed:\nstdout=\n{stdout}\nstderr=\n{stderr}"
);
assert!(stderr.contains("===PATCH VERIFIED==="), "stderr=\n{stderr}");
assert!(stdout.contains("===E2E PASS==="), "stdout=\n{stdout}");
}
#[tokio::test]
async fn npm_bun_install_full_apply_chain() {
let after_hash = git_sha256(PATCHED_BYTES);
let server = make_mock_server(&after_hash).await;
if host_mode() {
return;
}
if skip_if_no_docker_image() {
return;
}
let api = api_url_for_container(&server);
let out = run_in_container(&make_bun_script(&api));
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"bun install apply failed:\nstdout=\n{stdout}\nstderr=\n{stderr}"
);
assert!(stderr.contains("===PATCH VERIFIED==="), "stderr=\n{stderr}");
assert!(stdout.contains("===E2E PASS==="), "stdout=\n{stdout}");
}
#[tokio::test]
async fn npm_test_infrastructure_smoke() {
let after_hash = git_sha256(PATCHED_BYTES);
let server = make_mock_server(&after_hash).await;
let url = format!(
"http://127.0.0.1:{}/v0/orgs/{ORG}/patches/blob/{after_hash}",
server.address().port()
);
let body = reqwest::get(&url)
.await
.expect("GET mock")
.bytes()
.await
.expect("read body");
assert_eq!(body.as_ref(), PATCHED_BYTES);
}
const _: Option<Duration> = None;