Skip to content

Commit 3bcbf31

Browse files
committed
test(crawler/composer): 12 integration tests for vendor + installed.json paths
New `crawler_composer_e2e.rs`: - `find_by_purls`: vendor with installed.json discovery, no installed.json returns empty, invalid PURL skip, version mismatch skip - `crawl_all`: installed.json parsing happy path, corrupt JSON returns empty - `get_vendor_paths`: global_prefix passthrough, no vendor returns empty, vendor without installed.json returns empty, vendor + installed.json but no composer.json/lock returns empty, full setup with composer.json returns vendor, full setup with composer.lock also works Assisted-by: Claude Code:claude-opus-4-7
1 parent 3765c93 commit 3bcbf31

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
//! Integration coverage for `crawlers::composer_crawler`. Drives
2+
//! branches the apply-CLI suite skips: get_vendor_paths discovery,
3+
//! find_by_purls happy path, crawl_all via installed.json parsing,
4+
//! malformed installed.json variants.
5+
6+
#![cfg(feature = "composer")]
7+
8+
use std::path::Path;
9+
10+
use socket_patch_core::crawlers::types::CrawlerOptions;
11+
use socket_patch_core::crawlers::ComposerCrawler;
12+
13+
const ORG_PURL: &str = "pkg:composer/monolog/monolog@3.5.0";
14+
15+
fn options_at(root: &Path) -> CrawlerOptions {
16+
CrawlerOptions {
17+
cwd: root.to_path_buf(),
18+
global: false,
19+
global_prefix: None,
20+
batch_size: 100,
21+
}
22+
}
23+
24+
/// Stage a composer vendor layout: <root>/vendor/<vendor>/<name>/
25+
/// with `vendor/composer/installed.json` listing it.
26+
async fn stage_composer_project(root: &Path, vendor_name: &str, pkg_name: &str, version: &str) {
27+
let vendor = root.join("vendor");
28+
let pkg = vendor.join(vendor_name).join(pkg_name);
29+
tokio::fs::create_dir_all(&pkg).await.unwrap();
30+
31+
// composer/installed.json — what the crawler reads.
32+
let installed_dir = vendor.join("composer");
33+
tokio::fs::create_dir_all(&installed_dir).await.unwrap();
34+
let installed_json = format!(
35+
r#"{{
36+
"packages": [
37+
{{
38+
"name": "{vendor_name}/{pkg_name}",
39+
"version": "{version}",
40+
"version_normalized": "{version}.0"
41+
}}
42+
]
43+
}}"#
44+
);
45+
tokio::fs::write(installed_dir.join("installed.json"), installed_json).await.unwrap();
46+
47+
// composer.json marker on the project root.
48+
tokio::fs::write(root.join("composer.json"), b"{}").await.unwrap();
49+
}
50+
51+
// ── find_by_purls ──────────────────────────────────────────────
52+
53+
#[tokio::test]
54+
async fn find_by_purls_finds_package_in_vendor() {
55+
let tmp = tempfile::tempdir().unwrap();
56+
stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await;
57+
58+
let crawler = ComposerCrawler;
59+
let result = crawler
60+
.find_by_purls(&tmp.path().join("vendor"), &[ORG_PURL.to_string()])
61+
.await
62+
.unwrap();
63+
assert_eq!(result.len(), 1);
64+
let pkg = result.get(ORG_PURL).unwrap();
65+
assert_eq!(pkg.path, tmp.path().join("vendor").join("monolog").join("monolog"));
66+
}
67+
68+
#[tokio::test]
69+
async fn find_by_purls_no_installed_json_returns_empty() {
70+
let tmp = tempfile::tempdir().unwrap();
71+
let vendor = tmp.path().join("vendor");
72+
tokio::fs::create_dir(&vendor).await.unwrap();
73+
74+
let crawler = ComposerCrawler;
75+
let result = crawler
76+
.find_by_purls(&vendor, &[ORG_PURL.to_string()])
77+
.await
78+
.unwrap();
79+
assert!(result.is_empty());
80+
}
81+
82+
#[tokio::test]
83+
async fn find_by_purls_invalid_purl_skipped() {
84+
let tmp = tempfile::tempdir().unwrap();
85+
stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await;
86+
87+
let crawler = ComposerCrawler;
88+
let result = crawler
89+
.find_by_purls(
90+
&tmp.path().join("vendor"),
91+
&["pkg:not-composer/foo@1.0".to_string()],
92+
)
93+
.await
94+
.unwrap();
95+
assert!(result.is_empty());
96+
}
97+
98+
#[tokio::test]
99+
async fn find_by_purls_version_mismatch_returns_empty() {
100+
let tmp = tempfile::tempdir().unwrap();
101+
stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await;
102+
103+
let crawler = ComposerCrawler;
104+
let result = crawler
105+
.find_by_purls(
106+
&tmp.path().join("vendor"),
107+
&["pkg:composer/monolog/monolog@99.99.99".to_string()],
108+
)
109+
.await
110+
.unwrap();
111+
assert!(result.is_empty(), "version mismatch must skip");
112+
}
113+
114+
// ── crawl_all ─────────────────────────────────────────────────
115+
116+
#[tokio::test]
117+
async fn crawl_all_via_installed_json_returns_packages() {
118+
let tmp = tempfile::tempdir().unwrap();
119+
stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await;
120+
121+
let crawler = ComposerCrawler;
122+
let opts = CrawlerOptions {
123+
cwd: tmp.path().to_path_buf(),
124+
global: true,
125+
global_prefix: Some(tmp.path().join("vendor")),
126+
batch_size: 100,
127+
};
128+
let result = crawler.crawl_all(&opts).await;
129+
assert_eq!(result.len(), 1);
130+
assert_eq!(result[0].name, "monolog");
131+
assert_eq!(result[0].namespace.as_deref(), Some("monolog"));
132+
}
133+
134+
#[tokio::test]
135+
async fn crawl_all_with_corrupt_installed_json_returns_empty() {
136+
let tmp = tempfile::tempdir().unwrap();
137+
let vendor = tmp.path().join("vendor");
138+
let composer = vendor.join("composer");
139+
tokio::fs::create_dir_all(&composer).await.unwrap();
140+
tokio::fs::write(composer.join("installed.json"), b"{ this is not json").await.unwrap();
141+
tokio::fs::write(tmp.path().join("composer.json"), b"{}").await.unwrap();
142+
143+
let crawler = ComposerCrawler;
144+
let opts = CrawlerOptions {
145+
cwd: tmp.path().to_path_buf(),
146+
global: true,
147+
global_prefix: Some(vendor),
148+
batch_size: 100,
149+
};
150+
let result = crawler.crawl_all(&opts).await;
151+
assert!(result.is_empty(), "corrupt JSON must yield empty crawl");
152+
}
153+
154+
// ── get_vendor_paths ──────────────────────────────────────────
155+
156+
#[tokio::test]
157+
async fn get_vendor_paths_with_global_prefix_passthrough() {
158+
let tmp = tempfile::tempdir().unwrap();
159+
let crawler = ComposerCrawler;
160+
let opts = CrawlerOptions {
161+
cwd: tmp.path().to_path_buf(),
162+
global: true,
163+
global_prefix: Some(tmp.path().to_path_buf()),
164+
batch_size: 100,
165+
};
166+
let paths = crawler.get_vendor_paths(&opts).await.unwrap();
167+
assert_eq!(paths, vec![tmp.path().to_path_buf()]);
168+
}
169+
170+
#[tokio::test]
171+
async fn get_vendor_paths_local_no_vendor_returns_empty() {
172+
let tmp = tempfile::tempdir().unwrap();
173+
let crawler = ComposerCrawler;
174+
let paths = crawler.get_vendor_paths(&options_at(tmp.path())).await.unwrap();
175+
assert!(paths.is_empty());
176+
}
177+
178+
#[tokio::test]
179+
async fn get_vendor_paths_local_no_installed_json_returns_empty() {
180+
let tmp = tempfile::tempdir().unwrap();
181+
let vendor = tmp.path().join("vendor");
182+
tokio::fs::create_dir(&vendor).await.unwrap();
183+
// vendor exists but no installed.json inside.
184+
tokio::fs::write(tmp.path().join("composer.json"), b"{}").await.unwrap();
185+
186+
let crawler = ComposerCrawler;
187+
let paths = crawler.get_vendor_paths(&options_at(tmp.path())).await.unwrap();
188+
assert!(paths.is_empty(), "vendor without installed.json must not match");
189+
}
190+
191+
#[tokio::test]
192+
async fn get_vendor_paths_local_no_composer_marker_returns_empty() {
193+
let tmp = tempfile::tempdir().unwrap();
194+
let vendor = tmp.path().join("vendor");
195+
let composer = vendor.join("composer");
196+
tokio::fs::create_dir_all(&composer).await.unwrap();
197+
tokio::fs::write(composer.join("installed.json"), b"{\"packages\":[]}").await.unwrap();
198+
// No composer.json or composer.lock on the project root.
199+
200+
let crawler = ComposerCrawler;
201+
let paths = crawler.get_vendor_paths(&options_at(tmp.path())).await.unwrap();
202+
assert!(paths.is_empty(), "no composer.json must mean not-a-PHP-project");
203+
}
204+
205+
#[tokio::test]
206+
async fn get_vendor_paths_local_full_setup_returns_vendor() {
207+
let tmp = tempfile::tempdir().unwrap();
208+
let vendor = tmp.path().join("vendor");
209+
let composer = vendor.join("composer");
210+
tokio::fs::create_dir_all(&composer).await.unwrap();
211+
tokio::fs::write(composer.join("installed.json"), b"{\"packages\":[]}").await.unwrap();
212+
tokio::fs::write(tmp.path().join("composer.json"), b"{}").await.unwrap();
213+
214+
let crawler = ComposerCrawler;
215+
let paths = crawler.get_vendor_paths(&options_at(tmp.path())).await.unwrap();
216+
assert_eq!(paths, vec![vendor]);
217+
}
218+
219+
#[tokio::test]
220+
async fn get_vendor_paths_local_with_lock_marker_also_works() {
221+
let tmp = tempfile::tempdir().unwrap();
222+
let vendor = tmp.path().join("vendor");
223+
let composer = vendor.join("composer");
224+
tokio::fs::create_dir_all(&composer).await.unwrap();
225+
tokio::fs::write(composer.join("installed.json"), b"{\"packages\":[]}").await.unwrap();
226+
tokio::fs::write(tmp.path().join("composer.lock"), b"{}").await.unwrap();
227+
228+
let crawler = ComposerCrawler;
229+
let paths = crawler.get_vendor_paths(&options_at(tmp.path())).await.unwrap();
230+
assert_eq!(paths, vec![vendor]);
231+
}

0 commit comments

Comments
 (0)