github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/syft/formats/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/internal/sourcemetadata"
    18  	"github.com/anchore/syft/syft/pkg"
    19  	"github.com/anchore/syft/syft/sbom"
    20  	"github.com/anchore/syft/syft/source"
    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.JavaMetadata{
   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.JavaMetadata{
   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  				MetadataType: pkg.GolangBinMetadataType,
   325  				Metadata: pkg.GolangBinMetadata{
   326  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   327  				},
   328  			},
   329  			expected: []spdx.Checksum{
   330  				{
   331  					Algorithm: "SHA256",
   332  					Value:     "f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   333  				},
   334  			},
   335  			filesAnalyzed: false,
   336  		},
   337  		{
   338  			name: "Package with no metadata type",
   339  			pkg: pkg.Package{
   340  				Name:     "test",
   341  				Version:  "1.0.0",
   342  				Language: pkg.Java,
   343  				Metadata: struct{}{},
   344  			},
   345  			expected:      []spdx.Checksum{},
   346  			filesAnalyzed: false,
   347  		},
   348  	}
   349  
   350  	for _, test := range tests {
   351  		t.Run(test.name, func(t *testing.T) {
   352  			commonSum, filesAnalyzed := toPackageChecksums(test.pkg)
   353  			assert.ElementsMatch(t, test.expected, commonSum)
   354  			assert.Equal(t, test.filesAnalyzed, filesAnalyzed)
   355  		})
   356  	}
   357  }
   358  
   359  func Test_toFileTypes(t *testing.T) {
   360  
   361  	tests := []struct {
   362  		name     string
   363  		metadata file.Metadata
   364  		expected []string
   365  	}{
   366  		{
   367  			name: "application",
   368  			metadata: file.Metadata{
   369  				MIMEType: "application/vnd.unknown",
   370  			},
   371  			expected: []string{
   372  				string(ApplicationFileType),
   373  			},
   374  		},
   375  		{
   376  			name: "archive",
   377  			metadata: file.Metadata{
   378  				MIMEType: "application/zip",
   379  			},
   380  			expected: []string{
   381  				string(ApplicationFileType),
   382  				string(ArchiveFileType),
   383  			},
   384  		},
   385  		{
   386  			name: "audio",
   387  			metadata: file.Metadata{
   388  				MIMEType: "audio/ogg",
   389  			},
   390  			expected: []string{
   391  				string(AudioFileType),
   392  			},
   393  		},
   394  		{
   395  			name: "video",
   396  			metadata: file.Metadata{
   397  				MIMEType: "video/3gpp",
   398  			},
   399  			expected: []string{
   400  				string(VideoFileType),
   401  			},
   402  		},
   403  		{
   404  			name: "text",
   405  			metadata: file.Metadata{
   406  				MIMEType: "text/html",
   407  			},
   408  			expected: []string{
   409  				string(TextFileType),
   410  			},
   411  		},
   412  		{
   413  			name: "image",
   414  			metadata: file.Metadata{
   415  				MIMEType: "image/png",
   416  			},
   417  			expected: []string{
   418  				string(ImageFileType),
   419  			},
   420  		},
   421  		{
   422  			name: "binary",
   423  			metadata: file.Metadata{
   424  				MIMEType: "application/x-sharedlib",
   425  			},
   426  			expected: []string{
   427  				string(ApplicationFileType),
   428  				string(BinaryFileType),
   429  			},
   430  		},
   431  	}
   432  	for _, test := range tests {
   433  		t.Run(test.name, func(t *testing.T) {
   434  			assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
   435  		})
   436  	}
   437  }
   438  
   439  func Test_lookupRelationship(t *testing.T) {
   440  
   441  	tests := []struct {
   442  		input   artifact.RelationshipType
   443  		exists  bool
   444  		ty      RelationshipType
   445  		comment string
   446  	}{
   447  		{
   448  			input:  artifact.ContainsRelationship,
   449  			exists: true,
   450  			ty:     ContainsRelationship,
   451  		},
   452  		{
   453  			input:   artifact.OwnershipByFileOverlapRelationship,
   454  			exists:  true,
   455  			ty:      OtherRelationship,
   456  			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",
   457  		},
   458  		{
   459  			input:   artifact.EvidentByRelationship,
   460  			exists:  true,
   461  			ty:      OtherRelationship,
   462  			comment: "evident-by: indicates the package's existence is evident by the given file",
   463  		},
   464  		{
   465  			input:  "made-up",
   466  			exists: false,
   467  		},
   468  	}
   469  	for _, test := range tests {
   470  		t.Run(string(test.input), func(t *testing.T) {
   471  			exists, ty, comment := lookupRelationship(test.input)
   472  			assert.Equal(t, exists, test.exists)
   473  			assert.Equal(t, ty, test.ty)
   474  			assert.Equal(t, comment, test.comment)
   475  		})
   476  	}
   477  }
   478  
   479  func Test_toFileChecksums(t *testing.T) {
   480  	tests := []struct {
   481  		name     string
   482  		digests  []file.Digest
   483  		expected []spdx.Checksum
   484  	}{
   485  		{
   486  			name: "empty",
   487  		},
   488  		{
   489  			name: "has digests",
   490  			digests: []file.Digest{
   491  				{
   492  					Algorithm: "SHA256",
   493  					Value:     "deadbeefcafe",
   494  				},
   495  				{
   496  					Algorithm: "md5",
   497  					Value:     "meh",
   498  				},
   499  			},
   500  			expected: []spdx.Checksum{
   501  				{
   502  					Algorithm: "SHA256",
   503  					Value:     "deadbeefcafe",
   504  				},
   505  				{
   506  					Algorithm: "MD5",
   507  					Value:     "meh",
   508  				},
   509  			},
   510  		},
   511  	}
   512  	for _, test := range tests {
   513  		t.Run(test.name, func(t *testing.T) {
   514  			assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
   515  		})
   516  	}
   517  }
   518  
   519  func Test_fileIDsForPackage(t *testing.T) {
   520  	p := pkg.Package{
   521  		Name: "bogus",
   522  	}
   523  
   524  	c := file.Coordinates{
   525  		RealPath:     "/path",
   526  		FileSystemID: "nowhere",
   527  	}
   528  
   529  	docElementId := func(identifiable artifact.Identifiable) spdx.DocElementID {
   530  		return spdx.DocElementID{
   531  			ElementRefID: toSPDXID(identifiable),
   532  		}
   533  	}
   534  
   535  	tests := []struct {
   536  		name          string
   537  		relationships []artifact.Relationship
   538  		expected      []*spdx.Relationship
   539  	}{
   540  		{
   541  			name: "package-to-file contains relationships",
   542  			relationships: []artifact.Relationship{
   543  				{
   544  					From: p,
   545  					To:   c,
   546  					Type: artifact.ContainsRelationship,
   547  				},
   548  			},
   549  			expected: []*spdx.Relationship{
   550  				{
   551  					Relationship: "CONTAINS",
   552  					RefA:         docElementId(p),
   553  					RefB:         docElementId(c),
   554  				},
   555  			},
   556  		},
   557  		{
   558  			name: "package-to-package",
   559  			relationships: []artifact.Relationship{
   560  				{
   561  					From: p,
   562  					To:   p,
   563  					Type: artifact.ContainsRelationship,
   564  				},
   565  			},
   566  			expected: []*spdx.Relationship{
   567  				{
   568  					Relationship: "CONTAINS",
   569  					RefA:         docElementId(p),
   570  					RefB:         docElementId(p),
   571  				},
   572  			},
   573  		},
   574  		{
   575  			name: "ignore file-to-file",
   576  			relationships: []artifact.Relationship{
   577  				{
   578  					From: c,
   579  					To:   c,
   580  					Type: artifact.ContainsRelationship,
   581  				},
   582  			},
   583  			expected: nil,
   584  		},
   585  		{
   586  			name: "ignore file-to-package",
   587  			relationships: []artifact.Relationship{
   588  				{
   589  					From: c,
   590  					To:   p,
   591  					Type: artifact.ContainsRelationship,
   592  				},
   593  			},
   594  			expected: nil,
   595  		},
   596  		{
   597  			name: "include package-to-file overlap relationships",
   598  			relationships: []artifact.Relationship{
   599  				{
   600  					From: p,
   601  					To:   c,
   602  					Type: artifact.OwnershipByFileOverlapRelationship,
   603  				},
   604  			},
   605  			expected: []*spdx.Relationship{
   606  				{
   607  					Relationship:        "OTHER",
   608  					RefA:                docElementId(p),
   609  					RefB:                docElementId(c),
   610  					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",
   611  				},
   612  			},
   613  		},
   614  	}
   615  	for _, test := range tests {
   616  		t.Run(test.name, func(t *testing.T) {
   617  			relationships := toRelationships(test.relationships)
   618  			assert.Equal(t, test.expected, relationships)
   619  		})
   620  	}
   621  }
   622  
   623  func Test_H1Digest(t *testing.T) {
   624  	s := sbom.SBOM{}
   625  	tests := []struct {
   626  		name           string
   627  		pkg            pkg.Package
   628  		expectedDigest string
   629  	}{
   630  		{
   631  			name: "valid h1digest",
   632  			pkg: pkg.Package{
   633  				Name:         "github.com/googleapis/gnostic",
   634  				Version:      "v0.5.5",
   635  				MetadataType: pkg.GolangBinMetadataType,
   636  				Metadata: pkg.GolangBinMetadata{
   637  					H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   638  				},
   639  			},
   640  			expectedDigest: "SHA256:f5f1c0b4ad2e0dfa6f79eaaaa3586411925c16f61702208ddd4bad2fc17dc47c",
   641  		},
   642  		{
   643  			name: "invalid h1digest",
   644  			pkg: pkg.Package{
   645  				Name:         "github.com/googleapis/gnostic",
   646  				Version:      "v0.5.5",
   647  				MetadataType: pkg.GolangBinMetadataType,
   648  				Metadata: pkg.GolangBinMetadata{
   649  					H1Digest: "h1:9fHAtK0uzzz",
   650  				},
   651  			},
   652  			expectedDigest: "",
   653  		},
   654  		{
   655  			name: "unsupported h-digest",
   656  			pkg: pkg.Package{
   657  				Name:         "github.com/googleapis/gnostic",
   658  				Version:      "v0.5.5",
   659  				MetadataType: pkg.GolangBinMetadataType,
   660  				Metadata: pkg.GolangBinMetadata{
   661  					H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
   662  				},
   663  			},
   664  			expectedDigest: "",
   665  		},
   666  	}
   667  
   668  	for _, test := range tests {
   669  		t.Run(test.name, func(t *testing.T) {
   670  			catalog := pkg.NewCollection(test.pkg)
   671  			pkgs := toPackages(catalog, s)
   672  			require.Len(t, pkgs, 1)
   673  			for _, p := range pkgs {
   674  				if test.expectedDigest == "" {
   675  					require.Len(t, p.PackageChecksums, 0)
   676  				} else {
   677  					require.Len(t, p.PackageChecksums, 1)
   678  					for _, c := range p.PackageChecksums {
   679  						require.Equal(t, test.expectedDigest, fmt.Sprintf("%s:%s", c.Algorithm, c.Value))
   680  					}
   681  				}
   682  			}
   683  		})
   684  	}
   685  }
   686  
   687  func Test_OtherLicenses(t *testing.T) {
   688  	tests := []struct {
   689  		name     string
   690  		pkg      pkg.Package
   691  		expected []*spdx.OtherLicense
   692  	}{
   693  		{
   694  			name: "no licenseRef",
   695  			pkg: pkg.Package{
   696  				Licenses: pkg.NewLicenseSet(),
   697  			},
   698  			expected: nil,
   699  		},
   700  		{
   701  			name: "single licenseRef",
   702  			pkg: pkg.Package{
   703  				Licenses: pkg.NewLicenseSet(
   704  					pkg.NewLicense("foobar"),
   705  				),
   706  			},
   707  			expected: []*spdx.OtherLicense{
   708  				{
   709  					LicenseIdentifier: "LicenseRef-foobar",
   710  					ExtractedText:     "foobar",
   711  				},
   712  			},
   713  		},
   714  		{
   715  			name: "multiple licenseRef",
   716  			pkg: pkg.Package{
   717  				Licenses: pkg.NewLicenseSet(
   718  					pkg.NewLicense("internal made up license name"),
   719  					pkg.NewLicense("new apple license 2.0"),
   720  				),
   721  			},
   722  			expected: []*spdx.OtherLicense{
   723  				{
   724  					LicenseIdentifier: "LicenseRef-internal-made-up-license-name",
   725  					ExtractedText:     "internal made up license name",
   726  				},
   727  				{
   728  					LicenseIdentifier: "LicenseRef-new-apple-license-2.0",
   729  					ExtractedText:     "new apple license 2.0",
   730  				},
   731  			},
   732  		},
   733  	}
   734  
   735  	for _, test := range tests {
   736  		t.Run(test.name, func(t *testing.T) {
   737  			catalog := pkg.NewCollection(test.pkg)
   738  			otherLicenses := toOtherLicenses(catalog)
   739  			require.Len(t, otherLicenses, len(test.expected))
   740  			require.Equal(t, test.expected, otherLicenses)
   741  		})
   742  	}
   743  }
   744  
   745  func Test_toSPDXID(t *testing.T) {
   746  	tests := []struct {
   747  		name     string
   748  		it       artifact.Identifiable
   749  		expected string
   750  	}{
   751  		{
   752  			name: "short filename",
   753  			it: file.Coordinates{
   754  				RealPath: "/short/path/file.txt",
   755  			},
   756  			expected: "File-short-path-file.txt",
   757  		},
   758  		{
   759  			name: "long filename",
   760  			it: file.Coordinates{
   761  				RealPath: "/some/long/path/with/a/lot/of-text/that-contains-a/file.txt",
   762  			},
   763  			expected: "File-...a-lot-of-text-that-contains-a-file.txt",
   764  		},
   765  		{
   766  			name: "package",
   767  			it: pkg.Package{
   768  				Type: pkg.NpmPkg,
   769  				Name: "some-package",
   770  			},
   771  			expected: "Package-npm-some-package",
   772  		},
   773  	}
   774  
   775  	for _, test := range tests {
   776  		t.Run(test.name, func(t *testing.T) {
   777  			got := string(toSPDXID(test.it))
   778  			// trim the hash
   779  			got = regexp.MustCompile(`-[a-z0-9]*$`).ReplaceAllString(got, "")
   780  			require.Equal(t, test.expected, got)
   781  		})
   782  	}
   783  }