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 }