github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/javascript/packagejson/packagejson_test.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package packagejson_test 16 17 import ( 18 "io/fs" 19 "os" 20 "path/filepath" 21 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/google/go-cmp/cmp/cmpopts" 25 "github.com/google/osv-scalibr/extractor" 26 "github.com/google/osv-scalibr/extractor/filesystem" 27 "github.com/google/osv-scalibr/extractor/filesystem/internal/units" 28 "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagejson" 29 "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagejson/metadata" 30 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 31 scalibrfs "github.com/google/osv-scalibr/fs" 32 "github.com/google/osv-scalibr/inventory" 33 "github.com/google/osv-scalibr/purl" 34 "github.com/google/osv-scalibr/stats" 35 "github.com/google/osv-scalibr/testing/extracttest" 36 "github.com/google/osv-scalibr/testing/fakefs" 37 "github.com/google/osv-scalibr/testing/testcollector" 38 ) 39 40 func TestFileRequired(t *testing.T) { 41 tests := []struct { 42 name string 43 path string 44 fileSizeBytes int64 45 maxFileSizeBytes int64 46 wantRequired bool 47 wantResultMetric stats.FileRequiredResult 48 }{ 49 { 50 name: "package.json at root", 51 path: "package.json", 52 wantRequired: true, 53 wantResultMetric: stats.FileRequiredResultOK, 54 }, 55 { 56 name: "top level package.json", 57 path: "testdata/package.json", 58 wantRequired: true, 59 wantResultMetric: stats.FileRequiredResultOK, 60 }, 61 { 62 name: "tests library", 63 path: "testdata/deps/accepts/package.json", 64 wantRequired: true, 65 wantResultMetric: stats.FileRequiredResultOK, 66 }, 67 { 68 name: "not package.json", 69 path: "testdata/test.js", 70 wantRequired: false, 71 }, 72 { 73 name: "package.json required if size less than maxFileSizeBytes", 74 path: "package.json", 75 fileSizeBytes: 1000 * units.MiB, 76 maxFileSizeBytes: 2000 * units.MiB, 77 wantRequired: true, 78 wantResultMetric: stats.FileRequiredResultOK, 79 }, 80 { 81 name: "package.json required if size equal to maxFileSizeBytes", 82 path: "package.json", 83 fileSizeBytes: 1000 * units.MiB, 84 maxFileSizeBytes: 1000 * units.MiB, 85 wantRequired: true, 86 wantResultMetric: stats.FileRequiredResultOK, 87 }, 88 { 89 name: "package.json not required if size greater than maxFileSizeBytes", 90 path: "package.json", 91 fileSizeBytes: 10000 * units.MiB, 92 maxFileSizeBytes: 1000 * units.MiB, 93 wantRequired: false, 94 wantResultMetric: stats.FileRequiredResultSizeLimitExceeded, 95 }, 96 { 97 name: "package.json required if maxFileSizeBytes explicitly set to 0", 98 path: "package.json", 99 fileSizeBytes: 1000 * units.MiB, 100 maxFileSizeBytes: 0, 101 wantRequired: true, 102 wantResultMetric: stats.FileRequiredResultOK, 103 }, 104 } 105 106 for _, tt := range tests { 107 // Note the subtest here 108 t.Run(tt.name, func(t *testing.T) { 109 collector := testcollector.New() 110 e := packagejson.New(packagejson.Config{ 111 Stats: collector, 112 MaxFileSizeBytes: tt.maxFileSizeBytes, 113 }) 114 115 // Set a default file size if not specified. 116 fileSizeBytes := tt.fileSizeBytes 117 if fileSizeBytes == 0 { 118 fileSizeBytes = 1 * units.KiB 119 } 120 121 isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{ 122 FileName: filepath.Base(tt.path), 123 FileMode: fs.ModePerm, 124 FileSize: fileSizeBytes, 125 })) 126 if isRequired != tt.wantRequired { 127 t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired) 128 } 129 130 gotResultMetric := collector.FileRequiredResult(tt.path) 131 if gotResultMetric != tt.wantResultMetric { 132 t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric) 133 } 134 }) 135 } 136 } 137 138 func TestExtract(t *testing.T) { 139 tests := []struct { 140 name string 141 path string 142 cfg packagejson.Config 143 wantPackages []*extractor.Package 144 wantErr error 145 wantResultMetric stats.FileExtractedResult 146 }{ 147 { 148 name: "top_level_package.json", 149 path: "testdata/package.json", 150 wantPackages: []*extractor.Package{ 151 { 152 Name: "testdata", 153 Version: "10.46.8", 154 PURLType: purl.TypeNPM, 155 Locations: []string{"testdata/package.json"}, 156 Metadata: &metadata.JavascriptPackageJSONMetadata{ 157 Author: &metadata.Person{ 158 Name: "Developer", 159 Email: "dev@corp.com", 160 URL: "http://blog.dev.com", 161 }, 162 }, 163 }, 164 }, 165 }, 166 { 167 name: "accepts", 168 path: "testdata/deps/accepts/package.json", 169 wantPackages: []*extractor.Package{ 170 { 171 Name: "accepts", 172 Version: "1.3.8", 173 PURLType: purl.TypeNPM, 174 Locations: []string{"testdata/deps/accepts/package.json"}, 175 Metadata: &metadata.JavascriptPackageJSONMetadata{ 176 Contributors: []*metadata.Person{ 177 { 178 Name: "Douglas Christopher Wilson", 179 Email: "doug@somethingdoug.com", 180 }, 181 { 182 Name: "Jonathan Ong", 183 Email: "me@jongleberry.com", 184 URL: "http://jongleberry.com", 185 }, 186 }, 187 }, 188 }, 189 }, 190 }, 191 { 192 name: "no_person_name", 193 path: "testdata/deps/no-person-name/package.json", 194 wantPackages: []*extractor.Package{ 195 { 196 Name: "accepts", 197 Version: "1.3.8", 198 PURLType: purl.TypeNPM, 199 Locations: []string{"testdata/deps/no-person-name/package.json"}, 200 Metadata: &metadata.JavascriptPackageJSONMetadata{ 201 Contributors: []*metadata.Person{ 202 { 203 Name: "Jonathan Ong", 204 Email: "me@jongleberry.com", 205 URL: "http://jongleberry.com", 206 }, 207 }, 208 }, 209 }, 210 }, 211 }, 212 { 213 name: "nested_acorn", 214 path: "testdata/deps/with/deps/acorn/package.json", 215 wantPackages: []*extractor.Package{ 216 { 217 Name: "acorn", 218 Version: "1.2.2", 219 PURLType: purl.TypeNPM, 220 Locations: []string{"testdata/deps/with/deps/acorn/package.json"}, 221 Metadata: &metadata.JavascriptPackageJSONMetadata{ 222 Maintainers: []*metadata.Person{ 223 { 224 Name: "Marijn Haverbeke", 225 Email: "marijnh@gmail.com", 226 }, 227 { 228 Name: "Ingvar Stepanyan", 229 Email: "me@rreverser.com", 230 }, 231 }, 232 }, 233 }, 234 }, 235 }, 236 { 237 name: "empty name", 238 path: "testdata/deps/acorn/package.json", 239 wantPackages: []*extractor.Package{}, 240 }, 241 { 242 name: "empty version", 243 path: "testdata/deps/acorn-globals/package.json", 244 wantPackages: []*extractor.Package{}, 245 }, 246 { 247 name: "missing name and version", 248 path: "testdata/deps/window-size/package.json", 249 wantPackages: []*extractor.Package{}, 250 }, 251 { 252 name: "VSCode extension", 253 path: "testdata/vscode-extension.json", 254 wantPackages: []*extractor.Package{}, 255 }, 256 { 257 name: "VSCode extension with only required fields", 258 path: "testdata/vscode-extension-only-required.json", 259 wantPackages: []*extractor.Package{}, 260 }, 261 { 262 name: "Unity package", 263 path: "testdata/unity-package.json", 264 wantPackages: []*extractor.Package{}, 265 }, 266 { 267 name: "Undici_package_with_nonstandard_contributors_parsed_correctly", 268 path: "testdata/undici-package.json", 269 wantPackages: []*extractor.Package{ 270 { 271 Name: "undici", 272 Version: "5.28.3", 273 PURLType: purl.TypeNPM, 274 Locations: []string{ 275 "testdata/undici-package.json", 276 }, 277 Metadata: &metadata.JavascriptPackageJSONMetadata{ 278 Contributors: []*metadata.Person{ 279 { 280 Name: "Daniele Belardi", 281 URL: "https://github.com/dnlup", 282 }, 283 { 284 Name: "Tomas Della Vedova", 285 URL: "https://github.com/delvedor", 286 }, 287 { 288 Name: "Invalid URL NoCrash", 289 }, 290 }, 291 }, 292 }, 293 }, 294 }, 295 { 296 name: "npm_package_with_engine_field_set", 297 path: "testdata/not-vscode.json", 298 wantPackages: []*extractor.Package{ 299 { 300 Name: "jsonparse", 301 Version: "1.3.1", 302 PURLType: purl.TypeNPM, 303 Locations: []string{"testdata/not-vscode.json"}, 304 Metadata: &metadata.JavascriptPackageJSONMetadata{ 305 Author: &metadata.Person{ 306 Name: "Tim Caswell", 307 Email: "tim@creationix.com", 308 }, 309 }, 310 }, 311 }, 312 }, 313 { 314 name: "package_with_dependencies", 315 path: "testdata/package-with-deps.json", 316 cfg: packagejson.Config{IncludeDependencies: true}, 317 wantPackages: []*extractor.Package{ 318 { 319 Name: "package-with-deps", 320 Version: "1.2.3", 321 PURLType: purl.TypeNPM, 322 Locations: []string{"testdata/package-with-deps.json"}, 323 Metadata: &metadata.JavascriptPackageJSONMetadata{}, 324 }, 325 { 326 Name: "dep1", 327 Version: "1.0.0", 328 PURLType: purl.TypeNPM, 329 Locations: []string{"testdata/package-with-deps.json"}, 330 }, 331 { 332 Name: "dep2", 333 Version: "2.0.1", 334 PURLType: purl.TypeNPM, 335 Locations: []string{"testdata/package-with-deps.json"}, 336 }, 337 { 338 Name: "dep3", 339 Version: "3.1.0", 340 PURLType: purl.TypeNPM, 341 Locations: []string{"testdata/package-with-deps.json"}, 342 }, 343 { 344 Name: "dep4", 345 Version: "0.4.2", 346 PURLType: purl.TypeNPM, 347 Locations: []string{"testdata/package-with-deps.json"}, 348 }, 349 { 350 Name: "dep5", 351 Version: "5.0.0", 352 PURLType: purl.TypeNPM, 353 Locations: []string{"testdata/package-with-deps.json"}, 354 }, 355 // dep6 is invalid, so it should not be included. 356 { 357 Name: "dep7", 358 Version: "1.0.0", 359 PURLType: purl.TypeNPM, 360 Locations: []string{"testdata/package-with-deps.json"}, 361 }, 362 }, 363 }, 364 { 365 name: "invalid packagejson", 366 path: "testdata/invalid", 367 wantErr: cmpopts.AnyError, 368 wantResultMetric: stats.FileExtractedResultErrorUnknown, 369 }, 370 } 371 372 for _, tt := range tests { 373 // Note the subtest here 374 t.Run(tt.name, func(t *testing.T) { 375 r, err := os.Open(tt.path) 376 defer func() { 377 if err = r.Close(); err != nil { 378 t.Errorf("Close(): %v", err) 379 } 380 }() 381 if err != nil { 382 t.Fatal(err) 383 } 384 385 info, err := os.Stat(tt.path) 386 if err != nil { 387 t.Fatal(err) 388 } 389 390 collector := testcollector.New() 391 tt.cfg.Stats = collector 392 393 input := &filesystem.ScanInput{ 394 FS: scalibrfs.DirFS("."), 395 Path: tt.path, 396 Reader: r, 397 Info: info, 398 } 399 e := packagejson.New(defaultConfigWith(tt.cfg)) 400 got, err := e.Extract(t.Context(), input) 401 if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) { 402 t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.name, err, tt.wantErr) 403 } 404 405 var want inventory.Inventory 406 if tt.wantPackages != nil { 407 want = inventory.Inventory{Packages: tt.wantPackages} 408 } 409 410 if diff := cmp.Diff(want, got, cmpopts.SortSlices(extracttest.PackageCmpLess), cmpopts.EquateEmpty()); diff != "" { 411 t.Errorf("Extract(%s) (-want +got):\n%s", tt.path, diff) 412 } 413 414 wantResultMetric := tt.wantResultMetric 415 if wantResultMetric == "" && tt.wantErr == nil { 416 wantResultMetric = stats.FileExtractedResultSuccess 417 } 418 gotResultMetric := collector.FileExtractedResult(tt.path) 419 if gotResultMetric != wantResultMetric { 420 t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, wantResultMetric) 421 } 422 423 gotFileSizeMetric := collector.FileExtractedFileSize(tt.path) 424 if gotFileSizeMetric != info.Size() { 425 t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.path, gotFileSizeMetric, info.Size()) 426 } 427 }) 428 } 429 } 430 431 // defaultConfigWith combines any non-zero fields of cfg with packagejson.DefaultConfig(). 432 func defaultConfigWith(cfg packagejson.Config) packagejson.Config { 433 newCfg := packagejson.DefaultConfig() 434 newCfg.IncludeDependencies = cfg.IncludeDependencies 435 436 if cfg.Stats != nil { 437 newCfg.Stats = cfg.Stats 438 } 439 if cfg.MaxFileSizeBytes > 0 { 440 newCfg.MaxFileSizeBytes = cfg.MaxFileSizeBytes 441 } 442 return newCfg 443 }