github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/golang/gomod/gomod_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 gomod_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/golang/gomod"
    24  	"github.com/google/osv-scalibr/extractor/filesystem/simplefileapi"
    25  	"github.com/google/osv-scalibr/inventory"
    26  	"github.com/google/osv-scalibr/purl"
    27  	"github.com/google/osv-scalibr/testing/extracttest"
    28  )
    29  
    30  func TestExtractor_FileRequired(t *testing.T) {
    31  	tests := []struct {
    32  		name      string
    33  		inputPath string
    34  		want      bool
    35  	}{
    36  		{
    37  			inputPath: "",
    38  			want:      false,
    39  		},
    40  		{
    41  			inputPath: "go.mod",
    42  			want:      true,
    43  		},
    44  		{
    45  			inputPath: "path/to/my/go.mod",
    46  			want:      true,
    47  		},
    48  		{
    49  			inputPath: "path/to/my/go.mod/file",
    50  			want:      false,
    51  		},
    52  		{
    53  			inputPath: "path/to/my/go.mod.file",
    54  			want:      false,
    55  		},
    56  		{
    57  			inputPath: "path.to.my.go.mod",
    58  			want:      false,
    59  		},
    60  	}
    61  	for _, tt := range tests {
    62  		t.Run(tt.inputPath, func(t *testing.T) {
    63  			e := gomod.Extractor{}
    64  			got := e.FileRequired(simplefileapi.New(tt.inputPath, nil))
    65  			if got != tt.want {
    66  				t.Errorf("FileRequired(%s) got = %v, want %v", tt.inputPath, got, tt.want)
    67  			}
    68  		})
    69  	}
    70  }
    71  
    72  func TestExtractor_Extract(t *testing.T) {
    73  	tests := []*extracttest.TestTableEntry{
    74  		{
    75  			Name: "invalid",
    76  			InputConfig: extracttest.ScanInputMockConfig{
    77  				Path: "testdata/not-go-mod.mod",
    78  			},
    79  			WantErr: extracttest.ContainsErrStr{Str: "could not extract"},
    80  		},
    81  		{
    82  			Name: "no packages",
    83  			InputConfig: extracttest.ScanInputMockConfig{
    84  				Path: "testdata/empty.mod",
    85  			},
    86  			WantPackages: nil,
    87  		},
    88  		{
    89  			Name: "one package",
    90  			InputConfig: extracttest.ScanInputMockConfig{
    91  				Path: "testdata/one-package.mod",
    92  			},
    93  			WantPackages: []*extractor.Package{
    94  				{
    95  					Name:      "github.com/BurntSushi/toml",
    96  					Version:   "1.0.0",
    97  					PURLType:  purl.TypeGolang,
    98  					Locations: []string{"testdata/one-package.mod"},
    99  				},
   100  			},
   101  		},
   102  		{
   103  			Name: "two packages",
   104  			InputConfig: extracttest.ScanInputMockConfig{
   105  				Path: "testdata/two-packages.mod",
   106  			},
   107  			WantPackages: []*extractor.Package{
   108  				{
   109  					Name:      "github.com/BurntSushi/toml",
   110  					Version:   "1.0.0",
   111  					PURLType:  purl.TypeGolang,
   112  					Locations: []string{"testdata/two-packages.mod"},
   113  				},
   114  				{
   115  					Name:      "gopkg.in/yaml.v2",
   116  					Version:   "2.4.0",
   117  					PURLType:  purl.TypeGolang,
   118  					Locations: []string{"testdata/two-packages.mod"},
   119  				},
   120  				{
   121  					Name:      "stdlib",
   122  					Version:   "1.17",
   123  					PURLType:  purl.TypeGolang,
   124  					Locations: []string{"testdata/two-packages.mod"},
   125  				},
   126  			},
   127  		},
   128  		{
   129  			Name: "toolchain",
   130  			InputConfig: extracttest.ScanInputMockConfig{
   131  				Path: "testdata/toolchain.mod",
   132  			},
   133  			WantPackages: []*extractor.Package{
   134  				{
   135  					Name:      "github.com/BurntSushi/toml",
   136  					Version:   "1.0.0",
   137  					PURLType:  purl.TypeGolang,
   138  					Locations: []string{"testdata/toolchain.mod"},
   139  				},
   140  				{
   141  					Name:      "stdlib",
   142  					Version:   "1.23.6",
   143  					PURLType:  purl.TypeGolang,
   144  					Locations: []string{"testdata/toolchain.mod"},
   145  				},
   146  			},
   147  		},
   148  		{
   149  			Name: "toolchain with suffix",
   150  			InputConfig: extracttest.ScanInputMockConfig{
   151  				Path: "testdata/toolchain-with-suffix.mod",
   152  			},
   153  			WantPackages: []*extractor.Package{
   154  				{
   155  					Name:      "github.com/BurntSushi/toml",
   156  					Version:   "1.0.0",
   157  					PURLType:  purl.TypeGolang,
   158  					Locations: []string{"testdata/toolchain-with-suffix.mod"},
   159  				},
   160  				{
   161  					Name:      "stdlib",
   162  					Version:   "1.23.6",
   163  					PURLType:  purl.TypeGolang,
   164  					Locations: []string{"testdata/toolchain-with-suffix.mod"},
   165  				},
   166  			},
   167  		},
   168  		{
   169  			Name: "indirect packages",
   170  			InputConfig: extracttest.ScanInputMockConfig{
   171  				Path: "testdata/indirect-packages.mod",
   172  			},
   173  			WantPackages: []*extractor.Package{
   174  				{
   175  					Name:      "github.com/BurntSushi/toml",
   176  					Version:   "1.0.0",
   177  					PURLType:  purl.TypeGolang,
   178  					Locations: []string{"testdata/indirect-packages.mod"},
   179  				},
   180  				{
   181  					Name:      "gopkg.in/yaml.v2",
   182  					Version:   "2.4.0",
   183  					PURLType:  purl.TypeGolang,
   184  					Locations: []string{"testdata/indirect-packages.mod"},
   185  				},
   186  				{
   187  					Name:      "github.com/mattn/go-colorable",
   188  					Version:   "0.1.9",
   189  					PURLType:  purl.TypeGolang,
   190  					Locations: []string{"testdata/indirect-packages.mod"},
   191  				},
   192  				{
   193  					Name:      "github.com/mattn/go-isatty",
   194  					Version:   "0.0.14",
   195  					PURLType:  purl.TypeGolang,
   196  					Locations: []string{"testdata/indirect-packages.mod"},
   197  				},
   198  				{
   199  					Name:      "golang.org/x/sys",
   200  					Version:   "0.0.0-20210630005230-0f9fa26af87c",
   201  					PURLType:  purl.TypeGolang,
   202  					Locations: []string{"testdata/indirect-packages.mod"},
   203  				},
   204  				{
   205  					Name:      "stdlib",
   206  					Version:   "1.17",
   207  					PURLType:  purl.TypeGolang,
   208  					Locations: []string{"testdata/indirect-packages.mod"},
   209  				},
   210  			},
   211  		},
   212  		{
   213  			Name: "replacements_ one",
   214  			InputConfig: extracttest.ScanInputMockConfig{
   215  				Path: "testdata/replace-one.mod",
   216  			},
   217  			WantPackages: []*extractor.Package{
   218  				{
   219  					Name:      "example.com/fork/net",
   220  					Version:   "1.4.5",
   221  					PURLType:  purl.TypeGolang,
   222  					Locations: []string{"testdata/replace-one.mod"},
   223  				},
   224  			},
   225  		},
   226  		{
   227  			Name: "replacements_ mixed",
   228  			InputConfig: extracttest.ScanInputMockConfig{
   229  				Path: "testdata/replace-mixed.mod",
   230  			},
   231  			WantPackages: []*extractor.Package{
   232  				{
   233  					Name:      "example.com/fork/net",
   234  					Version:   "1.4.5",
   235  					PURLType:  purl.TypeGolang,
   236  					Locations: []string{"testdata/replace-mixed.mod"},
   237  				},
   238  				{
   239  					Name:      "golang.org/x/net",
   240  					Version:   "0.5.6",
   241  					PURLType:  purl.TypeGolang,
   242  					Locations: []string{"testdata/replace-mixed.mod"},
   243  				},
   244  			},
   245  		},
   246  		{
   247  			Name: "replacements_ local",
   248  			InputConfig: extracttest.ScanInputMockConfig{
   249  				Path: "testdata/replace-local.mod",
   250  			},
   251  			WantPackages: []*extractor.Package{
   252  				{
   253  					Name:      "./fork/net",
   254  					Version:   "",
   255  					PURLType:  purl.TypeGolang,
   256  					Locations: []string{"testdata/replace-local.mod"},
   257  				},
   258  				{
   259  					Name:      "github.com/BurntSushi/toml",
   260  					Version:   "1.0.0",
   261  					PURLType:  purl.TypeGolang,
   262  					Locations: []string{"testdata/replace-local.mod"},
   263  				},
   264  			},
   265  		},
   266  		{
   267  			Name: "replacements_ different",
   268  			InputConfig: extracttest.ScanInputMockConfig{
   269  				Path: "testdata/replace-different.mod",
   270  			},
   271  			WantPackages: []*extractor.Package{
   272  				{
   273  					Name:      "example.com/fork/foe",
   274  					Version:   "1.4.5",
   275  					PURLType:  purl.TypeGolang,
   276  					Locations: []string{"testdata/replace-different.mod"},
   277  				},
   278  				{
   279  					Name:      "example.com/fork/foe",
   280  					Version:   "1.4.2",
   281  					PURLType:  purl.TypeGolang,
   282  					Locations: []string{"testdata/replace-different.mod"},
   283  				},
   284  			},
   285  		},
   286  		{
   287  			Name: "replacements_ not required",
   288  			InputConfig: extracttest.ScanInputMockConfig{
   289  				Path: "testdata/replace-not-required.mod",
   290  			},
   291  			WantPackages: []*extractor.Package{
   292  				{
   293  					Name:      "golang.org/x/net",
   294  					Version:   "0.5.6",
   295  					PURLType:  purl.TypeGolang,
   296  					Locations: []string{"testdata/replace-not-required.mod"},
   297  				},
   298  				{
   299  					Name:      "github.com/BurntSushi/toml",
   300  					Version:   "1.0.0",
   301  					PURLType:  purl.TypeGolang,
   302  					Locations: []string{"testdata/replace-not-required.mod"},
   303  				},
   304  			},
   305  		},
   306  		{
   307  			Name: "replacements_ no version",
   308  			InputConfig: extracttest.ScanInputMockConfig{
   309  				Path: "testdata/replace-no-version.mod",
   310  			},
   311  			WantPackages: []*extractor.Package{
   312  				{
   313  					Name:      "example.com/fork/net",
   314  					Version:   "1.4.5",
   315  					PURLType:  purl.TypeGolang,
   316  					Locations: []string{"testdata/replace-no-version.mod"},
   317  				},
   318  			},
   319  		},
   320  		{
   321  			Name: "test extractor for go > 1.16",
   322  			InputConfig: extracttest.ScanInputMockConfig{
   323  				Path: "testdata/indirect-1.23.mod",
   324  			},
   325  			WantPackages: []*extractor.Package{
   326  				{
   327  					Name:      "github.com/sirupsen/logrus",
   328  					Version:   "1.9.3",
   329  					PURLType:  purl.TypeGolang,
   330  					Locations: []string{"testdata/indirect-1.23.mod"},
   331  				},
   332  				{
   333  					Name:      "golang.org/x/sys",
   334  					Version:   "0.0.0-20220715151400-c0bba94af5f8",
   335  					PURLType:  purl.TypeGolang,
   336  					Locations: []string{"testdata/indirect-1.23.mod"},
   337  				},
   338  				{
   339  					Name:      "stdlib",
   340  					Version:   "1.23",
   341  					PURLType:  purl.TypeGolang,
   342  					Locations: []string{"testdata/indirect-1.23.mod"},
   343  				},
   344  			},
   345  		},
   346  		{
   347  			Name: "test extractor for go <=1.16",
   348  			InputConfig: extracttest.ScanInputMockConfig{
   349  				Path: "testdata/indirect-1.16.mod",
   350  			},
   351  			WantPackages: []*extractor.Package{
   352  				{
   353  					Name:      "github.com/davecgh/go-spew",
   354  					Version:   "1.1.1",
   355  					PURLType:  purl.TypeGolang,
   356  					Locations: []string{"testdata/indirect-1.16.sum"},
   357  				},
   358  				{
   359  					Name:      "github.com/pmezard/go-difflib",
   360  					Version:   "1.0.0",
   361  					PURLType:  purl.TypeGolang,
   362  					Locations: []string{"testdata/indirect-1.16.sum"},
   363  				},
   364  				{
   365  					Name:     "github.com/sirupsen/logrus",
   366  					Version:  "1.9.3",
   367  					PURLType: purl.TypeGolang,
   368  					Locations: []string{
   369  						"testdata/indirect-1.16.mod", "testdata/indirect-1.16.sum",
   370  					},
   371  				},
   372  				{
   373  					Name:      "github.com/stretchr/testify",
   374  					Version:   "1.7.0",
   375  					PURLType:  purl.TypeGolang,
   376  					Locations: []string{"testdata/indirect-1.16.sum"},
   377  				},
   378  				{
   379  					Name:      "golang.org/x/sys",
   380  					Version:   "0.0.0-20220715151400-c0bba94af5f8",
   381  					PURLType:  purl.TypeGolang,
   382  					Locations: []string{"testdata/indirect-1.16.sum"},
   383  				},
   384  				{
   385  					Name:      "gopkg.in/yaml.v3",
   386  					Version:   "3.0.0-20200313102051-9f266ea9e77c",
   387  					PURLType:  purl.TypeGolang,
   388  					Locations: []string{"testdata/indirect-1.16.sum"},
   389  				},
   390  				{
   391  					Name:      "stdlib",
   392  					Version:   "1.16",
   393  					PURLType:  purl.TypeGolang,
   394  					Locations: []string{"testdata/indirect-1.16.mod"},
   395  				},
   396  			},
   397  		},
   398  	}
   399  
   400  	for _, tt := range tests {
   401  		t.Run(tt.Name, func(t *testing.T) {
   402  			extr := gomod.New()
   403  
   404  			scanInput := extracttest.GenerateScanInputMock(t, tt.InputConfig)
   405  			defer extracttest.CloseTestScanInput(t, scanInput)
   406  
   407  			got, err := extr.Extract(t.Context(), &scanInput)
   408  
   409  			if diff := cmp.Diff(tt.WantErr, err, cmpopts.EquateErrors()); diff != "" {
   410  				t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   411  				return
   412  			}
   413  
   414  			wantInv := inventory.Inventory{Packages: tt.WantPackages}
   415  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" {
   416  				t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), tt.InputConfig.Path, diff)
   417  			}
   418  		})
   419  	}
   420  }
   421  
   422  func TestExtractor_Extract_WithExcludeIndirectConfig(t *testing.T) {
   423  	tests := []struct {
   424  		name            string
   425  		config          gomod.Config
   426  		inputPath       string
   427  		wantPackages    []*extractor.Package
   428  		wantNotPackages []*extractor.Package
   429  		wantErr         error
   430  	}{
   431  		{
   432  			name:      "exclude indirect",
   433  			config:    gomod.Config{ExcludeIndirect: true},
   434  			inputPath: "testdata/indirect-packages.mod",
   435  			wantPackages: []*extractor.Package{
   436  				{
   437  					Name:      "github.com/BurntSushi/toml",
   438  					Version:   "1.0.0",
   439  					PURLType:  purl.TypeGolang,
   440  					Locations: []string{"testdata/indirect-packages.mod"},
   441  				},
   442  				{
   443  					Name:      "gopkg.in/yaml.v2",
   444  					Version:   "2.4.0",
   445  					PURLType:  purl.TypeGolang,
   446  					Locations: []string{"testdata/indirect-packages.mod"},
   447  				},
   448  				{
   449  					Name:      "stdlib",
   450  					Version:   "1.17",
   451  					PURLType:  purl.TypeGolang,
   452  					Locations: []string{"testdata/indirect-packages.mod"},
   453  				},
   454  			},
   455  			wantNotPackages: []*extractor.Package{
   456  				{
   457  					Name:      "github.com/mattn/go-colorable",
   458  					Version:   "0.1.9",
   459  					PURLType:  purl.TypeGolang,
   460  					Locations: []string{"testdata/indirect-packages.mod"},
   461  				},
   462  				{
   463  					Name:      "github.com/mattn/go-isatty",
   464  					Version:   "0.0.14",
   465  					PURLType:  purl.TypeGolang,
   466  					Locations: []string{"testdata/indirect-packages.mod"},
   467  				},
   468  				{
   469  					Name:      "golang.org/x/sys",
   470  					Version:   "0.0.0-20210630005230-0f9fa26af87c",
   471  					PURLType:  purl.TypeGolang,
   472  					Locations: []string{"testdata/indirect-packages.mod"},
   473  				},
   474  			},
   475  		},
   476  	}
   477  
   478  	for _, tt := range tests {
   479  		t.Run(tt.name, func(t *testing.T) {
   480  			extr := gomod.NewWithConfig(tt.config)
   481  
   482  			scanInput := extracttest.GenerateScanInputMock(t, extracttest.ScanInputMockConfig{
   483  				Path: tt.inputPath,
   484  			})
   485  			defer extracttest.CloseTestScanInput(t, scanInput)
   486  
   487  			got, err := extr.Extract(t.Context(), &scanInput)
   488  
   489  			if tt.wantErr != nil {
   490  				if err == nil {
   491  					t.Errorf("want error %v, got nil", tt.wantErr)
   492  				}
   493  
   494  				if diff := cmp.Diff(tt.wantErr, err, cmpopts.EquateErrors()); diff != "" {
   495  					t.Errorf("%s.Extract(%q) error diff (-want +got):\n%s", extr.Name(), scanInput.Path, diff)
   496  				}
   497  
   498  				return
   499  			}
   500  
   501  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   502  			if diff := cmp.Diff(wantInv, got, cmpopts.SortSlices(extracttest.PackageCmpLess)); diff != "" {
   503  				t.Errorf("%s.Extract(%q) diff (-want +got):\n%s", extr.Name(), scanInput.Path, diff)
   504  			}
   505  
   506  			// Verify that packages that should not be included are actually excluded
   507  			for _, shouldNotHave := range tt.wantNotPackages {
   508  				for _, gotPkg := range got.Packages {
   509  					if gotPkg.Name == shouldNotHave.Name && gotPkg.Version == shouldNotHave.Version {
   510  						t.Errorf("Package %s@%s should not be included but was found in results", shouldNotHave.Name, shouldNotHave.Version)
   511  					}
   512  				}
   513  			}
   514  		})
   515  	}
   516  }