Skip to content

Commit dc36eab

Browse files
committed
test(crawler/npm): 17 integration tests for npm crawler
New crawler_npm_e2e.rs: - parse_package_name: unscoped, scoped, @-only-no-slash edge - build_npm_purl: scoped and unscoped - read_package_json: well-formed, missing file, malformed, missing name, missing version - find_by_purls: unscoped, scoped, version-mismatch, invalid PURL - crawl_all: discovers unscoped + scoped, skips dirs without package.json, skips dirs with corrupt package.json Assisted-by: Claude Code:claude-opus-4-7
1 parent 05f226d commit dc36eab

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//! Integration coverage for `crawlers::npm_crawler`. Drives the
2+
//! local-discovery paths apply-CLI tests skip (parse_package_name,
3+
//! read_package_json, find_by_purls scoped vs unscoped, crawl_all
4+
//! over a synthetic node_modules tree).
5+
6+
use std::path::Path;
7+
8+
use socket_patch_core::crawlers::npm_crawler::{
9+
build_npm_purl, parse_package_name, read_package_json,
10+
};
11+
use socket_patch_core::crawlers::types::CrawlerOptions;
12+
use socket_patch_core::crawlers::NpmCrawler;
13+
14+
fn options_at(root: &Path) -> CrawlerOptions {
15+
CrawlerOptions {
16+
cwd: root.to_path_buf(),
17+
global: false,
18+
global_prefix: None,
19+
batch_size: 100,
20+
}
21+
}
22+
23+
/// Stage a package inside node_modules. `name` may include a `@scope/`
24+
/// prefix.
25+
async fn stage_npm_pkg(node_modules: &Path, name: &str, version: &str) {
26+
let pkg_dir = node_modules.join(name);
27+
tokio::fs::create_dir_all(&pkg_dir).await.unwrap();
28+
let pkg_json = format!(r#"{{"name":"{name}","version":"{version}"}}"#);
29+
tokio::fs::write(pkg_dir.join("package.json"), pkg_json).await.unwrap();
30+
}
31+
32+
// ── parse_package_name ─────────────────────────────────────────
33+
34+
#[test]
35+
fn parse_package_name_unscoped() {
36+
let (ns, name) = parse_package_name("lodash");
37+
assert_eq!(ns, None);
38+
assert_eq!(name, "lodash");
39+
}
40+
41+
#[test]
42+
fn parse_package_name_scoped() {
43+
let (ns, name) = parse_package_name("@types/node");
44+
assert_eq!(ns.as_deref(), Some("@types"));
45+
assert_eq!(name, "node");
46+
}
47+
48+
#[test]
49+
fn parse_package_name_at_only_no_slash() {
50+
// `@foo` with no `/` — treated as unscoped.
51+
let (ns, name) = parse_package_name("@oops");
52+
assert_eq!(ns, None);
53+
assert_eq!(name, "@oops");
54+
}
55+
56+
// ── build_npm_purl ─────────────────────────────────────────────
57+
58+
#[test]
59+
fn build_npm_purl_unscoped() {
60+
let purl = build_npm_purl(None, "lodash", "4.17.21");
61+
assert_eq!(purl, "pkg:npm/lodash@4.17.21");
62+
}
63+
64+
#[test]
65+
fn build_npm_purl_scoped() {
66+
let purl = build_npm_purl(Some("@types"), "node", "20.0.0");
67+
assert_eq!(purl, "pkg:npm/@types/node@20.0.0");
68+
}
69+
70+
// ── read_package_json ──────────────────────────────────────────
71+
72+
#[tokio::test]
73+
async fn read_package_json_well_formed() {
74+
let tmp = tempfile::tempdir().unwrap();
75+
let pkg = tmp.path().join("package.json");
76+
tokio::fs::write(&pkg, r#"{"name":"lodash","version":"4.17.21"}"#).await.unwrap();
77+
78+
let result = read_package_json(&pkg).await;
79+
assert_eq!(
80+
result,
81+
Some(("lodash".to_string(), "4.17.21".to_string()))
82+
);
83+
}
84+
85+
#[tokio::test]
86+
async fn read_package_json_missing_returns_none() {
87+
let tmp = tempfile::tempdir().unwrap();
88+
let result = read_package_json(&tmp.path().join("nope.json")).await;
89+
assert_eq!(result, None);
90+
}
91+
92+
#[tokio::test]
93+
async fn read_package_json_malformed_returns_none() {
94+
let tmp = tempfile::tempdir().unwrap();
95+
let pkg = tmp.path().join("package.json");
96+
tokio::fs::write(&pkg, b"{ this is not json").await.unwrap();
97+
98+
let result = read_package_json(&pkg).await;
99+
assert_eq!(result, None);
100+
}
101+
102+
#[tokio::test]
103+
async fn read_package_json_missing_name_returns_none() {
104+
let tmp = tempfile::tempdir().unwrap();
105+
let pkg = tmp.path().join("package.json");
106+
tokio::fs::write(&pkg, r#"{"version":"1.0.0"}"#).await.unwrap();
107+
108+
let result = read_package_json(&pkg).await;
109+
assert_eq!(result, None);
110+
}
111+
112+
#[tokio::test]
113+
async fn read_package_json_missing_version_returns_none() {
114+
let tmp = tempfile::tempdir().unwrap();
115+
let pkg = tmp.path().join("package.json");
116+
tokio::fs::write(&pkg, r#"{"name":"lodash"}"#).await.unwrap();
117+
118+
let result = read_package_json(&pkg).await;
119+
assert_eq!(result, None);
120+
}
121+
122+
// ── find_by_purls ──────────────────────────────────────────────
123+
124+
#[tokio::test]
125+
async fn find_by_purls_unscoped_package() {
126+
let tmp = tempfile::tempdir().unwrap();
127+
let nm = tmp.path().join("node_modules");
128+
stage_npm_pkg(&nm, "lodash", "4.17.21").await;
129+
130+
let crawler = NpmCrawler;
131+
let result = crawler
132+
.find_by_purls(&nm, &["pkg:npm/lodash@4.17.21".to_string()])
133+
.await
134+
.unwrap();
135+
assert_eq!(result.len(), 1);
136+
}
137+
138+
#[tokio::test]
139+
async fn find_by_purls_scoped_package() {
140+
let tmp = tempfile::tempdir().unwrap();
141+
let nm = tmp.path().join("node_modules");
142+
stage_npm_pkg(&nm, "@types/node", "20.0.0").await;
143+
144+
let crawler = NpmCrawler;
145+
let result = crawler
146+
.find_by_purls(&nm, &["pkg:npm/@types/node@20.0.0".to_string()])
147+
.await
148+
.unwrap();
149+
assert_eq!(result.len(), 1);
150+
}
151+
152+
#[tokio::test]
153+
async fn find_by_purls_version_mismatch_returns_empty() {
154+
let tmp = tempfile::tempdir().unwrap();
155+
let nm = tmp.path().join("node_modules");
156+
stage_npm_pkg(&nm, "lodash", "4.17.21").await;
157+
158+
let crawler = NpmCrawler;
159+
let result = crawler
160+
.find_by_purls(&nm, &["pkg:npm/lodash@99.99.99".to_string()])
161+
.await
162+
.unwrap();
163+
assert!(result.is_empty(), "version mismatch must skip");
164+
}
165+
166+
#[tokio::test]
167+
async fn find_by_purls_invalid_purl_skipped() {
168+
let tmp = tempfile::tempdir().unwrap();
169+
let crawler = NpmCrawler;
170+
let result = crawler
171+
.find_by_purls(
172+
tmp.path(),
173+
&["pkg:not-npm/foo@1.0".to_string()],
174+
)
175+
.await
176+
.unwrap();
177+
assert!(result.is_empty());
178+
}
179+
180+
// ── crawl_all ─────────────────────────────────────────────────
181+
182+
#[tokio::test]
183+
async fn crawl_all_discovers_unscoped_and_scoped() {
184+
let tmp = tempfile::tempdir().unwrap();
185+
let nm = tmp.path().join("node_modules");
186+
stage_npm_pkg(&nm, "lodash", "4.17.21").await;
187+
stage_npm_pkg(&nm, "@types/node", "20.0.0").await;
188+
189+
let crawler = NpmCrawler;
190+
let opts = options_at(tmp.path());
191+
let result = crawler.crawl_all(&opts).await;
192+
let names: Vec<&str> = result.iter().map(|p| p.name.as_str()).collect();
193+
assert!(names.contains(&"lodash"));
194+
assert!(names.contains(&"node"));
195+
}
196+
197+
#[tokio::test]
198+
async fn crawl_all_skips_dirs_without_package_json() {
199+
let tmp = tempfile::tempdir().unwrap();
200+
let nm = tmp.path().join("node_modules");
201+
tokio::fs::create_dir_all(nm.join("not_a_pkg")).await.unwrap();
202+
// No package.json — must be skipped.
203+
204+
let crawler = NpmCrawler;
205+
let opts = options_at(tmp.path());
206+
let result = crawler.crawl_all(&opts).await;
207+
assert!(result.is_empty());
208+
}
209+
210+
#[tokio::test]
211+
async fn crawl_all_skips_dirs_with_corrupt_package_json() {
212+
let tmp = tempfile::tempdir().unwrap();
213+
let nm = tmp.path().join("node_modules");
214+
let bad = nm.join("broken");
215+
tokio::fs::create_dir_all(&bad).await.unwrap();
216+
tokio::fs::write(bad.join("package.json"), b"{ corrupt").await.unwrap();
217+
218+
let crawler = NpmCrawler;
219+
let opts = options_at(tmp.path());
220+
let result = crawler.crawl_all(&opts).await;
221+
assert!(result.is_empty());
222+
}

0 commit comments

Comments
 (0)