sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/external/external_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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      http://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 external
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"testing"
    25  
    26  	. "github.com/onsi/ginkgo/v2"
    27  	. "github.com/onsi/gomega"
    28  	"github.com/spf13/afero"
    29  	"github.com/spf13/pflag"
    30  
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
    33  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
    34  )
    35  
    36  func TestExternalPlugin(t *testing.T) {
    37  	RegisterFailHandler(Fail)
    38  	RunSpecs(t, "Scaffold")
    39  }
    40  
    41  type mockValidOutputGetter struct{}
    42  
    43  type mockInValidOutputGetter struct{}
    44  
    45  var _ ExecOutputGetter = &mockValidOutputGetter{}
    46  
    47  func (m *mockValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) {
    48  	return []byte(`{
    49  		"command": "init", 
    50  		"error": false, 
    51  		"error_msg": "none", 
    52  		"universe": {"LICENSE": "Apache 2.0 License\n"}
    53  		}`), nil
    54  }
    55  
    56  var _ ExecOutputGetter = &mockInValidOutputGetter{}
    57  
    58  func (m *mockInValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) {
    59  	return nil, fmt.Errorf("error getting exec command output")
    60  }
    61  
    62  type mockValidOsWdGetter struct{}
    63  
    64  var _ OsWdGetter = &mockValidOsWdGetter{}
    65  
    66  func (m *mockValidOsWdGetter) GetCurrentDir() (string, error) {
    67  	return "tmp/externalPlugin", nil
    68  }
    69  
    70  type mockInValidOsWdGetter struct{}
    71  
    72  var _ OsWdGetter = &mockInValidOsWdGetter{}
    73  
    74  func (m *mockInValidOsWdGetter) GetCurrentDir() (string, error) {
    75  	return "", fmt.Errorf("error getting current directory")
    76  }
    77  
    78  type mockValidFlagOutputGetter struct{}
    79  
    80  func (m *mockValidFlagOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) {
    81  	response := external.PluginResponse{
    82  		Command:  "flag",
    83  		Error:    false,
    84  		Universe: nil,
    85  		Flags:    getFlags(),
    86  	}
    87  	return json.Marshal(response)
    88  }
    89  
    90  type mockValidMEOutputGetter struct{}
    91  
    92  func (m *mockValidMEOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) {
    93  	response := external.PluginResponse{
    94  		Command:  "metadata",
    95  		Error:    false,
    96  		Universe: nil,
    97  		Metadata: getMetadata(),
    98  	}
    99  
   100  	return json.Marshal(response)
   101  }
   102  
   103  const (
   104  	externalPlugin = "myexternalplugin.sh"
   105  	floatVal       = "float"
   106  )
   107  
   108  var _ = Describe("Run external plugin using Scaffold", func() {
   109  	Context("with valid mock values", func() {
   110  		const filePerm os.FileMode = 755
   111  		var (
   112  			pluginFileName string
   113  			args           []string
   114  			f              afero.File
   115  			fs             machinery.Filesystem
   116  
   117  			err error
   118  		)
   119  
   120  		BeforeEach(func() {
   121  			outputGetter = &mockValidOutputGetter{}
   122  			currentDirGetter = &mockValidOsWdGetter{}
   123  			fs = machinery.Filesystem{
   124  				FS: afero.NewMemMapFs(),
   125  			}
   126  
   127  			pluginFileName = "externalPlugin.sh"
   128  			pluginFilePath := filepath.Join("tmp", "externalPlugin", pluginFileName)
   129  
   130  			err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), filePerm)
   131  			Expect(err).To(BeNil())
   132  
   133  			f, err = fs.FS.Create(pluginFilePath)
   134  			Expect(err).To(BeNil())
   135  			Expect(f).ToNot(BeNil())
   136  
   137  			_, err = fs.FS.Stat(pluginFilePath)
   138  			Expect(err).To(BeNil())
   139  
   140  			args = []string{"--domain", "example.com"}
   141  		})
   142  
   143  		AfterEach(func() {
   144  			filename := filepath.Join("tmp", "externalPlugin", "LICENSE")
   145  			fileInfo, err := fs.FS.Stat(filename)
   146  			Expect(err).To(BeNil())
   147  			Expect(fileInfo).NotTo(BeNil())
   148  		})
   149  
   150  		It("should successfully run init subcommand on the external plugin", func() {
   151  			i := initSubcommand{
   152  				Path: pluginFileName,
   153  				Args: args,
   154  			}
   155  
   156  			err = i.Scaffold(fs)
   157  			Expect(err).To(BeNil())
   158  		})
   159  
   160  		It("should successfully run edit subcommand on the external plugin", func() {
   161  			e := editSubcommand{
   162  				Path: pluginFileName,
   163  				Args: args,
   164  			}
   165  
   166  			err = e.Scaffold(fs)
   167  			Expect(err).To(BeNil())
   168  		})
   169  
   170  		It("should successfully run create api subcommand on the external plugin", func() {
   171  			c := createAPISubcommand{
   172  				Path: pluginFileName,
   173  				Args: args,
   174  			}
   175  
   176  			err = c.Scaffold(fs)
   177  			Expect(err).To(BeNil())
   178  		})
   179  
   180  		It("should successfully run create webhook subcommand on the external plugin", func() {
   181  			c := createWebhookSubcommand{
   182  				Path: pluginFileName,
   183  				Args: args,
   184  			}
   185  
   186  			err = c.Scaffold(fs)
   187  			Expect(err).To(BeNil())
   188  		})
   189  	})
   190  
   191  	Context("with invalid mock values of GetExecOutput() and GetCurrentDir()", func() {
   192  		var (
   193  			pluginFileName string
   194  			args           []string
   195  			fs             machinery.Filesystem
   196  			err            error
   197  		)
   198  		BeforeEach(func() {
   199  			outputGetter = &mockInValidOutputGetter{}
   200  			currentDirGetter = &mockValidOsWdGetter{}
   201  			fs = machinery.Filesystem{
   202  				FS: afero.NewMemMapFs(),
   203  			}
   204  
   205  			pluginFileName = externalPlugin
   206  			args = []string{"--domain", "example.com"}
   207  		})
   208  
   209  		It("should return error upon running init subcommand on the external plugin", func() {
   210  			i := initSubcommand{
   211  				Path: pluginFileName,
   212  				Args: args,
   213  			}
   214  
   215  			err = i.Scaffold(fs)
   216  			Expect(err).NotTo(BeNil())
   217  			Expect(err.Error()).To(ContainSubstring("error getting exec command output"))
   218  
   219  			outputGetter = &mockValidOutputGetter{}
   220  			currentDirGetter = &mockInValidOsWdGetter{}
   221  
   222  			err = i.Scaffold(fs)
   223  			Expect(err).NotTo(BeNil())
   224  			Expect(err.Error()).To(ContainSubstring("error getting current directory"))
   225  		})
   226  
   227  		It("should return error upon running edit subcommand on the external plugin", func() {
   228  			e := editSubcommand{
   229  				Path: pluginFileName,
   230  				Args: args,
   231  			}
   232  
   233  			err = e.Scaffold(fs)
   234  			Expect(err).NotTo(BeNil())
   235  			Expect(err.Error()).To(ContainSubstring("error getting exec command output"))
   236  
   237  			outputGetter = &mockValidOutputGetter{}
   238  			currentDirGetter = &mockInValidOsWdGetter{}
   239  
   240  			err = e.Scaffold(fs)
   241  			Expect(err).NotTo(BeNil())
   242  			Expect(err.Error()).To(ContainSubstring("error getting current directory"))
   243  		})
   244  
   245  		It("should return error upon running create api subcommand on the external plugin", func() {
   246  			c := createAPISubcommand{
   247  				Path: pluginFileName,
   248  				Args: args,
   249  			}
   250  
   251  			err = c.Scaffold(fs)
   252  			Expect(err).NotTo(BeNil())
   253  			Expect(err.Error()).To(ContainSubstring("error getting exec command output"))
   254  
   255  			outputGetter = &mockValidOutputGetter{}
   256  			currentDirGetter = &mockInValidOsWdGetter{}
   257  
   258  			err = c.Scaffold(fs)
   259  			Expect(err).NotTo(BeNil())
   260  			Expect(err.Error()).To(ContainSubstring("error getting current directory"))
   261  		})
   262  
   263  		It("should return error upon running create webhook subcommand on the external plugin", func() {
   264  			c := createWebhookSubcommand{
   265  				Path: pluginFileName,
   266  				Args: args,
   267  			}
   268  
   269  			err = c.Scaffold(fs)
   270  			Expect(err).NotTo(BeNil())
   271  			Expect(err.Error()).To(ContainSubstring("error getting exec command output"))
   272  
   273  			outputGetter = &mockValidOutputGetter{}
   274  			currentDirGetter = &mockInValidOsWdGetter{}
   275  
   276  			err = c.Scaffold(fs)
   277  			Expect(err).NotTo(BeNil())
   278  			Expect(err.Error()).To(ContainSubstring("error getting current directory"))
   279  		})
   280  	})
   281  
   282  	Context("with successfully getting flags from external plugin", func() {
   283  		var (
   284  			pluginFileName string
   285  			args           []string
   286  			flagset        *pflag.FlagSet
   287  
   288  			// Make an array of flags to represent the ones that should be returned in these tests
   289  			flags = getFlags()
   290  
   291  			checkFlagset func()
   292  		)
   293  		BeforeEach(func() {
   294  			outputGetter = &mockValidFlagOutputGetter{}
   295  			currentDirGetter = &mockValidOsWdGetter{}
   296  
   297  			pluginFileName = externalPlugin
   298  			args = []string{"--captain", "black-beard", "--sail"}
   299  			flagset = pflag.NewFlagSet("test", pflag.ContinueOnError)
   300  
   301  			checkFlagset = func() {
   302  				Expect(flagset.HasFlags()).To(BeTrue())
   303  
   304  				for _, flag := range flags {
   305  					Expect(flagset.Lookup(flag.Name)).NotTo(BeNil())
   306  					// we parse floats as float64 Go type so this check will account for that
   307  					if flag.Type != floatVal {
   308  						Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type))
   309  					} else {
   310  						Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal("float64"))
   311  					}
   312  					Expect(flagset.Lookup(flag.Name).Usage).To(Equal(flag.Usage))
   313  					Expect(flagset.Lookup(flag.Name).DefValue).To(Equal(flag.Default))
   314  				}
   315  			}
   316  		})
   317  
   318  		It("should successfully bind external plugin specified flags for `init` subcommand", func() {
   319  			sc := initSubcommand{
   320  				Path: pluginFileName,
   321  				Args: args,
   322  			}
   323  
   324  			sc.BindFlags(flagset)
   325  
   326  			checkFlagset()
   327  		})
   328  
   329  		It("should successfully bind external plugin specified flags for `create api` subcommand", func() {
   330  			sc := createAPISubcommand{
   331  				Path: pluginFileName,
   332  				Args: args,
   333  			}
   334  
   335  			sc.BindFlags(flagset)
   336  
   337  			checkFlagset()
   338  		})
   339  
   340  		It("should successfully bind external plugin specified  flags for `create webhook` subcommand", func() {
   341  			sc := createWebhookSubcommand{
   342  				Path: pluginFileName,
   343  				Args: args,
   344  			}
   345  
   346  			sc.BindFlags(flagset)
   347  
   348  			checkFlagset()
   349  		})
   350  
   351  		It("should successfully bind external plugin specified flags for `edit` subcommand", func() {
   352  			sc := editSubcommand{
   353  				Path: pluginFileName,
   354  				Args: args,
   355  			}
   356  
   357  			sc.BindFlags(flagset)
   358  
   359  			checkFlagset()
   360  		})
   361  	})
   362  
   363  	Context("with failure to get flags from external plugin", func() {
   364  		var (
   365  			pluginFileName string
   366  			args           []string
   367  			flagset        *pflag.FlagSet
   368  			usage          string
   369  			checkFlagset   func()
   370  		)
   371  		BeforeEach(func() {
   372  			outputGetter = &mockInValidOutputGetter{}
   373  			currentDirGetter = &mockValidOsWdGetter{}
   374  
   375  			pluginFileName = externalPlugin
   376  			args = []string{"--captain", "black-beard", "--sail"}
   377  			flagset = pflag.NewFlagSet("test", pflag.ContinueOnError)
   378  			usage = "Kubebuilder could not validate this flag with the external plugin. " +
   379  				"Consult the external plugin documentation for more information."
   380  
   381  			checkFlagset = func() {
   382  				Expect(flagset.HasFlags()).To(BeTrue())
   383  
   384  				Expect(flagset.Lookup("captain")).NotTo(BeNil())
   385  				Expect(flagset.Lookup("captain").Value.Type()).To(Equal("string"))
   386  				Expect(flagset.Lookup("captain").Usage).To(Equal(usage))
   387  
   388  				Expect(flagset.Lookup("sail")).NotTo(BeNil())
   389  				Expect(flagset.Lookup("sail").Value.Type()).To(Equal("bool"))
   390  				Expect(flagset.Lookup("sail").Usage).To(Equal(usage))
   391  			}
   392  		})
   393  
   394  		It("should successfully bind all user passed flags for `init` subcommand", func() {
   395  			sc := initSubcommand{
   396  				Path: pluginFileName,
   397  				Args: args,
   398  			}
   399  
   400  			sc.BindFlags(flagset)
   401  
   402  			checkFlagset()
   403  		})
   404  
   405  		It("should successfully bind all user passed flags for `create api` subcommand", func() {
   406  			sc := createAPISubcommand{
   407  				Path: pluginFileName,
   408  				Args: args,
   409  			}
   410  
   411  			sc.BindFlags(flagset)
   412  
   413  			checkFlagset()
   414  		})
   415  
   416  		It("should successfully bind all user passed flags for `create webhook` subcommand", func() {
   417  			sc := createWebhookSubcommand{
   418  				Path: pluginFileName,
   419  				Args: args,
   420  			}
   421  
   422  			sc.BindFlags(flagset)
   423  
   424  			checkFlagset()
   425  		})
   426  
   427  		It("should successfully bind all user passed flags for `edit` subcommand", func() {
   428  			sc := editSubcommand{
   429  				Path: pluginFileName,
   430  				Args: args,
   431  			}
   432  
   433  			sc.BindFlags(flagset)
   434  
   435  			checkFlagset()
   436  		})
   437  	})
   438  
   439  	Context("Flag Parsing Filter Functions", func() {
   440  		It("gvk(Arg/Flag)Filter should filter out (--)group, (--)version, (--)kind", func() {
   441  			for _, toBeFiltered := range []string{
   442  				"group", "version", "kind",
   443  			} {
   444  				Expect(gvkArgFilter("--" + toBeFiltered)).To(BeFalse())
   445  				Expect(gvkArgFilter(toBeFiltered)).To(BeFalse())
   446  				Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse())
   447  				Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse())
   448  			}
   449  			Expect(gvkArgFilter("somerandomflag")).To(BeTrue())
   450  			Expect(gvkFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue())
   451  		})
   452  
   453  		It("helpArgFilter should filter out (--)help", func() {
   454  			Expect(helpArgFilter("--help")).To(BeFalse())
   455  			Expect(helpArgFilter("help")).To(BeFalse())
   456  			Expect(helpArgFilter("somerandomflag")).To(BeTrue())
   457  			Expect(helpFlagFilter(external.Flag{Name: "--help"})).To(BeFalse())
   458  			Expect(helpFlagFilter(external.Flag{Name: "help"})).To(BeFalse())
   459  			Expect(helpFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue())
   460  		})
   461  	})
   462  
   463  	Context("Flag Parsing Helper Functions", func() {
   464  		var (
   465  			fs   *pflag.FlagSet
   466  			args = []string{
   467  				"--domain", "something.com",
   468  				"--boolean",
   469  				"--another", "flag",
   470  				"--help",
   471  				"--group", "somegroup",
   472  				"--kind", "somekind",
   473  				"--version", "someversion",
   474  			}
   475  			forbidden = []string{
   476  				"help", "group", "kind", "version",
   477  			}
   478  			flags               []external.Flag
   479  			argFilters          []argFilterFunc
   480  			externalFlagFilters []externalFlagFilterFunc
   481  		)
   482  
   483  		BeforeEach(func() {
   484  			fs = pflag.NewFlagSet("test", pflag.ContinueOnError)
   485  
   486  			flagsToAppend := getFlags()
   487  
   488  			flags = make([]external.Flag, len(flagsToAppend))
   489  			copy(flags, flagsToAppend)
   490  
   491  			argFilters = []argFilterFunc{
   492  				gvkArgFilter, helpArgFilter,
   493  			}
   494  			externalFlagFilters = []externalFlagFilterFunc{
   495  				gvkFlagFilter, helpFlagFilter,
   496  			}
   497  		})
   498  
   499  		It("isBooleanFlag should return true if boolean flag provided at index", func() {
   500  			Expect(isBooleanFlag(2, args)).To(BeTrue())
   501  		})
   502  
   503  		It("isBooleanFlag should return false if boolean flag not provided at index", func() {
   504  			Expect(isBooleanFlag(0, args)).To(BeFalse())
   505  		})
   506  
   507  		It("bindAllFlags should bind all flags", func() {
   508  			usage := "Kubebuilder could not validate this flag with the external plugin. " +
   509  				"Consult the external plugin documentation for more information."
   510  
   511  			bindAllFlags(fs, filterArgs(args, argFilters))
   512  			Expect(fs.HasFlags()).To(BeTrue())
   513  			Expect(fs.Lookup("domain")).NotTo(BeNil())
   514  			Expect(fs.Lookup("domain").Value.Type()).To(Equal("string"))
   515  			Expect(fs.Lookup("domain").Usage).To(Equal(usage))
   516  			Expect(fs.Lookup("boolean")).NotTo(BeNil())
   517  			Expect(fs.Lookup("boolean").Value.Type()).To(Equal("bool"))
   518  			Expect(fs.Lookup("boolean").Usage).To(Equal(usage))
   519  			Expect(fs.Lookup("another")).NotTo(BeNil())
   520  			Expect(fs.Lookup("another").Value.Type()).To(Equal("string"))
   521  			Expect(fs.Lookup("another").Usage).To(Equal(usage))
   522  
   523  			By("bindAllFlags not have bound any forbidden flag after filtering")
   524  			for i := range forbidden {
   525  				Expect(fs.Lookup(forbidden[i])).To(BeNil())
   526  			}
   527  		})
   528  
   529  		It("bindSpecificFlags should bind all flags in given []Flag", func() {
   530  			filteredFlags := filterFlags(flags, externalFlagFilters)
   531  			bindSpecificFlags(fs, filteredFlags)
   532  
   533  			Expect(fs.HasFlags()).To(BeTrue())
   534  
   535  			for _, flag := range filteredFlags {
   536  				Expect(fs.Lookup(flag.Name)).NotTo(BeNil())
   537  				// we parse floats as float64 Go type so this check will account for that
   538  				if flag.Type != floatVal {
   539  					Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type))
   540  				} else {
   541  					Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal("float64"))
   542  				}
   543  				Expect(fs.Lookup(flag.Name).Usage).To(Equal(flag.Usage))
   544  				Expect(fs.Lookup(flag.Name).DefValue).To(Equal(flag.Default))
   545  			}
   546  
   547  			By("bindSpecificFlags not have bound any forbidden flag after filtering")
   548  			for i := range forbidden {
   549  				Expect(fs.Lookup(forbidden[i])).To(BeNil())
   550  			}
   551  		})
   552  	})
   553  
   554  	// TODO(everettraven): Add tests for an external plugin setting the Metadata and Examples
   555  	Context("Successfully retrieving metadata and examples from external plugin", func() {
   556  		var (
   557  			pluginFileName string
   558  			metadata       *plugin.SubcommandMetadata
   559  			checkMetadata  func()
   560  		)
   561  		BeforeEach(func() {
   562  			outputGetter = &mockValidMEOutputGetter{}
   563  			currentDirGetter = &mockValidOsWdGetter{}
   564  
   565  			pluginFileName = externalPlugin
   566  			metadata = &plugin.SubcommandMetadata{}
   567  
   568  			checkMetadata = func() {
   569  				Expect(metadata.Description).Should(Equal(getMetadata().Description))
   570  				Expect(metadata.Examples).Should(Equal(getMetadata().Examples))
   571  			}
   572  		})
   573  
   574  		It("should use the external plugin's metadata and examples for `init` subcommand", func() {
   575  			sc := initSubcommand{
   576  				Path: pluginFileName,
   577  				Args: nil,
   578  			}
   579  
   580  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   581  
   582  			checkMetadata()
   583  		})
   584  
   585  		It("should use the external plugin's metadata and examples for `create api` subcommand", func() {
   586  			sc := createAPISubcommand{
   587  				Path: pluginFileName,
   588  				Args: nil,
   589  			}
   590  
   591  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   592  
   593  			checkMetadata()
   594  		})
   595  
   596  		It("should use the external plugin's metadata and examples for `create webhook` subcommand", func() {
   597  			sc := createWebhookSubcommand{
   598  				Path: pluginFileName,
   599  				Args: nil,
   600  			}
   601  
   602  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   603  
   604  			checkMetadata()
   605  		})
   606  
   607  		It("should use the external plugin's metadata and examples for `edit` subcommand", func() {
   608  			sc := editSubcommand{
   609  				Path: pluginFileName,
   610  				Args: nil,
   611  			}
   612  
   613  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   614  
   615  			checkMetadata()
   616  		})
   617  	})
   618  
   619  	Context("Failing to retrieve metadata and examples from external plugin", func() {
   620  		var (
   621  			pluginFileName string
   622  			metadata       *plugin.SubcommandMetadata
   623  			checkMetadata  func()
   624  		)
   625  		BeforeEach(func() {
   626  			outputGetter = &mockInValidOutputGetter{}
   627  			currentDirGetter = &mockValidOsWdGetter{}
   628  
   629  			pluginFileName = externalPlugin
   630  			metadata = &plugin.SubcommandMetadata{}
   631  
   632  			checkMetadata = func() {
   633  				Expect(metadata.Description).Should(Equal(fmt.Sprintf(defaultMetadataTemplate, "myexternalplugin")))
   634  				Expect(metadata.Examples).Should(BeEmpty())
   635  			}
   636  		})
   637  
   638  		It("should use the default metadata and examples for `init` subcommand", func() {
   639  			sc := initSubcommand{
   640  				Path: pluginFileName,
   641  				Args: nil,
   642  			}
   643  
   644  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   645  
   646  			checkMetadata()
   647  		})
   648  
   649  		It("should use the default metadata and examples for `create api` subcommand", func() {
   650  			sc := createAPISubcommand{
   651  				Path: pluginFileName,
   652  				Args: nil,
   653  			}
   654  
   655  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   656  
   657  			checkMetadata()
   658  		})
   659  
   660  		It("should use the default metadata and examples for `create webhook` subcommand", func() {
   661  			sc := createWebhookSubcommand{
   662  				Path: pluginFileName,
   663  				Args: nil,
   664  			}
   665  
   666  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   667  
   668  			checkMetadata()
   669  		})
   670  
   671  		It("should use the default metadata and examples for `edit` subcommand", func() {
   672  			sc := editSubcommand{
   673  				Path: pluginFileName,
   674  				Args: nil,
   675  			}
   676  
   677  			sc.UpdateMetadata(plugin.CLIMetadata{}, metadata)
   678  
   679  			checkMetadata()
   680  		})
   681  	})
   682  
   683  	Context("Helper functions for Sending request to external plugin and parsing response", func() {
   684  		It("getUniverseMap should return path to content mapping of all files in Filesystem", func() {
   685  			fs := machinery.Filesystem{
   686  				FS: afero.NewMemMapFs(),
   687  			}
   688  
   689  			files := []struct {
   690  				path    string
   691  				name    string
   692  				content string
   693  			}{
   694  				{
   695  					path:    "./",
   696  					name:    "file",
   697  					content: "level 0 file",
   698  				},
   699  				{
   700  					path:    "dir/",
   701  					name:    "file",
   702  					content: "level 1 file",
   703  				},
   704  				{
   705  					path:    "dir/subdir",
   706  					name:    "file",
   707  					content: "level 2 file",
   708  				},
   709  			}
   710  
   711  			// create files in Filesystem
   712  			for _, file := range files {
   713  				err := fs.FS.MkdirAll(file.path, 0o700)
   714  				Expect(err).ToNot(HaveOccurred())
   715  
   716  				f, err := fs.FS.Create(filepath.Join(file.path, file.name))
   717  				Expect(err).ToNot(HaveOccurred())
   718  
   719  				_, err = f.Write([]byte(file.content))
   720  				Expect(err).ToNot(HaveOccurred())
   721  
   722  				err = f.Close()
   723  				Expect(err).ToNot(HaveOccurred())
   724  			}
   725  
   726  			universe, err := getUniverseMap(fs)
   727  
   728  			Expect(err).ToNot(HaveOccurred())
   729  			Expect(len(universe)).To(Equal(len(files)))
   730  
   731  			for _, file := range files {
   732  				content := universe[filepath.Join(file.path, file.name)]
   733  				Expect(content).To(Equal(file.content))
   734  			}
   735  		})
   736  	})
   737  })
   738  
   739  func getFlags() []external.Flag {
   740  	return []external.Flag{
   741  		{
   742  			Name:    "captain",
   743  			Type:    "string",
   744  			Usage:   "specify the ship captain",
   745  			Default: "jack-sparrow",
   746  		},
   747  		{
   748  			Name:    "sail",
   749  			Type:    "bool",
   750  			Usage:   "deploy the sail",
   751  			Default: "false",
   752  		},
   753  		{
   754  			Name:    "crew-count",
   755  			Type:    "int",
   756  			Usage:   "number of crew members",
   757  			Default: "123",
   758  		},
   759  		{
   760  			Name:    "treasure-value",
   761  			Type:    "float",
   762  			Usage:   "value of treasure on board the ship",
   763  			Default: "123.45",
   764  		},
   765  	}
   766  }
   767  
   768  func getMetadata() plugin.SubcommandMetadata {
   769  	return plugin.SubcommandMetadata{
   770  		Description: "Test description",
   771  		Examples:    "Test examples",
   772  	}
   773  }