github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/common/spdxhelpers/to_format_model_test.go (about)

     1  package spdxhelpers
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/google/go-cmp/cmp/cmpopts"
    10  	"github.com/spdx/tools-golang/spdx"
    11  	"github.com/spdx/tools-golang/spdx/v2/v2_3"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/anchore/syft/syft/artifact"
    16  	"github.com/anchore/syft/syft/file"
    17  	"github.com/anchore/syft/syft/pkg"
    18  	"github.com/anchore/syft/syft/sbom"
    19  	"github.com/anchore/syft/syft/source"
    20  	"github.com/lineaje-labs/syft/syft/internal/sourcemetadata"
    21  )
    22  
    23  func Test_toFormatModel(t *testing.T) {
    24  	tracker := sourcemetadata.NewCompletionTester(t)
    25  
    26  	tests := []struct {
    27  		name     string
    28  		in       sbom.SBOM
    29  		expected *spdx.Document
    30  	}{
    31  		{
    32  			name: "container",
    33  			in: sbom.SBOM{
    34  				Source: source.Description{
    35  					Name:    "alpine",
    36  					Version: "sha256:d34db33f",
    37  					Metadata: source.StereoscopeImageSourceMetadata{
    38  						UserInput:      "alpine:latest",
    39  						ManifestDigest: "sha256:d34db33f",
    40  					},
    41  				},
    42  				Artifacts: sbom.Artifacts{
    43  					Packages: pkg.NewCollection(pkg.Package{
    44  						Name:    "pkg-1",
    45  						Version: "version-1",
    46  					}),
    47  				},
    48  			},
    49  			expected: &spdx.Document{
    50  				SPDXIdentifier: "DOCUMENT",
    51  				SPDXVersion:    spdx.Version,
    52  				DataLicense:    spdx.DataLicense,
    53  				DocumentName:   "alpine",
    54  				Packages: []*spdx.Package{
    55  					{
    56  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
    57  						PackageName:           "pkg-1",
    58  						PackageVersion:        "version-1",
    59  						PackageSupplier: &spdx.Supplier{
    60  							Supplier: "NOASSERTION",
    61  						},
    62  					},
    63  					{
    64  						PackageSPDXIdentifier: "DocumentRoot-Image-alpine",
    65  						PackageName:           "alpine",
    66  						PackageVersion:        "sha256:d34db33f",
    67  						PrimaryPackagePurpose: "CONTAINER",
    68  						PackageChecksums:      []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
    69  						PackageExternalReferences: []*v2_3.PackageExternalReference{
    70  							{
    71  								Category: "PACKAGE-MANAGER",
    72  								RefType:  "purl",
    73  								Locator:  "pkg:oci/alpine@sha256:d34db33f?arch=&tag=latest",
    74  							},
    75  						},
    76  						PackageSupplier: &spdx.Supplier{
    77  							Supplier: "NOASSERTION",
    78  						},
    79  					},
    80  				},
    81  				Relationships: []*spdx.Relationship{
    82  					{
    83  						RefA: spdx.DocElementID{
    84  							ElementRefID: "DocumentRoot-Image-alpine",
    85  						},
    86  						RefB: spdx.DocElementID{
    87  							ElementRefID: "Package-pkg-1-pkg-1",
    88  						},
    89  						Relationship: spdx.RelationshipContains,
    90  					},
    91  					{
    92  						RefA: spdx.DocElementID{
    93  							ElementRefID: "DOCUMENT",
    94  						},
    95  						RefB: spdx.DocElementID{
    96  							ElementRefID: "DocumentRoot-Image-alpine",
    97  						},
    98  						Relationship: spdx.RelationshipDescribes,
    99  					},
   100  				},
   101  			},
   102  		},
   103  		{
   104  			name: "directory",
   105  			in: sbom.SBOM{
   106  				Source: source.Description{
   107  					Name: "some/directory",
   108  					Metadata: source.DirectorySourceMetadata{
   109  						Path: "some/directory",
   110  					},
   111  				},
   112  				Artifacts: sbom.Artifacts{
   113  					Packages: pkg.NewCollection(pkg.Package{
   114  						Name:    "pkg-1",
   115  						Version: "version-1",
   116  					}),
   117  				},
   118  			},
   119  			expected: &spdx.Document{
   120  				SPDXIdentifier: "DOCUMENT",
   121  				SPDXVersion:    spdx.Version,
   122  				DataLicense:    spdx.DataLicense,
   123  				DocumentName:   "some/directory",
   124  
   125  				Packages: []*spdx.Package{
   126  					{
   127  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
   128  						PackageName:           "pkg-1",
   129  						PackageVersion:        "version-1",
   130  						PackageSupplier: &spdx.Supplier{
   131  							Supplier: "NOASSERTION",
   132  						},
   133  					},
   134  					{
   135  						PackageSPDXIdentifier: "DocumentRoot-Directory-some-directory",
   136  						PackageName:           "some/directory",
   137  						PackageVersion:        "",
   138  						PrimaryPackagePurpose: "FILE",
   139  						PackageSupplier: &spdx.Supplier{
   140  							Supplier: "NOASSERTION",
   141  						},
   142  					},
   143  				},
   144  				Relationships: []*spdx.Relationship{
   145  					{
   146  						RefA: spdx.DocElementID{
   147  							ElementRefID: "DocumentRoot-Directory-some-directory",
   148  						},
   149  						RefB: spdx.DocElementID{
   150  							ElementRefID: "Package-pkg-1-pkg-1",
   151  						},
   152  						Relationship: spdx.RelationshipContains,
   153  					},
   154  					{
   155  						RefA: spdx.DocElementID{
   156  							ElementRefID: "DOCUMENT",
   157  						},
   158  						RefB: spdx.DocElementID{
   159  							ElementRefID: "DocumentRoot-Directory-some-directory",
   160  						},
   161  						Relationship: spdx.RelationshipDescribes,
   162  					},
   163  				},
   164  			},
   165  		},
   166  		{
   167  			name: "file",
   168  			in: sbom.SBOM{
   169  				Source: source.Description{
   170  					Name:    "path/to/some.file",
   171  					Version: "sha256:d34db33f",
   172  					Metadata: source.FileSourceMetadata{
   173  						Path: "path/to/some.file",
   174  						Digests: []file.Digest{
   175  							{
   176  								Algorithm: "sha256",
   177  								Value:     "d34db33f",
   178  							},
   179  						},
   180  					},
   181  				},
   182  				Artifacts: sbom.Artifacts{
   183  					Packages: pkg.NewCollection(pkg.Package{
   184  						Name:    "pkg-1",
   185  						Version: "version-1",
   186  					}),
   187  				},
   188  			},
   189  			expected: &spdx.Document{
   190  				SPDXIdentifier: "DOCUMENT",
   191  				SPDXVersion:    spdx.Version,
   192  				DataLicense:    spdx.DataLicense,
   193  				DocumentName:   "path/to/some.file",
   194  				Packages: []*spdx.Package{
   195  					{
   196  						PackageSPDXIdentifier: "Package-pkg-1-pkg-1",
   197  						PackageName:           "pkg-1",
   198  						PackageVersion:        "version-1",
   199  						PackageSupplier: &spdx.Supplier{
   200  							Supplier: "NOASSERTION",
   201  						},
   202  					},
   203  					{
   204  						PackageSPDXIdentifier: "DocumentRoot-File-path-to-some.file",
   205  						PackageName:           "path/to/some.file",
   206  						PackageVersion:        "sha256:d34db33f",
   207  						PrimaryPackagePurpose: "FILE",
   208  						PackageChecksums:      []spdx.Checksum{{Algorithm: "SHA256", Value: "d34db33f"}},
   209  						PackageSupplier: &spdx.Supplier{
   210  							Supplier: "NOASSERTION",
   211  						},
   212  					},
   213  				},
   214  				Relationships: []*spdx.Relationship{
   215  					{
   216  						RefA: spdx.DocElementID{
   217  							ElementRefID: "DocumentRoot-File-path-to-some.file",
   218  						},
   219  						RefB: spdx.DocElementID{
   220  							ElementRefID: "Package-pkg-1-pkg-1",
   221  						},
   222  						Relationship: spdx.RelationshipContains,
   223  					},
   224  					{
   225  						RefA: spdx.DocElementID{
   226  							ElementRefID: "DOCUMENT",
   227  						},
   228  						RefB: spdx.DocElementID{
   229  							ElementRefID: "DocumentRoot-File-path-to-some.file",
   230  						},
   231  						Relationship: spdx.RelationshipDescribes,
   232  					},
   233  				},
   234  			},
   235  		},
   236  	}
   237  
   238  	for _, test := range tests {
   239  		t.Run(test.name, func(t *testing.T) {
   240  			tracker.Tested(t, test.in.Source.Metadata)
   241  
   242  			// replace IDs with package names
   243  			var pkgs []pkg.Package
   244  			for p := range test.in.Artifacts.Packages.Enumerate() {
   245  				p.OverrideID(artifact.ID(p.Name))
   246  				pkgs = append(pkgs, p)
   247  			}
   248  			test.in.Artifacts.Packages = pkg.NewCollection(pkgs...)
   249  
   250  			// convert
   251  			got := ToFormatModel(test.in)
   252  
   253  			// check differences
   254  			if diff := cmp.Diff(test.expected, got,
   255  				cmpopts.IgnoreUnexported(spdx.Document{}, spdx.Package{}),
   256  				cmpopts.IgnoreFields(spdx.Document{}, "CreationInfo", "DocumentNamespace"),
   257  				cmpopts.IgnoreFields(spdx.Package{}, "PackageDownloadLocation", "IsFilesAnalyzedTagPresent", "PackageSourceInfo", "PackageLicenseConcluded", "PackageLicenseDeclared", "PackageCopyrightText"),
   258  			); diff != "" {
   259  				t.Error(diff)
   260  			}
   261  		})
   262  	}
   263  }
   264  
   265  func Test_toPackageChecksums(t *testing.T) {
   266  	tests := []struct {
   267  		name          string
   268  		pkg           pkg.Package
   269  		expected      []spdx.Checksum
   270  		filesAnalyzed bool
   271  	}{
   272  		{
   273  			name: "Java Package",
   274  			pkg: pkg.Package{
   275  				Name:     "test",
   276  				Version:  "1.0.0",
   277  				Language: pkg.Java,
   278  				Metadata: pkg.JavaArchive{
   279  					ArchiveDigests: []file.Digest{
   280  						{
   281  							Algorithm: "sha1", // SPDX expects these to be uppercase
   282  							Value:     "1234",
   283  						},
   284  					},
   285  				},
   286  			},
   287  			expected: []spdx.Checksum{
   288  				{
   289  					Algorithm: "SHA1",
   290  					Value:     "1234",
   291  				},
   292  			},
   293  			filesAnalyzed: true,
   294  		},
   295  		{
   296  			name: "Java Package with no archive digests",
   297  			pkg: pkg.Package{
   298  				Name:     "test",
   299  				Version:  "1.0.0",
   300  				Language: pkg.Java,
   301  				Metadata: pkg.JavaArchive{
   302  					ArchiveDigests: []file.Digest{},
   303  				},
   304  			},
   305  			expected:      []spdx.Checksum{},
   306  			filesAnalyzed: false,
   307  		},
   308  		{
   309  			name: "Java Package with no metadata",
   310  			pkg: pkg.Package{
   311  				Name:     "test",
   312  				Version:  "1.0.0",
   313  				Language: pkg.Java,
   314  			},
   315  			expected:      []spdx.Checksum{},
   316  			filesAnalyzed: false,
   317  		},
   318  		{
   319  			name: "Go Binary Package",
   320  			pkg: pkg.Package{
   321  				Name:     "test",
   322  				Version:  "1.0.0",
   323  				Language: pkg.Go,
   324  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   325  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   326  				},
   327  			},
   328  			expected: []spdx.Checksum{
   329  				{
   330  					Algorithm: "SHA256",
   331  					Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   332  				},
   333  			},
   334  			filesAnalyzed: false,
   335  		},
   336  		{
   337  			name: "Package with no metadata type",
   338  			pkg: pkg.Package{
   339  				Name:     "test",
   340  				Version:  "1.0.0",
   341  				Language: pkg.Java,
   342  				Metadata: struct{}{},
   343  			},
   344  			expected:      []spdx.Checksum{},
   345  			filesAnalyzed: false,
   346  		},
   347  	}
   348  
   349  	for _, test := range tests {
   350  		t.Run(test.name, func(t *testing.T) {
   351  			commonSum, filesAnalyzed := toPackageChecksums(test.pkg)
   352  			assert.ElementsMatch(t, test.expected, commonSum)
   353  			assert.Equal(t, test.filesAnalyzed, filesAnalyzed)
   354  		})
   355  	}
   356  }
   357  
   358  func Test_toFileTypes(t *testing.T) {
   359  
   360  	tests := []struct {
   361  		name     string
   362  		metadata file.Metadata
   363  		expected []string
   364  	}{
   365  		{
   366  			name: "application",
   367  			metadata: file.Metadata{
   368  				MIMEType: "application/vnd.unknown",
   369  			},
   370  			expected: []string{
   371  				string(ApplicationFileType),
   372  			},
   373  		},
   374  		{
   375  			name: "archive",
   376  			metadata: file.Metadata{
   377  				MIMEType: "application/zip",
   378  			},
   379  			expected: []string{
   380  				string(ApplicationFileType),
   381  				string(ArchiveFileType),
   382  			},
   383  		},
   384  		{
   385  			name: "audio",
   386  			metadata: file.Metadata{
   387  				MIMEType: "audio/ogg",
   388  			},
   389  			expected: []string{
   390  				string(AudioFileType),
   391  			},
   392  		},
   393  		{
   394  			name: "video",
   395  			metadata: file.Metadata{
   396  				MIMEType: "video/3gpp",
   397  			},
   398  			expected: []string{
   399  				string(VideoFileType),
   400  			},
   401  		},
   402  		{
   403  			name: "text",
   404  			metadata: file.Metadata{
   405  				MIMEType: "text/html",
   406  			},
   407  			expected: []string{
   408  				string(TextFileType),
   409  			},
   410  		},
   411  		{
   412  			name: "image",
   413  			metadata: file.Metadata{
   414  				MIMEType: "image/png",
   415  			},
   416  			expected: []string{
   417  				string(ImageFileType),
   418  			},
   419  		},
   420  		{
   421  			name: "binary",
   422  			metadata: file.Metadata{
   423  				MIMEType: "application/x-sharedlib",
   424  			},
   425  			expected: []string{
   426  				string(ApplicationFileType),
   427  				string(BinaryFileType),
   428  			},
   429  		},
   430  	}
   431  	for _, test := range tests {
   432  		t.Run(test.name, func(t *testing.T) {
   433  			assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
   434  		})
   435  	}
   436  }
   437  
   438  func Test_lookupRelationship(t *testing.T) {
   439  
   440  	tests := []struct {
   441  		input   artifact.RelationshipType
   442  		exists  bool
   443  		ty      RelationshipType
   444  		comment string
   445  	}{
   446  		{
   447  			input:  artifact.ContainsRelationship,
   448  			exists: true,
   449  			ty:     ContainsRelationship,
   450  		},
   451  		{
   452  			input:   artifact.OwnershipByFileOverlapRelationship,
   453  			exists:  true,
   454  			ty:      OtherRelationship,
   455  			comment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by",
   456  		},
   457  		{
   458  			input:   artifact.EvidentByRelationship,
   459  			exists:  true,
   460  			ty:      OtherRelationship,
   461  			comment: "evident-by: indicates the package's existence is evident by the given file",
   462  		},
   463  		{
   464  			input:  "made-up",
   465  			exists: false,
   466  		},
   467  	}
   468  	for _, test := range tests {
   469  		t.Run(string(test.input), func(t *testing.T) {
   470  			exists, ty, comment := lookupRelationship(test.input)
   471  			assert.Equal(t, exists, test.exists)
   472  			assert.Equal(t, ty, test.ty)
   473  			assert.Equal(t, comment, test.comment)
   474  		})
   475  	}
   476  }
   477  
   478  func Test_toFileChecksums(t *testing.T) {
   479  	tests := []struct {
   480  		name     string
   481  		digests  []file.Digest
   482  		expected []spdx.Checksum
   483  	}{
   484  		{
   485  			name: "empty",
   486  		},
   487  		{
   488  			name: "has digests",
   489  			digests: []file.Digest{
   490  				{
   491  					Algorithm: "SHA256",
   492  					Value:     "deadbeefcafe",
   493  				},
   494  				{
   495  					Algorithm: "md5",
   496  					Value:     "meh",
   497  				},
   498  			},
   499  			expected: []spdx.Checksum{
   500  				{
   501  					Algorithm: "SHA256",
   502  					Value:     "deadbeefcafe",
   503  				},
   504  				{
   505  					Algorithm: "MD5",
   506  					Value:     "meh",
   507  				},
   508  			},
   509  		},
   510  	}
   511  	for _, test := range tests {
   512  		t.Run(test.name, func(t *testing.T) {
   513  			assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
   514  		})
   515  	}
   516  }
   517  
   518  func Test_fileIDsForPackage(t *testing.T) {
   519  	p := pkg.Package{
   520  		Name: "bogus",
   521  	}
   522  
   523  	c := file.Coordinates{
   524  		RealPath:     "/path",
   525  		FileSystemID: "nowhere",
   526  	}
   527  
   528  	docElementId := func(identifiable artifact.Identifiable) spdx.DocElementID {
   529  		return spdx.DocElementID{
   530  			ElementRefID: toSPDXID(identifiable),
   531  		}
   532  	}
   533  
   534  	tests := []struct {
   535  		name          string
   536  		relationships []artifact.Relationship
   537  		expected      []*spdx.Relationship
   538  	}{
   539  		{
   540  			name: "package-to-file contains relationships",
   541  			relationships: []artifact.Relationship{
   542  				{
   543  					From: p,
   544  					To:   c,
   545  					Type: artifact.ContainsRelationship,
   546  				},
   547  			},
   548  			expected: []*spdx.Relationship{
   549  				{
   550  					Relationship: "CONTAINS",
   551  					RefA:         docElementId(p),
   552  					RefB:         docElementId(c),
   553  				},
   554  			},
   555  		},
   556  		{
   557  			name: "package-to-package",
   558  			relationships: []artifact.Relationship{
   559  				{
   560  					From: p,
   561  					To:   p,
   562  					Type: artifact.ContainsRelationship,
   563  				},
   564  			},
   565  			expected: []*spdx.Relationship{
   566  				{
   567  					Relationship: "CONTAINS",
   568  					RefA:         docElementId(p),
   569  					RefB:         docElementId(p),
   570  				},
   571  			},
   572  		},
   573  		{
   574  			name: "ignore file-to-file",
   575  			relationships: []artifact.Relationship{
   576  				{
   577  					From: c,
   578  					To:   c,
   579  					Type: artifact.ContainsRelationship,
   580  				},
   581  			},
   582  			expected: nil,
   583  		},
   584  		{
   585  			name: "ignore file-to-package",
   586  			relationships: []artifact.Relationship{
   587  				{
   588  					From: c,
   589  					To:   p,
   590  					Type: artifact.ContainsRelationship,
   591  				},
   592  			},
   593  			expected: nil,
   594  		},
   595  		{
   596  			name: "include package-to-file overlap relationships",
   597  			relationships: []artifact.Relationship{
   598  				{
   599  					From: p,
   600  					To:   c,
   601  					Type: artifact.OwnershipByFileOverlapRelationship,
   602  				},
   603  			},
   604  			expected: []*spdx.Relationship{
   605  				{
   606  					Relationship:        "OTHER",
   607  					RefA:                docElementId(p),
   608  					RefB:                docElementId(c),
   609  					RelationshipComment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by",
   610  				},
   611  			},
   612  		},
   613  	}
   614  	for _, test := range tests {
   615  		t.Run(test.name, func(t *testing.T) {
   616  			relationships := toRelationships(test.relationships)
   617  			assert.Equal(t, test.expected, relationships)
   618  		})
   619  	}
   620  }
   621  
   622  func Test_H1Digest(t *testing.T) {
   623  	s := sbom.SBOM{}
   624  	tests := []struct {
   625  		name           string
   626  		pkg            pkg.Package
   627  		expectedDigest string
   628  	}{
   629  		{
   630  			name: "valid h1digest",
   631  			pkg: pkg.Package{
   632  				Name:    "github.com/googleapis/gnostic",
   633  				Version: "v0.5.5",
   634  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   635  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   636  				},
   637  			},
   638  			expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   639  		},
   640  		{
   641  			name: "invalid h1digest",
   642  			pkg: pkg.Package{
   643  				Name:    "github.com/googleapis/gnostic",
   644  				Version: "v0.5.5",
   645  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   646  					H1Digest: "h1:9fHAtK0uzzz",
   647  				},
   648  			},
   649  			expectedDigest: "",
   650  		},
   651  		{
   652  			name: "unsupported h-digest",
   653  			pkg: pkg.Package{
   654  				Name:    "github.com/googleapis/gnostic",
   655  				Version: "v0.5.5",
   656  				Metadata: pkg.GolangBinaryBuildinfoEntry{
   657  					H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   658  				},
   659  			},
   660  			expectedDigest: "",
   661  		},
   662  	}
   663  
   664  	for _, test := range tests {
   665  		t.Run(test.name, func(t *testing.T) {
   666  			catalog := pkg.NewCollection(test.pkg)
   667  			pkgs := toPackages(catalog, s)
   668  			require.Len(t, pkgs, 1)
   669  			for _, p := range pkgs {
   670  				if test.expectedDigest == "" {
   671  					require.Len(t, p.PackageChecksums, 0)
   672  				} else {
   673  					require.Len(t, p.PackageChecksums, 1)
   674  					for _, c := range p.PackageChecksums {
   675  						require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value))
   676  					}
   677  				}
   678  			}
   679  		})
   680  	}
   681  }
   682  
   683  func Test_OtherLicenses(t *testing.T) {
   684  	tests := []struct {
   685  		name     string
   686  		pkg      pkg.Package
   687  		expected []*spdx.OtherLicense
   688  	}{
   689  		{
   690  			name: "no licenseRef",
   691  			pkg: pkg.Package{
   692  				Licenses: pkg.NewLicenseSet(),
   693  			},
   694  			expected: nil,
   695  		},
   696  		{
   697  			name: "single licenseRef",
   698  			pkg: pkg.Package{
   699  				Licenses: pkg.NewLicenseSet(
   700  					pkg.NewLicense("foobar"),
   701  				),
   702  			},
   703  			expected: []*spdx.OtherLicense{
   704  				{
   705  					LicenseIdentifier: "LicenseRef-foobar",
   706  					ExtractedText:     "foobar",
   707  				},
   708  			},
   709  		},
   710  		{
   711  			name: "multiple licenseRef",
   712  			pkg: pkg.Package{
   713  				Licenses: pkg.NewLicenseSet(
   714  					pkg.NewLicense("internal made up license name"),
   715  					pkg.NewLicense("new apple license 2.0"),
   716  				),
   717  			},
   718  			expected: []*spdx.OtherLicense{
   719  				{
   720  					LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
   721  					ExtractedText:     "internal made up license name",
   722  				},
   723  				{
   724  					LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
   725  					ExtractedText:     "new apple license 2.0",
   726  				},
   727  			},
   728  		},
   729  	}
   730  
   731  	for _, test := range tests {
   732  		t.Run(test.name, func(t *testing.T) {
   733  			catalog := pkg.NewCollection(test.pkg)
   734  			otherLicenses := toOtherLicenses(catalog)
   735  			require.Len(t, otherLicenses, len(test.expected))
   736  			require.Equal(t, test.expected, otherLicenses)
   737  		})
   738  	}
   739  }
   740  
   741  func Test_toSPDXID(t *testing.T) {
   742  	tests := []struct {
   743  		name     string
   744  		it       artifact.Identifiable
   745  		expected string
   746  	}{
   747  		{
   748  			name: "short filename",
   749  			it: file.Coordinates{
   750  				RealPath: "/short/path/file.txt",
   751  			},
   752  			expected: "File-short-path-file.txt",
   753  		},
   754  		{
   755  			name: "long filename",
   756  			it: file.Coordinates{
   757  				RealPath: "/some/long/path/with/a/lot/of-text/that-contains-a/file.txt",
   758  			},
   759  			expected: "File-...a-lot-of-text-that-contains-a-file.txt",
   760  		},
   761  		{
   762  			name: "package",
   763  			it: pkg.Package{
   764  				Type: pkg.NpmPkg,
   765  				Name: "some-package",
   766  			},
   767  			expected: "Package-npm-some-package",
   768  		},
   769  	}
   770  
   771  	for _, test := range tests {
   772  		t.Run(test.name, func(t *testing.T) {
   773  			got := string(toSPDXID(test.it))
   774  			// trim the hash
   775  			got = regexp.MustCompile(`-[a-z0-9]*$`).ReplaceAllString(got, "")
   776  			require.Equal(t, test.expected, got)
   777  		})
   778  	}
   779  }