github.com/michaellihs/golab@v0.1.0-beta3.0.20180726222757-f5cdabc76dfd/cmd/mapper/flag_mapper_test.go (about)

     1  // Copyright © 2017 Michael Lihs
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package mapper
    22  
    23  import (
    24  	"bytes"
    25  	"io"
    26  	"os"
    27  	"reflect"
    28  	"strings"
    29  
    30  	"time"
    31  
    32  	. "github.com/onsi/ginkgo"
    33  	. "github.com/onsi/gomega"
    34  	. "github.com/spf13/cobra"
    35  	"github.com/xanzy/go-gitlab"
    36  )
    37  
    38  func mockCmd() *Command {
    39  	mock := &Command{
    40  		Use:   "mock",
    41  		Short: "mock",
    42  		Long:  `mock`,
    43  		RunE: func(cmd *Command, args []string) error {
    44  			return nil
    45  		},
    46  	}
    47  	return mock
    48  }
    49  
    50  var _ = Describe("FlagMapper", func() {
    51  
    52  	type testFlags struct {
    53  		Flag1 *bool     `flag_name:"flag1" short:"f" type:"bool" required:"yes" description:"first flag"`
    54  		Flag2 *string   `flag_name:"flag2" type:"string" required:"no" description:"second flag"`
    55  		Flag3 *int      `flag_name:"flag3" type:"string" required:"no" description:"third flag"`
    56  		Flag4 *[]string `flag_name:"flag4" type:"[]string" required:"no" description:"fourth flag"`
    57  		Flag5 []int     `flag_name:"flag5" type:"[]int" required:"no" description:"fifth flag"`
    58  	}
    59  
    60  	type testOpts struct {
    61  		Flag1 *bool
    62  		Flag2 *string
    63  		Flag3 *int
    64  		Flag4 *[]string
    65  		Flag5 []int
    66  	}
    67  
    68  	type testOptsNonMatching struct {
    69  		Flag1 *bool
    70  		Flag2 *string
    71  		Flag3 *int
    72  		Flag4 *string // non matching with flags
    73  	}
    74  
    75  	type testFlagsWithTransformation struct {
    76  		Flag1 *string `flag_name:"visibility" type:"bool" required:"no" description:"first flag" transform:"string2visibility"`
    77  	}
    78  
    79  	type optsRequireTransformation struct {
    80  		Flag1 *gitlab.VisibilityValue
    81  	}
    82  
    83  	type testFlagsWithLabelsTransformation struct {
    84  		Labels *string `flag_name:"labels" type:"string" required:"no" description:"labels" transform:"string2Labels"`
    85  	}
    86  
    87  	type optsRequireLabelsTransformation struct {
    88  		Labels gitlab.Labels
    89  	}
    90  
    91  	type testFlagsWithPropertyNotInOpts struct {
    92  		Id   *string `flag_name:"id" short:"i" type:"string" required:"yes" description:"id"`
    93  		Name *string `flag_name:"name" short:"n" type:"string" required:"yes" description:"name"`
    94  	}
    95  
    96  	type testOptsWithMissingProperty struct {
    97  		Name *string `flag_name:"name" short:"n" type:"string" required:"yes" description:"name"`
    98  	}
    99  
   100  	It("provides a constructor that takes a cobra command as parameter", func() {
   101  		mockCmd := mockCmd()
   102  		var flagMapper = New(mockCmd)
   103  		Expect(reflect.TypeOf(flagMapper).String()).To(Equal("mapper.FlagMapper"))
   104  	})
   105  
   106  	It("sets expected flags on command", func() {
   107  		flags := &testFlags{}
   108  
   109  		cmd := mockCmd()
   110  		var mapper = New(cmd)
   111  		mapper.SetFlags(flags)
   112  
   113  		Expect(cmd.Flag("flag1")).NotTo(BeNil())
   114  		Expect(cmd.Flag("flag1").Name).To(Equal("flag1"))
   115  		Expect(cmd.Flag("flag1").Usage).To(Equal("(required) first flag"))
   116  		Expect(cmd.Flag("flag1").Shorthand).To(Equal("f"))
   117  
   118  		Expect(cmd.Flag("flag2")).NotTo(BeNil())
   119  		Expect(cmd.Flag("flag2").Name).To(Equal("flag2"))
   120  		Expect(cmd.Flag("flag2").Usage).To(Equal("(optional) second flag"))
   121  		Expect(cmd.Flag("flag2").Shorthand).To(Equal(""))
   122  
   123  		Expect(cmd.Flag("flag3")).NotTo(BeNil())
   124  		Expect(cmd.Flag("flag3").Name).To(Equal("flag3"))
   125  		Expect(cmd.Flag("flag3").Usage).To(Equal("(optional) third flag"))
   126  		Expect(cmd.Flag("flag3").Shorthand).To(Equal(""))
   127  
   128  		Expect(cmd.Flag("flag4")).NotTo(BeNil())
   129  		Expect(cmd.Flag("flag4").Name).To(Equal("flag4"))
   130  		Expect(cmd.Flag("flag4").Usage).To(Equal("(optional) fourth flag"))
   131  		Expect(cmd.Flag("flag4").Shorthand).To(Equal(""))
   132  
   133  		Expect(cmd.Flag("flag5")).NotTo(BeNil())
   134  		Expect(cmd.Flag("flag5").Name).To(Equal("flag5"))
   135  		Expect(cmd.Flag("flag5").Usage).To(Equal("(optional) fifth flag"))
   136  		Expect(cmd.Flag("flag5").Shorthand).To(Equal(""))
   137  	})
   138  
   139  	It("sets no flags if given flags are nil", func() {
   140  		mockCmd := mockCmd()
   141  		mapper := New(mockCmd)
   142  		mapper.SetFlags(nil)
   143  	})
   144  
   145  	It("maps flags and opts as expected with AutoMap", func() {
   146  		mockCmd := mockCmd()
   147  		var mapper = InitializedMapper(mockCmd, &testFlags{}, &testOpts{})
   148  
   149  		executeCommand(mockCmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3", "--flag5", "1,2,3,4")
   150  		_, _, err := mapper.AutoMap()
   151  		flags := mapper.MappedFlags().(*testFlags)
   152  		opts := mapper.MappedOpts().(*testOpts)
   153  
   154  		Expect(err).To(BeNil())
   155  		Expect(*flags.Flag1).To(Equal(true))
   156  		Expect(*opts.Flag1).To(Equal(true))
   157  	})
   158  
   159  	It("maps valid args to given opts struct as expected", func() {
   160  		flags := &testFlags{}
   161  		opts := &testOpts{}
   162  		cmd := mockCmd()
   163  		var mapper = New(cmd)
   164  		mapper.SetFlags(flags)
   165  
   166  		executeCommand(cmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3", "--flag5", "1,2,3,4")
   167  		mapper.Map(flags, opts)
   168  
   169  		Expect(*opts.Flag1).To(Equal(true))
   170  		Expect(*opts.Flag2).To(Equal("string"))
   171  		Expect(*opts.Flag3).To(Equal(4))
   172  		// TODO there seems to be bug in cobra, when parsing array flags
   173  		Expect(*opts.Flag4).Should(ConsistOf("v1, v2, v3"))
   174  		Expect(opts.Flag5).Should(ConsistOf(1, 2, 3, 4))
   175  	})
   176  
   177  	It("maps args to given flags struct as expected", func() {
   178  		flags := &testFlags{}
   179  		opts := &testOpts{}
   180  		mockCmd := mockCmd()
   181  		var flagMapper = New(mockCmd)
   182  		flagMapper.SetFlags(flags)
   183  
   184  		executeCommand(mockCmd, "mock", "--flag1", "false", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3")
   185  		flagMapper.Map(flags, opts)
   186  
   187  		Expect(*flags.Flag1).To(Equal(true))
   188  		Expect(*flags.Flag2).To(Equal("string"))
   189  		Expect(*flags.Flag3).To(Equal(4))
   190  		Expect(*flags.Flag4).Should(ConsistOf("v1, v2, v3"))
   191  	})
   192  
   193  	It("maps nil flags as expected", func() {
   194  		cmd := mockCmd()
   195  		mapper := InitializedMapper(cmd, nil, nil)
   196  		opts, flags, err := mapper.AutoMap()
   197  		Expect(opts).To(BeNil())
   198  		Expect(flags).To(BeNil())
   199  		Expect(err).To(BeNil())
   200  	})
   201  
   202  	It("skips args with non-matching types as expected", func() {
   203  		flags := &testFlags{}
   204  		opts := &testOptsNonMatching{}
   205  		mockCmd := mockCmd()
   206  		var flagMapper = New(mockCmd)
   207  		flagMapper.SetFlags(flags)
   208  
   209  		executeCommand(mockCmd, "mock", "--flag1", "true", "--flag2", "string", "--flag3", "4", "--flag4", "v1, v2, v3")
   210  		flagMapper.Map(flags, opts)
   211  
   212  		Expect(opts.Flag4).To(BeNil())
   213  	})
   214  
   215  	It("calls a transform function as expected", func() {
   216  		flags := &testFlagsWithTransformation{}
   217  		opts := &optsRequireTransformation{}
   218  		cmd := mockCmd()
   219  		var flagMapper = InitializedMapper(cmd, flags, opts)
   220  
   221  		executeCommand(cmd, "mock", "--visibility", "private")
   222  		flagMapper.AutoMap()
   223  
   224  		Expect(*opts.Flag1).To(Equal(*gitlab.Visibility(gitlab.PrivateVisibility)))
   225  	})
   226  
   227  	It("transforms string to gitlab.Labels as expected", func() {
   228  		flags := &testFlagsWithLabelsTransformation{}
   229  		opts := &optsRequireLabelsTransformation{}
   230  		cmd := mockCmd()
   231  		var mapper = InitializedMapper(cmd, flags, opts)
   232  
   233  		executeCommand(cmd, "mock", "--labels", "label1,label2,label3")
   234  		mapper.AutoMap()
   235  
   236  		Expect(opts.Labels).Should(ConsistOf("label1", "label2", "label3"))
   237  	})
   238  
   239  	It("transforms string to time.Time value as expected", func() {
   240  		type str2timeValFlags struct {
   241  			Time *string `flag_name:"time" type:"string" required:"no" description:"time" transform:"string2TimeVal"`
   242  		}
   243  		type str2timeValOpts struct {
   244  			Time time.Time
   245  		}
   246  		flags := &str2timeValFlags{}
   247  		opts := &str2timeValOpts{}
   248  		cmd := mockCmd()
   249  		var mapper = InitializedMapper(cmd, flags, opts)
   250  
   251  		executeCommand(cmd, "mock", "--time", "2017-12-13")
   252  		mapper.AutoMap()
   253  
   254  		Expect(opts.Time).NotTo(BeNil())
   255  		Expect(opts.Time.Day()).To(Equal(13))
   256  		Expect(opts.Time.Month()).To(Equal(time.Month(12)))
   257  		Expect(opts.Time.Year()).To(Equal(2017))
   258  	})
   259  
   260  	It("transforms string to ISOTime as expected", func() {
   261  		type str2ISOTime struct {
   262  			Time *string `flag_name:"time" type:"string" required:"no" description:"time" transform:"string2IsoTime"`
   263  		}
   264  		type str2ISTOTime struct {
   265  			Time *gitlab.ISOTime
   266  		}
   267  		flags := &str2ISOTime{}
   268  		opts := &str2ISTOTime{}
   269  		cmd := mockCmd()
   270  		var mapper = InitializedMapper(cmd, flags, opts)
   271  
   272  		executeCommand(cmd, "mock", "--time", "2017-12-13")
   273  		mapper.AutoMap()
   274  
   275  		Expect(opts.Time).NotTo(BeNil())
   276  		s, err := opts.Time.MarshalJSON()
   277  		Expect(err).To(BeNil())
   278  		Expect(string(s)).To(Equal(`"2017-12-13"`))
   279  	})
   280  
   281  	It("transforms JSON to commit actions as expected", func() {
   282  		type json2CommitActionsFlags struct {
   283  			Actions *string `flag_name:"actions" transform:"json2CommitActions" type:"array" required:"yes" description:"A JSON encoded array of action hashes to commit as a batch."`
   284  		}
   285  		type json2CommitActionsOpts struct {
   286  			Actions []*gitlab.CommitAction
   287  		}
   288  		json := `[
   289  			{
   290  			  "action": "create",
   291  			  "file_path": "foo/bar",
   292  			  "content": "some content"
   293  			},
   294  			{
   295  			  "action": "delete",
   296  			  "file_path": "foo/bar2"
   297  			},
   298  			{
   299  			  "action": "move",
   300  			  "file_path": "foo/bar3",
   301  			  "previous_path": "foo/bar4",
   302  			  "content": "some content"
   303  			},
   304  			{
   305  			  "action": "update",
   306  			  "file_path": "foo/bar5",
   307  			  "content": "new content"
   308  			}
   309  	  	]`
   310  		flags := &json2CommitActionsFlags{}
   311  		opts := &json2CommitActionsOpts{}
   312  		cmd := mockCmd()
   313  		var mapper = InitializedMapper(cmd, flags, opts)
   314  
   315  		executeCommand(cmd, "mock", "--actions", json)
   316  		mapper.AutoMap()
   317  
   318  		Expect(len(opts.Actions)).To(Equal(4))
   319  	})
   320  
   321  	It("silently ignores properties in flags that are not available in opts", func() {
   322  		flags := &testFlagsWithPropertyNotInOpts{}
   323  		opts := &testOptsWithMissingProperty{}
   324  		mockCmd := mockCmd()
   325  		var mapper = New(mockCmd)
   326  		mapper.SetFlags(flags)
   327  
   328  		executeCommand(mockCmd, "mock", "-i", "34", "-n", "name")
   329  		mapper.Map(flags, opts)
   330  
   331  		Expect(*opts.Name).To(Equal("name"))
   332  	})
   333  
   334  	It("can handle nil opts", func() {
   335  		flags := &testFlagsWithPropertyNotInOpts{}
   336  		mockCmd := mockCmd()
   337  		var mapper = New(mockCmd)
   338  		mapper.SetFlags(flags)
   339  
   340  		executeCommand(mockCmd, "mock", "-i", "34", "-n", "name")
   341  		mapper.Map(flags, nil)
   342  
   343  		Expect(*flags.Name).To(Equal("name"))
   344  	})
   345  
   346  	It("returns an error during mapping, if required flag is not set in mapping", func() {
   347  		flags := &testFlags{}
   348  		mockCmd := mockCmd()
   349  		var mapper = New(mockCmd)
   350  		mapper.SetFlags(flags)
   351  
   352  		executeCommand(mockCmd, "mock", "-n", "name")
   353  		err := mapper.Map(flags, nil)
   354  
   355  		Expect(err).NotTo(BeNil())
   356  		Expect(err.Error()).To(Equal("required flag --flag1 was empty"))
   357  	})
   358  
   359  	It("returns an error during mapping, if required flag is not set in auto-mapping", func() {
   360  		flags := &testFlags{}
   361  		mockCmd := mockCmd()
   362  		var mapper = InitializedMapper(mockCmd, flags, nil)
   363  
   364  		executeCommand(mockCmd, "mock", "-n", "name")
   365  		_, _, err := mapper.AutoMap()
   366  
   367  		Expect(err).NotTo(BeNil())
   368  		Expect(err.Error()).To(Equal("required flag --flag1 was empty"))
   369  	})
   370  
   371  })
   372  
   373  // TODO put the following methods into a testhelper
   374  
   375  // see https://github.com/spf13/cobra/blob/master/command_test.go for basic implementation of this method
   376  func executeCommand(root *Command, args ...string) (stdout string, output string, err error) {
   377  	stdout, output, err = executeCommandC(root, args...)
   378  	return strings.TrimRight(stdout, "\n"), output, err
   379  }
   380  
   381  // see https://github.com/spf13/cobra/blob/master/command_test.go for basic implementation of this method
   382  func executeCommandC(root *Command, args ...string) (stdout string, output string, err error) {
   383  	buf := new(bytes.Buffer)
   384  	root.SetOutput(buf) // this only redirects stderr, not stdout!
   385  	root.SetArgs(args)
   386  
   387  	// for capturing stdout, see https://stackoverflow.com/questions/10473800/in-go-how-do-i-capture-stdout-of-a-function-into-a-string
   388  	old := os.Stdout // keep backup of the real stdout
   389  	r, w, _ := os.Pipe()
   390  	os.Stdout = w
   391  
   392  	_, err = root.ExecuteC()
   393  
   394  	outC := make(chan string)
   395  	// copy the output in a separate goroutine so printing can't block indefinitely
   396  	go func() {
   397  		var buf bytes.Buffer
   398  		io.Copy(&buf, r)
   399  		outC <- buf.String()
   400  	}()
   401  
   402  	// back to normal state
   403  	w.Close()
   404  	os.Stdout = old // restoring the real stdout
   405  	stdout = <-outC
   406  
   407  	return stdout, buf.String(), err
   408  }