@@ -14,6 +14,7 @@ import (
1414 "go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory"
1515 "go.mondoo.com/cnquery/v12/providers-sdk/v1/plugin"
1616 "go.mondoo.com/cnquery/v12/providers/os/connection/fs"
17+ "go.mondoo.com/cnquery/v12/providers/os/resources/languages"
1718 "go.mondoo.com/cnquery/v12/types"
1819 "go.mondoo.com/cnquery/v12/utils/syncx"
1920)
@@ -96,3 +97,223 @@ func (p *providerCallbacks) GetRecording(req *plugin.DataReq) (*plugin.ResourceD
9697func (p * providerCallbacks ) Collect (req * plugin.DataRes ) error {
9798 return nil
9899}
100+
101+ func TestCollectNpmPackagesInPaths (t * testing.T ) {
102+ tests := []struct {
103+ name string
104+ setup func (* testing.T , afero.Fs ) string
105+ validate func (* testing.T , []* languages.Package , []* languages.Package , []string )
106+ }{
107+ {
108+ name : "root package.json, no lock file, no node_modules" ,
109+ setup : func (t * testing.T , mockFS afero.Fs ) string {
110+ // Create a test project directory with package.json at root (no lock file, no node_modules)
111+ testProjectPath := "/app"
112+ err := mockFS .MkdirAll (testProjectPath , 0o755 )
113+ require .NoError (t , err )
114+
115+ // Create a simple package.json at root
116+ rootPackageJSON := `{
117+ "name": "test-app",
118+ "version": "1.0.0",
119+ "dependencies": {
120+ "lodash": "^4.17.21"
121+ }
122+ }`
123+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "package.json" ), []byte (rootPackageJSON ), 0o644 )
124+ require .NoError (t , err )
125+ return testProjectPath
126+ },
127+ validate : func (t * testing.T , direct , transitive []* languages.Package , evidenceFiles []string ) {
128+ require .Empty (t , evidenceFiles )
129+ require .Greater (t , len (direct ), 0 , "should find at least the root package" )
130+
131+ foundRoot := false
132+ for _ , pkg := range direct {
133+ if pkg .Name == "test-app" {
134+ foundRoot = true
135+ require .Equal (t , "1.0.0" , pkg .Version )
136+ break
137+ }
138+ }
139+ require .True (t , foundRoot , "should find root package 'test-app'" )
140+ },
141+ },
142+ {
143+ name : "with lock file" ,
144+ setup : func (t * testing.T , mockFS afero.Fs ) string {
145+ // Create a test project with package-lock.json at root
146+ testProjectPath := "/app"
147+ err := mockFS .MkdirAll (testProjectPath , 0o755 )
148+ require .NoError (t , err )
149+
150+ // Create package.json at root
151+ rootPackageJSON := `{
152+ "name": "test-app",
153+ "version": "1.0.0"
154+ }`
155+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "package.json" ), []byte (rootPackageJSON ), 0o644 )
156+ require .NoError (t , err )
157+
158+ // Create package-lock.json at root (this should cause node_modules to be skipped)
159+ packageLockJSON := `{
160+ "name": "test-app",
161+ "version": "1.0.0",
162+ "lockfileVersion": 2
163+ }`
164+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "package-lock.json" ), []byte (packageLockJSON ), 0o644 )
165+ require .NoError (t , err )
166+
167+ // Create node_modules directory with a package (should be skipped due to lock file)
168+ err = mockFS .MkdirAll (filepath .Join (testProjectPath , "node_modules" , "some-package" ), 0o755 )
169+ require .NoError (t , err )
170+ nodeModulesPackageJSON := `{
171+ "name": "some-package",
172+ "version": "2.0.0"
173+ }`
174+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "node_modules" , "some-package" , "package.json" ), []byte (nodeModulesPackageJSON ), 0o644 )
175+ require .NoError (t , err )
176+ return testProjectPath
177+ },
178+ validate : func (t * testing.T , direct , transitive []* languages.Package , evidenceFiles []string ) {
179+ require .Empty (t , evidenceFiles )
180+ require .Greater (t , len (direct ), 0 , "should find at least the root package" )
181+
182+ // Should NOT find the node_modules package (because lock file exists)
183+ foundNodeModulesPackage := false
184+ for _ , pkg := range direct {
185+ if pkg .Name == "some-package" {
186+ foundNodeModulesPackage = true
187+ break
188+ }
189+ }
190+ require .False (t , foundNodeModulesPackage , "should not find node_modules package when lock file exists" )
191+ },
192+ },
193+ {
194+ name : "root and node_modules, no lock file" ,
195+ setup : func (t * testing.T , mockFS afero.Fs ) string {
196+ // Create a test project with both root package.json and node_modules
197+ testProjectPath := "/app"
198+ err := mockFS .MkdirAll (testProjectPath , 0o755 )
199+ require .NoError (t , err )
200+
201+ // Create package.json at root (no lock file)
202+ rootPackageJSON := `{
203+ "name": "test-app",
204+ "version": "1.0.0"
205+ }`
206+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "package.json" ), []byte (rootPackageJSON ), 0o644 )
207+ require .NoError (t , err )
208+
209+ // Create node_modules directory with a package (should be checked since no lock file)
210+ err = mockFS .MkdirAll (filepath .Join (testProjectPath , "node_modules" , "lodash" ), 0o755 )
211+ require .NoError (t , err )
212+
213+ // Read test data for a real package
214+ yoPkg , err := os .ReadFile (filepath .Join ("packages" , "testdata" , "yo_package.json" ))
215+ require .NoError (t , err )
216+ err = afero .WriteFile (mockFS , filepath .Join (testProjectPath , "node_modules" , "lodash" , "package.json" ), yoPkg , 0o644 )
217+ require .NoError (t , err )
218+ return testProjectPath
219+ },
220+ validate : func (t * testing.T , direct , transitive []* languages.Package , evidenceFiles []string ) {
221+ require .Empty (t , evidenceFiles )
222+ // Should find packages from both root and node_modules
223+ require .Greater (t , len (direct )+ len (transitive ), 0 , "should find packages from root and/or node_modules" )
224+ },
225+ },
226+ }
227+
228+ for _ , tt := range tests {
229+ t .Run (tt .name , func (t * testing.T ) {
230+ mockFS := afero .NewMemMapFs ()
231+ testProjectPath := tt .setup (t , mockFS )
232+
233+ conn , err := fs .NewFileSystemConnectionWithFs (0 , & inventory.Config {}, & inventory.Asset {}, "" , nil , mockFS )
234+ require .NoError (t , err )
235+
236+ r := & plugin.Runtime {
237+ Resources : & syncx.Map [plugin.Resource ]{},
238+ Connection : conn ,
239+ Callback : & providerCallbacks {},
240+ }
241+
242+ direct , transitive , evidenceFiles , err := collectNpmPackagesInPaths (r , conn .FileSystem (), []string {testProjectPath })
243+ require .NoError (t , err )
244+
245+ tt .validate (t , direct , transitive , evidenceFiles )
246+ })
247+ }
248+ }
249+
250+ // TestCollectNpmPackagesInPaths_filtering tests the filtering logic for non-existent paths
251+ func TestCollectNpmPackagesInPaths_skipsNoneExistentPaths (t * testing.T ) {
252+ mockFS := afero .NewMemMapFs ()
253+
254+ // Create a valid path with package.json
255+ validPath := "/app1"
256+ err := mockFS .MkdirAll (validPath , 0o755 )
257+ require .NoError (t , err )
258+
259+ rootPackageJSON := `{
260+ "name": "test-app-1",
261+ "version": "1.0.0"
262+ }`
263+ err = afero .WriteFile (mockFS , filepath .Join (validPath , "package.json" ), []byte (rootPackageJSON ), 0o644 )
264+ require .NoError (t , err )
265+
266+ // Create another valid path with package.json
267+ validPath2 := "/app2"
268+ err = mockFS .MkdirAll (validPath2 , 0o755 )
269+ require .NoError (t , err )
270+
271+ rootPackageJSON2 := `{
272+ "name": "test-app-2",
273+ "version": "2.0.0"
274+ }`
275+ err = afero .WriteFile (mockFS , filepath .Join (validPath2 , "package.json" ), []byte (rootPackageJSON2 ), 0o644 )
276+ require .NoError (t , err )
277+
278+ conn , err := fs .NewFileSystemConnectionWithFs (0 , & inventory.Config {}, & inventory.Asset {}, "" , nil , mockFS )
279+ require .NoError (t , err )
280+
281+ r := & plugin.Runtime {
282+ Resources : & syncx.Map [plugin.Resource ]{},
283+ Connection : conn ,
284+ Callback : & providerCallbacks {},
285+ }
286+
287+ // Test with multiple paths: some exist, some don't
288+ // This tests the filtering logic in collectNpmPackages and hasLockfile
289+ paths := []string {
290+ validPath , // exists
291+ "/nonexistent/path1" , // doesn't exist
292+ validPath2 , // exists
293+ "/nonexistent/path2" , // doesn't exist
294+ "/another/missing" , // doesn't exist
295+ }
296+
297+ direct , _ , evidenceFiles , err := collectNpmPackagesInPaths (r , conn .FileSystem (), paths )
298+ require .NoError (t , err )
299+ require .Empty (t , evidenceFiles )
300+
301+ // Should find packages from existing paths only
302+ require .Greater (t , len (direct ), 0 , "should find packages from existing paths" )
303+
304+ // Verify we found both valid packages
305+ foundApp1 := false
306+ foundApp2 := false
307+ for _ , pkg := range direct {
308+ if pkg .Name == "test-app-1" {
309+ foundApp1 = true
310+ require .Equal (t , "1.0.0" , pkg .Version )
311+ }
312+ if pkg .Name == "test-app-2" {
313+ foundApp2 = true
314+ require .Equal (t , "2.0.0" , pkg .Version )
315+ }
316+ }
317+ require .True (t , foundApp1 , "should find test-app-1 from valid path" )
318+ require .True (t , foundApp2 , "should find test-app-2 from valid path" )
319+ }
0 commit comments