github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/format/spdxjson/encoder_test.go (about)

     1  package spdxjson
     2  
     3  import (
     4  	"bytes"
     5  	"flag"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/anchore/syft/syft/pkg"
    13  	"github.com/anchore/syft/syft/sbom"
    14  	"github.com/lineaje-labs/syft/syft/format/internal/spdxutil"
    15  	"github.com/lineaje-labs/syft/syft/format/internal/testutil"
    16  )
    17  
    18  var updateSnapshot = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json encoders")
    19  var updateImage = flag.Bool("update-image", false, "update the golden image used for image encoder testing")
    20  
    21  func getEncoder(t testing.TB) sbom.FormatEncoder {
    22  	cfg := DefaultEncoderConfig()
    23  	cfg.Pretty = true
    24  
    25  	enc, err := NewFormatEncoderWithConfig(cfg)
    26  	require.NoError(t, err)
    27  	return enc
    28  }
    29  
    30  func TestPrettyOutput(t *testing.T) {
    31  	run := func(opt bool) string {
    32  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{
    33  			Version: spdxutil.DefaultVersion,
    34  			Pretty:  opt,
    35  		})
    36  		require.NoError(t, err)
    37  
    38  		dir := t.TempDir()
    39  		s := testutil.DirectoryInput(t, dir)
    40  
    41  		var buffer bytes.Buffer
    42  		err = enc.Encode(&buffer, s)
    43  		require.NoError(t, err)
    44  
    45  		return strings.TrimSpace(buffer.String())
    46  	}
    47  
    48  	t.Run("pretty", func(t *testing.T) {
    49  		actual := run(true)
    50  		assert.Contains(t, actual, "\n")
    51  	})
    52  
    53  	t.Run("compact", func(t *testing.T) {
    54  		actual := run(false)
    55  		assert.NotContains(t, actual, "\n")
    56  	})
    57  }
    58  
    59  func TestEscapeHTML(t *testing.T) {
    60  	dir := t.TempDir()
    61  	s := testutil.DirectoryInput(t, dir)
    62  	s.Artifacts.Packages.Add(pkg.Package{
    63  		Name: "<html-package>",
    64  	})
    65  
    66  	// by default we do not escape HTML
    67  	t.Run("default", func(t *testing.T) {
    68  		cfg := DefaultEncoderConfig()
    69  
    70  		enc, err := NewFormatEncoderWithConfig(cfg)
    71  		require.NoError(t, err)
    72  
    73  		var buffer bytes.Buffer
    74  		err = enc.Encode(&buffer, s)
    75  		require.NoError(t, err)
    76  
    77  		actual := buffer.String()
    78  		assert.Contains(t, actual, "<html-package>")
    79  		assert.NotContains(t, actual, "\\u003chtml-package\\u003e")
    80  	})
    81  
    82  }
    83  
    84  func TestSPDXJSONDirectoryEncoder(t *testing.T) {
    85  	dir := t.TempDir()
    86  	testutil.AssertEncoderAgainstGoldenSnapshot(t,
    87  		testutil.EncoderSnapshotTestConfig{
    88  			Subject:                     testutil.DirectoryInput(t, dir),
    89  			Format:                      getEncoder(t),
    90  			UpdateSnapshot:              *updateSnapshot,
    91  			PersistRedactionsInSnapshot: true,
    92  			IsJSON:                      true,
    93  			Redactor:                    redactor(dir),
    94  		},
    95  	)
    96  }
    97  
    98  func TestSPDXJSONImageEncoder(t *testing.T) {
    99  	testImage := "image-simple"
   100  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
   101  		testutil.ImageSnapshotTestConfig{
   102  			Image:               testImage,
   103  			UpdateImageSnapshot: *updateImage,
   104  		},
   105  		testutil.EncoderSnapshotTestConfig{
   106  			Subject:                     testutil.ImageInput(t, testImage, testutil.FromSnapshot()),
   107  			Format:                      getEncoder(t),
   108  			UpdateSnapshot:              *updateSnapshot,
   109  			PersistRedactionsInSnapshot: true,
   110  			IsJSON:                      true,
   111  			Redactor:                    redactor(),
   112  		},
   113  	)
   114  }
   115  
   116  func TestSPDXRelationshipOrder(t *testing.T) {
   117  	testImage := "image-simple"
   118  
   119  	s := testutil.ImageInput(t, testImage, testutil.FromSnapshot())
   120  	testutil.AddSampleFileRelationships(&s)
   121  
   122  	testutil.AssertEncoderAgainstGoldenImageSnapshot(t,
   123  		testutil.ImageSnapshotTestConfig{
   124  			Image:               testImage,
   125  			UpdateImageSnapshot: *updateImage,
   126  		},
   127  		testutil.EncoderSnapshotTestConfig{
   128  			Subject:                     s,
   129  			Format:                      getEncoder(t),
   130  			UpdateSnapshot:              *updateSnapshot,
   131  			PersistRedactionsInSnapshot: true,
   132  			IsJSON:                      true,
   133  			Redactor:                    redactor(),
   134  		},
   135  	)
   136  }
   137  
   138  func redactor(values ...string) testutil.Redactor {
   139  	return testutil.NewRedactions().
   140  		WithValuesRedacted(values...).
   141  		WithPatternRedactors(
   142  			map[string]string{
   143  				// each SBOM reports the time it was generated, which is not useful during snapshot testing
   144  				`"created":\s+"[^"]*"`: `"created":"redacted"`,
   145  
   146  				// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
   147  				`"documentNamespace":\s+"[^"]*"`: `"documentNamespace":"redacted"`,
   148  
   149  				// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
   150  				`"licenseListVersion":\s+"[^"]*"`: `"licenseListVersion":"redacted"`,
   151  			},
   152  		)
   153  }
   154  
   155  func TestSupportedVersions(t *testing.T) {
   156  	encs := defaultFormatEncoders()
   157  	require.NotEmpty(t, encs)
   158  
   159  	versions := SupportedVersions()
   160  	require.Equal(t, len(versions), len(encs))
   161  
   162  	subject := testutil.DirectoryInput(t, t.TempDir())
   163  	dec := NewFormatDecoder()
   164  
   165  	relationshipOffsetPerVersion := map[string]int{
   166  		// the package representing the source gets a relationship from the source package to all other packages found
   167  		// these relationships cannot be removed until the primaryPackagePurpose info is available in 2.3
   168  		"2.1": 2,
   169  		"2.2": 2,
   170  		// the source-to-package relationships can be removed since the primaryPackagePurpose info is available in 2.3
   171  		"2.3": 0,
   172  	}
   173  
   174  	pkgCountOffsetPerVersion := map[string]int{
   175  		"2.1": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3
   176  		"2.2": 1, // the source is mapped as a package, but cannot distinguish it since the primaryPackagePurpose info is not available until 2.3
   177  		"2.3": 0, // the source package can be removed since the primaryPackagePurpose info is available
   178  	}
   179  
   180  	for _, enc := range encs {
   181  		t.Run(enc.Version(), func(t *testing.T) {
   182  			require.Contains(t, versions, enc.Version())
   183  
   184  			var buf bytes.Buffer
   185  			require.NoError(t, enc.Encode(&buf, subject))
   186  
   187  			id, version := dec.Identify(bytes.NewReader(buf.Bytes()))
   188  			assert.Equal(t, enc.ID(), id)
   189  			assert.Equal(t, enc.Version(), version)
   190  
   191  			var s *sbom.SBOM
   192  			var err error
   193  			s, id, version, err = dec.Decode(bytes.NewReader(buf.Bytes()))
   194  			require.NoError(t, err)
   195  
   196  			assert.Equal(t, enc.ID(), id)
   197  			assert.Equal(t, enc.Version(), version)
   198  
   199  			require.NotEmpty(t, s.Artifacts.Packages.PackageCount())
   200  
   201  			offset := relationshipOffsetPerVersion[enc.Version()]
   202  
   203  			assert.Equal(t, len(subject.Relationships)+offset, len(s.Relationships), "mismatched relationship count")
   204  
   205  			offset = pkgCountOffsetPerVersion[enc.Version()]
   206  
   207  			if !assert.Equal(t, subject.Artifacts.Packages.PackageCount()+offset, s.Artifacts.Packages.PackageCount(), "mismatched package count") {
   208  				t.Logf("expected: %d", subject.Artifacts.Packages.PackageCount())
   209  				for _, p := range subject.Artifacts.Packages.Sorted() {
   210  					t.Logf("  - %s", p.String())
   211  				}
   212  				t.Logf("actual: %d", s.Artifacts.Packages.PackageCount())
   213  				for _, p := range s.Artifacts.Packages.Sorted() {
   214  					t.Logf("  - %s", p.String())
   215  				}
   216  			}
   217  		})
   218  	}
   219  }
   220  
   221  func defaultFormatEncoders() []sbom.FormatEncoder {
   222  	var encs []sbom.FormatEncoder
   223  	for _, version := range SupportedVersions() {
   224  		enc, err := NewFormatEncoderWithConfig(EncoderConfig{Version: version})
   225  		if err != nil {
   226  			panic(err)
   227  		}
   228  		encs = append(encs, enc)
   229  	}
   230  	return encs
   231  }