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 }