github.com/blp1526/goa@v1.4.0/goagen/gen_client/cli_generator_test.go (about)

     1  package genclient_test
     2  
     3  import (
     4  	"bytes"
     5  	"html/template"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/goadesign/goa/design"
    12  	"github.com/goadesign/goa/dslengine"
    13  	"github.com/goadesign/goa/goagen/codegen"
    14  	"github.com/goadesign/goa/goagen/gen_client"
    15  	"github.com/goadesign/goa/version"
    16  	. "github.com/onsi/ginkgo"
    17  	. "github.com/onsi/gomega"
    18  	"github.com/onsi/gomega/gexec"
    19  )
    20  
    21  var _ = Describe("Generate", func() {
    22  	var testgenPackagePath = filepath.FromSlash("github.com/goadesign/goa/goagen/gen_client/test_")
    23  
    24  	var outDir string
    25  	var files []string
    26  	var genErr error
    27  
    28  	BeforeEach(func() {
    29  		gopath := filepath.SplitList(os.Getenv("GOPATH"))[0]
    30  		outDir = filepath.Join(gopath, "src", testgenPackagePath)
    31  		err := os.MkdirAll(outDir, 0777)
    32  		Ω(err).ShouldNot(HaveOccurred())
    33  		os.Args = []string{"goagen", "--out=" + outDir, "--design=foo", "--version=" + version.String()}
    34  	})
    35  
    36  	JustBeforeEach(func() {
    37  		files, genErr = genclient.Generate()
    38  	})
    39  
    40  	AfterEach(func() {
    41  		os.RemoveAll(outDir)
    42  		delete(codegen.Reserved, "client")
    43  	})
    44  
    45  	Context("with a dummy API", func() {
    46  		BeforeEach(func() {
    47  			design.Design = &design.APIDefinition{
    48  				Name:        "testapi",
    49  				Title:       "dummy API with no resource",
    50  				Description: "I told you it's dummy",
    51  				Consumes:    design.DefaultEncoders,
    52  			}
    53  		})
    54  
    55  		It("generates a dummy app", func() {
    56  			Ω(genErr).Should(BeNil())
    57  			Ω(files).Should(HaveLen(8))
    58  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "testapi-cli", "main.go"))
    59  			content := string(c)
    60  			Ω(err).ShouldNot(HaveOccurred())
    61  			Ω(len(strings.Split(content, "\n"))).Should(BeNumerically(">=", 16))
    62  			_, err = gexec.Build(filepath.Join(testgenPackagePath, "tool", "testapi-cli"))
    63  			Ω(err).ShouldNot(HaveOccurred())
    64  		})
    65  
    66  		Context("generated commands.go", func() {
    67  			var commandHeader string
    68  
    69  			funcs := template.FuncMap{
    70  				"sep": func() string { return string(os.PathSeparator) },
    71  			}
    72  
    73  			BeforeEach(func() {
    74  				data := map[string]string{
    75  					"outDir":  outDir,
    76  					"design":  "foo",
    77  					"tmpDir":  testgenPackagePath,
    78  					"version": version.String(),
    79  					"title":   "CLI Commands",
    80  				}
    81  
    82  				// Generate Headers
    83  				clientHeaderT, err := template.New("context").Funcs(funcs).Parse(clientHeaderTmpl)
    84  				Ω(err).ShouldNot(HaveOccurred())
    85  				var b bytes.Buffer
    86  				err = clientHeaderT.Execute(&b, data)
    87  				Ω(err).ShouldNot(HaveOccurred())
    88  				commandHeader = b.String()
    89  			})
    90  
    91  			It("generates \"generated by\" header", func() {
    92  				Ω(genErr).Should(BeNil())
    93  				c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
    94  				content := string(c)
    95  				Ω(err).ShouldNot(HaveOccurred())
    96  				Ω(content).Should(HavePrefix(commandHeader))
    97  			})
    98  		})
    99  	})
   100  
   101  	Context("with an action with two parameters", func() {
   102  		BeforeEach(func() {
   103  			codegen.TempCount = 0
   104  			design.Design = &design.APIDefinition{
   105  				Name:        "testapi",
   106  				Title:       "dummy API with no resource",
   107  				Description: "I told you it's dummy",
   108  				Consumes:    design.DefaultEncoders,
   109  				Resources: map[string]*design.ResourceDefinition{
   110  					"foo": {
   111  						Name: "foo",
   112  						Actions: map[string]*design.ActionDefinition{
   113  							"show": {
   114  								Name: "show",
   115  								Params: &design.AttributeDefinition{
   116  									Type: design.Object{
   117  										"nicID":     &design.AttributeDefinition{Type: design.String},
   118  										"ipAddress": &design.AttributeDefinition{Type: design.String},
   119  									},
   120  								},
   121  								Routes: []*design.RouteDefinition{
   122  									{
   123  										Verb: "GET",
   124  										Path: "/nics/:nicID/add/:ipAddress",
   125  									},
   126  								},
   127  							},
   128  						},
   129  					},
   130  				},
   131  			}
   132  			fooRes := design.Design.Resources["foo"]
   133  			showAct := fooRes.Actions["show"]
   134  			showAct.Parent = fooRes
   135  			showAct.Routes[0].Parent = showAct
   136  		})
   137  
   138  		It("generate the correct command path formatting code", func() {
   139  			Ω(genErr).Should(BeNil())
   140  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   141  			content := string(c)
   142  			Ω(err).ShouldNot(HaveOccurred())
   143  			Ω(content).Should(ContainSubstring(`path = fmt.Sprintf("/nics/%v/add/%v", url.QueryEscape(cmd.NicID), url.QueryEscape(cmd.IPAddress)`))
   144  		})
   145  	})
   146  
   147  	Context("with a resource name with underscores characters", func() {
   148  		BeforeEach(func() {
   149  			codegen.TempCount = 0
   150  			design.Design = &design.APIDefinition{
   151  				Name:        "testapi",
   152  				Title:       "dummy API with no resource",
   153  				Description: "I told you it's dummy",
   154  				Consumes:    design.DefaultEncoders,
   155  				Resources: map[string]*design.ResourceDefinition{
   156  					"foo_test": {
   157  						Name: "foo_test",
   158  						Actions: map[string]*design.ActionDefinition{
   159  							"show": {
   160  								Name: "show",
   161  								Params: &design.AttributeDefinition{
   162  									Type: design.Object{
   163  										"nicID":     &design.AttributeDefinition{Type: design.String},
   164  										"ipAddress": &design.AttributeDefinition{Type: design.String},
   165  									},
   166  								},
   167  								Routes: []*design.RouteDefinition{
   168  									{
   169  										Verb: "GET",
   170  										Path: "/nics/:nicID/add/:ipAddress",
   171  									},
   172  								},
   173  							},
   174  						},
   175  					},
   176  				},
   177  			}
   178  			fooRes := design.Design.Resources["foo_test"]
   179  			showAct := fooRes.Actions["show"]
   180  			showAct.Parent = fooRes
   181  			showAct.Routes[0].Parent = showAct
   182  		})
   183  
   184  		It("generate the correct command path formatting code", func() {
   185  			Ω(genErr).Should(BeNil())
   186  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   187  			content := string(c)
   188  			Ω(err).ShouldNot(HaveOccurred())
   189  			Ω(content).Should(ContainSubstring("foo-test"))
   190  		})
   191  	})
   192  
   193  	Context("with an action with an integer parameter with no default value", func() {
   194  		BeforeEach(func() {
   195  			codegen.TempCount = 0
   196  			design.Design = &design.APIDefinition{
   197  				Name:        "testapi",
   198  				Title:       "dummy API with no resource",
   199  				Description: "I told you it's dummy",
   200  				Consumes:    design.DefaultEncoders,
   201  				Resources: map[string]*design.ResourceDefinition{
   202  					"foo": {
   203  						Name: "foo",
   204  						Actions: map[string]*design.ActionDefinition{
   205  							"show": {
   206  								Name: "show",
   207  								QueryParams: &design.AttributeDefinition{
   208  									Type: design.Object{
   209  										"param":       &design.AttributeDefinition{Type: design.Integer},
   210  										"time":        &design.AttributeDefinition{Type: design.DateTime},
   211  										"uuid":        &design.AttributeDefinition{Type: design.UUID},
   212  										"any":         &design.AttributeDefinition{Type: design.Any},
   213  										"bool":        &design.AttributeDefinition{Type: design.Boolean},
   214  										"number":      &design.AttributeDefinition{Type: design.Number},
   215  										"boolReq":     &design.AttributeDefinition{Type: design.Boolean},
   216  										"timeArray":   &design.AttributeDefinition{Type: &design.Array{ElemType: &design.AttributeDefinition{Type: design.DateTime}}},
   217  										"uuidArray":   &design.AttributeDefinition{Type: &design.Array{ElemType: &design.AttributeDefinition{Type: design.UUID}}},
   218  										"anyArray":    &design.AttributeDefinition{Type: &design.Array{ElemType: &design.AttributeDefinition{Type: design.Any}}},
   219  										"boolArray":   &design.AttributeDefinition{Type: &design.Array{ElemType: &design.AttributeDefinition{Type: design.Boolean}}},
   220  										"numberArray": &design.AttributeDefinition{Type: &design.Array{ElemType: &design.AttributeDefinition{Type: design.Number}}},
   221  									},
   222  									Validation: &dslengine.ValidationDefinition{Required: []string{"boolReq"}},
   223  								},
   224  								Routes: []*design.RouteDefinition{
   225  									{
   226  										Verb: "GET",
   227  										Path: "",
   228  									},
   229  								},
   230  							},
   231  						},
   232  					},
   233  				},
   234  			}
   235  			fooRes := design.Design.Resources["foo"]
   236  			showAct := fooRes.Actions["show"]
   237  			showAct.Parent = fooRes
   238  			showAct.Routes[0].Parent = showAct
   239  		})
   240  
   241  		It("generate the correct handling for special type DateTime", func() {
   242  			Ω(genErr).Should(BeNil())
   243  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   244  			content := string(c)
   245  			Ω(err).ShouldNot(HaveOccurred())
   246  			Ω(content).Should(ContainSubstring(", err = timeVal(cmd.Time)"))
   247  			Ω(content).Should(ContainSubstring(", tmp"))
   248  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.Time, "))
   249  		})
   250  		It("generate the correct handling for special type DateTime Array", func() {
   251  			Ω(genErr).Should(BeNil())
   252  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   253  			content := string(c)
   254  			Ω(err).ShouldNot(HaveOccurred())
   255  			Ω(content).Should(ContainSubstring(", err = timeArray(cmd.TimeArray)"))
   256  			Ω(content).Should(ContainSubstring(", tmp"))
   257  			Ω(content).Should(ContainSubstring("cc.Flags().StringSliceVar(&cmd.TimeArray, "))
   258  		})
   259  		It("generate the correct handling for special type Number", func() {
   260  			Ω(genErr).Should(BeNil())
   261  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   262  			content := string(c)
   263  			Ω(err).ShouldNot(HaveOccurred())
   264  			Ω(content).Should(ContainSubstring(", err = float64Val(cmd.Number)"))
   265  			Ω(content).Should(ContainSubstring(", tmp"))
   266  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.Number, "))
   267  		})
   268  		It("generate the correct handling for special type Number Array", func() {
   269  			Ω(genErr).Should(BeNil())
   270  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   271  			content := string(c)
   272  			Ω(err).ShouldNot(HaveOccurred())
   273  			Ω(content).Should(ContainSubstring(", err = float64Array(cmd.NumberArray)"))
   274  			Ω(content).Should(ContainSubstring(", tmp"))
   275  			Ω(content).Should(ContainSubstring("cc.Flags().StringSliceVar(&cmd.NumberArray, "))
   276  		})
   277  		It("generate the correct handling for special type Boolean", func() {
   278  			Ω(genErr).Should(BeNil())
   279  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   280  			content := string(c)
   281  			Ω(err).ShouldNot(HaveOccurred())
   282  			Ω(content).Should(ContainSubstring(", err = boolVal(cmd.Bool)"))
   283  			Ω(content).Should(ContainSubstring(", tmp"))
   284  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.Bool, "))
   285  		})
   286  		It("generate the correct handling for special type Boolean Array", func() {
   287  			Ω(genErr).Should(BeNil())
   288  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   289  			content := string(c)
   290  			Ω(err).ShouldNot(HaveOccurred())
   291  			Ω(content).Should(ContainSubstring(", err = boolArray(cmd.BoolArray)"))
   292  			Ω(content).Should(ContainSubstring(", tmp"))
   293  			Ω(content).Should(ContainSubstring("cc.Flags().StringSliceVar(&cmd.BoolArray, "))
   294  		})
   295  		It("generate the correct handling for special type required Boolean", func() {
   296  			Ω(genErr).Should(BeNil())
   297  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   298  			content := string(c)
   299  			Ω(err).ShouldNot(HaveOccurred())
   300  			Ω(content).Should(ContainSubstring(", err = boolVal(cmd.BoolReq)"))
   301  			Ω(content).Should(ContainSubstring(", *tmp"))
   302  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.BoolReq, "))
   303  		})
   304  		It("generate the correct handling for special type UUID", func() {
   305  			Ω(genErr).Should(BeNil())
   306  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   307  			content := string(c)
   308  			Ω(err).ShouldNot(HaveOccurred())
   309  			Ω(content).Should(ContainSubstring(", err = uuidVal(cmd.UUID)"))
   310  			Ω(content).Should(ContainSubstring(", tmp"))
   311  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.UUID, "))
   312  		})
   313  		It("generate the correct handling for special type UUID Array", func() {
   314  			Ω(genErr).Should(BeNil())
   315  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   316  			content := string(c)
   317  			Ω(err).ShouldNot(HaveOccurred())
   318  			Ω(content).Should(ContainSubstring(", err = uuidArray(cmd.UUIDArray)"))
   319  			Ω(content).Should(ContainSubstring(", tmp"))
   320  			Ω(content).Should(ContainSubstring("cc.Flags().StringSliceVar(&cmd.UUIDArray, "))
   321  		})
   322  		It("generate the correct handling for special type Any", func() {
   323  			Ω(genErr).Should(BeNil())
   324  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   325  			content := string(c)
   326  			Ω(err).ShouldNot(HaveOccurred())
   327  			Ω(content).Should(ContainSubstring(", err = jsonVal(cmd.Any)"))
   328  			Ω(content).Should(ContainSubstring(", tmp"))
   329  			Ω(content).Should(ContainSubstring("cc.Flags().StringVar(&cmd.Any, "))
   330  		})
   331  		It("generate the correct handling for special type Any Array", func() {
   332  			Ω(genErr).Should(BeNil())
   333  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   334  			content := string(c)
   335  			Ω(err).ShouldNot(HaveOccurred())
   336  			Ω(content).Should(ContainSubstring(", err = jsonArray(cmd.AnyArray)"))
   337  			Ω(content).Should(ContainSubstring(", tmp"))
   338  			Ω(content).Should(ContainSubstring("cc.Flags().StringSliceVar(&cmd.AnyArray, "))
   339  		})
   340  
   341  		It("generates the correct command flag initialization code", func() {
   342  			Ω(genErr).Should(BeNil())
   343  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   344  			content := string(c)
   345  			Ω(err).ShouldNot(HaveOccurred())
   346  			Ω(len(strings.Split(content, "\n"))).Should(BeNumerically(">=", 3))
   347  			Ω(content).Should(ContainSubstring("var param int"))
   348  			Ω(content).Should(ContainSubstring("var time_ string"))
   349  			Ω(content).Should(ContainSubstring(".Flags()"))
   350  			_, err = gexec.Build(filepath.Join(testgenPackagePath, "tool", "testapi-cli"))
   351  			Ω(err).ShouldNot(HaveOccurred())
   352  
   353  		})
   354  
   355  		Context("with an action with a multiline description", func() {
   356  			const multiline = "multi\nline"
   357  
   358  			BeforeEach(func() {
   359  				design.Design.Resources["foo"].Actions["show"].Description = multiline
   360  			})
   361  
   362  			It("properly escapes the multi-line string used in the short description", func() {
   363  				Ω(genErr).Should(BeNil())
   364  				Ω(files).Should(HaveLen(9))
   365  				c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   366  				content := string(c)
   367  				Ω(err).ShouldNot(HaveOccurred())
   368  				Ω(content).Should(ContainSubstring(multiline))
   369  			})
   370  		})
   371  
   372  		Context("with an action with a description containing backticks", func() {
   373  			const pre = "pre"
   374  			const post = "post"
   375  
   376  			BeforeEach(func() {
   377  				design.Design.Resources["foo"].Actions["show"].Description = pre + "`" + post
   378  			})
   379  
   380  			It("properly escapes the multi-line string used in the short description", func() {
   381  				Ω(genErr).Should(BeNil())
   382  				Ω(files).Should(HaveLen(9))
   383  				c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   384  				content := string(c)
   385  				Ω(err).ShouldNot(HaveOccurred())
   386  				Ω(content).Should(ContainSubstring(pre + "` + \"`\" + `" + post))
   387  			})
   388  		})
   389  	})
   390  	Context("with an action with a special typed UUID path param", func() {
   391  		BeforeEach(func() {
   392  			codegen.TempCount = 0
   393  			design.Design = &design.APIDefinition{
   394  				Name:        "testapi",
   395  				Title:       "dummy API with no resource",
   396  				Description: "I told you it's dummy",
   397  				Consumes:    design.DefaultEncoders,
   398  				Resources: map[string]*design.ResourceDefinition{
   399  					"foo": {
   400  						Name: "foo",
   401  						Actions: map[string]*design.ActionDefinition{
   402  							"show": {
   403  								Name: "show",
   404  								Params: &design.AttributeDefinition{
   405  									Type: design.Object{
   406  										"id": &design.AttributeDefinition{Type: design.UUID},
   407  									},
   408  									Validation: &dslengine.ValidationDefinition{Required: []string{"id"}},
   409  								},
   410  								Routes: []*design.RouteDefinition{
   411  									{
   412  										Verb: "GET",
   413  										Path: "resource/:id",
   414  									},
   415  								},
   416  							},
   417  						},
   418  					},
   419  				},
   420  			}
   421  			fooRes := design.Design.Resources["foo"]
   422  			showAct := fooRes.Actions["show"]
   423  			showAct.Parent = fooRes
   424  			showAct.Routes[0].Parent = showAct
   425  		})
   426  
   427  		It("generates direct access to Command field when resolving path", func() {
   428  			Ω(genErr).Should(BeNil())
   429  			Ω(files).Should(HaveLen(9))
   430  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "cli", "commands.go"))
   431  			content := string(c)
   432  			Ω(err).ShouldNot(HaveOccurred())
   433  			Ω(content).Should(ContainSubstring("path = fmt.Sprintf(\"/resource/%v\", cmd.ID)"))
   434  		})
   435  	})
   436  
   437  	Context("with an action with security configured", func() {
   438  		BeforeEach(func() {
   439  			codegen.TempCount = 0
   440  			securitySchemeDef := &design.SecuritySchemeDefinition{
   441  				SchemeName: "jwt-1",
   442  				Kind:       design.JWTSecurityKind,
   443  			}
   444  			design.Design = &design.APIDefinition{
   445  				Name:        "testapi",
   446  				Title:       "dummy API with no resource",
   447  				Description: "I told you it's dummy",
   448  				Consumes:    design.DefaultEncoders,
   449  				SecuritySchemes: []*design.SecuritySchemeDefinition{
   450  					securitySchemeDef,
   451  				},
   452  				Resources: map[string]*design.ResourceDefinition{
   453  					"foo": {
   454  						Name: "foo",
   455  						Actions: map[string]*design.ActionDefinition{
   456  							"show": {
   457  								Name: "show",
   458  								QueryParams: &design.AttributeDefinition{
   459  									Type: design.Object{
   460  										"param": &design.AttributeDefinition{Type: design.Integer},
   461  										"time":  &design.AttributeDefinition{Type: design.DateTime},
   462  									},
   463  								},
   464  								Routes: []*design.RouteDefinition{
   465  									{
   466  										Verb: "GET",
   467  										Path: "",
   468  									},
   469  								},
   470  								Security: &design.SecurityDefinition{
   471  									Scheme: securitySchemeDef,
   472  								},
   473  							},
   474  						},
   475  					},
   476  				},
   477  			}
   478  			fooRes := design.Design.Resources["foo"]
   479  			showAct := fooRes.Actions["show"]
   480  			showAct.Parent = fooRes
   481  			showAct.Routes[0].Parent = showAct
   482  		})
   483  
   484  		It("generates registers the signer flags from main", func() {
   485  			Ω(genErr).Should(BeNil())
   486  			Ω(files).Should(HaveLen(9))
   487  			c, err := ioutil.ReadFile(filepath.Join(outDir, "tool", "testapi-cli", "main.go"))
   488  			content := string(c)
   489  			Ω(err).ShouldNot(HaveOccurred())
   490  			Ω(content).Should(ContainSubstring("jwt1Signer := newJWT1Signer()"))
   491  			Ω(content).Should(ContainSubstring("c.SetJWT1Signer(jwt1Signer)"))
   492  		})
   493  	})
   494  })