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  }