github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/pkg/package_test.go (about)

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