github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/download_sbom_test.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  
    13  	"github.com/buildpacks/imgutil/fakes"
    14  	"github.com/golang/mock/gomock"
    15  	"github.com/heroku/color"
    16  	"github.com/sclevine/spec"
    17  	"github.com/sclevine/spec/report"
    18  
    19  	"github.com/buildpacks/pack/pkg/archive"
    20  	"github.com/buildpacks/pack/pkg/image"
    21  	"github.com/buildpacks/pack/pkg/logging"
    22  	"github.com/buildpacks/pack/pkg/testmocks"
    23  	h "github.com/buildpacks/pack/testhelpers"
    24  )
    25  
    26  func TestDownloadSBOM(t *testing.T) {
    27  	color.Disable(true)
    28  	defer color.Disable(false)
    29  	spec.Run(t, "DownloadSBOM", testDownloadSBOM, spec.Parallel(), spec.Report(report.Terminal{}))
    30  }
    31  
    32  func testDownloadSBOM(t *testing.T, when spec.G, it spec.S) {
    33  	var (
    34  		subject          *Client
    35  		mockImageFetcher *testmocks.MockImageFetcher
    36  		mockDockerClient *testmocks.MockCommonAPIClient
    37  		mockController   *gomock.Controller
    38  		out              bytes.Buffer
    39  	)
    40  
    41  	it.Before(func() {
    42  		mockController = gomock.NewController(t)
    43  		mockImageFetcher = testmocks.NewMockImageFetcher(mockController)
    44  		mockDockerClient = testmocks.NewMockCommonAPIClient(mockController)
    45  
    46  		var err error
    47  		subject, err = NewClient(WithLogger(logging.NewLogWithWriters(&out, &out)), WithFetcher(mockImageFetcher), WithDockerClient(mockDockerClient))
    48  		h.AssertNil(t, err)
    49  	})
    50  
    51  	it.After(func() {
    52  		mockController.Finish()
    53  	})
    54  
    55  	when("the image exists", func() {
    56  		var (
    57  			mockImage *testmocks.MockImage
    58  			tmpDir    string
    59  			tmpFile   string
    60  		)
    61  
    62  		it.Before(func() {
    63  			var err error
    64  			tmpDir, err = os.MkdirTemp("", "pack.download.sbom.test.")
    65  			h.AssertNil(t, err)
    66  
    67  			f, err := os.CreateTemp("", "pack.download.sbom.test.")
    68  			h.AssertNil(t, err)
    69  			tmpFile = f.Name()
    70  
    71  			err = archive.CreateSingleFileTar(tmpFile, "sbom", "some-sbom-content")
    72  			h.AssertNil(t, err)
    73  
    74  			data, err := os.ReadFile(tmpFile)
    75  			h.AssertNil(t, err)
    76  
    77  			hsh := sha256.New()
    78  			hsh.Write(data)
    79  			shasum := hex.EncodeToString(hsh.Sum(nil))
    80  
    81  			mockImage = testmocks.NewImage("some/image", "", nil)
    82  			mockImage.AddLayerWithDiffID(tmpFile, fmt.Sprintf("sha256:%s", shasum))
    83  			h.AssertNil(t, mockImage.SetLabel(
    84  				"io.buildpacks.lifecycle.metadata",
    85  				fmt.Sprintf(
    86  					`{
    87    "sbom": {
    88      "sha": "sha256:%s"
    89    }
    90  }`, shasum)))
    91  
    92  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(mockImage, nil)
    93  		})
    94  
    95  		it.After(func() {
    96  			os.RemoveAll(tmpDir)
    97  			os.RemoveAll(tmpFile)
    98  		})
    99  
   100  		it("returns the stack ID", func() {
   101  			err := subject.DownloadSBOM("some/image", DownloadSBOMOptions{Daemon: true, DestinationDir: tmpDir})
   102  			h.AssertNil(t, err)
   103  
   104  			contents, err := os.ReadFile(filepath.Join(tmpDir, "sbom"))
   105  			h.AssertNil(t, err)
   106  
   107  			h.AssertEq(t, string(contents), "some-sbom-content")
   108  		})
   109  	})
   110  
   111  	when("the image doesn't exist", func() {
   112  		it("returns nil", func() {
   113  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/non-existent-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, image.ErrNotFound)
   114  
   115  			err := subject.DownloadSBOM("some/non-existent-image", DownloadSBOMOptions{Daemon: true, DestinationDir: ""})
   116  			expectedError := fmt.Sprintf("image '%s' cannot be found", "some/non-existent-image")
   117  			h.AssertError(t, err, expectedError)
   118  
   119  			expectedMessage := fmt.Sprintf("Warning: if the image is saved on a registry run with the flag '--remote', for example: 'pack sbom download --remote %s'", "some/non-existent-image")
   120  			h.AssertContains(t, out.String(), expectedMessage)
   121  		})
   122  	})
   123  
   124  	when("there is an error fetching the image", func() {
   125  		it("returns the error", func() {
   126  			mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/image", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).Return(nil, errors.New("some-error"))
   127  
   128  			err := subject.DownloadSBOM("some/image", DownloadSBOMOptions{Daemon: true, DestinationDir: ""})
   129  			h.AssertError(t, err, "some-error")
   130  		})
   131  	})
   132  
   133  	when("the image is SBOM metadata", func() {
   134  		it("returns empty data", func() {
   135  			mockImageFetcher.EXPECT().
   136  				Fetch(gomock.Any(), "some/image-without-labels", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).
   137  				Return(fakes.NewImage("some/image-without-labels", "", nil), nil)
   138  
   139  			err := subject.DownloadSBOM("some/image-without-labels", DownloadSBOMOptions{Daemon: true, DestinationDir: ""})
   140  			h.AssertError(t, err, "could not find SBoM information on 'some/image-without-labels'")
   141  		})
   142  	})
   143  
   144  	when("the image has malformed metadata", func() {
   145  		var badImage *fakes.Image
   146  
   147  		it.Before(func() {
   148  			badImage = fakes.NewImage("some/image-with-malformed-metadata", "", nil)
   149  			mockImageFetcher.EXPECT().
   150  				Fetch(gomock.Any(), "some/image-with-malformed-metadata", image.FetchOptions{Daemon: true, PullPolicy: image.PullNever}).
   151  				Return(badImage, nil)
   152  		})
   153  
   154  		it("returns an error when layers md cannot parse", func() {
   155  			h.AssertNil(t, badImage.SetLabel("io.buildpacks.lifecycle.metadata", "not   ----  json"))
   156  
   157  			err := subject.DownloadSBOM("some/image-with-malformed-metadata", DownloadSBOMOptions{Daemon: true, DestinationDir: ""})
   158  			h.AssertError(t, err, "unmarshalling label 'io.buildpacks.lifecycle.metadata'")
   159  		})
   160  	})
   161  }