github.com/google/osv-scalibr@v0.4.1/annotator/misc/npmsource/npmsource_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 npmsource_test 16 17 import ( 18 "os" 19 "path/filepath" 20 "testing" 21 22 "github.com/google/go-cmp/cmp" 23 "github.com/google/go-cpy/cpy" 24 "github.com/google/osv-scalibr/annotator" 25 "github.com/google/osv-scalibr/annotator/misc/npmsource" 26 "github.com/google/osv-scalibr/extractor" 27 "github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagejson/metadata" 28 scalibrfs "github.com/google/osv-scalibr/fs" 29 "github.com/google/osv-scalibr/inventory" 30 "google.golang.org/protobuf/proto" 31 "google.golang.org/protobuf/testing/protocmp" 32 ) 33 34 func TestAnnotate_AbsolutePackagePath(t *testing.T) { 35 copier := cpy.New( 36 cpy.Func(proto.Clone), 37 cpy.IgnoreAllUnexported(), 38 ) 39 40 lockfiles := map[string]string{ 41 "testproject/package-lock.json": "testdata/package-lock.v1.json", 42 } 43 44 root := setupNPMLockfiles(t, lockfiles) 45 46 inputPackage := &extractor.Package{ 47 Name: "wrappy", 48 PURLType: "npm", 49 // Locations is the absolute path of the package.json file. 50 Locations: []string{filepath.Join(root, "testproject/node_modules/dependency-1/package.json")}, 51 } 52 inv := &inventory.Inventory{Packages: []*extractor.Package{copier.Copy(inputPackage).(*extractor.Package)}} 53 54 input := &annotator.ScanInput{ 55 ScanRoot: scalibrfs.RealFSScanRoot(root), 56 } 57 58 wantPackage := &extractor.Package{ 59 Name: "wrappy", 60 PURLType: "npm", 61 Locations: []string{filepath.Join(root, "testproject/node_modules/dependency-1/package.json")}, 62 Metadata: &metadata.JavascriptPackageJSONMetadata{ 63 // We want to assert that the package was resolved from the NPM repository which means that 64 // the lockfile was read from the relative path in the scan root. 65 Source: metadata.PublicRegistry, 66 }, 67 } 68 69 err := npmsource.New().Annotate(t.Context(), input, inv) 70 if err != nil { 71 t.Errorf("Annotate(%v) error: %v; want error presence = false", inputPackage, err) 72 } 73 74 want := &inventory.Inventory{Packages: []*extractor.Package{wantPackage}} 75 if diff := cmp.Diff(want, inv, protocmp.Transform()); diff != "" { 76 t.Errorf("Annotate(%v): unexpected diff (-want +got):\n%s", inputPackage, diff) 77 } 78 } 79 80 func TestAnnotate_LockfileV1(t *testing.T) { 81 copier := cpy.New( 82 cpy.Func(proto.Clone), 83 cpy.IgnoreAllUnexported(), 84 ) 85 86 testCases := []struct { 87 name string 88 lockfiles map[string]string 89 inputPackage *extractor.Package 90 wantPackage *extractor.Package 91 wantAnyErr bool 92 }{ 93 { 94 name: "unfound_dependency_in_lockfile", 95 lockfiles: map[string]string{ 96 "testproject/package-lock.json": "testdata/package-lock.v1.json", 97 }, 98 inputPackage: &extractor.Package{ 99 Name: "abandoned-package", 100 PURLType: "npm", 101 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 102 }, 103 wantPackage: &extractor.Package{ 104 Name: "abandoned-package", 105 PURLType: "npm", 106 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 107 Metadata: &metadata.JavascriptPackageJSONMetadata{ 108 Source: metadata.Unknown, 109 }, 110 }, 111 }, 112 { 113 name: "dependency_from_private_registry", 114 lockfiles: map[string]string{ 115 "testproject/package-lock.json": "testdata/package-lock.v1.json", 116 }, 117 inputPackage: &extractor.Package{ 118 Name: "supports-color", 119 PURLType: "npm", 120 Locations: []string{"testproject/node_modules/supports-color/package.json"}, 121 }, 122 wantPackage: &extractor.Package{ 123 Name: "supports-color", 124 PURLType: "npm", 125 Locations: []string{"testproject/node_modules/supports-color/package.json"}, 126 Metadata: &metadata.JavascriptPackageJSONMetadata{ 127 Source: metadata.Other, 128 }, 129 }, 130 }, 131 { 132 name: "custom_package_from_github_(private_registry)", 133 lockfiles: map[string]string{ 134 "testproject/package-lock.json": "testdata/package-lock.v1.json", 135 }, 136 inputPackage: &extractor.Package{ 137 Name: "custom-package", 138 PURLType: "npm", 139 Locations: []string{"testproject/node_modules/custom-package/package.json"}, 140 }, 141 wantPackage: &extractor.Package{ 142 Name: "custom-package", 143 PURLType: "npm", 144 Locations: []string{"testproject/node_modules/custom-package/package.json"}, 145 Metadata: &metadata.JavascriptPackageJSONMetadata{ 146 Source: metadata.Other, 147 }, 148 }, 149 }, 150 { 151 name: "local_package", 152 lockfiles: map[string]string{ 153 "testproject/package-lock.json": "testdata/package-lock.v1.json", 154 }, 155 inputPackage: &extractor.Package{ 156 Name: "local-package", 157 PURLType: "npm", 158 Locations: []string{"testproject/node_modules/local-package/package.json"}, 159 }, 160 wantPackage: &extractor.Package{ 161 Name: "local-package", 162 PURLType: "npm", 163 Locations: []string{"testproject/node_modules/local-package/package.json"}, 164 Metadata: &metadata.JavascriptPackageJSONMetadata{ 165 Source: metadata.Local, 166 }, 167 }, 168 }, 169 { 170 name: "nested_dependency", 171 lockfiles: map[string]string{ 172 "testproject/package-lock.json": "testdata/package-lock.v1.json", 173 }, 174 inputPackage: &extractor.Package{ 175 Name: "wrappy", 176 PURLType: "npm", 177 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 178 }, 179 wantPackage: &extractor.Package{ 180 Name: "wrappy", 181 PURLType: "npm", 182 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 183 Metadata: &metadata.JavascriptPackageJSONMetadata{ 184 Source: metadata.PublicRegistry, 185 }, 186 }, 187 }, 188 { 189 name: "alias_package", 190 lockfiles: map[string]string{ 191 "testproject/package-lock.json": "testdata/package-lock.v1.json", 192 }, 193 inputPackage: &extractor.Package{ 194 Name: "string-width", 195 PURLType: "npm", 196 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 197 }, 198 wantPackage: &extractor.Package{ 199 Name: "string-width", 200 PURLType: "npm", 201 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 202 Metadata: &metadata.JavascriptPackageJSONMetadata{ 203 Source: metadata.PublicRegistry, 204 }, 205 }, 206 }, 207 { 208 name: "duplicated_dependency", 209 lockfiles: map[string]string{ 210 "testproject/package-lock.json": "testdata/package-lock.v1.json", 211 }, 212 inputPackage: &extractor.Package{ 213 Name: "@babel/highlight", 214 PURLType: "npm", 215 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 216 }, 217 wantPackage: &extractor.Package{ 218 Name: "@babel/highlight", 219 PURLType: "npm", 220 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 221 Metadata: &metadata.JavascriptPackageJSONMetadata{ 222 Source: metadata.PublicRegistry, 223 }, 224 }, 225 }, 226 { 227 name: "same_package_different_group", 228 lockfiles: map[string]string{ 229 "testproject/package-lock.json": "testdata/package-lock.v1.json", 230 }, 231 inputPackage: &extractor.Package{ 232 Name: "ajv", 233 PURLType: "npm", 234 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 235 }, 236 wantPackage: &extractor.Package{ 237 Name: "ajv", 238 PURLType: "npm", 239 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 240 Metadata: &metadata.JavascriptPackageJSONMetadata{ 241 Source: metadata.PublicRegistry, 242 }, 243 }, 244 }, 245 { 246 name: "no lockfile present", 247 lockfiles: map[string]string{}, 248 inputPackage: &extractor.Package{ 249 Name: "abandoned-package", 250 PURLType: "npm", 251 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 252 }, 253 wantPackage: &extractor.Package{ 254 Name: "abandoned-package", 255 PURLType: "npm", 256 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 257 Metadata: &metadata.JavascriptPackageJSONMetadata{ 258 Source: metadata.Unknown, 259 }, 260 }, 261 wantAnyErr: false, 262 }, 263 } 264 265 for _, tt := range testCases { 266 t.Run(tt.name, func(t *testing.T) { 267 packages := []*extractor.Package{copier.Copy(tt.inputPackage).(*extractor.Package)} 268 inv := &inventory.Inventory{Packages: packages} 269 270 root := setupNPMLockfiles(t, tt.lockfiles) 271 input := &annotator.ScanInput{ 272 ScanRoot: scalibrfs.RealFSScanRoot(root), 273 } 274 275 err := npmsource.New().Annotate(t.Context(), input, inv) 276 gotErr := err != nil 277 if gotErr != tt.wantAnyErr { 278 t.Errorf("Annotate_LockfileV1(%v) error: %v; want error presence = %v", tt.inputPackage, err, tt.wantAnyErr) 279 } 280 281 want := &inventory.Inventory{Packages: []*extractor.Package{tt.wantPackage}} 282 if diff := cmp.Diff(want, inv, protocmp.Transform()); diff != "" { 283 t.Errorf("Annotate_LockfileV1(%v): unexpected diff (-want +got):\n%s", tt.inputPackage, diff) 284 } 285 }) 286 } 287 } 288 289 func TestAnnotate_LockfileV2(t *testing.T) { 290 copier := cpy.New( 291 cpy.Func(proto.Clone), 292 cpy.IgnoreAllUnexported(), 293 ) 294 295 testCases := []struct { 296 name string 297 lockfiles map[string]string 298 inputPackage *extractor.Package 299 wantPackage *extractor.Package 300 wantAnyErr bool 301 }{ 302 { 303 name: "unfound_package_in_lockfile", 304 lockfiles: map[string]string{ 305 "testproject/package-lock.json": "testdata/package-lock.json", 306 }, 307 inputPackage: &extractor.Package{ 308 Name: "abandoned-package", 309 PURLType: "npm", 310 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 311 }, 312 wantPackage: &extractor.Package{ 313 Name: "abandoned-package", 314 PURLType: "npm", 315 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 316 Metadata: &metadata.JavascriptPackageJSONMetadata{ 317 Source: metadata.Unknown, 318 }, 319 }, 320 }, 321 { 322 name: "dependency_from_private_registry", 323 lockfiles: map[string]string{ 324 "testproject/package-lock.json": "testdata/package-lock.json", 325 }, 326 inputPackage: &extractor.Package{ 327 Name: "supports-color", 328 PURLType: "npm", 329 Locations: []string{"testproject/node_modules/supports-color/package.json"}, 330 }, 331 wantPackage: &extractor.Package{ 332 Name: "supports-color", 333 PURLType: "npm", 334 Locations: []string{"testproject/node_modules/supports-color/package.json"}, 335 Metadata: &metadata.JavascriptPackageJSONMetadata{ 336 Source: metadata.Other, 337 }, 338 }, 339 }, 340 { 341 name: "local_package", 342 lockfiles: map[string]string{ 343 "testproject/package-lock.json": "testdata/package-lock.json", 344 }, 345 inputPackage: &extractor.Package{ 346 Name: "local-package", 347 PURLType: "npm", 348 Locations: []string{"testproject/node_modules/local-package/package.json"}, 349 }, 350 wantPackage: &extractor.Package{ 351 Name: "local-package", 352 PURLType: "npm", 353 Locations: []string{"testproject/node_modules/local-package/package.json"}, 354 Metadata: &metadata.JavascriptPackageJSONMetadata{ 355 Source: metadata.Local, 356 }, 357 }, 358 }, 359 { 360 name: "scoped_packages_from_npm_repository", 361 lockfiles: map[string]string{ 362 "testproject/package-lock.json": "testdata/package-lock.json", 363 }, 364 inputPackage: &extractor.Package{ 365 Name: "@babel/code-frame", 366 PURLType: "npm", 367 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 368 }, 369 wantPackage: &extractor.Package{ 370 Name: "@babel/code-frame", 371 PURLType: "npm", 372 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 373 Metadata: &metadata.JavascriptPackageJSONMetadata{ 374 Source: metadata.PublicRegistry, 375 }, 376 }, 377 }, 378 { 379 name: "alias_package", 380 lockfiles: map[string]string{ 381 "testproject/package-lock.json": "testdata/package-lock.json", 382 }, 383 inputPackage: &extractor.Package{ 384 Name: "string-width", 385 PURLType: "npm", 386 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 387 }, 388 wantPackage: &extractor.Package{ 389 Name: "string-width", 390 PURLType: "npm", 391 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 392 Metadata: &metadata.JavascriptPackageJSONMetadata{ 393 Source: metadata.PublicRegistry, 394 }, 395 }, 396 }, 397 { 398 name: "custom_package_from_github_(private_registry)", 399 lockfiles: map[string]string{ 400 "testproject/package-lock.json": "testdata/package-lock.json", 401 }, 402 inputPackage: &extractor.Package{ 403 Name: "custom-package", 404 PURLType: "npm", 405 Locations: []string{"testproject/node_modules/custom-package/package.json"}, 406 }, 407 wantPackage: &extractor.Package{ 408 Name: "custom-package", 409 PURLType: "npm", 410 Locations: []string{"testproject/node_modules/custom-package/package.json"}, 411 Metadata: &metadata.JavascriptPackageJSONMetadata{ 412 Source: metadata.Other, 413 }, 414 }, 415 }, 416 { 417 name: "nested_packages", 418 lockfiles: map[string]string{ 419 "testproject/package-lock.json": "testdata/package-lock.json", 420 }, 421 inputPackage: &extractor.Package{ 422 Name: "wrappy", 423 PURLType: "npm", 424 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 425 }, 426 wantPackage: &extractor.Package{ 427 Name: "wrappy", 428 PURLType: "npm", 429 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 430 Metadata: &metadata.JavascriptPackageJSONMetadata{ 431 Source: metadata.PublicRegistry, 432 }, 433 }, 434 }, 435 { 436 name: "duplicated_packages", 437 lockfiles: map[string]string{ 438 "testproject/package-lock.json": "testdata/package-lock.json", 439 }, 440 inputPackage: &extractor.Package{ 441 Name: "@babel/highlight", 442 PURLType: "npm", 443 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 444 }, 445 wantPackage: &extractor.Package{ 446 Name: "@babel/highlight", 447 PURLType: "npm", 448 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 449 Metadata: &metadata.JavascriptPackageJSONMetadata{ 450 Source: metadata.PublicRegistry, 451 }, 452 }, 453 }, 454 { 455 name: "same_package_different_group", 456 lockfiles: map[string]string{ 457 "testproject/package-lock.json": "testdata/package-lock.json", 458 }, 459 inputPackage: &extractor.Package{ 460 Name: "ajv", 461 PURLType: "npm", 462 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 463 }, 464 wantPackage: &extractor.Package{ 465 Name: "ajv", 466 PURLType: "npm", 467 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 468 Metadata: &metadata.JavascriptPackageJSONMetadata{ 469 Source: metadata.PublicRegistry, 470 }, 471 }, 472 }, 473 { 474 name: "no lockfile present", 475 lockfiles: map[string]string{}, 476 inputPackage: &extractor.Package{ 477 Name: "abandoned-package", 478 PURLType: "npm", 479 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 480 }, 481 wantPackage: &extractor.Package{ 482 Name: "abandoned-package", 483 PURLType: "npm", 484 Locations: []string{"testproject/node_modules/abandoned-package/package.json"}, 485 Metadata: &metadata.JavascriptPackageJSONMetadata{ 486 Source: metadata.Unknown, 487 }, 488 }, 489 wantAnyErr: false, 490 }, 491 } 492 493 for _, tt := range testCases { 494 t.Run(tt.name, func(t *testing.T) { 495 packages := []*extractor.Package{copier.Copy(tt.inputPackage).(*extractor.Package)} 496 inv := &inventory.Inventory{Packages: packages} 497 498 root := setupNPMLockfiles(t, tt.lockfiles) 499 input := &annotator.ScanInput{ 500 ScanRoot: scalibrfs.RealFSScanRoot(root), 501 } 502 503 err := npmsource.New().Annotate(t.Context(), input, inv) 504 gotErr := err != nil 505 if gotErr != tt.wantAnyErr { 506 t.Errorf("Annotate_LockfileV1(%v) error: %v; want error presence = %v", tt.inputPackage, err, tt.wantAnyErr) 507 } 508 509 want := &inventory.Inventory{Packages: []*extractor.Package{tt.wantPackage}} 510 if diff := cmp.Diff(want, inv, protocmp.Transform()); diff != "" { 511 t.Errorf("Annotate_LockfileV2(%v): unexpected diff (-want +got):\n%s", tt.inputPackage, diff) 512 } 513 }) 514 } 515 } 516 517 func TestMapNPMProjectRootsToPackages(t *testing.T) { 518 testCases := []struct { 519 name string 520 inputPackages []*extractor.Package 521 want map[string][]*extractor.Package 522 }{ 523 { 524 name: "maps_root_directory_to_package_from_node_modules/../package.json", 525 inputPackages: []*extractor.Package{ 526 { 527 Name: "acorn", 528 Version: "1.0.0", 529 PURLType: "npm", 530 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 531 }, 532 }, 533 want: map[string][]*extractor.Package{ 534 "testproject": []*extractor.Package{ 535 { 536 Name: "acorn", 537 Version: "1.0.0", 538 PURLType: "npm", 539 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 540 }, 541 }, 542 }, 543 }, 544 { 545 name: "maps_root_directory_to_package_from_node_modules/../package.json", 546 inputPackages: []*extractor.Package{ 547 { 548 Name: "acorn", 549 Version: "1.0.0", 550 PURLType: "npm", 551 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 552 }, 553 }, 554 want: map[string][]*extractor.Package{ 555 "testproject": []*extractor.Package{ 556 { 557 Name: "acorn", 558 Version: "1.0.0", 559 PURLType: "npm", 560 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 561 }, 562 }, 563 }, 564 }, 565 { 566 name: "no_map_for_non-npm_packages", 567 inputPackages: []*extractor.Package{ 568 { 569 Name: "acorn", 570 Version: "1.0.0", 571 PURLType: "pypi", 572 Locations: []string{"testproject/node_modules/dependency-1/package.json"}, 573 }, 574 }, 575 want: make(map[string][]*extractor.Package), 576 }, 577 { 578 name: "no_map_for_non-package.json", 579 inputPackages: []*extractor.Package{ 580 { 581 Name: "acorn", 582 Version: "1.0.0", 583 PURLType: "npm", 584 Locations: []string{"testproject/node_modules/dependency-2/package2.json"}, 585 }, 586 }, 587 want: make(map[string][]*extractor.Package), 588 }, 589 { 590 name: "no_map_for_non-node_modules_directory", 591 inputPackages: []*extractor.Package{ 592 { 593 Name: "acorn", 594 Version: "1.0.0", 595 PURLType: "npm", 596 Locations: []string{"testproject/package.json"}, 597 }, 598 }, 599 want: make(map[string][]*extractor.Package), 600 }, 601 { 602 name: "no_map_for_empty_locations", 603 inputPackages: []*extractor.Package{ 604 { 605 Name: "acorn", 606 Version: "1.0.0", 607 PURLType: "npm", 608 Locations: []string{""}, 609 }, 610 }, 611 want: make(map[string][]*extractor.Package), 612 }, 613 } 614 615 for _, tt := range testCases { 616 t.Run(tt.name, func(t *testing.T) { 617 got := npmsource.MapNPMProjectRootsToPackages(tt.inputPackages) 618 if diff := cmp.Diff(tt.want, got); diff != "" { 619 t.Errorf("MapNPMProjectRootsToPackages(%v): unexpected diff (-want +got): %v", tt.inputPackages, diff) 620 } 621 }) 622 } 623 } 624 625 func TestResolvedFromLockfile(t *testing.T) { 626 testCases := []struct { 627 name string 628 lockfiles map[string]string 629 wantDeps map[string]metadata.NPMPackageSource 630 wantAnyErr bool 631 skipWindows bool 632 }{ 633 // All 3 lockfiles have the same file structure. 634 { 635 name: "parse_package-lock.json", 636 lockfiles: map[string]string{ 637 "testproject/package-lock.json": "testdata/package-lock.json", 638 }, 639 wantDeps: map[string]metadata.NPMPackageSource{ 640 "acorn": metadata.PublicRegistry, 641 "wrappy": metadata.PublicRegistry, 642 "custom-package": metadata.Other, 643 "supports-color": metadata.Other, 644 "ajv": metadata.PublicRegistry, 645 "@babel/highlight": metadata.PublicRegistry, 646 "@babel/code-frame": metadata.PublicRegistry, 647 "string-width": metadata.PublicRegistry, 648 "@parcel/watcher": metadata.Unknown, 649 "local-package": metadata.Local, 650 }, 651 skipWindows: true, 652 }, 653 { 654 name: "parse_npm-shrinkwrap.json", 655 lockfiles: map[string]string{ 656 "testproject/npm-shrinkwrap.json": "testdata/package-lock.json", 657 }, 658 wantDeps: map[string]metadata.NPMPackageSource{ 659 "acorn": metadata.PublicRegistry, 660 "wrappy": metadata.PublicRegistry, 661 "custom-package": metadata.Other, 662 "supports-color": metadata.Other, 663 "ajv": metadata.PublicRegistry, 664 "@babel/highlight": metadata.PublicRegistry, 665 "@babel/code-frame": metadata.PublicRegistry, 666 "string-width": metadata.PublicRegistry, 667 "@parcel/watcher": metadata.Unknown, 668 "local-package": metadata.Local, 669 }, 670 skipWindows: true, 671 }, 672 { 673 name: "parse_hidden_package-lock.json_in_/node_modules", 674 lockfiles: map[string]string{ 675 "testproject/node_modules/.package-lock.json": "testdata/package-lock.json", 676 }, 677 wantDeps: map[string]metadata.NPMPackageSource{ 678 "acorn": metadata.PublicRegistry, 679 "wrappy": metadata.PublicRegistry, 680 "custom-package": metadata.Other, 681 "supports-color": metadata.Other, 682 "ajv": metadata.PublicRegistry, 683 "@babel/highlight": metadata.PublicRegistry, 684 "@babel/code-frame": metadata.PublicRegistry, 685 "string-width": metadata.PublicRegistry, 686 "@parcel/watcher": metadata.Unknown, 687 "local-package": metadata.Local, 688 }, 689 skipWindows: true, 690 }, 691 { 692 name: "parse with no lockfiles returns nothing", 693 lockfiles: map[string]string{}, 694 wantDeps: nil, 695 wantAnyErr: false, 696 skipWindows: false, 697 }, 698 { 699 name: "parse_empty_lockfiles_returns_error", 700 lockfiles: map[string]string{ 701 "testproject/node_modules/.package-lock.json": "empty-file.json", 702 }, 703 wantDeps: nil, 704 wantAnyErr: true, 705 skipWindows: true, 706 }, 707 { 708 name: "parse_lockfiles_without_dependencies_and_packages_returns_nothing", 709 lockfiles: map[string]string{ 710 "testproject/node_modules/.package-lock.json": "testdata/no-dep-list-package-lock.json", 711 }, 712 wantDeps: map[string]metadata.NPMPackageSource{}, 713 wantAnyErr: false, 714 skipWindows: true, 715 }, 716 } 717 718 for _, tt := range testCases { 719 t.Run(tt.name, func(t *testing.T) { 720 root := setupNPMLockfiles(t, tt.lockfiles) 721 fsys := scalibrfs.DirFS(root) 722 723 got, err := npmsource.ResolvedFromLockfile("testproject", fsys) 724 gotErr := err != nil 725 if gotErr != tt.wantAnyErr { 726 t.Errorf("ResolvedFromLockfile(testproject) error: %v; want error presence = %v", err, tt.wantAnyErr) 727 } 728 if diff := cmp.Diff(tt.wantDeps, got); diff != "" { 729 t.Errorf("ResolvedFromLockfile(testproject): unexpected diff (-want +got): %v", diff) 730 } 731 }) 732 } 733 } 734 735 func setupNPMLockfiles(t *testing.T, dbPaths map[string]string) string { 736 t.Helper() 737 root := t.TempDir() 738 for dbPath, contentFile := range dbPaths { 739 dbDir := filepath.Join(root, filepath.Dir(dbPath)) 740 if err := os.MkdirAll(dbDir, 0777); err != nil { 741 t.Fatalf("Error creating directory %q: %v", dbDir, err) 742 } 743 744 if contentFile != "empty-file.json" { 745 content, err := os.ReadFile(contentFile) 746 if err != nil { 747 t.Fatalf("Error reading content file %q: %v", contentFile, err) 748 } 749 writeFile(t, filepath.Join(root, dbPath), content) 750 } else { 751 writeFile(t, filepath.Join(root, dbPath), []byte{}) 752 } 753 } 754 return root 755 } 756 757 func writeFile(t *testing.T, path string, content []byte) { 758 t.Helper() 759 if err := os.WriteFile(path, content, 0644); err != nil { 760 t.Fatalf("Error creating file %q: %v", path, err) 761 } 762 }