github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/inspectimage/writer/structured_bom_format_test.go (about)

     1  package writer_test
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"testing"
     8  
     9  	"github.com/buildpacks/lifecycle/buildpack"
    10  	"github.com/heroku/color"
    11  	"github.com/sclevine/spec"
    12  	"github.com/sclevine/spec/report"
    13  
    14  	"github.com/buildpacks/pack/internal/config"
    15  	"github.com/buildpacks/pack/internal/inspectimage"
    16  	"github.com/buildpacks/pack/internal/inspectimage/writer"
    17  	"github.com/buildpacks/pack/pkg/client"
    18  	"github.com/buildpacks/pack/pkg/dist"
    19  	"github.com/buildpacks/pack/pkg/logging"
    20  	h "github.com/buildpacks/pack/testhelpers"
    21  )
    22  
    23  func TestStructuredBOMFormat(t *testing.T) {
    24  	color.Disable(true)
    25  	defer color.Disable(false)
    26  	spec.Run(t, "StructuredBOMFormat Writer", testStructuredBOMFormat, spec.Parallel(), spec.Report(report.Terminal{}))
    27  }
    28  
    29  func testStructuredBOMFormat(t *testing.T, when spec.G, it spec.S) {
    30  	var (
    31  		assert = h.NewAssertionManager(t)
    32  		outBuf *bytes.Buffer
    33  
    34  		remoteInfo              *client.ImageInfo
    35  		localInfo               *client.ImageInfo
    36  		remoteWithExtensionInfo *client.ImageInfo
    37  		localWithExtensionInfo  *client.ImageInfo
    38  		generalInfo             inspectimage.GeneralInfo
    39  		logger                  *logging.LogWithWriters
    40  	)
    41  
    42  	when("Print", func() {
    43  		it.Before(func() {
    44  			outBuf = bytes.NewBuffer(nil)
    45  			logger = logging.NewLogWithWriters(outBuf, outBuf)
    46  			remoteInfo = &client.ImageInfo{
    47  				BOM: []buildpack.BOMEntry{
    48  					{
    49  						Require: buildpack.Require{
    50  							Name:    "remote-require",
    51  							Version: "1.2.3",
    52  							Metadata: map[string]interface{}{
    53  								"cool-remote": "beans",
    54  							},
    55  						},
    56  						Buildpack: buildpack.GroupElement{
    57  							ID:      "remote-buildpack",
    58  							Version: "remote-buildpack-version",
    59  						},
    60  					},
    61  				},
    62  			}
    63  			localInfo = &client.ImageInfo{
    64  				BOM: []buildpack.BOMEntry{
    65  					{
    66  						Require: buildpack.Require{
    67  							Name:    "local-require",
    68  							Version: "4.5.6",
    69  							Metadata: map[string]interface{}{
    70  								"cool-local": "beans",
    71  							},
    72  						},
    73  						Buildpack: buildpack.GroupElement{
    74  							ID:      "local-buildpack",
    75  							Version: "local-buildpack-version",
    76  						},
    77  					},
    78  				},
    79  			}
    80  
    81  			remoteWithExtensionInfo = &client.ImageInfo{
    82  				BOM: []buildpack.BOMEntry{
    83  					{
    84  						Require: buildpack.Require{
    85  							Name:    "remote-require",
    86  							Version: "1.2.3",
    87  							Metadata: map[string]interface{}{
    88  								"cool-remote": "beans",
    89  							},
    90  						},
    91  						Buildpack: buildpack.GroupElement{
    92  							ID:      "remote-buildpack",
    93  							Version: "remote-buildpack-version",
    94  						},
    95  					},
    96  				},
    97  			}
    98  			localWithExtensionInfo = &client.ImageInfo{
    99  				BOM: []buildpack.BOMEntry{
   100  					{
   101  						Require: buildpack.Require{
   102  							Name:    "local-require",
   103  							Version: "4.5.6",
   104  							Metadata: map[string]interface{}{
   105  								"cool-local": "beans",
   106  							},
   107  						},
   108  						Buildpack: buildpack.GroupElement{
   109  							ID:      "local-buildpack",
   110  							Version: "local-buildpack-version",
   111  						},
   112  					},
   113  				},
   114  			}
   115  
   116  			generalInfo = inspectimage.GeneralInfo{
   117  				Name: "some-image-name",
   118  				RunImageMirrors: []config.RunImage{
   119  					{
   120  						Image:   "some-run-image",
   121  						Mirrors: []string{"first-mirror", "second-mirror"},
   122  					},
   123  				},
   124  			}
   125  		})
   126  
   127  		when("structured output", func() {
   128  			var (
   129  				localBomDisplay               []inspectimage.BOMEntryDisplay
   130  				remoteBomDisplay              []inspectimage.BOMEntryDisplay
   131  				localBomWithExtensionDisplay  []inspectimage.BOMEntryDisplay
   132  				remoteBomWithExtensionDisplay []inspectimage.BOMEntryDisplay
   133  			)
   134  			it.Before(func() {
   135  				localBomDisplay = []inspectimage.BOMEntryDisplay{{
   136  					Name:    "local-require",
   137  					Version: "4.5.6",
   138  					Metadata: map[string]interface{}{
   139  						"cool-local": "beans",
   140  					},
   141  					Buildpack: dist.ModuleRef{
   142  						ModuleInfo: dist.ModuleInfo{
   143  							ID:      "local-buildpack",
   144  							Version: "local-buildpack-version",
   145  						},
   146  					},
   147  				}}
   148  				remoteBomDisplay = []inspectimage.BOMEntryDisplay{{
   149  					Name:    "remote-require",
   150  					Version: "1.2.3",
   151  					Metadata: map[string]interface{}{
   152  						"cool-remote": "beans",
   153  					},
   154  					Buildpack: dist.ModuleRef{
   155  						ModuleInfo: dist.ModuleInfo{
   156  							ID:      "remote-buildpack",
   157  							Version: "remote-buildpack-version",
   158  						},
   159  					},
   160  				}}
   161  
   162  				localBomWithExtensionDisplay = []inspectimage.BOMEntryDisplay{{
   163  					Name:    "local-require",
   164  					Version: "4.5.6",
   165  					Metadata: map[string]interface{}{
   166  						"cool-local": "beans",
   167  					},
   168  					Buildpack: dist.ModuleRef{
   169  						ModuleInfo: dist.ModuleInfo{
   170  							ID:      "local-buildpack",
   171  							Version: "local-buildpack-version",
   172  						},
   173  					},
   174  				}}
   175  				remoteBomWithExtensionDisplay = []inspectimage.BOMEntryDisplay{{
   176  					Name:    "remote-require",
   177  					Version: "1.2.3",
   178  					Metadata: map[string]interface{}{
   179  						"cool-remote": "beans",
   180  					},
   181  					Buildpack: dist.ModuleRef{
   182  						ModuleInfo: dist.ModuleInfo{
   183  							ID:      "remote-buildpack",
   184  							Version: "remote-buildpack-version",
   185  						},
   186  					},
   187  				}}
   188  			})
   189  			it("passes correct info to structuredBOMWriter", func() {
   190  				var marshalInput interface{}
   191  
   192  				structuredBOMWriter := writer.StructuredBOMFormat{
   193  					MarshalFunc: func(i interface{}) ([]byte, error) {
   194  						marshalInput = i
   195  						return []byte("marshalled"), nil
   196  					},
   197  				}
   198  
   199  				err := structuredBOMWriter.Print(logger, generalInfo, localInfo, remoteInfo, nil, nil)
   200  				assert.Nil(err)
   201  
   202  				assert.Equal(marshalInput, inspectimage.BOMDisplay{
   203  					Remote: remoteBomDisplay,
   204  					Local:  localBomDisplay,
   205  				})
   206  			})
   207  
   208  			it("passes correct info to structuredBOMWriter", func() {
   209  				var marshalInput interface{}
   210  
   211  				structuredBOMWriter := writer.StructuredBOMFormat{
   212  					MarshalFunc: func(i interface{}) ([]byte, error) {
   213  						marshalInput = i
   214  						return []byte("marshalled"), nil
   215  					},
   216  				}
   217  
   218  				err := structuredBOMWriter.Print(logger, generalInfo, localWithExtensionInfo, remoteWithExtensionInfo, nil, nil)
   219  				assert.Nil(err)
   220  
   221  				assert.Equal(marshalInput, inspectimage.BOMDisplay{
   222  					Remote: remoteBomWithExtensionDisplay,
   223  					Local:  localBomWithExtensionDisplay,
   224  				})
   225  			})
   226  			when("a localErr is passed to Print", func() {
   227  				it("still marshals remote information", func() {
   228  					var marshalInput interface{}
   229  
   230  					localErr := errors.New("a local error occurred")
   231  					structuredBOMWriter := writer.StructuredBOMFormat{
   232  						MarshalFunc: func(i interface{}) ([]byte, error) {
   233  							marshalInput = i
   234  							return []byte("marshalled"), nil
   235  						},
   236  					}
   237  
   238  					err := structuredBOMWriter.Print(logger, generalInfo, nil, remoteInfo, localErr, nil)
   239  					assert.Nil(err)
   240  
   241  					assert.Equal(marshalInput, inspectimage.BOMDisplay{
   242  						Remote:   remoteBomDisplay,
   243  						Local:    nil,
   244  						LocalErr: localErr.Error(),
   245  					})
   246  				})
   247  			})
   248  
   249  			when("a localErr is passed to Print", func() {
   250  				it("still marshals remote information", func() {
   251  					var marshalInput interface{}
   252  
   253  					localErr := errors.New("a local error occurred")
   254  					structuredBOMWriter := writer.StructuredBOMFormat{
   255  						MarshalFunc: func(i interface{}) ([]byte, error) {
   256  							marshalInput = i
   257  							return []byte("marshalled"), nil
   258  						},
   259  					}
   260  
   261  					err := structuredBOMWriter.Print(logger, generalInfo, nil, remoteWithExtensionInfo, localErr, nil)
   262  					assert.Nil(err)
   263  
   264  					assert.Equal(marshalInput, inspectimage.BOMDisplay{
   265  						Remote:   remoteBomWithExtensionDisplay,
   266  						Local:    nil,
   267  						LocalErr: localErr.Error(),
   268  					})
   269  				})
   270  			})
   271  
   272  			when("a remoteErr is passed to Print", func() {
   273  				it("still marshals local information", func() {
   274  					var marshalInput interface{}
   275  
   276  					remoteErr := errors.New("a remote error occurred")
   277  					structuredBOMWriter := writer.StructuredBOMFormat{
   278  						MarshalFunc: func(i interface{}) ([]byte, error) {
   279  							marshalInput = i
   280  							return []byte("marshalled"), nil
   281  						},
   282  					}
   283  
   284  					err := structuredBOMWriter.Print(logger, generalInfo, localInfo, nil, nil, remoteErr)
   285  					assert.Nil(err)
   286  
   287  					assert.Equal(marshalInput, inspectimage.BOMDisplay{
   288  						Remote:    nil,
   289  						Local:     localBomDisplay,
   290  						RemoteErr: remoteErr.Error(),
   291  					})
   292  				})
   293  			})
   294  
   295  			when("a remoteErr is passed to Print", func() {
   296  				it("still marshals local information", func() {
   297  					var marshalInput interface{}
   298  
   299  					remoteErr := errors.New("a remote error occurred")
   300  					structuredBOMWriter := writer.StructuredBOMFormat{
   301  						MarshalFunc: func(i interface{}) ([]byte, error) {
   302  							marshalInput = i
   303  							return []byte("marshalled"), nil
   304  						},
   305  					}
   306  
   307  					err := structuredBOMWriter.Print(logger, generalInfo, localWithExtensionInfo, nil, nil, remoteErr)
   308  					assert.Nil(err)
   309  
   310  					assert.Equal(marshalInput, inspectimage.BOMDisplay{
   311  						Remote:    nil,
   312  						Local:     localBomWithExtensionDisplay,
   313  						RemoteErr: remoteErr.Error(),
   314  					})
   315  				})
   316  			})
   317  		})
   318  
   319  		// Just test error cases, all error-free cases will be tested in JSON, TOML, and YAML subclasses.
   320  		when("failure cases", func() {
   321  			when("both info objects are nil", func() {
   322  				it("displays a 'missing image' error message'", func() {
   323  					structuredBOMWriter := writer.StructuredBOMFormat{
   324  						MarshalFunc: testMarshalFunc,
   325  					}
   326  
   327  					err := structuredBOMWriter.Print(logger, generalInfo, nil, nil, nil, nil)
   328  					assert.ErrorWithMessage(err, fmt.Sprintf("unable to find image '%s' locally or remotely", "some-image-name"))
   329  				})
   330  			})
   331  			when("fetching local and remote info errors", func() {
   332  				it("returns an error", func() {
   333  					structuredBOMWriter := writer.StructuredBOMFormat{
   334  						MarshalFunc: func(i interface{}) ([]byte, error) {
   335  							return []byte("cool"), nil
   336  						},
   337  					}
   338  					remoteErr := errors.New("a remote error occurred")
   339  					localErr := errors.New("a local error occurred")
   340  
   341  					err := structuredBOMWriter.Print(logger, generalInfo, localInfo, remoteInfo, localErr, remoteErr)
   342  					assert.ErrorContains(err, remoteErr.Error())
   343  					assert.ErrorContains(err, localErr.Error())
   344  				})
   345  			})
   346  
   347  			when("fetching local and remote info errors", func() {
   348  				it("returns an error", func() {
   349  					structuredBOMWriter := writer.StructuredBOMFormat{
   350  						MarshalFunc: func(i interface{}) ([]byte, error) {
   351  							return []byte("cool"), nil
   352  						},
   353  					}
   354  					remoteErr := errors.New("a remote error occurred")
   355  					localErr := errors.New("a local error occurred")
   356  
   357  					err := structuredBOMWriter.Print(logger, generalInfo, localWithExtensionInfo, remoteWithExtensionInfo, localErr, remoteErr)
   358  					assert.ErrorContains(err, remoteErr.Error())
   359  					assert.ErrorContains(err, localErr.Error())
   360  				})
   361  			})
   362  		})
   363  	})
   364  }