github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/javascript/packagelockjson/packagelockjson_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 packagelockjson_test
    16  
    17  import (
    18  	"io/fs"
    19  	"path/filepath"
    20  	"testing"
    21  
    22  	"github.com/google/go-cmp/cmp"
    23  	"github.com/google/go-cmp/cmp/cmpopts"
    24  	"github.com/google/osv-scalibr/extractor"
    25  	"github.com/google/osv-scalibr/extractor/filesystem"
    26  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson"
    28  	"github.com/google/osv-scalibr/extractor/filesystem/osv"
    29  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    30  	"github.com/google/osv-scalibr/inventory"
    31  	"github.com/google/osv-scalibr/purl"
    32  	"github.com/google/osv-scalibr/stats"
    33  	"github.com/google/osv-scalibr/testing/extracttest"
    34  	"github.com/google/osv-scalibr/testing/fakefs"
    35  	"github.com/google/osv-scalibr/testing/testcollector"
    36  )
    37  
    38  func TestExtractor_FileRequired(t *testing.T) {
    39  	tests := []struct {
    40  		name             string
    41  		path             string
    42  		fileSizeBytes    int64
    43  		maxFileSizeBytes int64
    44  		wantRequired     bool
    45  		wantResultMetric stats.FileRequiredResult
    46  	}{
    47  		{
    48  			name:         "Empty path",
    49  			path:         filepath.FromSlash(""),
    50  			wantRequired: false,
    51  		},
    52  		{
    53  			name:             "package-lock.json",
    54  			path:             filepath.FromSlash("package-lock.json"),
    55  			wantRequired:     true,
    56  			wantResultMetric: stats.FileRequiredResultOK,
    57  		},
    58  		{
    59  			name:             "package-lock.json at the end of a path",
    60  			path:             filepath.FromSlash("path/to/my/package-lock.json"),
    61  			wantRequired:     true,
    62  			wantResultMetric: stats.FileRequiredResultOK,
    63  		},
    64  		{
    65  			name:         "package-lock.json as path segment",
    66  			path:         filepath.FromSlash("path/to/my/package-lock.json/file"),
    67  			wantRequired: false,
    68  		},
    69  		{
    70  			name:         "package-lock.json.file (wrong extension)",
    71  			path:         filepath.FromSlash("path/to/my/package-lock.json.file"),
    72  			wantRequired: false,
    73  		},
    74  		{
    75  			name:         "path.to.my.package.lock.json",
    76  			path:         filepath.FromSlash("path.to.my.package.lock.json"),
    77  			wantRequired: false,
    78  		},
    79  		{
    80  			name:         "skip from inside node_modules dir",
    81  			path:         filepath.FromSlash("foo/node_modules/bar/package-lock.json"),
    82  			wantRequired: false,
    83  		},
    84  		{
    85  			name:             "package-lock.json required if file size < max file size",
    86  			path:             "foo/package-lock.json",
    87  			fileSizeBytes:    100 * units.KiB,
    88  			maxFileSizeBytes: 1 * units.MiB,
    89  			wantRequired:     true,
    90  			wantResultMetric: stats.FileRequiredResultOK,
    91  		},
    92  		{
    93  			name:             "package-lock.json required if file size == max file size",
    94  			path:             "foo/package-lock.json",
    95  			fileSizeBytes:    1 * units.MiB,
    96  			maxFileSizeBytes: 1 * units.MiB,
    97  			wantRequired:     true,
    98  			wantResultMetric: stats.FileRequiredResultOK,
    99  		},
   100  		{
   101  			name:             "package-lock.json not required if file size > max file size",
   102  			path:             "foo/package-lock.json",
   103  			fileSizeBytes:    1 * units.MiB,
   104  			maxFileSizeBytes: 100 * units.KiB,
   105  			wantRequired:     false,
   106  			wantResultMetric: stats.FileRequiredResultSizeLimitExceeded,
   107  		},
   108  		{
   109  			name:             "package-lock.json required if max file size set to 0",
   110  			path:             "foo/package-lock.json",
   111  			fileSizeBytes:    1 * units.MiB,
   112  			maxFileSizeBytes: 0,
   113  			wantRequired:     true,
   114  			wantResultMetric: stats.FileRequiredResultOK,
   115  		},
   116  		{
   117  			name:             "npm-shrinkwrap.json",
   118  			path:             filepath.FromSlash("npm-shrinkwrap.json"),
   119  			wantRequired:     true,
   120  			wantResultMetric: stats.FileRequiredResultOK,
   121  		},
   122  		{
   123  			name:             "npm-shrinkwrap.json at the end of a path",
   124  			path:             filepath.FromSlash("path/to/my/npm-shrinkwrap.json"),
   125  			wantRequired:     true,
   126  			wantResultMetric: stats.FileRequiredResultOK,
   127  		},
   128  		{
   129  			name:         "npm-shrinkwrap.json as path segment",
   130  			path:         filepath.FromSlash("path/to/my/npm-shrinkwrap.json/file"),
   131  			wantRequired: false,
   132  		},
   133  		{
   134  			name:         "npm-shrinkwrap.json.file (wrong extension)",
   135  			path:         filepath.FromSlash("path/to/my/npm-shrinkwrap.json.file"),
   136  			wantRequired: false,
   137  		},
   138  		{
   139  			name:         "path.to.my.npm-shrinkwrap.json",
   140  			path:         filepath.FromSlash("path.to.my.npm-shrinkwrap.json"),
   141  			wantRequired: false,
   142  		},
   143  	}
   144  
   145  	for _, tt := range tests {
   146  		t.Run(tt.name, func(t *testing.T) {
   147  			collector := testcollector.New()
   148  			var e filesystem.Extractor = packagelockjson.New(
   149  				packagelockjson.Config{
   150  					Stats:            collector,
   151  					MaxFileSizeBytes: tt.maxFileSizeBytes,
   152  				},
   153  			)
   154  
   155  			// Set default size if not provided.
   156  			fileSizeBytes := tt.fileSizeBytes
   157  			if fileSizeBytes == 0 {
   158  				fileSizeBytes = 100 * units.KiB
   159  			}
   160  
   161  			isRequired := e.FileRequired(simplefileapi.New(tt.path, fakefs.FakeFileInfo{
   162  				FileName: filepath.Base(tt.path),
   163  				FileMode: fs.ModePerm,
   164  				FileSize: fileSizeBytes,
   165  			}))
   166  			if isRequired != tt.wantRequired {
   167  				t.Fatalf("FileRequired(%s): got %v, want %v", tt.path, isRequired, tt.wantRequired)
   168  			}
   169  
   170  			gotResultMetric := collector.FileRequiredResult(tt.path)
   171  			if gotResultMetric != tt.wantResultMetric {
   172  				t.Errorf("FileRequired(%s) recorded result metric %v, want result metric %v", tt.path, gotResultMetric, tt.wantResultMetric)
   173  			}
   174  		})
   175  	}
   176  }
   177  
   178  func TestMetricCollector(t *testing.T) {
   179  	tests := []struct {
   180  		name             string
   181  		inputConfig      extracttest.ScanInputMockConfig
   182  		wantResultMetric stats.FileExtractedResult
   183  	}{
   184  		{
   185  			name: "invalid_package-lock.json",
   186  			inputConfig: extracttest.ScanInputMockConfig{
   187  				Path: "testdata/not-json.txt",
   188  			},
   189  			wantResultMetric: stats.FileExtractedResultErrorUnknown,
   190  		},
   191  		{
   192  			name: "valid_package-lock.json",
   193  			inputConfig: extracttest.ScanInputMockConfig{
   194  				Path: "testdata/one-package.v1.json",
   195  			},
   196  			wantResultMetric: stats.FileExtractedResultSuccess,
   197  		},
   198  	}
   199  
   200  	for _, tt := range tests {
   201  		t.Run(tt.name, func(t *testing.T) {
   202  			collector := testcollector.New()
   203  			extr := packagelockjson.New(packagelockjson.Config{
   204  				Stats: collector,
   205  			})
   206  
   207  			scanInput := extracttest.GenerateScanInputMock(t, tt.inputConfig)
   208  			defer extracttest.CloseTestScanInput(t, scanInput)
   209  
   210  			// Results are tested in the other files
   211  			_, _ = extr.Extract(t.Context(), &scanInput)
   212  
   213  			gotResultMetric := collector.FileExtractedResult(tt.inputConfig.Path)
   214  			if gotResultMetric != tt.wantResultMetric {
   215  				t.Errorf("Extract(%s) recorded result metric %v, want result metric %v", tt.inputConfig.Path, gotResultMetric, tt.wantResultMetric)
   216  			}
   217  
   218  			gotFileSizeMetric := collector.FileExtractedFileSize(tt.inputConfig.Path)
   219  			if gotFileSizeMetric != scanInput.Info.Size() {
   220  				t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.inputConfig.Path, gotFileSizeMetric, scanInput.Info.Size())
   221  			}
   222  		})
   223  	}
   224  }
   225  
   226  func TestExtractor_Extract_Shrinkwrap_JSON(t *testing.T) {
   227  	tests := []extracttest.TestTableEntry{
   228  		{
   229  			Name: "invalid json",
   230  			InputConfig: extracttest.ScanInputMockConfig{
   231  				Path: "testdata/not-json.txt",
   232  			},
   233  			WantErr: extracttest.ContainsErrStr{Str: "could not extract"},
   234  		},
   235  		{
   236  			Name: "valid package-lock.json only",
   237  			InputConfig: extracttest.ScanInputMockConfig{
   238  				Path: "testdata/package-lock-only/package-lock.json",
   239  			},
   240  			WantPackages: []*extractor.Package{
   241  				{
   242  					Name:      "wrappy",
   243  					Version:   "1.0.2",
   244  					PURLType:  purl.TypeNPM,
   245  					Locations: []string{"testdata/package-lock-only/package-lock.json"},
   246  					SourceCode: &extractor.SourceCodeIdentifier{
   247  						Commit: "",
   248  					},
   249  					Metadata: osv.DepGroupMetadata{
   250  						DepGroupVals: []string{},
   251  					},
   252  				},
   253  				{
   254  					Name:      "supports-color",
   255  					Version:   "5.5.0",
   256  					PURLType:  purl.TypeNPM,
   257  					Locations: []string{"testdata/package-lock-only/package-lock.json"},
   258  					SourceCode: &extractor.SourceCodeIdentifier{
   259  						Commit: "",
   260  					},
   261  					Metadata: osv.DepGroupMetadata{
   262  						DepGroupVals: []string{},
   263  					},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			Name: "valid npm-shrinkwrap.json only",
   269  			InputConfig: extracttest.ScanInputMockConfig{
   270  				Path: "testdata/npm-shrinkwrap-only/npm-shrinkwrap.json",
   271  			},
   272  			WantPackages: []*extractor.Package{
   273  				{
   274  					Name:      "wrappy",
   275  					Version:   "1.0.2",
   276  					PURLType:  purl.TypeNPM,
   277  					Locations: []string{"testdata/npm-shrinkwrap-only/npm-shrinkwrap.json"},
   278  					SourceCode: &extractor.SourceCodeIdentifier{
   279  						Commit: "",
   280  					},
   281  					Metadata: osv.DepGroupMetadata{
   282  						DepGroupVals: []string{},
   283  					},
   284  				},
   285  				{
   286  					Name:      "supports-color",
   287  					Version:   "5.5.0",
   288  					PURLType:  purl.TypeNPM,
   289  					Locations: []string{"testdata/npm-shrinkwrap-only/npm-shrinkwrap.json"},
   290  					SourceCode: &extractor.SourceCodeIdentifier{
   291  						Commit: "",
   292  					},
   293  					Metadata: osv.DepGroupMetadata{
   294  						DepGroupVals: []string{},
   295  					},
   296  				},
   297  			},
   298  		},
   299  		{
   300  			Name: "valid package-lock.json and npm-shrinkwrap.json and extract package-lock.json",
   301  			InputConfig: extracttest.ScanInputMockConfig{
   302  				Path: "testdata/both/package-lock.json",
   303  			},
   304  			WantPackages: nil,
   305  		},
   306  		{
   307  			Name: "valid package-lock.json and npm-shrinkwrap.json and extract npm-shrinkwrap.json",
   308  			InputConfig: extracttest.ScanInputMockConfig{
   309  				Path: "testdata/both/npm-shrinkwrap.json",
   310  			},
   311  			WantPackages: []*extractor.Package{
   312  				{
   313  					Name:      "wrappy",
   314  					Version:   "1.0.2",
   315  					PURLType:  purl.TypeNPM,
   316  					Locations: []string{"testdata/both/npm-shrinkwrap.json"},
   317  					SourceCode: &extractor.SourceCodeIdentifier{
   318  						Commit: "",
   319  					},
   320  					Metadata: osv.DepGroupMetadata{
   321  						DepGroupVals: []string{},
   322  					},
   323  				},
   324  				{
   325  					Name:      "supports-color",
   326  					Version:   "5.5.0",
   327  					PURLType:  purl.TypeNPM,
   328  					Locations: []string{"testdata/both/npm-shrinkwrap.json"},
   329  					SourceCode: &extractor.SourceCodeIdentifier{
   330  						Commit: "",
   331  					},
   332  					Metadata: osv.DepGroupMetadata{
   333  						DepGroupVals: []string{},
   334  					},
   335  				},
   336  			},
   337  		},
   338  	}
   339  
   340  	for _, tt := range tests {
   341  		t.Run(tt.Name, func(t *testing.T) {
   342  			collector := testcollector.New()
   343  			extr := packagelockjson.New(packagelockjson.Config{
   344  				Stats: collector,
   345  			})
   346  
   347  			scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig)
   348  			defer extracttest.CloseTestScanInput(t, scanInput)
   349  
   350  			got, err := extr.Extract(t.Context(), &scanInput)
   351  
   352  			if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" {
   353  				t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   354  				return
   355  			}
   356  
   357  			wantInv := inventory.Inventory{Packages: tt.WantPackages}
   358  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" {
   359  				t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   360  			}
   361  
   362  			gotFileSizeMetric := collector.FileExtractedFileSize(tt.InputConfig.Path)
   363  			if gotFileSizeMetric != scanInput.Info.Size() {
   364  				t.Errorf("Extract(%s) recorded file size %v, want file size %v", tt.InputConfig.Path, gotFileSizeMetric, scanInput.Info.Size())
   365  			}
   366  		})
   367  	}
   368  }