github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/python/poetrylock/poetrylock_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 poetrylock_test 16 17 import ( 18 "testing" 19 20 "github.com/google/go-cmp/cmp" 21 "github.com/google/go-cmp/cmp/cmpopts" 22 "github.com/google/osv-scalibr/extractor" 23 "github.com/google/osv-scalibr/extractor/filesystem/language/python/poetrylock" 24 "github.com/google/osv-scalibr/extractor/filesystem/osv" 25 "github.com/google/osv-scalibr/extractor/filesystem/simplefileapi" 26 "github.com/google/osv-scalibr/inventory" 27 "github.com/google/osv-scalibr/purl" 28 "github.com/google/osv-scalibr/testing/extracttest" 29 ) 30 31 func TestExtractor_FileRequired(t *testing.T) { 32 tests := []struct { 33 name string 34 inputPath string 35 want bool 36 }{ 37 { 38 name: "", 39 inputPath: "", 40 want: false, 41 }, 42 { 43 name: "", 44 inputPath: "poetry.lock", 45 want: true, 46 }, 47 { 48 name: "", 49 inputPath: "path/to/my/poetry.lock", 50 want: true, 51 }, 52 { 53 name: "", 54 inputPath: "path/to/my/poetry.lock/file", 55 want: false, 56 }, 57 { 58 name: "", 59 inputPath: "path/to/my/poetry.lock.file", 60 want: false, 61 }, 62 { 63 name: "", 64 inputPath: "path.to.my.poetry.lock", 65 want: false, 66 }, 67 } 68 for _, tt := range tests { 69 t.Run(tt.name, func(t *testing.T) { 70 e := poetrylock.Extractor{} 71 got := e.FileRequired(simplefileapi.New(tt.inputPath, nil)) 72 if got != tt.want { 73 t.Errorf("FileRequired(%q, FileInfo) got = %v, want %v", tt.inputPath, got, tt.want) 74 } 75 }) 76 } 77 } 78 79 func TestExtractor_Extract(t *testing.T) { 80 tests := []extracttest.TestTableEntry{ 81 { 82 Name: "invalid toml", 83 InputConfig: extracttest.ScanInputMockConfig{ 84 Path: "testdata/not-toml.txt", 85 }, 86 WantErr: extracttest.ContainsErrStr{Str: "could not extract"}, 87 WantPackages: nil, 88 }, 89 { 90 Name: "no packages", 91 InputConfig: extracttest.ScanInputMockConfig{ 92 Path: "testdata/empty.lock", 93 }, 94 WantPackages: []*extractor.Package{}, 95 }, 96 { 97 Name: "one package", 98 InputConfig: extracttest.ScanInputMockConfig{ 99 Path: "testdata/one-package.lock", 100 }, 101 WantPackages: []*extractor.Package{ 102 { 103 Name: "numpy", 104 Version: "1.23.3", 105 PURLType: purl.TypePyPi, 106 Locations: []string{"testdata/one-package.lock"}, 107 Metadata: osv.DepGroupMetadata{ 108 DepGroupVals: []string{}, 109 }, 110 }, 111 }, 112 }, 113 { 114 Name: "two packages", 115 InputConfig: extracttest.ScanInputMockConfig{ 116 Path: "testdata/two-packages.lock", 117 }, 118 WantPackages: []*extractor.Package{ 119 { 120 Name: "proto-plus", 121 Version: "1.22.0", 122 PURLType: purl.TypePyPi, 123 Locations: []string{"testdata/two-packages.lock"}, 124 Metadata: osv.DepGroupMetadata{ 125 DepGroupVals: []string{}, 126 }, 127 }, 128 { 129 Name: "protobuf", 130 Version: "4.21.5", 131 PURLType: purl.TypePyPi, 132 Locations: []string{"testdata/two-packages.lock"}, 133 Metadata: osv.DepGroupMetadata{ 134 DepGroupVals: []string{}, 135 }, 136 }, 137 }, 138 }, 139 { 140 Name: "package with metadata", 141 InputConfig: extracttest.ScanInputMockConfig{ 142 Path: "testdata/one-package-with-metadata.lock", 143 }, 144 WantPackages: []*extractor.Package{ 145 { 146 Name: "emoji", 147 Version: "2.0.0", 148 PURLType: purl.TypePyPi, 149 Locations: []string{"testdata/one-package-with-metadata.lock"}, 150 Metadata: osv.DepGroupMetadata{ 151 DepGroupVals: []string{}, 152 }, 153 }, 154 }, 155 }, 156 { 157 Name: "package with git source", 158 InputConfig: extracttest.ScanInputMockConfig{ 159 Path: "testdata/source-git.lock", 160 }, 161 WantPackages: []*extractor.Package{ 162 { 163 Name: "ike", 164 Version: "0.2.0", 165 PURLType: purl.TypePyPi, 166 Locations: []string{"testdata/source-git.lock"}, 167 SourceCode: &extractor.SourceCodeIdentifier{ 168 Commit: "cd66602cd29f61a2d2e7fb995fef1e61708c034d", 169 }, 170 Metadata: osv.DepGroupMetadata{ 171 DepGroupVals: []string{}, 172 }, 173 }, 174 }, 175 }, 176 { 177 Name: "package with legacy source", 178 InputConfig: extracttest.ScanInputMockConfig{ 179 Path: "testdata/source-legacy.lock", 180 }, 181 WantPackages: []*extractor.Package{ 182 { 183 Name: "appdirs", 184 Version: "1.4.4", 185 PURLType: purl.TypePyPi, 186 Locations: []string{"testdata/source-legacy.lock"}, 187 Metadata: osv.DepGroupMetadata{ 188 DepGroupVals: []string{}, 189 }, 190 }, 191 }, 192 }, 193 { 194 Name: "optional package", 195 InputConfig: extracttest.ScanInputMockConfig{ 196 Path: "testdata/optional-package.lock", 197 }, 198 WantPackages: []*extractor.Package{ 199 { 200 Name: "numpy", 201 Version: "1.23.3", 202 PURLType: purl.TypePyPi, 203 Locations: []string{"testdata/optional-package.lock"}, 204 Metadata: osv.DepGroupMetadata{ 205 DepGroupVals: []string{"optional"}, 206 }, 207 }, 208 }, 209 }, 210 { 211 Name: "multiple packages with a v2 lockfile", 212 InputConfig: extracttest.ScanInputMockConfig{ 213 Path: "testdata/multiple-packages.v2.lock", 214 }, 215 WantPackages: []*extractor.Package{ 216 { 217 Name: "async-timeout", 218 Version: "5.0.1", 219 PURLType: purl.TypePyPi, 220 Locations: []string{"testdata/multiple-packages.v2.lock"}, 221 Metadata: osv.DepGroupMetadata{ 222 DepGroupVals: []string{"optional"}, 223 }, 224 }, 225 { 226 Name: "factory-boy", 227 Version: "3.3.1", 228 PURLType: purl.TypePyPi, 229 Locations: []string{"testdata/multiple-packages.v2.lock"}, 230 Metadata: osv.DepGroupMetadata{ 231 DepGroupVals: []string{"dev"}, 232 }, 233 }, 234 { 235 Name: "faker", 236 Version: "33.3.0", 237 PURLType: purl.TypePyPi, 238 Locations: []string{"testdata/multiple-packages.v2.lock"}, 239 Metadata: osv.DepGroupMetadata{ 240 DepGroupVals: []string{"dev", "test"}, 241 }, 242 }, 243 { 244 Name: "proto-plus", 245 Version: "1.22.0", 246 PURLType: purl.TypePyPi, 247 Locations: []string{"testdata/multiple-packages.v2.lock"}, 248 Metadata: osv.DepGroupMetadata{ 249 DepGroupVals: []string{}, 250 }, 251 }, 252 { 253 Name: "proto-plus", 254 Version: "1.23.0", 255 PURLType: purl.TypePyPi, 256 Locations: []string{"testdata/multiple-packages.v2.lock"}, 257 Metadata: osv.DepGroupMetadata{ 258 DepGroupVals: []string{}, 259 }, 260 }, 261 { 262 Name: "protobuf", 263 Version: "4.25.5", 264 PURLType: purl.TypePyPi, 265 Locations: []string{"testdata/multiple-packages.v2.lock"}, 266 Metadata: osv.DepGroupMetadata{ 267 DepGroupVals: []string{}, 268 }, 269 }, 270 { 271 Name: "python-dateutil", 272 Version: "2.9.0.post0", 273 PURLType: purl.TypePyPi, 274 Locations: []string{"testdata/multiple-packages.v2.lock"}, 275 Metadata: osv.DepGroupMetadata{ 276 DepGroupVals: []string{"dev", "test"}, 277 }, 278 }, 279 { 280 Name: "six", 281 Version: "1.17.0", 282 PURLType: purl.TypePyPi, 283 Locations: []string{"testdata/multiple-packages.v2.lock"}, 284 Metadata: osv.DepGroupMetadata{ 285 DepGroupVals: []string{}, 286 }, 287 }, 288 { 289 Name: "typing-extensions", 290 Version: "4.12.2", 291 PURLType: purl.TypePyPi, 292 Locations: []string{"testdata/multiple-packages.v2.lock"}, 293 Metadata: osv.DepGroupMetadata{ 294 DepGroupVals: []string{"dev", "test"}, 295 }, 296 }, 297 { 298 Name: "urllib3", 299 Version: "2.3.0", 300 PURLType: purl.TypePyPi, 301 Locations: []string{"testdata/multiple-packages.v2.lock"}, 302 Metadata: osv.DepGroupMetadata{ 303 DepGroupVals: []string{"dev"}, 304 }, 305 }, 306 { 307 Name: "redis", 308 Version: "5.2.1", 309 PURLType: purl.TypePyPi, 310 Locations: []string{"testdata/multiple-packages.v2.lock"}, 311 Metadata: osv.DepGroupMetadata{ 312 DepGroupVals: []string{"optional"}, 313 }, 314 }, 315 }, 316 }, 317 } 318 319 for _, tt := range tests { 320 t.Run(tt.Name, func(t *testing.T) { 321 extr := poetrylock.Extractor{} 322 323 scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig) 324 defer extracttest.CloseTestScanInput(t, scanInput) 325 326 got, err := extr.Extract(t.Context(), &scanInput) 327 328 if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" { 329 t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) 330 return 331 } 332 333 wantInv := inventory.Inventory{Packages: tt.WantPackages} 334 if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" { 335 t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff) 336 } 337 }) 338 } 339 }