Repository navigation
Expand file tree
/
Copy pathe2e_maven.rs
More file actions
335 lines (310 loc) · 12.7 KB
/
Copy pathe2e_maven.rs
File metadata and controls
335 lines (310 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//! End-to-end tests for the Maven/Java package patching lifecycle.
//!
//! These tests exercise crawling against a temporary directory with a fake
//! Maven local repository layout. They do **not** require network access or a
//! real Maven/Java installation: the scan's patch lookup is pinned to an
//! in-test [`wiremock`] public-proxy stand-in via `--proxy-url`. That pinning
//! is load-bearing, not cosmetic — an unreachable API is a hard scan failure (exit 1, `status: "error"`), so an
//! unpinned scan would phone home to the live proxy on every test run and go
//! red whenever the network (or an ambient `SOCKET_*` variable) misbehaved.
//!
//! # Running
//! ```sh
//! cargo test -p socket-patch-cli --test e2e_maven
//! ```
#[path = "common/mod.rs"]
mod common;
use common::binary;
use std::path::Path;
use std::process::{Command, Output};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
// ---------------------------------------------------------------------------
// 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`.
///
/// `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.
async fn run(args: &[&str], cwd: &Path, m2_repo: &Path, proxy_url: &str) -> Output {
let mut args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
args.extend(["--proxy-url".to_string(), proxy_url.to_string()]);
let cwd = cwd.to_path_buf();
let m2_repo = m2_repo.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)
.current_dir(&cwd)
// Point the crawler at the fake local repo.
.env("MAVEN_REPO_LOCAL", &m2_repo)
// And keep it off the machine's Coursier / Ivy caches, which a
// Maven marker also makes it crawl (their homes are derived
// from these; none exists below the fixture).
.env("HOME", m2_repo.with_file_name("no-home"))
.env("USERPROFILE", m2_repo.with_file_name("no-home"))
.env("XDG_CACHE_HOME", m2_repo.with_file_name("no-xdg-cache"))
.env("LOCALAPPDATA", m2_repo.with_file_name("no-localappdata"))
.env_remove("COURSIER_CACHE")
.env_remove("SBT_OPTS")
.env_remove("JAVA_OPTS")
// Keep the run hermetic: no ambient token, no inherited repo path.
.env_remove("SOCKET_API_TOKEN")
.env_remove("SOCKET_CLI_API_TOKEN")
.env_remove("M2_HOME")
.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")
})
.await
.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
// ---------------------------------------------------------------------------
/// Verify that `socket-patch scan` discovers artifacts in a fake Maven local repo.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scan_discovers_maven_artifacts() {
let server = start_proxy().await;
let proxy_url = server.uri();
let dir = tempfile::tempdir().unwrap();
// Set up a fake Maven local repository
let m2_repo = dir.path().join("m2-repo");
// Create commons-lang3 3.12.0
let lang_dir = m2_repo
.join("org")
.join("apache")
.join("commons")
.join("commons-lang3")
.join("3.12.0");
std::fs::create_dir_all(&lang_dir).unwrap();
std::fs::write(
lang_dir.join("commons-lang3-3.12.0.pom"),
r#"<project>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</project>"#,
)
.unwrap();
// Create guava 32.1.2-jre
let guava_dir = m2_repo
.join("com")
.join("google")
.join("guava")
.join("guava")
.join("32.1.2-jre");
std::fs::create_dir_all(&guava_dir).unwrap();
std::fs::write(
guava_dir.join("guava-32.1.2-jre.pom"),
r#"<project>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.2-jre</version>
</project>"#,
)
.unwrap();
// A pom.xml in the project directory activates local mode; it declares
// both artifacts, since a project-mode crawl keeps only what the
// project's poms reach (#265).
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(
project_dir.join("pom.xml"),
r#"<project><modelVersion>4.0.0</modelVersion><dependencies>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency>
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.2-jre</version></dependency>
</dependencies></project>"#,
)
.unwrap();
// --- Human-readable run: proves the count AND the ecosystem ----------
// The crawl summary line ("Found N packages (N maven)") is the
// strongest discovery oracle: it pins both how many artifacts were
// found and that they were attributed to the Maven ecosystem. We
// created exactly two artifacts (commons-lang3, guava), so the
// expected line is derived independently from the fixture, not copied
// from the implementation's output.
let output = run(
&["scan", "--cwd", project_dir.to_str().unwrap()],
&project_dir,
&m2_repo,
&proxy_url,
)
.await;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stdout}{stderr}");
assert!(
output.status.success(),
"scan should exit 0; got {:?}\n{combined}",
output.status.code()
);
// Must NOT have hit the empty-crawl path — that line *also* contains
// the word "packages".
assert!(
!combined.contains("No packages found") && !combined.contains("No packages found"),
"scan reported zero packages — Maven discovery did not run:\n{combined}"
);
assert!(
combined.contains("Found 2 packages"),
"expected exactly 2 discovered packages, got:\n{combined}"
);
// Anchor the full parenthesized breakdown: `(2 maven)` forces Maven to
// be the *sole* ecosystem with exactly 2 artifacts. A loose `2 maven`
// substring would also match `12 maven` or `(2 maven, 1 npm)`.
assert!(
combined.contains("(2 maven)"),
"expected all 2 artifacts attributed solely to the Maven ecosystem, got:\n{combined}"
);
// --- JSON run: locks the stable `scannedPackages` contract field -----
let json_out = run(
&["scan", "--json", "--cwd", project_dir.to_str().unwrap()],
&project_dir,
&m2_repo,
&proxy_url,
)
.await;
let json = String::from_utf8_lossy(&json_out.stdout);
assert!(
json_out.status.success(),
"scan --json should exit 0:\n{json}"
);
// Anchor on the trailing comma so this matches *exactly* 2, not any
// number that merely starts with "2" (20, 25, 200, ...). Without the
// comma, `contains("scannedPackages\": 2")` is satisfied by an
// over-counting crawler reporting e.g. 25, masking a discovery bug.
assert!(
json.contains("\"scannedPackages\": 2,"),
"expected scannedPackages == exactly 2 in JSON output, got:\n{json}"
);
assert!(
json.contains("\"status\": \"success\""),
"expected status == success in JSON output, got:\n{json}"
);
assert_proxy_served_scans(&server, 2).await;
}
/// Verify that `socket-patch scan` discovers Gradle project artifacts.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn scan_discovers_gradle_project_artifacts() {
let server = start_proxy().await;
let proxy_url = server.uri();
let dir = tempfile::tempdir().unwrap();
// Set up a fake Maven local repository
let m2_repo = dir.path().join("m2-repo");
// Create a single artifact
let jackson_dir = m2_repo
.join("com")
.join("fasterxml")
.join("jackson")
.join("core")
.join("jackson-core")
.join("2.15.0");
std::fs::create_dir_all(&jackson_dir).unwrap();
std::fs::write(
jackson_dir.join("jackson-core-2.15.0.pom"),
r#"<project>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.0</version>
</project>"#,
)
.unwrap();
// Create a build.gradle in the project directory (Gradle project). It
// declares mavenLocal(): a Gradle-only build without it never
// reads the Maven local repository, so scan leaves m2 out for it (#551).
let project_dir = dir.path().join("project");
std::fs::create_dir_all(&project_dir).unwrap();
std::fs::write(
project_dir.join("build.gradle"),
"plugins { id 'java' }\nrepositories { mavenLocal() }\n",
)
.unwrap();
// --- JSON run: the `scannedPackages` count is the contract field -----
// A single artifact lives in the repo. We assert the *value* (1), not
// merely the presence of the key — the field is always emitted, even
// when nothing was discovered.
let output = run(
&["scan", "--json", "--cwd", project_dir.to_str().unwrap()],
&project_dir,
&m2_repo,
&proxy_url,
)
.await;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"scan --json should exit 0; got {:?}\n{stdout}{stderr}",
output.status.code()
);
// Anchor on the trailing comma: a bare `contains("scannedPackages\": 1")`
// is also satisfied by 10..=19, 100, etc., so an over-counting crawler
// would pass while claiming to find "1". The comma pins it to exactly 1.
assert!(
stdout.contains("\"scannedPackages\": 1,"),
"expected exactly 1 artifact discovered via the build.gradle marker, got:\n{stdout}"
);
assert!(
!stdout.contains("\"scannedPackages\": 0,"),
"scannedPackages was 0 — the Gradle project marker did not activate Maven discovery:\n{stdout}"
);
assert!(
stdout.contains("\"status\": \"success\""),
"expected status == success, got:\n{stdout}"
);
// --- Human run: confirm the artifact is attributed to Maven ----------
// build.gradle (not pom.xml) is what must trigger local-mode Maven
// discovery here; the eco summary proves the single package is Maven.
let human = run(
&["scan", "--cwd", project_dir.to_str().unwrap()],
&project_dir,
&m2_repo,
&proxy_url,
)
.await;
let h_combined = format!(
"{}{}",
String::from_utf8_lossy(&human.stdout),
String::from_utf8_lossy(&human.stderr)
);
assert!(
h_combined.contains("Found 1 package (1 maven)"),
"expected the Gradle project to discover exactly 1 Maven artifact, got:\n{h_combined}"
);
assert_proxy_served_scans(&server, 2).await;
}