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 }