github.com/buildpacks/libcnb@v1.30.3/build_test.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package libcnb_test
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"testing"
    25  	"text/template"
    26  
    27  	. "github.com/onsi/gomega"
    28  	"github.com/sclevine/spec"
    29  	"github.com/stretchr/testify/mock"
    30  
    31  	"github.com/buildpacks/libcnb"
    32  	"github.com/buildpacks/libcnb/internal"
    33  	"github.com/buildpacks/libcnb/mocks"
    34  )
    35  
    36  func testBuild(t *testing.T, context spec.G, it spec.S) {
    37  	var (
    38  		Expect = NewWithT(t).Expect
    39  
    40  		applicationPath   string
    41  		builder           *mocks.Builder
    42  		buildpackPath     string
    43  		buildpackPlanPath string
    44  		bpTOMLContents    string
    45  		commandPath       string
    46  		environmentWriter *mocks.EnvironmentWriter
    47  		exitHandler       *mocks.ExitHandler
    48  		layerContributor  *mocks.LayerContributor
    49  		layersPath        string
    50  		platformPath      string
    51  		tomlWriter        *mocks.TOMLWriter
    52  		buildpackTOML     *template.Template
    53  
    54  		workingDir string
    55  	)
    56  
    57  	it.Before(func() {
    58  		var err error
    59  
    60  		applicationPath = t.TempDir()
    61  		applicationPath, err = filepath.EvalSymlinks(applicationPath)
    62  		Expect(err).NotTo(HaveOccurred())
    63  
    64  		builder = &mocks.Builder{}
    65  
    66  		buildpackPath = t.TempDir()
    67  		Expect(os.Setenv("CNB_BUILDPACK_DIR", buildpackPath)).To(Succeed())
    68  
    69  		bpTOMLContents = `
    70  api = "{{.APIVersion}}"
    71  
    72  [buildpack]
    73  id = "test-id"
    74  name = "test-name"
    75  version = "1.1.1"
    76  clear-env = true
    77  description = "A test buildpack"
    78  keywords = ["test", "buildpack"]
    79  
    80  [[buildpack.licenses]]
    81  type = "Apache-2.0"
    82  uri = "https://spdx.org/licenses/Apache-2.0.html"
    83  
    84  [[buildpack.licenses]]
    85  type = "Apache-1.1"
    86  uri = "https://spdx.org/licenses/Apache-1.1.html"
    87  
    88  [[order]]
    89  [[order.group]]
    90  id = "test-id"
    91  version = "2.2.2"
    92  optional = true
    93  
    94  [[stacks]]
    95  id = "test-id"
    96  mixins = ["test-name"]
    97  
    98  [metadata]
    99  test-key = "test-value"
   100  `
   101  		buildpackTOML, err = template.New("buildpack.toml").Parse(bpTOMLContents)
   102  		Expect(err).ToNot(HaveOccurred())
   103  
   104  		var b bytes.Buffer
   105  		err = buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.6"})
   106  		Expect(err).ToNot(HaveOccurred())
   107  
   108  		Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed())
   109  
   110  		f, err := os.CreateTemp("", "build-buildpackplan-path")
   111  		Expect(err).NotTo(HaveOccurred())
   112  		Expect(f.Close()).NotTo(HaveOccurred())
   113  		buildpackPlanPath = f.Name()
   114  
   115  		Expect(os.WriteFile(buildpackPlanPath,
   116  			[]byte(`
   117  [[entries]]
   118  name = "test-name"
   119  version = "test-version"
   120  
   121  [entries.metadata]
   122  test-key = "test-value"
   123  `),
   124  			0600),
   125  		).To(Succeed())
   126  
   127  		commandPath = filepath.Join("bin", "build")
   128  
   129  		environmentWriter = &mocks.EnvironmentWriter{}
   130  		environmentWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
   131  
   132  		exitHandler = &mocks.ExitHandler{}
   133  		exitHandler.On("Error", mock.Anything)
   134  
   135  		layerContributor = &mocks.LayerContributor{}
   136  
   137  		layersPath = t.TempDir()
   138  
   139  		Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"),
   140  			[]byte(`
   141  [metadata]
   142  test-key = "test-value"
   143  `),
   144  			0600),
   145  		).To(Succeed())
   146  
   147  		platformPath = t.TempDir()
   148  
   149  		Expect(os.MkdirAll(filepath.Join(platformPath, "bindings", "alpha"), 0755)).To(Succeed())
   150  		Expect(os.WriteFile(filepath.Join(platformPath, "bindings", "alpha", "test-secret-key"),
   151  			[]byte("test-secret-value"), 0600)).To(Succeed())
   152  
   153  		Expect(os.MkdirAll(filepath.Join(platformPath, "env"), 0755)).To(Succeed())
   154  		Expect(os.WriteFile(filepath.Join(platformPath, "env", "TEST_ENV"), []byte("test-value"), 0600)).
   155  			To(Succeed())
   156  
   157  		tomlWriter = &mocks.TOMLWriter{}
   158  		tomlWriter.On("Write", mock.Anything, mock.Anything).Return(nil)
   159  
   160  		Expect(os.Setenv("CNB_STACK_ID", "test-stack-id")).To(Succeed())
   161  		Expect(os.Setenv("CNB_LAYERS_DIR", layersPath)).To(Succeed())
   162  		Expect(os.Setenv("CNB_PLATFORM_DIR", platformPath)).To(Succeed())
   163  		Expect(os.Setenv("CNB_BP_PLAN_PATH", buildpackPlanPath)).To(Succeed())
   164  
   165  		workingDir, err = os.Getwd()
   166  		Expect(err).NotTo(HaveOccurred())
   167  		Expect(os.Chdir(applicationPath)).To(Succeed())
   168  	})
   169  
   170  	it.After(func() {
   171  		Expect(os.Chdir(workingDir)).To(Succeed())
   172  		Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
   173  		Expect(os.Unsetenv("CNB_STACK_ID")).To(Succeed())
   174  		Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
   175  		Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
   176  		Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed())
   177  
   178  		Expect(os.RemoveAll(applicationPath)).To(Succeed())
   179  		Expect(os.RemoveAll(buildpackPath)).To(Succeed())
   180  		Expect(os.RemoveAll(buildpackPlanPath)).To(Succeed())
   181  		Expect(os.RemoveAll(layersPath)).To(Succeed())
   182  		Expect(os.RemoveAll(platformPath)).To(Succeed())
   183  	})
   184  
   185  	context("buildpack API is not within the supported range", func() {
   186  		it.Before(func() {
   187  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   188  				[]byte(`
   189  api = "0.4"
   190  
   191  [buildpack]
   192  id = "test-id"
   193  name = "test-name"
   194  version = "1.1.1"
   195  `),
   196  				0600),
   197  			).To(Succeed())
   198  		})
   199  
   200  		it("fails", func() {
   201  			libcnb.Build(builder,
   202  				libcnb.WithBOMLabel(true),
   203  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   204  				libcnb.WithExitHandler(exitHandler),
   205  			)
   206  
   207  			Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
   208  				fmt.Sprintf("this version of libcnb is only compatible with buildpack APIs >= %s, <= %s", libcnb.MinSupportedBPVersion, libcnb.MaxSupportedBPVersion),
   209  			))
   210  		})
   211  	})
   212  
   213  	context("errors if required env vars are not set for buildpack API >=0.8", func() {
   214  		for _, e := range []string{"CNB_LAYERS_DIR", "CNB_PLATFORM_DIR", "CNB_BP_PLAN_PATH"} {
   215  			// We need to do this assignment because of the way that spec binds variables
   216  			envVar := e
   217  			context(fmt.Sprintf("when %s is unset", envVar), func() {
   218  				it.Before(func() {
   219  					Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   220  						[]byte(`
   221  		api = "0.8"
   222  		
   223  		[buildpack]
   224  		id = "test-id"
   225  		name = "test-name"
   226  		version = "1.1.1"
   227  		`),
   228  						0600),
   229  					).To(Succeed())
   230  					os.Unsetenv(envVar)
   231  				})
   232  
   233  				it("fails", func() {
   234  					libcnb.Build(builder,
   235  						libcnb.WithBOMLabel(true),
   236  						libcnb.WithArguments([]string{commandPath}),
   237  						libcnb.WithExitHandler(exitHandler),
   238  					)
   239  					Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError(
   240  						fmt.Sprintf("expected %s to be set", envVar),
   241  					))
   242  				})
   243  			})
   244  		}
   245  	})
   246  
   247  	it("encounters the wrong number of arguments", func() {
   248  		builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
   249  
   250  		libcnb.Build(builder,
   251  			libcnb.WithBOMLabel(true),
   252  			libcnb.WithArguments([]string{commandPath}),
   253  			libcnb.WithExitHandler(exitHandler),
   254  		)
   255  
   256  		Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("expected 3 arguments and received 0"))
   257  	})
   258  
   259  	context("when BP API >= 0.8", func() {
   260  		it.Before(func() {
   261  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   262  				[]byte(`
   263  	api = "0.8"
   264  	
   265  	[buildpack]
   266  	id = "test-id"
   267  	name = "test-name"
   268  	version = "1.1.1"
   269  	`),
   270  				0600),
   271  			).To(Succeed())
   272  		})
   273  
   274  		it("creates context", func() {
   275  			builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
   276  
   277  			libcnb.Build(builder,
   278  				libcnb.WithBOMLabel(true),
   279  				libcnb.WithArguments([]string{commandPath}),
   280  			)
   281  
   282  			ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext)
   283  			Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath}))
   284  			Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
   285  				API: "0.8",
   286  				Info: libcnb.BuildpackInfo{
   287  					ID:      "test-id",
   288  					Name:    "test-name",
   289  					Version: "1.1.1",
   290  				},
   291  				Path: buildpackPath,
   292  			}))
   293  			Expect(ctx.Layers).To(Equal(libcnb.Layers{Path: layersPath}))
   294  			Expect(ctx.PersistentMetadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
   295  			Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{
   296  				Entries: []libcnb.BuildpackPlanEntry{
   297  					{
   298  						Name: "test-name",
   299  						Metadata: map[string]interface{}{
   300  							"test-key": "test-value",
   301  						},
   302  					},
   303  				},
   304  			}))
   305  			Expect(ctx.Platform).To(Equal(libcnb.Platform{
   306  				Bindings: libcnb.Bindings{
   307  					libcnb.Binding{
   308  						Name: "alpha",
   309  						Path: filepath.Join(platformPath, "bindings", "alpha"),
   310  						Secret: map[string]string{
   311  							"test-secret-key": "test-secret-value",
   312  						},
   313  					},
   314  				},
   315  				Environment: map[string]string{"TEST_ENV": "test-value"},
   316  				Path:        platformPath,
   317  			}))
   318  			Expect(ctx.StackID).To(Equal("test-stack-id"))
   319  		})
   320  	})
   321  
   322  	context("when BP API < 0.8", func() {
   323  		it.Before(func() {
   324  			Expect(os.Unsetenv("CNB_PLATFORM_DIR")).To(Succeed())
   325  			Expect(os.Unsetenv("CNB_BP_PLAN_PATH")).To(Succeed())
   326  			Expect(os.Unsetenv("CNB_LAYERS_DIR")).To(Succeed())
   327  		})
   328  
   329  		it("creates context", func() {
   330  			builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
   331  
   332  			libcnb.Build(builder,
   333  				libcnb.WithBOMLabel(true),
   334  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   335  			)
   336  
   337  			ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext)
   338  			Expect(ctx.Application).To(Equal(libcnb.Application{Path: applicationPath}))
   339  			Expect(ctx.Buildpack).To(Equal(libcnb.Buildpack{
   340  				API: "0.6",
   341  				Info: libcnb.BuildpackInfo{
   342  					ID:               "test-id",
   343  					Name:             "test-name",
   344  					Version:          "1.1.1",
   345  					ClearEnvironment: true,
   346  					Description:      "A test buildpack",
   347  					Keywords:         []string{"test", "buildpack"},
   348  					Licenses: []libcnb.License{
   349  						{Type: "Apache-2.0", URI: "https://spdx.org/licenses/Apache-2.0.html"},
   350  						{Type: "Apache-1.1", URI: "https://spdx.org/licenses/Apache-1.1.html"},
   351  					},
   352  				},
   353  				Path: buildpackPath,
   354  				Stacks: []libcnb.BuildpackStack{
   355  					{
   356  						ID:     "test-id",
   357  						Mixins: []string{"test-name"},
   358  					},
   359  				},
   360  				Metadata: map[string]interface{}{"test-key": "test-value"},
   361  			}))
   362  			Expect(ctx.Layers).To(Equal(libcnb.Layers{Path: layersPath}))
   363  			Expect(ctx.PersistentMetadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
   364  			Expect(ctx.Plan).To(Equal(libcnb.BuildpackPlan{
   365  				Entries: []libcnb.BuildpackPlanEntry{
   366  					{
   367  						Name: "test-name",
   368  						Metadata: map[string]interface{}{
   369  							"test-key": "test-value",
   370  						},
   371  					},
   372  				},
   373  			}))
   374  			Expect(ctx.Platform).To(Equal(libcnb.Platform{
   375  				Bindings: libcnb.Bindings{
   376  					libcnb.Binding{
   377  						Name: "alpha",
   378  						Path: filepath.Join(platformPath, "bindings", "alpha"),
   379  						Secret: map[string]string{
   380  							"test-secret-key": "test-secret-value",
   381  						},
   382  					},
   383  				},
   384  				Environment: map[string]string{"TEST_ENV": "test-value"},
   385  				Path:        platformPath,
   386  			}))
   387  			Expect(ctx.StackID).To(Equal("test-stack-id"))
   388  		})
   389  	})
   390  
   391  	it("extracts buildpack path from command path if CNB_BUILDPACK_PATH is not set", func() {
   392  		Expect(os.Unsetenv("CNB_BUILDPACK_DIR")).To(Succeed())
   393  
   394  		builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
   395  
   396  		libcnb.Build(builder,
   397  			libcnb.WithBOMLabel(true),
   398  			libcnb.WithArguments([]string{filepath.Join(buildpackPath, commandPath), layersPath, platformPath, buildpackPlanPath}),
   399  		)
   400  
   401  		ctx := builder.Calls[0].Arguments[0].(libcnb.BuildContext)
   402  
   403  		Expect(ctx.Buildpack.Path).To(Equal(buildpackPath))
   404  	})
   405  
   406  	it("handles error from BuildFunc", func() {
   407  		builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), fmt.Errorf("test-error"))
   408  
   409  		libcnb.Build(builder,
   410  			libcnb.WithBOMLabel(true),
   411  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   412  			libcnb.WithExitHandler(exitHandler),
   413  		)
   414  
   415  		Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("test-error"))
   416  	})
   417  
   418  	it("calls layer contributor", func() {
   419  		layerContributor.On("Contribute", mock.Anything).Return(libcnb.Layer{}, nil)
   420  		layerContributor.On("Name").Return("test-name")
   421  		builder.On("Build", mock.Anything).
   422  			Return(libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}, nil)
   423  
   424  		libcnb.Build(builder,
   425  			libcnb.WithBOMLabel(true),
   426  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   427  			libcnb.WithTOMLWriter(tomlWriter),
   428  		)
   429  
   430  		Expect(layerContributor.Calls).To(HaveLen(2))
   431  	})
   432  
   433  	it("writes env.build", func() {
   434  		layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), BuildEnvironment: libcnb.Environment{}}
   435  		layer.BuildEnvironment.Defaultf("test-build", "test-%s", "value")
   436  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   437  		layerContributor.On("Name").Return("test-name")
   438  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   439  		builder.On("Build", mock.Anything).Return(result, nil)
   440  
   441  		libcnb.Build(builder,
   442  			libcnb.WithBOMLabel(true),
   443  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   444  			libcnb.WithEnvironmentWriter(environmentWriter),
   445  		)
   446  
   447  		Expect(environmentWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env.build")))
   448  		Expect(environmentWriter.Calls[0].Arguments[1]).To(Equal(map[string]string{"test-build.default": "test-value"}))
   449  	})
   450  
   451  	it("writes env.launch", func() {
   452  		layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), LaunchEnvironment: libcnb.Environment{}}
   453  		layer.LaunchEnvironment.Defaultf("test-launch", "test-%s", "value")
   454  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   455  		layerContributor.On("Name").Return("test-name")
   456  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   457  		builder.On("Build", mock.Anything).Return(result, nil)
   458  
   459  		libcnb.Build(builder,
   460  			libcnb.WithBOMLabel(true),
   461  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   462  			libcnb.WithEnvironmentWriter(environmentWriter),
   463  		)
   464  
   465  		Expect(environmentWriter.Calls[1].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env.launch")))
   466  		Expect(environmentWriter.Calls[1].Arguments[1]).To(Equal(map[string]string{"test-launch.default": "test-value"}))
   467  	})
   468  
   469  	it("writes env", func() {
   470  		layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), SharedEnvironment: libcnb.Environment{}}
   471  		layer.SharedEnvironment.Defaultf("test-shared", "test-%s", "value")
   472  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   473  		layerContributor.On("Name").Return("test-name")
   474  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   475  		builder.On("Build", mock.Anything).Return(result, nil)
   476  
   477  		libcnb.Build(builder,
   478  			libcnb.WithBOMLabel(true),
   479  
   480  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   481  			libcnb.WithEnvironmentWriter(environmentWriter),
   482  		)
   483  
   484  		Expect(environmentWriter.Calls[2].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "env")))
   485  		Expect(environmentWriter.Calls[2].Arguments[1]).To(Equal(map[string]string{"test-shared.default": "test-value"}))
   486  	})
   487  
   488  	it("writes profile.d", func() {
   489  		layer := libcnb.Layer{Path: filepath.Join(layersPath, "test-name"), Profile: libcnb.Profile{}}
   490  		layer.Profile.Addf("test-profile", "test-%s", "value")
   491  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   492  		layerContributor.On("Name").Return("test-name")
   493  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   494  		builder.On("Build", mock.Anything).Return(result, nil)
   495  
   496  		libcnb.Build(builder,
   497  			libcnb.WithBOMLabel(true),
   498  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   499  			libcnb.WithEnvironmentWriter(environmentWriter),
   500  		)
   501  
   502  		Expect(environmentWriter.Calls[3].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name", "profile.d")))
   503  		Expect(environmentWriter.Calls[3].Arguments[1]).To(Equal(map[string]string{"test-profile": "test-value"}))
   504  	})
   505  
   506  	it("writes 0.5 layer metadata", func() {
   507  		var b bytes.Buffer
   508  		err := buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.5"})
   509  		Expect(err).ToNot(HaveOccurred())
   510  
   511  		Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed())
   512  
   513  		layer := libcnb.Layer{
   514  			Name: "test-name",
   515  			Path: filepath.Join(layersPath, "test-name"),
   516  			LayerTypes: libcnb.LayerTypes{
   517  				Build:  true,
   518  				Cache:  true,
   519  				Launch: true,
   520  			},
   521  			Metadata: map[string]interface{}{"test-key": "test-value"},
   522  		}
   523  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   524  		layerContributor.On("Name").Return("test-name")
   525  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   526  		builder.On("Build", mock.Anything).Return(result, nil)
   527  
   528  		libcnb.Build(builder,
   529  			libcnb.WithBOMLabel(true),
   530  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   531  			libcnb.WithTOMLWriter(tomlWriter),
   532  		)
   533  
   534  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name.toml")))
   535  
   536  		layer5, ok := tomlWriter.Calls[0].Arguments[1].(internal.LayerAPI5)
   537  		Expect(ok).To(BeTrue())
   538  		Expect(layer5.Build).To(BeTrue())
   539  		Expect(layer5.Cache).To(BeTrue())
   540  		Expect(layer5.Launch).To(BeTrue())
   541  		Expect(layer5.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
   542  	})
   543  
   544  	it("writes 0.6 layer metadata", func() {
   545  		layer := libcnb.Layer{
   546  			Name: "test-name",
   547  			Path: filepath.Join(layersPath, "test-name"),
   548  			LayerTypes: libcnb.LayerTypes{
   549  				Build:  true,
   550  				Cache:  true,
   551  				Launch: true,
   552  			},
   553  			Metadata: map[string]interface{}{"test-key": "test-value"},
   554  		}
   555  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   556  		layerContributor.On("Name").Return("test-name")
   557  		result := libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}
   558  		builder.On("Build", mock.Anything).Return(result, nil)
   559  
   560  		libcnb.Build(builder,
   561  			libcnb.WithBOMLabel(true),
   562  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   563  			libcnb.WithTOMLWriter(tomlWriter),
   564  		)
   565  
   566  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "test-name.toml")))
   567  
   568  		layer, ok := tomlWriter.Calls[0].Arguments[1].(libcnb.Layer)
   569  		Expect(ok).To(BeTrue())
   570  		Expect(layer.LayerTypes.Build).To(BeTrue())
   571  		Expect(layer.LayerTypes.Cache).To(BeTrue())
   572  		Expect(layer.LayerTypes.Launch).To(BeTrue())
   573  		Expect(layer.Metadata).To(Equal(map[string]interface{}{"test-key": "test-value"}))
   574  	})
   575  
   576  	it("writes launch.toml", func() {
   577  		builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   578  			BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{
   579  				{
   580  					Name:     "test-launch-bom-entry",
   581  					Metadata: map[string]interface{}{"test-key": "test-value"},
   582  					Launch:   true,
   583  				},
   584  				{
   585  					Name:     "test-build-bom-entry",
   586  					Metadata: map[string]interface{}{"test-key": "test-value"},
   587  				},
   588  			}},
   589  			Labels: []libcnb.Label{
   590  				{
   591  					Key:   "test-key",
   592  					Value: "test-value",
   593  				},
   594  			},
   595  			Processes: []libcnb.Process{
   596  				{
   597  					Type:    "test-type",
   598  					Command: "test-command",
   599  					Default: true,
   600  				},
   601  			},
   602  			Slices: []libcnb.Slice{
   603  				{
   604  					Paths: []string{"test-path"},
   605  				},
   606  			},
   607  		}, nil)
   608  
   609  		libcnb.Build(builder,
   610  			libcnb.WithBOMLabel(true),
   611  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   612  			libcnb.WithTOMLWriter(tomlWriter),
   613  		)
   614  
   615  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml")))
   616  		Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{
   617  			Labels: []libcnb.Label{
   618  				{
   619  					Key:   "test-key",
   620  					Value: "test-value",
   621  				},
   622  			},
   623  			Processes: []libcnb.Process{
   624  				{
   625  					Type:    "test-type",
   626  					Command: "test-command",
   627  					Default: true,
   628  				},
   629  			},
   630  			Slices: []libcnb.Slice{
   631  				{
   632  					Paths: []string{"test-path"},
   633  				},
   634  			},
   635  			BOM: []libcnb.BOMEntry{
   636  				{
   637  					Name:     "test-launch-bom-entry",
   638  					Metadata: map[string]interface{}{"test-key": "test-value"},
   639  					Launch:   true,
   640  				},
   641  			},
   642  		}))
   643  	})
   644  
   645  	it("ignore working-directory setting and writes launch.toml (API<0.8)", func() {
   646  		builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   647  			Processes: []libcnb.Process{
   648  				{
   649  					Type:             "test-type",
   650  					Command:          "test-command-in-dir",
   651  					Default:          true,
   652  					WorkingDirectory: "/my/directory/",
   653  				},
   654  			},
   655  		}, nil)
   656  
   657  		libcnb.Build(builder,
   658  			libcnb.WithBOMLabel(true),
   659  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   660  			libcnb.WithTOMLWriter(tomlWriter),
   661  		)
   662  
   663  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml")))
   664  		Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{
   665  			Processes: []libcnb.Process{
   666  				{
   667  					Type:    "test-type",
   668  					Command: "test-command-in-dir",
   669  					Default: true,
   670  				},
   671  			},
   672  		}))
   673  	})
   674  
   675  	it("writes launch.toml with working-directory setting(API>=0.8)", func() {
   676  		var b bytes.Buffer
   677  		err := buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.8"})
   678  		Expect(err).ToNot(HaveOccurred())
   679  
   680  		Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed())
   681  		builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   682  			Processes: []libcnb.Process{
   683  				{
   684  					Type:             "test-type",
   685  					Command:          "test-command-in-dir",
   686  					Default:          true,
   687  					WorkingDirectory: "/my/directory/",
   688  				},
   689  			},
   690  		}, nil)
   691  
   692  		libcnb.Build(builder,
   693  			libcnb.WithBOMLabel(true),
   694  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   695  			libcnb.WithTOMLWriter(tomlWriter),
   696  		)
   697  
   698  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml")))
   699  		Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{
   700  			Processes: []libcnb.Process{
   701  				{
   702  					Type:             "test-type",
   703  					Command:          "test-command-in-dir",
   704  					Default:          true,
   705  					WorkingDirectory: "/my/directory/",
   706  				},
   707  			},
   708  		}))
   709  	})
   710  
   711  	it("writes persistent metadata", func() {
   712  		m := map[string]interface{}{"test-key": "test-value"}
   713  
   714  		builder.On("Build", mock.Anything).Return(libcnb.BuildResult{PersistentMetadata: m}, nil)
   715  
   716  		libcnb.Build(builder,
   717  			libcnb.WithBOMLabel(true),
   718  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   719  			libcnb.WithTOMLWriter(tomlWriter),
   720  		)
   721  
   722  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "store.toml")))
   723  		Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.Store{Metadata: m}))
   724  	})
   725  
   726  	it("does not write empty files", func() {
   727  		builder.On("Build", mock.Anything).Return(libcnb.NewBuildResult(), nil)
   728  
   729  		libcnb.Build(builder,
   730  			libcnb.WithBOMLabel(true),
   731  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   732  			libcnb.WithTOMLWriter(tomlWriter),
   733  		)
   734  
   735  		Expect(tomlWriter.Calls).To(HaveLen(0))
   736  	})
   737  
   738  	it("removes stale layers", func() {
   739  		Expect(os.WriteFile(filepath.Join(layersPath, "alpha.toml"), []byte(""), 0600)).To(Succeed())
   740  		Expect(os.WriteFile(filepath.Join(layersPath, "bravo.toml"), []byte(""), 0600)).To(Succeed())
   741  		Expect(os.WriteFile(filepath.Join(layersPath, "store.toml"), []byte(""), 0600)).To(Succeed())
   742  
   743  		layer := libcnb.Layer{Name: "alpha"}
   744  		layerContributor.On("Contribute", mock.Anything).Return(layer, nil)
   745  		layerContributor.On("Name").Return("alpha")
   746  
   747  		builder.On("Build", mock.Anything).
   748  			Return(libcnb.BuildResult{Layers: []libcnb.LayerContributor{layerContributor}}, nil)
   749  
   750  		libcnb.Build(builder,
   751  			libcnb.WithBOMLabel(true),
   752  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   753  			libcnb.WithTOMLWriter(tomlWriter),
   754  		)
   755  
   756  		Expect(tomlWriter.Calls).To(HaveLen(1))
   757  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "alpha.toml")))
   758  		Expect(filepath.Join(layersPath, "bravo.toml")).NotTo(BeARegularFile())
   759  		Expect(filepath.Join(layersPath, "store.toml")).To(BeARegularFile())
   760  	})
   761  
   762  	it("writes build.toml", func() {
   763  		builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   764  			BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{
   765  				{
   766  					Name:     "test-build-bom-entry",
   767  					Metadata: map[string]interface{}{"test-key": "test-value"},
   768  					Build:    true,
   769  				},
   770  				{
   771  					Name:     "test-launch-bom-entry",
   772  					Metadata: map[string]interface{}{"test-key": "test-value"},
   773  					Build:    false,
   774  				},
   775  			}},
   776  			Unmet: []libcnb.UnmetPlanEntry{
   777  				{
   778  					Name: "test-entry",
   779  				},
   780  			},
   781  		}, nil)
   782  
   783  		libcnb.Build(builder,
   784  			libcnb.WithBOMLabel(true),
   785  			libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   786  			libcnb.WithTOMLWriter(tomlWriter),
   787  		)
   788  
   789  		Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "build.toml")))
   790  		Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.BuildTOML{
   791  			BOM: []libcnb.BOMEntry{
   792  				{
   793  					Name:     "test-build-bom-entry",
   794  					Metadata: map[string]interface{}{"test-key": "test-value"},
   795  					Build:    true,
   796  				},
   797  			},
   798  			Unmet: []libcnb.UnmetPlanEntry{
   799  				{
   800  					Name: "test-entry",
   801  				},
   802  			},
   803  		}))
   804  	})
   805  
   806  	context("Config bomLabel is false", func() {
   807  		it.Before(func() {
   808  			var err error
   809  
   810  			buildpackTOML, err = template.New("buildpack.toml").Parse(bpTOMLContents)
   811  			Expect(err).ToNot(HaveOccurred())
   812  
   813  			var b bytes.Buffer
   814  			err = buildpackTOML.Execute(&b, map[string]string{"APIVersion": "0.7"})
   815  			Expect(err).ToNot(HaveOccurred())
   816  
   817  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"), b.Bytes(), 0600)).To(Succeed())
   818  		})
   819  
   820  		it("writes launch.toml without BOM entries", func() {
   821  			builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   822  				BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{
   823  					{
   824  						Name:     "test-launch-bom-entry",
   825  						Metadata: map[string]interface{}{"test-key": "test-value"},
   826  						Launch:   true,
   827  					},
   828  					{
   829  						Name:     "test-build-bom-entry",
   830  						Metadata: map[string]interface{}{"test-key": "test-value"},
   831  					},
   832  				}},
   833  				Processes: []libcnb.Process{
   834  					{
   835  						Type:    "test-type",
   836  						Command: "test-command",
   837  						Default: true,
   838  					},
   839  				},
   840  			}, nil)
   841  
   842  			libcnb.Build(builder,
   843  				libcnb.WithBOMLabel(false),
   844  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   845  				libcnb.WithTOMLWriter(tomlWriter),
   846  			)
   847  
   848  			Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "launch.toml")))
   849  			Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.LaunchTOML{
   850  				Processes: []libcnb.Process{
   851  					{
   852  						Type:    "test-type",
   853  						Command: "test-command",
   854  						Default: true,
   855  					},
   856  				},
   857  				BOM: nil,
   858  			}))
   859  		})
   860  
   861  		it("writes build.toml without BOM entries", func() {
   862  			builder.On("Build", mock.Anything).Return(libcnb.BuildResult{
   863  				BOM: &libcnb.BOM{Entries: []libcnb.BOMEntry{
   864  					{
   865  						Name:     "test-build-bom-entry",
   866  						Metadata: map[string]interface{}{"test-key": "test-value"},
   867  						Build:    true,
   868  					},
   869  					{
   870  						Name:     "test-launch-bom-entry",
   871  						Metadata: map[string]interface{}{"test-key": "test-value"},
   872  						Build:    false,
   873  					},
   874  				}},
   875  				Unmet: []libcnb.UnmetPlanEntry{
   876  					{
   877  						Name: "test-entry",
   878  					},
   879  				},
   880  			}, nil)
   881  
   882  			libcnb.Build(builder,
   883  				libcnb.WithBOMLabel(false),
   884  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   885  				libcnb.WithTOMLWriter(tomlWriter),
   886  			)
   887  
   888  			Expect(tomlWriter.Calls[0].Arguments[0]).To(Equal(filepath.Join(layersPath, "build.toml")))
   889  			Expect(tomlWriter.Calls[0].Arguments[1]).To(Equal(libcnb.BuildTOML{
   890  				BOM: nil,
   891  				Unmet: []libcnb.UnmetPlanEntry{
   892  					{
   893  						Name: "test-entry",
   894  					},
   895  				},
   896  			}))
   897  		})
   898  	})
   899  
   900  	context("Validates SBOM entries", func() {
   901  		it.Before(func() {
   902  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   903  				[]byte(`
   904  api = "0.7"
   905  
   906  [buildpack]
   907  id = "test-id"
   908  name = "test-name"
   909  version = "1.1.1"
   910  sbom-formats = ["application/vnd.cyclonedx+json"]
   911  `),
   912  				0600),
   913  			).To(Succeed())
   914  
   915  			builder.On("Build", mock.Anything).Return(libcnb.BuildResult{}, nil)
   916  		})
   917  
   918  		it("has no SBOM files", func() {
   919  			libcnb.Build(builder,
   920  				libcnb.WithBOMLabel(true),
   921  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   922  				libcnb.WithExitHandler(exitHandler),
   923  			)
   924  
   925  			Expect(exitHandler.Calls).To(BeEmpty())
   926  		})
   927  
   928  		it("has no accepted formats", func() {
   929  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   930  				[]byte(`
   931  api = "0.7"
   932  
   933  [buildpack]
   934  id = "test-id"
   935  name = "test-name"
   936  version = "1.1.1"
   937  sbom-formats = []
   938  `),
   939  				0600),
   940  			).To(Succeed())
   941  
   942  			Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())
   943  
   944  			libcnb.Build(builder,
   945  				libcnb.WithBOMLabel(true),
   946  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   947  				libcnb.WithExitHandler(exitHandler),
   948  			)
   949  
   950  			Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types []"))
   951  		})
   952  
   953  		it("skips if API is not 0.7", func() {
   954  			Expect(os.WriteFile(filepath.Join(buildpackPath, "buildpack.toml"),
   955  				[]byte(`
   956  api = "0.6"
   957  
   958  [buildpack]
   959  id = "test-id"
   960  name = "test-name"
   961  version = "1.1.1"
   962  sbom-formats = []
   963  `),
   964  				0600),
   965  			).To(Succeed())
   966  
   967  			Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())
   968  
   969  			libcnb.Build(builder,
   970  				libcnb.WithBOMLabel(true),
   971  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   972  				libcnb.WithExitHandler(exitHandler),
   973  			)
   974  
   975  			Expect(exitHandler.Calls).To(BeEmpty())
   976  		})
   977  
   978  		it("has no matching formats", func() {
   979  			Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.spdx.json"), []byte{}, 0600)).To(Succeed())
   980  
   981  			libcnb.Build(builder,
   982  				libcnb.WithBOMLabel(true),
   983  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   984  				libcnb.WithExitHandler(exitHandler),
   985  			)
   986  
   987  			Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to find actual SBOM Type application/spdx+json in list of supported SBOM types [application/vnd.cyclonedx+json]"))
   988  		})
   989  
   990  		it("has a matching format", func() {
   991  			Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
   992  			Expect(os.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
   993  			libcnb.Build(builder,
   994  				libcnb.WithBOMLabel(true),
   995  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
   996  				libcnb.WithExitHandler(exitHandler),
   997  			)
   998  
   999  			Expect(exitHandler.Calls).To(BeEmpty())
  1000  		})
  1001  
  1002  		it("has a junk format", func() {
  1003  			Expect(os.WriteFile(filepath.Join(layersPath, "launch.sbom.random.json"), []byte{}, 0600)).To(Succeed())
  1004  			Expect(os.WriteFile(filepath.Join(layersPath, "layer.sbom.cdx.json"), []byte{}, 0600)).To(Succeed())
  1005  			libcnb.Build(builder,
  1006  				libcnb.WithBOMLabel(true),
  1007  				libcnb.WithArguments([]string{commandPath, layersPath, platformPath, buildpackPlanPath}),
  1008  				libcnb.WithExitHandler(exitHandler),
  1009  			)
  1010  
  1011  			Expect(exitHandler.Calls[0].Arguments.Get(0)).To(MatchError("unable to validate SBOM\nunable to parse SBOM unknown\nunable to translate from random.json to SBOMFormat"))
  1012  		})
  1013  	})
  1014  }