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

     1  package writer_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"testing"
     9  
    10  	"github.com/Masterminds/semver"
    11  	"github.com/buildpacks/lifecycle/api"
    12  	"github.com/heroku/color"
    13  	"github.com/sclevine/spec"
    14  	"github.com/sclevine/spec/report"
    15  
    16  	pubbldr "github.com/buildpacks/pack/builder"
    17  	"github.com/buildpacks/pack/internal/builder"
    18  	"github.com/buildpacks/pack/internal/builder/writer"
    19  	"github.com/buildpacks/pack/internal/config"
    20  	"github.com/buildpacks/pack/pkg/client"
    21  	"github.com/buildpacks/pack/pkg/dist"
    22  	"github.com/buildpacks/pack/pkg/logging"
    23  	h "github.com/buildpacks/pack/testhelpers"
    24  )
    25  
    26  func TestJSON(t *testing.T) {
    27  	color.Disable(true)
    28  	defer color.Disable(false)
    29  	spec.Run(t, "Builder Writer", testJSON, spec.Parallel(), spec.Report(report.Terminal{}))
    30  }
    31  
    32  func testJSON(t *testing.T, when spec.G, it spec.S) {
    33  	const (
    34  		expectedRemoteRunImages = `"run_images": [
    35        {
    36          "name": "first/local",
    37          "user_configured": true
    38        },
    39        {
    40          "name": "second/local",
    41          "user_configured": true
    42        },
    43        {
    44          "name": "some/run-image"
    45        },
    46        {
    47          "name": "first/default"
    48        },
    49        {
    50          "name": "second/default"
    51        }
    52      ]`
    53  		expectedLocalRunImages = `"run_images": [
    54        {
    55          "name": "first/local",
    56          "user_configured": true
    57        },
    58        {
    59          "name": "second/local",
    60          "user_configured": true
    61        },
    62        {
    63          "name": "some/run-image"
    64        },
    65        {
    66          "name": "first/local-default"
    67        },
    68        {
    69          "name": "second/local-default"
    70        }
    71      ]`
    72  
    73  		expectedBuildpacks = `"buildpacks": [
    74        {
    75          "id": "test.top.nested",
    76          "version": "test.top.nested.version"
    77        },
    78        {
    79          "id": "test.nested",
    80          "homepage": "http://geocities.com/top-bp"
    81        },
    82        {
    83          "id": "test.bp.one",
    84          "version": "test.bp.one.version",
    85          "homepage": "http://geocities.com/cool-bp"
    86        },
    87        {
    88          "id": "test.bp.two",
    89          "version": "test.bp.two.version"
    90        },
    91        {
    92          "id": "test.bp.three",
    93          "version": "test.bp.three.version"
    94        }
    95      ]`
    96  
    97  		expectedExtensions = `"extensions": [
    98        {
    99          "homepage": "http://geocities.com/cool-bp",
   100          "id": "test.bp.one",
   101          "version": "test.bp.one.version"
   102        },
   103        {
   104          "id": "test.bp.two",
   105          "version": "test.bp.two.version"
   106        },
   107        {
   108          "id": "test.bp.three",
   109          "version": "test.bp.three.version"
   110        }
   111      ]`
   112  		expectedDetectionOrder = `"detection_order": [
   113        {
   114          "buildpacks": [
   115            {
   116              "id": "test.top.nested",
   117              "version": "test.top.nested.version",
   118              "buildpacks": [
   119                {
   120                  "id": "test.nested",
   121                  "homepage": "http://geocities.com/top-bp",
   122                  "buildpacks": [
   123                    {
   124                      "id": "test.bp.one",
   125                      "version": "test.bp.one.version",
   126                      "homepage": "http://geocities.com/cool-bp",
   127                      "optional": true
   128                    }
   129                  ]
   130                },
   131                {
   132                  "id": "test.bp.three",
   133                  "version": "test.bp.three.version",
   134                  "optional": true
   135                },
   136                {
   137                  "id": "test.nested.two",
   138                  "version": "test.nested.two.version",
   139                  "buildpacks": [
   140                    {
   141                      "id": "test.bp.one",
   142                      "version": "test.bp.one.version",
   143                      "homepage": "http://geocities.com/cool-bp",
   144                      "optional": true,
   145                      "cyclic": true
   146                    }
   147                  ]
   148                }
   149              ]
   150            },
   151            {
   152              "id": "test.bp.two",
   153              "version": "test.bp.two.version",
   154              "optional": true
   155            }
   156          ]
   157        },
   158        {
   159          "id": "test.bp.three",
   160          "version": "test.bp.three.version"
   161        }
   162      ]`
   163  		expectedOrderExtensions = `"order_extensions": [
   164  	  {
   165  		"id": "test.top.nested",
   166  		"version": "test.top.nested.version"
   167  	  },
   168  	  {
   169  		"homepage": "http://geocities.com/cool-bp",
   170  		"id": "test.bp.one",
   171  		"version": "test.bp.one.version",
   172  		"optional": true
   173  	  },
   174  	  {
   175  		"id": "test.bp.two",
   176  		"version": "test.bp.two.version",
   177  		"optional": true
   178  	  },
   179        {
   180          "id": "test.bp.three",
   181          "version": "test.bp.three.version"
   182        }
   183      ]`
   184  		expectedStackWithMixins = `"stack": {
   185        "id": "test.stack.id",
   186        "mixins": [
   187          "mixin1",
   188          "mixin2",
   189          "build:mixin3",
   190          "build:mixin4"
   191        ]
   192      }`
   193  	)
   194  
   195  	var (
   196  		assert = h.NewAssertionManager(t)
   197  		outBuf bytes.Buffer
   198  
   199  		remoteInfo *client.BuilderInfo
   200  		localInfo  *client.BuilderInfo
   201  
   202  		expectedRemoteInfo = fmt.Sprintf(`"remote_info": {
   203      "description": "Some remote description",
   204      "created_by": {
   205        "name": "Pack CLI",
   206        "version": "1.2.3"
   207      },
   208      "stack": {
   209        "id": "test.stack.id"
   210      },
   211      "lifecycle": {
   212        "version": "6.7.8",
   213        "buildpack_apis": {
   214          "deprecated": null,
   215          "supported": [
   216            "1.2",
   217            "2.3"
   218          ]
   219        },
   220        "platform_apis": {
   221          "deprecated": [
   222            "0.1",
   223            "1.2"
   224          ],
   225          "supported": [
   226            "4.5"
   227          ]
   228        }
   229      },
   230      %s,
   231      %s,
   232      %s,
   233  	%s,
   234  	%s
   235    }`, expectedRemoteRunImages, expectedBuildpacks, expectedDetectionOrder, expectedExtensions, expectedOrderExtensions)
   236  
   237  		expectedLocalInfo = fmt.Sprintf(`"local_info": {
   238      "description": "Some local description",
   239      "created_by": {
   240        "name": "Pack CLI",
   241        "version": "4.5.6"
   242      },
   243      "stack": {
   244        "id": "test.stack.id"
   245      },
   246      "lifecycle": {
   247        "version": "4.5.6",
   248        "buildpack_apis": {
   249          "deprecated": [
   250            "4.5",
   251            "6.7"
   252          ],
   253          "supported": [
   254            "8.9",
   255            "10.11"
   256          ]
   257        },
   258        "platform_apis": {
   259          "deprecated": null,
   260          "supported": [
   261            "7.8"
   262          ]
   263        }
   264      },
   265      %s,
   266      %s,
   267      %s,
   268  	%s,
   269  	%s
   270    }`, expectedLocalRunImages, expectedBuildpacks, expectedDetectionOrder, expectedExtensions, expectedOrderExtensions)
   271  
   272  		expectedPrettifiedJSON = fmt.Sprintf(`{
   273    "builder_name": "test-builder",
   274    "trusted": false,
   275    "default": false,
   276    %s,
   277    %s
   278  }
   279  `, expectedRemoteInfo, expectedLocalInfo)
   280  	)
   281  
   282  	when("Print", func() {
   283  		it.Before(func() {
   284  			remoteInfo = &client.BuilderInfo{
   285  				Description:     "Some remote description",
   286  				Stack:           "test.stack.id",
   287  				Mixins:          []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"},
   288  				RunImages:       []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/default", "second/default"}}},
   289  				Buildpacks:      buildpacks,
   290  				Order:           order,
   291  				Extensions:      extensions,
   292  				OrderExtensions: orderExtensions,
   293  				BuildpackLayers: dist.ModuleLayers{},
   294  				Lifecycle: builder.LifecycleDescriptor{
   295  					Info: builder.LifecycleInfo{
   296  						Version: &builder.Version{
   297  							Version: *semver.MustParse("6.7.8"),
   298  						},
   299  					},
   300  					APIs: builder.LifecycleAPIs{
   301  						Buildpack: builder.APIVersions{
   302  							Deprecated: nil,
   303  							Supported:  builder.APISet{api.MustParse("1.2"), api.MustParse("2.3")},
   304  						},
   305  						Platform: builder.APIVersions{
   306  							Deprecated: builder.APISet{api.MustParse("0.1"), api.MustParse("1.2")},
   307  							Supported:  builder.APISet{api.MustParse("4.5")},
   308  						},
   309  					},
   310  				},
   311  				CreatedBy: builder.CreatorMetadata{
   312  					Name:    "Pack CLI",
   313  					Version: "1.2.3",
   314  				},
   315  			}
   316  
   317  			localInfo = &client.BuilderInfo{
   318  				Description:     "Some local description",
   319  				Stack:           "test.stack.id",
   320  				Mixins:          []string{"mixin1", "mixin2", "build:mixin3", "build:mixin4"},
   321  				RunImages:       []pubbldr.RunImageConfig{{Image: "some/run-image", Mirrors: []string{"first/local-default", "second/local-default"}}},
   322  				Buildpacks:      buildpacks,
   323  				Order:           order,
   324  				Extensions:      extensions,
   325  				OrderExtensions: orderExtensions,
   326  				BuildpackLayers: dist.ModuleLayers{},
   327  				Lifecycle: builder.LifecycleDescriptor{
   328  					Info: builder.LifecycleInfo{
   329  						Version: &builder.Version{
   330  							Version: *semver.MustParse("4.5.6"),
   331  						},
   332  					},
   333  					APIs: builder.LifecycleAPIs{
   334  						Buildpack: builder.APIVersions{
   335  							Deprecated: builder.APISet{api.MustParse("4.5"), api.MustParse("6.7")},
   336  							Supported:  builder.APISet{api.MustParse("8.9"), api.MustParse("10.11")},
   337  						},
   338  						Platform: builder.APIVersions{
   339  							Deprecated: nil,
   340  							Supported:  builder.APISet{api.MustParse("7.8")},
   341  						},
   342  					},
   343  				},
   344  				CreatedBy: builder.CreatorMetadata{
   345  					Name:    "Pack CLI",
   346  					Version: "4.5.6",
   347  				},
   348  			}
   349  		})
   350  
   351  		it("prints both local remote builders as valid JSON", func() {
   352  			jsonWriter := writer.NewJSON()
   353  
   354  			logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   355  			err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   356  			assert.Nil(err)
   357  
   358  			prettyJSON, err := validPrettifiedJSONOutput(outBuf)
   359  			assert.Nil(err)
   360  
   361  			assert.ContainsJSON(prettyJSON, expectedPrettifiedJSON)
   362  		})
   363  
   364  		when("builder doesn't exist locally or remotely", func() {
   365  			it("returns an error", func() {
   366  				jsonWriter := writer.NewJSON()
   367  
   368  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   369  				err := jsonWriter.Print(logger, localRunImages, nil, nil, nil, nil, sharedBuilderInfo)
   370  				assert.ErrorWithMessage(err, "unable to find builder 'test-builder' locally or remotely")
   371  			})
   372  		})
   373  
   374  		when("builder doesn't exist locally", func() {
   375  			it("shows null for local builder, and normal output for remote", func() {
   376  				jsonWriter := writer.NewJSON()
   377  
   378  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   379  				err := jsonWriter.Print(logger, localRunImages, nil, remoteInfo, nil, nil, sharedBuilderInfo)
   380  				assert.Nil(err)
   381  
   382  				prettyJSON, err := validPrettifiedJSONOutput(outBuf)
   383  				assert.Nil(err)
   384  
   385  				assert.ContainsJSON(prettyJSON, `{"local_info": null}`)
   386  				assert.ContainsJSON(prettyJSON, fmt.Sprintf("{%s}", expectedRemoteInfo))
   387  			})
   388  		})
   389  
   390  		when("builder doesn't exist remotely", func() {
   391  			it("shows null for remote builder, and normal output for local", func() {
   392  				jsonWriter := writer.NewJSON()
   393  
   394  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   395  				err := jsonWriter.Print(logger, localRunImages, localInfo, nil, nil, nil, sharedBuilderInfo)
   396  				assert.Nil(err)
   397  
   398  				prettyJSON, err := validPrettifiedJSONOutput(outBuf)
   399  				assert.Nil(err)
   400  
   401  				assert.ContainsJSON(prettyJSON, `{"remote_info": null}`)
   402  				assert.ContainsJSON(prettyJSON, fmt.Sprintf("{%s}", expectedLocalInfo))
   403  			})
   404  		})
   405  
   406  		when("localErr is an error", func() {
   407  			it("returns the error, and doesn't write any json output", func() {
   408  				expectedErr := errors.New("failed to retrieve local info")
   409  
   410  				jsonWriter := writer.NewJSON()
   411  
   412  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   413  				err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, expectedErr, nil, sharedBuilderInfo)
   414  				assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve local info")
   415  
   416  				assert.Equal(outBuf.String(), "")
   417  			})
   418  		})
   419  
   420  		when("remoteErr is an error", func() {
   421  			it("returns the error, and doesn't write any json output", func() {
   422  				expectedErr := errors.New("failed to retrieve remote info")
   423  
   424  				jsonWriter := writer.NewJSON()
   425  
   426  				logger := logging.NewLogWithWriters(&outBuf, &outBuf)
   427  				err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, expectedErr, sharedBuilderInfo)
   428  				assert.ErrorWithMessage(err, "preparing output for 'test-builder': failed to retrieve remote info")
   429  
   430  				assert.Equal(outBuf.String(), "")
   431  			})
   432  		})
   433  
   434  		when("logger is verbose", func() {
   435  			it("displays mixins associated with the stack", func() {
   436  				jsonWriter := writer.NewJSON()
   437  
   438  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   439  				err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   440  				assert.Nil(err)
   441  
   442  				prettifiedJSON, err := validPrettifiedJSONOutput(outBuf)
   443  				assert.Nil(err)
   444  
   445  				assert.ContainsJSON(prettifiedJSON, fmt.Sprintf("{%s}", expectedStackWithMixins))
   446  			})
   447  		})
   448  
   449  		when("no run images are specified", func() {
   450  			it("displays run images as empty list", func() {
   451  				localInfo.RunImages = []pubbldr.RunImageConfig{}
   452  				remoteInfo.RunImages = []pubbldr.RunImageConfig{}
   453  				emptyLocalRunImages := []config.RunImage{}
   454  
   455  				jsonWriter := writer.NewJSON()
   456  
   457  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   458  				err := jsonWriter.Print(logger, emptyLocalRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   459  				assert.Nil(err)
   460  
   461  				prettifiedJSON, err := validPrettifiedJSONOutput(outBuf)
   462  				assert.Nil(err)
   463  
   464  				assert.ContainsJSON(prettifiedJSON, `{"run_images": []}`)
   465  			})
   466  		})
   467  
   468  		when("no buildpacks are specified", func() {
   469  			it("displays buildpacks as empty list", func() {
   470  				localInfo.Buildpacks = []dist.ModuleInfo{}
   471  				remoteInfo.Buildpacks = []dist.ModuleInfo{}
   472  
   473  				jsonWriter := writer.NewJSON()
   474  
   475  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   476  				err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   477  				assert.Nil(err)
   478  
   479  				prettifiedJSON, err := validPrettifiedJSONOutput(outBuf)
   480  				assert.Nil(err)
   481  
   482  				assert.ContainsJSON(prettifiedJSON, `{"buildpacks": []}`)
   483  			})
   484  		})
   485  
   486  		when("no detection order is specified", func() {
   487  			it("displays detection order as empty list", func() {
   488  				localInfo.Order = pubbldr.DetectionOrder{}
   489  				remoteInfo.Order = pubbldr.DetectionOrder{}
   490  
   491  				jsonWriter := writer.NewJSON()
   492  
   493  				logger := logging.NewLogWithWriters(&outBuf, &outBuf, logging.WithVerbose())
   494  				err := jsonWriter.Print(logger, localRunImages, localInfo, remoteInfo, nil, nil, sharedBuilderInfo)
   495  				assert.Nil(err)
   496  
   497  				prettifiedJSON, err := validPrettifiedJSONOutput(outBuf)
   498  				assert.Nil(err)
   499  
   500  				assert.ContainsJSON(prettifiedJSON, `{"detection_order": []}`)
   501  			})
   502  		})
   503  	})
   504  }
   505  
   506  func validPrettifiedJSONOutput(source bytes.Buffer) (string, error) {
   507  	err := json.Unmarshal(source.Bytes(), &struct{}{})
   508  	if err != nil {
   509  		return "", fmt.Errorf("failed to unmarshal to json: %w", err)
   510  	}
   511  
   512  	var prettifiedOutput bytes.Buffer
   513  	err = json.Indent(&prettifiedOutput, source.Bytes(), "", "  ")
   514  	if err != nil {
   515  		return "", fmt.Errorf("failed to prettify source json: %w", err)
   516  	}
   517  
   518  	return prettifiedOutput.String(), nil
   519  }