github.com/anchore/syft@v1.38.2/syft/pkg/package_test.go (about)

     1  package pkg
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  
     7  	"github.com/google/go-cmp/cmp"
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	"github.com/anchore/syft/syft/cpe"
    12  	"github.com/anchore/syft/syft/file"
    13  )
    14  
    15  func TestIDUniqueness(t *testing.T) {
    16  	ctx := context.TODO()
    17  	originalLocation := file.NewVirtualLocationFromCoordinates(
    18  		file.Coordinates{
    19  			RealPath:     "39.0742° N, 21.8243° E",
    20  			FileSystemID: "Earth",
    21  		},
    22  		"/Ancient-Greece",
    23  	)
    24  
    25  	originalPkg := Package{
    26  		Name:    "pi",
    27  		Version: "3.14",
    28  		FoundBy: "Archimedes",
    29  		Locations: file.NewLocationSet(
    30  			originalLocation,
    31  		),
    32  		Licenses: NewLicenseSet(
    33  			NewLicenseWithContext(ctx, "MIT"),
    34  			NewLicenseWithContext(ctx, "cc0-1.0"),
    35  		),
    36  		Language: "math",
    37  		Type:     PythonPkg,
    38  		CPEs: []cpe.CPE{
    39  			cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
    40  		},
    41  		PURL: "pkg:pypi/pi@3.14",
    42  		Metadata: PythonPackage{
    43  			Name:                 "pi",
    44  			Version:              "3.14",
    45  			Author:               "Archimedes",
    46  			AuthorEmail:          "Archimedes@circles.io",
    47  			Platform:             "universe",
    48  			SitePackagesRootPath: "Pi",
    49  		},
    50  	}
    51  
    52  	// this is a set of differential tests, ensuring that select mutations are reflected in the fingerprint (or not)
    53  	tests := []struct {
    54  		name                 string
    55  		transform            func(pkg Package) Package
    56  		expectedIDComparison assert.ComparisonAssertionFunc
    57  	}{
    58  		{
    59  			name: "go case (no transform)",
    60  			transform: func(pkg Package) Package {
    61  				// do nothing!
    62  				return pkg
    63  			},
    64  			expectedIDComparison: assert.Equal,
    65  		},
    66  		{
    67  			name: "same metadata is ignored",
    68  			transform: func(pkg Package) Package {
    69  				// note: this is the same as the original values, just a new allocation
    70  				pkg.Metadata = PythonPackage{
    71  					Name:                 "pi",
    72  					Version:              "3.14",
    73  					Author:               "Archimedes",
    74  					AuthorEmail:          "Archimedes@circles.io",
    75  					Platform:             "universe",
    76  					SitePackagesRootPath: "Pi",
    77  				}
    78  				return pkg
    79  			},
    80  			expectedIDComparison: assert.Equal,
    81  		},
    82  		{
    83  			name: "licenses order is ignored",
    84  			transform: func(pkg Package) Package {
    85  				// note: same as the original package, only a different order
    86  				pkg.Licenses = NewLicenseSet(
    87  					NewLicenseWithContext(ctx, "cc0-1.0"),
    88  					NewLicenseWithContext(ctx, "MIT"),
    89  				)
    90  				return pkg
    91  			},
    92  			expectedIDComparison: assert.Equal,
    93  		},
    94  		{
    95  			name: "name is reflected",
    96  			transform: func(pkg Package) Package {
    97  				pkg.Name = "new!"
    98  				return pkg
    99  			},
   100  			expectedIDComparison: assert.NotEqual,
   101  		},
   102  		{
   103  			name: "location is reflected",
   104  			transform: func(pkg Package) Package {
   105  				locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
   106  				locations.Add(file.NewLocation("/somewhere/new"))
   107  				pkg.Locations = locations
   108  				return pkg
   109  			},
   110  			expectedIDComparison: assert.NotEqual,
   111  		},
   112  		{
   113  			name: "licenses is reflected",
   114  			transform: func(pkg Package) Package {
   115  				pkg.Licenses = NewLicenseSet(NewLicenseWithContext(ctx, "new!"))
   116  				return pkg
   117  			},
   118  			expectedIDComparison: assert.NotEqual,
   119  		},
   120  		{
   121  			name: "same path for different filesystem is NOT reflected",
   122  			transform: func(pkg Package) Package {
   123  				newLocation := originalLocation
   124  				newLocation.FileSystemID = "Mars"
   125  
   126  				pkg.Locations = file.NewLocationSet(newLocation)
   127  				return pkg
   128  			},
   129  			expectedIDComparison: assert.Equal,
   130  		},
   131  		{
   132  			name: "multiple equivalent paths for different filesystem is NOT reflected",
   133  			transform: func(pkg Package) Package {
   134  				newLocation := originalLocation
   135  				newLocation.FileSystemID = "Mars"
   136  
   137  				locations := file.NewLocationSet(pkg.Locations.ToSlice()...)
   138  				locations.Add(newLocation, originalLocation)
   139  
   140  				pkg.Locations = locations
   141  				return pkg
   142  			},
   143  			expectedIDComparison: assert.Equal,
   144  		},
   145  		{
   146  			name: "version is reflected",
   147  			transform: func(pkg Package) Package {
   148  				pkg.Version = "new!"
   149  				return pkg
   150  			},
   151  			expectedIDComparison: assert.NotEqual,
   152  		},
   153  		{
   154  			name: "type is reflected",
   155  			transform: func(pkg Package) Package {
   156  				pkg.Type = RustPkg
   157  				return pkg
   158  			},
   159  			expectedIDComparison: assert.NotEqual,
   160  		},
   161  		{
   162  			name: "CPEs is ignored",
   163  			transform: func(pkg Package) Package {
   164  				pkg.CPEs = []cpe.CPE{}
   165  				return pkg
   166  			},
   167  			expectedIDComparison: assert.Equal,
   168  		},
   169  		{
   170  			name: "pURL is ignored",
   171  			transform: func(pkg Package) Package {
   172  				pkg.PURL = "new!"
   173  				return pkg
   174  			},
   175  			expectedIDComparison: assert.Equal,
   176  		},
   177  		{
   178  			name: "language is NOT reflected",
   179  			transform: func(pkg Package) Package {
   180  				pkg.Language = Rust
   181  				return pkg
   182  			},
   183  			expectedIDComparison: assert.Equal,
   184  		},
   185  		{
   186  			name: "metadata mutation is reflected",
   187  			transform: func(pkg Package) Package {
   188  				metadata := pkg.Metadata.(PythonPackage)
   189  				metadata.Name = "new!"
   190  				pkg.Metadata = metadata
   191  				return pkg
   192  			},
   193  			expectedIDComparison: assert.NotEqual,
   194  		},
   195  		{
   196  			name: "new metadata is reflected",
   197  			transform: func(pkg Package) Package {
   198  				pkg.Metadata = PythonPackage{
   199  					Name: "new!",
   200  				}
   201  				return pkg
   202  			},
   203  			expectedIDComparison: assert.NotEqual,
   204  		},
   205  		{
   206  			name: "nil metadata is reflected",
   207  			transform: func(pkg Package) Package {
   208  				pkg.Metadata = nil
   209  				return pkg
   210  			},
   211  			expectedIDComparison: assert.NotEqual,
   212  		},
   213  	}
   214  
   215  	for _, test := range tests {
   216  		t.Run(test.name, func(t *testing.T) {
   217  			originalPkg.SetID()
   218  			transformedPkg := test.transform(originalPkg)
   219  			transformedPkg.SetID()
   220  
   221  			originalFingerprint := originalPkg.ID()
   222  			assert.NotEmpty(t, originalFingerprint)
   223  			transformedFingerprint := transformedPkg.ID()
   224  			assert.NotEmpty(t, transformedFingerprint)
   225  
   226  			test.expectedIDComparison(t, originalFingerprint, transformedFingerprint)
   227  		})
   228  	}
   229  }
   230  
   231  func TestPackage_Merge(t *testing.T) {
   232  	originalLocation := file.NewVirtualLocationFromCoordinates(
   233  		file.Coordinates{
   234  			RealPath:     "39.0742° N, 21.8243° E",
   235  			FileSystemID: "Earth",
   236  		},
   237  		"/Ancient-Greece",
   238  	)
   239  
   240  	similarLocation := originalLocation
   241  	similarLocation.FileSystemID = "Mars"
   242  
   243  	tests := []struct {
   244  		name     string
   245  		subject  Package
   246  		other    Package
   247  		expected *Package
   248  	}{
   249  		{
   250  			name: "merge two packages (different cpes + locations)",
   251  			subject: Package{
   252  				Name:    "pi",
   253  				Version: "3.14",
   254  				FoundBy: "Archimedes",
   255  				Locations: file.NewLocationSet(
   256  					originalLocation,
   257  				),
   258  				Language: "math",
   259  				Type:     PythonPkg,
   260  				CPEs: []cpe.CPE{
   261  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   262  				},
   263  				PURL: "pkg:pypi/pi@3.14",
   264  				Metadata: PythonPackage{
   265  					Name:                 "pi",
   266  					Version:              "3.14",
   267  					Author:               "Archimedes",
   268  					AuthorEmail:          "Archimedes@circles.io",
   269  					Platform:             "universe",
   270  					SitePackagesRootPath: "Pi",
   271  				},
   272  			},
   273  			other: Package{
   274  				Name:    "pi",
   275  				Version: "3.14",
   276  				FoundBy: "Archimedes",
   277  				Locations: file.NewLocationSet(
   278  					similarLocation, // NOTE: difference; we have a different layer but the same path
   279  				),
   280  				Language: "math",
   281  				Type:     PythonPkg,
   282  				CPEs: []cpe.CPE{
   283  					cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: difference
   284  				},
   285  				PURL: "pkg:pypi/pi@3.14",
   286  				Metadata: PythonPackage{
   287  					Name:                 "pi",
   288  					Version:              "3.14",
   289  					Author:               "Archimedes",
   290  					AuthorEmail:          "Archimedes@circles.io",
   291  					Platform:             "universe",
   292  					SitePackagesRootPath: "Pi",
   293  				},
   294  			},
   295  			expected: &Package{
   296  				Name:    "pi",
   297  				Version: "3.14",
   298  				FoundBy: "Archimedes",
   299  				Locations: file.NewLocationSet(
   300  					originalLocation,
   301  					similarLocation, // NOTE: merge!
   302  				),
   303  				Language: "math",
   304  				Type:     PythonPkg,
   305  				CPEs: []cpe.CPE{
   306  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   307  					cpe.Must(`cpe:2.3:a:DIFFERENT:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource), // NOTE: merge!
   308  				},
   309  				PURL: "pkg:pypi/pi@3.14",
   310  				Metadata: PythonPackage{
   311  					Name:                 "pi",
   312  					Version:              "3.14",
   313  					Author:               "Archimedes",
   314  					AuthorEmail:          "Archimedes@circles.io",
   315  					Platform:             "universe",
   316  					SitePackagesRootPath: "Pi",
   317  				},
   318  			},
   319  		},
   320  		{
   321  			name: "error when there are different IDs",
   322  			subject: Package{
   323  				Name:    "pi",
   324  				Version: "3.14",
   325  				FoundBy: "Archimedes",
   326  				Locations: file.NewLocationSet(
   327  					originalLocation,
   328  				),
   329  				Language: "math",
   330  				Type:     PythonPkg,
   331  				CPEs: []cpe.CPE{
   332  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   333  				},
   334  				PURL: "pkg:pypi/pi@3.14",
   335  				Metadata: PythonPackage{
   336  					Name:                 "pi",
   337  					Version:              "3.14",
   338  					Author:               "Archimedes",
   339  					AuthorEmail:          "Archimedes@circles.io",
   340  					Platform:             "universe",
   341  					SitePackagesRootPath: "Pi",
   342  				},
   343  			},
   344  			other: Package{
   345  				Name:    "pi-DIFFERENT", // difference
   346  				Version: "3.14",
   347  				FoundBy: "Archimedes",
   348  				Locations: file.NewLocationSet(
   349  					originalLocation,
   350  				),
   351  				Language: "math",
   352  				Type:     PythonPkg,
   353  				CPEs: []cpe.CPE{
   354  					cpe.Must(`cpe:2.3:a:Archimedes:pi:3.14:*:*:*:*:math:*:*`, cpe.NVDDictionaryLookupSource),
   355  				},
   356  				PURL: "pkg:pypi/pi@3.14",
   357  				Metadata: PythonPackage{
   358  					Name:                 "pi",
   359  					Version:              "3.14",
   360  					Author:               "Archimedes",
   361  					AuthorEmail:          "Archimedes@circles.io",
   362  					Platform:             "universe",
   363  					SitePackagesRootPath: "Pi",
   364  				},
   365  			},
   366  		},
   367  	}
   368  	for _, tt := range tests {
   369  		t.Run(tt.name, func(t *testing.T) {
   370  			tt.subject.SetID()
   371  			tt.other.SetID()
   372  
   373  			err := tt.subject.merge(tt.other)
   374  			if tt.expected == nil {
   375  				require.Error(t, err)
   376  				return
   377  			}
   378  			require.NoError(t, err)
   379  
   380  			tt.expected.SetID()
   381  			require.Equal(t, tt.expected.id, tt.subject.id)
   382  
   383  			if diff := cmp.Diff(*tt.expected, tt.subject,
   384  				cmp.AllowUnexported(Package{}),
   385  				cmp.Comparer(
   386  					func(x, y file.LocationSet) bool {
   387  						xs := x.ToSlice()
   388  						ys := y.ToSlice()
   389  
   390  						if len(xs) != len(ys) {
   391  							return false
   392  						}
   393  						for i, xe := range xs {
   394  							ye := ys[i]
   395  							if !locationComparer(xe, ye) {
   396  								return false
   397  							}
   398  						}
   399  
   400  						return true
   401  					},
   402  				),
   403  				cmp.Comparer(
   404  					func(x, y LicenseSet) bool {
   405  						xs := x.ToSlice()
   406  						ys := y.ToSlice()
   407  
   408  						if len(xs) != len(ys) {
   409  							return false
   410  						}
   411  						for i, xe := range xs {
   412  							ye := ys[i]
   413  							if !licenseComparer(xe, ye) {
   414  								return false
   415  							}
   416  						}
   417  
   418  						return true
   419  					},
   420  				),
   421  				cmp.Comparer(locationComparer),
   422  			); diff != "" {
   423  				t.Errorf("unexpected result from parsing (-expected +actual)\n%s", diff)
   424  			}
   425  		})
   426  	}
   427  }
   428  
   429  func licenseComparer(x, y License) bool {
   430  	return cmp.Equal(x, y, cmp.Comparer(locationComparer))
   431  }
   432  
   433  func locationComparer(x, y file.Location) bool {
   434  	return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.AccessPath, y.AccessPath)
   435  }
   436  
   437  func TestIsValid(t *testing.T) {
   438  	cases := []struct {
   439  		name  string
   440  		given *Package
   441  		want  bool
   442  	}{
   443  		{
   444  			name:  "nil",
   445  			given: nil,
   446  			want:  false,
   447  		},
   448  		{
   449  			name:  "has-name",
   450  			given: &Package{Name: "paul"},
   451  			want:  true,
   452  		},
   453  		{
   454  			name:  "has-no-name",
   455  			given: &Package{},
   456  			want:  false,
   457  		},
   458  	}
   459  
   460  	for _, c := range cases {
   461  		require.Equal(t, c.want, IsValid(c.given), "when package: %s", c.name)
   462  	}
   463  }