github.com/geneva/gqlgen@v0.17.7-0.20230801155730-7b9317164836/plugin/modelgen/models_test.go (about) 1 package modelgen 2 3 import ( 4 "errors" 5 "fmt" 6 "go/ast" 7 "go/parser" 8 "go/token" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "reflect" 13 "sort" 14 "strings" 15 "testing" 16 17 "github.com/geneva/gqlgen/codegen/config" 18 "github.com/geneva/gqlgen/graphql" 19 "github.com/geneva/gqlgen/plugin/modelgen/internal/extrafields" 20 "github.com/geneva/gqlgen/plugin/modelgen/out" 21 "github.com/geneva/gqlgen/plugin/modelgen/out_enable_model_json_omitempty_tag_false" 22 "github.com/geneva/gqlgen/plugin/modelgen/out_enable_model_json_omitempty_tag_nil" 23 "github.com/geneva/gqlgen/plugin/modelgen/out_enable_model_json_omitempty_tag_true" 24 "github.com/geneva/gqlgen/plugin/modelgen/out_nullable_input_omittable" 25 "github.com/geneva/gqlgen/plugin/modelgen/out_struct_pointers" 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 ) 29 30 func TestModelGeneration(t *testing.T) { 31 cfg, err := config.LoadConfig("testdata/gqlgen.yml") 32 require.NoError(t, err) 33 require.NoError(t, cfg.Init()) 34 p := Plugin{ 35 MutateHook: mutateHook, 36 FieldHook: DefaultFieldMutateHook, 37 } 38 require.NoError(t, p.MutateConfig(cfg)) 39 require.NoError(t, goBuild(t, "./out/")) 40 41 require.True(t, cfg.Models.UserDefined("MissingTypeNotNull")) 42 require.True(t, cfg.Models.UserDefined("MissingTypeNullable")) 43 require.True(t, cfg.Models.UserDefined("MissingEnum")) 44 require.True(t, cfg.Models.UserDefined("MissingUnion")) 45 require.True(t, cfg.Models.UserDefined("MissingInterface")) 46 require.True(t, cfg.Models.UserDefined("TypeWithDescription")) 47 require.True(t, cfg.Models.UserDefined("EnumWithDescription")) 48 require.True(t, cfg.Models.UserDefined("InterfaceWithDescription")) 49 require.True(t, cfg.Models.UserDefined("UnionWithDescription")) 50 require.True(t, cfg.Models.UserDefined("RenameFieldTest")) 51 require.True(t, cfg.Models.UserDefined("ExtraFieldsTest")) 52 53 t.Run("no pointer pointers", func(t *testing.T) { 54 generated, err := os.ReadFile("./out/generated.go") 55 require.NoError(t, err) 56 require.NotContains(t, string(generated), "**") 57 }) 58 59 t.Run("description is generated", func(t *testing.T) { 60 node, err := parser.ParseFile(token.NewFileSet(), "./out/generated.go", nil, parser.ParseComments) 61 require.NoError(t, err) 62 for _, commentGroup := range node.Comments { 63 text := commentGroup.Text() 64 words := strings.Split(text, " ") 65 require.True(t, len(words) > 1, "expected description %q to have more than one word", text) 66 } 67 }) 68 69 t.Run("tags are applied", func(t *testing.T) { 70 file, err := os.ReadFile("./out/generated.go") 71 require.NoError(t, err) 72 73 fileText := string(file) 74 75 expectedTags := []string{ 76 `json:"missing2" database:"MissingTypeNotNullmissing2"`, 77 `json:"name,omitempty" database:"MissingInputname"`, 78 `json:"missing2,omitempty" database:"MissingTypeNullablemissing2"`, 79 `json:"name,omitempty" database:"TypeWithDescriptionname"`, 80 } 81 82 for _, tag := range expectedTags { 83 require.True(t, strings.Contains(fileText, tag), "\nexpected:\n"+tag+"\ngot\n"+fileText) 84 } 85 }) 86 87 t.Run("field hooks are applied", func(t *testing.T) { 88 file, err := os.ReadFile("./out/generated.go") 89 require.NoError(t, err) 90 91 fileText := string(file) 92 93 expectedTags := []string{ 94 `json:"name,omitempty" anotherTag:"tag"`, 95 `json:"enum,omitempty" yetAnotherTag:"12"`, 96 `json:"noVal,omitempty" yaml:"noVal" repeated:"true"`, 97 `json:"repeated,omitempty" someTag:"value" repeated:"true"`, 98 } 99 100 for _, tag := range expectedTags { 101 require.True(t, strings.Contains(fileText, tag), "\nexpected:\n"+tag+"\ngot\n"+fileText) 102 } 103 }) 104 105 t.Run("concrete types implement interface", func(t *testing.T) { 106 var _ out.FooBarer = out.FooBarr{} 107 }) 108 109 t.Run("implemented interfaces", func(t *testing.T) { 110 pkg, err := parseAst("out") 111 require.NoError(t, err) 112 113 path := filepath.Join("out", "generated.go") 114 generated := pkg.Files[path] 115 116 type field struct { 117 typ string 118 name string 119 } 120 cases := []struct { 121 name string 122 wantFields []field 123 }{ 124 { 125 name: "A", 126 wantFields: []field{ 127 { 128 typ: "method", 129 name: "IsA", 130 }, 131 { 132 typ: "method", 133 name: "GetA", 134 }, 135 }, 136 }, 137 { 138 name: "B", 139 wantFields: []field{ 140 { 141 typ: "method", 142 name: "IsB", 143 }, 144 { 145 typ: "method", 146 name: "GetB", 147 }, 148 }, 149 }, 150 { 151 name: "C", 152 wantFields: []field{ 153 { 154 typ: "method", 155 name: "IsA", 156 }, 157 { 158 typ: "method", 159 name: "IsC", 160 }, 161 { 162 typ: "method", 163 name: "GetA", 164 }, 165 { 166 typ: "method", 167 name: "GetC", 168 }, 169 }, 170 }, 171 { 172 name: "D", 173 wantFields: []field{ 174 { 175 typ: "method", 176 name: "IsA", 177 }, 178 { 179 typ: "method", 180 name: "IsB", 181 }, 182 { 183 typ: "method", 184 name: "IsD", 185 }, 186 { 187 typ: "method", 188 name: "GetA", 189 }, 190 { 191 typ: "method", 192 name: "GetB", 193 }, 194 { 195 typ: "method", 196 name: "GetD", 197 }, 198 }, 199 }, 200 } 201 for _, tc := range cases { 202 tc := tc 203 t.Run(tc.name, func(t *testing.T) { 204 typeSpec, ok := generated.Scope.Lookup(tc.name).Decl.(*ast.TypeSpec) 205 require.True(t, ok) 206 207 fields := typeSpec.Type.(*ast.InterfaceType).Methods.List 208 for i, want := range tc.wantFields { 209 if want.typ == "ident" { 210 ident, ok := fields[i].Type.(*ast.Ident) 211 require.True(t, ok) 212 assert.Equal(t, want.name, ident.Name) 213 } 214 if want.typ == "method" { 215 require.GreaterOrEqual(t, 1, len(fields[i].Names)) 216 name := fields[i].Names[0].Name 217 assert.Equal(t, want.name, name) 218 } 219 } 220 }) 221 } 222 }) 223 224 t.Run("implemented interfaces type CDImplemented", func(t *testing.T) { 225 pkg, err := parseAst("out") 226 require.NoError(t, err) 227 228 path := filepath.Join("out", "generated.go") 229 generated := pkg.Files[path] 230 231 wantMethods := []string{ 232 "IsA", 233 "IsB", 234 "IsC", 235 "IsD", 236 } 237 238 gots := make([]string, 0, len(wantMethods)) 239 for _, decl := range generated.Decls { 240 if funcDecl, ok := decl.(*ast.FuncDecl); ok { 241 switch funcDecl.Name.Name { 242 case "IsA", "IsB", "IsC", "IsD": 243 gots = append(gots, funcDecl.Name.Name) 244 require.Len(t, funcDecl.Recv.List, 1) 245 recvIdent, ok := funcDecl.Recv.List[0].Type.(*ast.Ident) 246 require.True(t, ok) 247 require.Equal(t, "CDImplemented", recvIdent.Name) 248 } 249 } 250 } 251 252 sort.Strings(gots) 253 require.Equal(t, wantMethods, gots) 254 }) 255 256 t.Run("cyclical struct fields become pointers", func(t *testing.T) { 257 require.Nil(t, out.CyclicalA{}.FieldOne) 258 require.Nil(t, out.CyclicalA{}.FieldTwo) 259 require.Nil(t, out.CyclicalA{}.FieldThree) 260 require.NotNil(t, out.CyclicalA{}.FieldFour) 261 require.Nil(t, out.CyclicalB{}.FieldOne) 262 require.Nil(t, out.CyclicalB{}.FieldTwo) 263 require.Nil(t, out.CyclicalB{}.FieldThree) 264 require.Nil(t, out.CyclicalB{}.FieldFour) 265 require.NotNil(t, out.CyclicalB{}.FieldFive) 266 }) 267 268 t.Run("non-cyclical struct fields become pointers", func(t *testing.T) { 269 require.NotNil(t, out.NotCyclicalB{}.FieldOne) 270 require.Nil(t, out.NotCyclicalB{}.FieldTwo) 271 }) 272 273 t.Run("recursive struct fields become pointers", func(t *testing.T) { 274 require.Nil(t, out.Recursive{}.FieldOne) 275 require.Nil(t, out.Recursive{}.FieldTwo) 276 require.Nil(t, out.Recursive{}.FieldThree) 277 require.NotNil(t, out.Recursive{}.FieldFour) 278 }) 279 280 t.Run("overridden struct field names use same capitalization as config", func(t *testing.T) { 281 require.NotNil(t, out.RenameFieldTest{}.GOODnaME) 282 }) 283 284 t.Run("nullable input fields can be made omittable with goField", func(t *testing.T) { 285 require.IsType(t, out.MissingInput{}.NullString, graphql.Omittable[*string]{}) 286 require.IsType(t, out.MissingInput{}.NullEnum, graphql.Omittable[*out.MissingEnum]{}) 287 require.IsType(t, out.MissingInput{}.NullObject, graphql.Omittable[*out.ExistingInput]{}) 288 }) 289 290 t.Run("extra fields are present", func(t *testing.T) { 291 var m out.ExtraFieldsTest 292 293 require.IsType(t, m.FieldInt, int64(0)) 294 require.IsType(t, m.FieldInternalType, extrafields.Type{}) 295 require.IsType(t, m.FieldStringPtr, new(string)) 296 require.IsType(t, m.FieldIntSlice, []int64{}) 297 }) 298 } 299 300 func TestModelGenerationStructFieldPointers(t *testing.T) { 301 cfg, err := config.LoadConfig("testdata/gqlgen_struct_field_pointers.yml") 302 require.NoError(t, err) 303 require.NoError(t, cfg.Init()) 304 p := Plugin{ 305 MutateHook: mutateHook, 306 FieldHook: DefaultFieldMutateHook, 307 } 308 require.NoError(t, p.MutateConfig(cfg)) 309 310 t.Run("no pointer pointers", func(t *testing.T) { 311 generated, err := os.ReadFile("./out_struct_pointers/generated.go") 312 require.NoError(t, err) 313 require.NotContains(t, string(generated), "**") 314 }) 315 316 t.Run("cyclical struct fields become pointers", func(t *testing.T) { 317 require.Nil(t, out_struct_pointers.CyclicalA{}.FieldOne) 318 require.Nil(t, out_struct_pointers.CyclicalA{}.FieldTwo) 319 require.Nil(t, out_struct_pointers.CyclicalA{}.FieldThree) 320 require.NotNil(t, out_struct_pointers.CyclicalA{}.FieldFour) 321 require.Nil(t, out_struct_pointers.CyclicalB{}.FieldOne) 322 require.Nil(t, out_struct_pointers.CyclicalB{}.FieldTwo) 323 require.Nil(t, out_struct_pointers.CyclicalB{}.FieldThree) 324 require.Nil(t, out_struct_pointers.CyclicalB{}.FieldFour) 325 require.NotNil(t, out_struct_pointers.CyclicalB{}.FieldFive) 326 }) 327 328 t.Run("non-cyclical struct fields do not become pointers", func(t *testing.T) { 329 require.NotNil(t, out_struct_pointers.NotCyclicalB{}.FieldOne) 330 require.NotNil(t, out_struct_pointers.NotCyclicalB{}.FieldTwo) 331 }) 332 333 t.Run("recursive struct fields become pointers", func(t *testing.T) { 334 require.Nil(t, out_struct_pointers.Recursive{}.FieldOne) 335 require.Nil(t, out_struct_pointers.Recursive{}.FieldTwo) 336 require.Nil(t, out_struct_pointers.Recursive{}.FieldThree) 337 require.NotNil(t, out_struct_pointers.Recursive{}.FieldFour) 338 }) 339 340 t.Run("no getters", func(t *testing.T) { 341 generated, err := os.ReadFile("./out_struct_pointers/generated.go") 342 require.NoError(t, err) 343 require.NotContains(t, string(generated), "func (this") 344 }) 345 } 346 347 func TestModelGenerationNullableInputOmittable(t *testing.T) { 348 cfg, err := config.LoadConfig("testdata/gqlgen_nullable_input_omittable.yml") 349 require.NoError(t, err) 350 require.NoError(t, cfg.Init()) 351 p := Plugin{ 352 MutateHook: mutateHook, 353 FieldHook: DefaultFieldMutateHook, 354 } 355 require.NoError(t, p.MutateConfig(cfg)) 356 357 t.Run("nullable input fields are omittable", func(t *testing.T) { 358 require.IsType(t, out_nullable_input_omittable.MissingInput{}.Name, graphql.Omittable[*string]{}) 359 require.IsType(t, out_nullable_input_omittable.MissingInput{}.Enum, graphql.Omittable[*out_nullable_input_omittable.MissingEnum]{}) 360 require.IsType(t, out_nullable_input_omittable.MissingInput{}.NullString, graphql.Omittable[*string]{}) 361 require.IsType(t, out_nullable_input_omittable.MissingInput{}.NullEnum, graphql.Omittable[*out_nullable_input_omittable.MissingEnum]{}) 362 require.IsType(t, out_nullable_input_omittable.MissingInput{}.NullObject, graphql.Omittable[*out_nullable_input_omittable.ExistingInput]{}) 363 }) 364 365 t.Run("non-nullable input fields are not omittable", func(t *testing.T) { 366 require.IsType(t, out_nullable_input_omittable.MissingInput{}.NonNullString, "") 367 }) 368 } 369 370 func TestModelGenerationOmitemptyConfig(t *testing.T) { 371 suites := []struct { 372 n string 373 cfg string 374 enabled bool 375 t any 376 }{ 377 { 378 n: "nil", 379 cfg: "gqlgen_enable_model_json_omitempty_tag_nil.yml", 380 enabled: true, 381 t: out_enable_model_json_omitempty_tag_nil.OmitEmptyJSONTagTest{}, 382 }, 383 { 384 n: "true", 385 cfg: "gqlgen_enable_model_json_omitempty_tag_true.yml", 386 enabled: true, 387 t: out_enable_model_json_omitempty_tag_true.OmitEmptyJSONTagTest{}, 388 }, 389 { 390 n: "false", 391 cfg: "gqlgen_enable_model_json_omitempty_tag_false.yml", 392 enabled: false, 393 t: out_enable_model_json_omitempty_tag_false.OmitEmptyJSONTagTest{}, 394 }, 395 } 396 397 for _, s := range suites { 398 t.Run(s.n, func(t *testing.T) { 399 cfg, err := config.LoadConfig(fmt.Sprintf("testdata/%s", s.cfg)) 400 require.NoError(t, err) 401 require.NoError(t, cfg.Init()) 402 p := Plugin{ 403 MutateHook: mutateHook, 404 FieldHook: DefaultFieldMutateHook, 405 } 406 require.NoError(t, p.MutateConfig(cfg)) 407 rt := reflect.TypeOf(s.t) 408 409 // ensure non-nullable fields are never omitempty 410 sfn, ok := rt.FieldByName("ValueNonNil") 411 require.True(t, ok) 412 require.Equal(t, "ValueNonNil", sfn.Tag.Get("json")) 413 414 // test nullable fields for configured omitempty 415 sf, ok := rt.FieldByName("Value") 416 require.True(t, ok) 417 418 var expected string 419 if s.enabled { 420 expected = "Value,omitempty" 421 } else { 422 expected = "Value" 423 } 424 require.Equal(t, expected, sf.Tag.Get("json")) 425 }) 426 } 427 } 428 429 func mutateHook(b *ModelBuild) *ModelBuild { 430 for _, model := range b.Models { 431 for _, field := range model.Fields { 432 field.Tag += ` database:"` + model.Name + field.Name + `"` 433 } 434 } 435 436 return b 437 } 438 439 func parseAst(path string) (*ast.Package, error) { 440 // test setup to parse the types 441 fset := token.NewFileSet() 442 pkgs, err := parser.ParseDir(fset, path, nil, parser.AllErrors) 443 if err != nil { 444 return nil, err 445 } 446 return pkgs["out"], nil 447 } 448 449 func goBuild(t *testing.T, path string) error { 450 t.Helper() 451 cmd := exec.Command("go", "build", path) 452 out, err := cmd.CombinedOutput() 453 if err != nil { 454 return errors.New(string(out)) 455 } 456 457 return nil 458 } 459 460 func TestRemoveDuplicate(t *testing.T) { 461 type args struct { 462 t string 463 } 464 tests := []struct { 465 name string 466 args args 467 want string 468 wantPanic bool 469 }{ 470 { 471 name: "Duplicate Test with 1", 472 args: args{ 473 t: "json:\"name\"", 474 }, 475 want: "json:\"name\"", 476 }, 477 { 478 name: "Duplicate Test with 2", 479 args: args{ 480 t: "json:\"name\" json:\"name2\"", 481 }, 482 want: "json:\"name2\"", 483 }, 484 { 485 name: "Duplicate Test with 3", 486 args: args{ 487 t: "json:\"name\" json:\"name2\" json:\"name3\"", 488 }, 489 want: "json:\"name3\"", 490 }, 491 { 492 name: "Duplicate Test with 3 and 1 unrelated", 493 args: args{ 494 t: "json:\"name\" something:\"name2\" json:\"name3\"", 495 }, 496 want: "something:\"name2\" json:\"name3\"", 497 }, 498 { 499 name: "Duplicate Test with 3 and 2 unrelated", 500 args: args{ 501 t: "something:\"name1\" json:\"name\" something:\"name2\" json:\"name3\"", 502 }, 503 want: "something:\"name2\" json:\"name3\"", 504 }, 505 { 506 name: "Test tag value with leading empty space", 507 args: args{ 508 t: "json:\"name, name2\"", 509 }, 510 want: "json:\"name, name2\"", 511 wantPanic: true, 512 }, 513 { 514 name: "Test tag value with trailing empty space", 515 args: args{ 516 t: "json:\"name,name2 \"", 517 }, 518 want: "json:\"name,name2 \"", 519 wantPanic: true, 520 }, 521 { 522 name: "Test tag value with space in between", 523 args: args{ 524 t: "gorm:\"unique;not null\"", 525 }, 526 want: "gorm:\"unique;not null\"", 527 wantPanic: false, 528 }, 529 { 530 name: "Test mix use of gorm and json tags", 531 args: args{ 532 t: "gorm:\"unique;not null\" json:\"name,name2\"", 533 }, 534 want: "gorm:\"unique;not null\" json:\"name,name2\"", 535 wantPanic: false, 536 }, 537 } 538 for _, tt := range tests { 539 t.Run(tt.name, func(t *testing.T) { 540 if tt.wantPanic { 541 assert.Panics(t, func() { removeDuplicateTags(tt.args.t) }, "The code did not panic") 542 } else { 543 if got := removeDuplicateTags(tt.args.t); got != tt.want { 544 t.Errorf("removeDuplicate() = %v, want %v", got, tt.want) 545 } 546 } 547 }) 548 } 549 } 550 551 func Test_containsInvalidSpace(t *testing.T) { 552 type args struct { 553 valuesString string 554 } 555 tests := []struct { 556 name string 557 args args 558 want bool 559 }{ 560 { 561 name: "Test tag value with leading empty space", 562 args: args{ 563 valuesString: "name, name2", 564 }, 565 want: true, 566 }, 567 { 568 name: "Test tag value with trailing empty space", 569 args: args{ 570 valuesString: "name ,name2", 571 }, 572 want: true, 573 }, 574 { 575 name: "Test tag value with valid empty space in words", 576 args: args{ 577 valuesString: "accept this,name2", 578 }, 579 want: false, 580 }, 581 } 582 for _, tt := range tests { 583 t.Run(tt.name, func(t *testing.T) { 584 assert.Equalf(t, tt.want, containsInvalidSpace(tt.args.valuesString), "containsInvalidSpace(%v)", tt.args.valuesString) 585 }) 586 } 587 } 588 589 func Test_splitTagsBySpace(t *testing.T) { 590 type args struct { 591 tagsString string 592 } 593 tests := []struct { 594 name string 595 args args 596 want []string 597 }{ 598 { 599 name: "multiple tags, single value", 600 args: args{ 601 tagsString: "json:\"name\" something:\"name2\" json:\"name3\"", 602 }, 603 want: []string{"json:\"name\"", "something:\"name2\"", "json:\"name3\""}, 604 }, 605 { 606 name: "multiple tag, multiple values", 607 args: args{ 608 tagsString: "json:\"name\" something:\"name2\" json:\"name3,name4\"", 609 }, 610 want: []string{"json:\"name\"", "something:\"name2\"", "json:\"name3,name4\""}, 611 }, 612 { 613 name: "single tag, single value", 614 args: args{ 615 tagsString: "json:\"name\"", 616 }, 617 want: []string{"json:\"name\""}, 618 }, 619 { 620 name: "single tag, multiple values", 621 args: args{ 622 tagsString: "json:\"name,name2\"", 623 }, 624 want: []string{"json:\"name,name2\""}, 625 }, 626 { 627 name: "space in value", 628 args: args{ 629 tagsString: "gorm:\"not nul,name2\"", 630 }, 631 want: []string{"gorm:\"not nul,name2\""}, 632 }, 633 } 634 for _, tt := range tests { 635 t.Run(tt.name, func(t *testing.T) { 636 assert.Equalf(t, tt.want, splitTagsBySpace(tt.args.tagsString), "splitTagsBySpace(%v)", tt.args.tagsString) 637 }) 638 } 639 } 640 641 func TestCustomTemplate(t *testing.T) { 642 cfg, err := config.LoadConfig("testdata/gqlgen_custom_model_template.yml") 643 require.NoError(t, err) 644 require.NoError(t, cfg.Init()) 645 p := Plugin{ 646 MutateHook: mutateHook, 647 FieldHook: DefaultFieldMutateHook, 648 } 649 require.NoError(t, p.MutateConfig(cfg)) 650 }