package gen import ( "bytes" "encoding/json" "errors" "fmt" "log" "os" "os/exec" "path" "path/filepath" "plugin" "strings" "testing" "git.ipao.vip/rogeecn/atomctl/pkg/swag" "github.com/go-openapi/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const searchDir = "../testdata/simple" var outputTypes = []string{"go", "json", "yaml"} func TestGen_Build(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_SpecificOutputTypes(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: []string{"go", "unknownType"}, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) tt := []struct { expectedFile string shouldExist bool }{ {filepath.Join(config.OutputDir, "docs.go"), true}, {filepath.Join(config.OutputDir, "swagger.json"), false}, {filepath.Join(config.OutputDir, "swagger.yaml"), false}, } for _, tc := range tt { _, err := os.Stat(tc.expectedFile) if tc.shouldExist { if os.IsNotExist(err) { require.NoError(t, err) } } else { require.Error(t, err) require.True(t, errors.Is(err, os.ErrNotExist)) } _ = os.Remove(tc.expectedFile) } } func TestGen_BuildInstanceName(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) goSourceFile := filepath.Join(config.OutputDir, "docs.go") // Validate default registration name expectedCode, err := os.ReadFile(goSourceFile) if err != nil { require.NoError(t, err) } if !strings.Contains( string(expectedCode), "swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)", ) { t.Fatal(errors.New("generated go code does not contain the correct default registration sequence")) } if !strings.Contains( string(expectedCode), "var SwaggerInfo =", ) { t.Fatal(errors.New("generated go code does not contain the correct default variable declaration")) } // Custom name config.InstanceName = "Custom" goSourceFile = filepath.Join(config.OutputDir, config.InstanceName+"_"+"docs.go") assert.NoError(t, New().Build(config)) expectedCode, err = os.ReadFile(goSourceFile) if err != nil { require.NoError(t, err) } if !strings.Contains( string(expectedCode), "swag.Register(SwaggerInfoCustom.InstanceName(), SwaggerInfoCustom)", ) { t.Fatal(errors.New("generated go code does not contain the correct registration sequence")) } if !strings.Contains( string(expectedCode), "var SwaggerInfoCustom =", ) { t.Fatal(errors.New("generated go code does not contain the correct variable declaration")) } // cleanup expectedFiles := []string{ filepath.Join(config.OutputDir, config.InstanceName+"_"+"docs.go"), filepath.Join(config.OutputDir, config.InstanceName+"_"+"swagger.json"), filepath.Join(config.OutputDir, config.InstanceName+"_"+"swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_BuildSnakeCase(t *testing.T) { config := &Config{ SearchDir: "../testdata/simple2", MainAPIFile: "./main.go", OutputDir: "../testdata/simple2/docs", OutputTypes: outputTypes, PropNamingStrategy: swag.SnakeCase, } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_BuildLowerCamelcase(t *testing.T) { config := &Config{ SearchDir: "../testdata/simple3", MainAPIFile: "./main.go", OutputDir: "../testdata/simple3/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_BuildDescriptionWithQuotes(t *testing.T) { config := &Config{ SearchDir: "../testdata/quotes", MainAPIFile: "./main.go", OutputDir: "../testdata/quotes/docs", OutputTypes: outputTypes, MarkdownFilesDir: "../testdata/quotes", } require.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } } cmd := exec.Command("go", "build", "-buildmode=plugin", "git.ipao.vip/rogeecn/atomctl/pkg/swag/testdata/quotes") cmd.Dir = config.SearchDir output, err := cmd.CombinedOutput() if err != nil { require.NoError(t, err, string(output)) } p, err := plugin.Open(filepath.Join(config.SearchDir, "quotes.so")) if err != nil { require.NoError(t, err) } defer os.Remove("quotes.so") readDoc, err := p.Lookup("ReadDoc") if err != nil { require.NoError(t, err) } jsonOutput := readDoc.(func() string)() var jsonDoc interface{} if err := json.Unmarshal([]byte(jsonOutput), &jsonDoc); err != nil { require.NoError(t, err) } expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) if err != nil { require.NoError(t, err) } assert.JSONEq(t, string(expectedJSON), jsonOutput) } func TestGen_BuildDocCustomDelims(t *testing.T) { config := &Config{ SearchDir: "../testdata/delims", MainAPIFile: "./main.go", OutputDir: "../testdata/delims/docs", OutputTypes: outputTypes, MarkdownFilesDir: "../testdata/delims", InstanceName: "CustomDelims", LeftTemplateDelim: "{%", RightTemplateDelim: "%}", } require.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "CustomDelims_docs.go"), filepath.Join(config.OutputDir, "CustomDelims_swagger.json"), filepath.Join(config.OutputDir, "CustomDelims_swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } } cmd := exec.Command("go", "build", "-buildmode=plugin", "git.ipao.vip/rogeecn/atomctl/pkg/swag/testdata/delims") cmd.Dir = config.SearchDir output, err := cmd.CombinedOutput() if err != nil { require.NoError(t, err, string(output)) } p, err := plugin.Open(filepath.Join(config.SearchDir, "delims.so")) if err != nil { require.NoError(t, err) } defer os.Remove("delims.so") readDoc, err := p.Lookup("ReadDoc") if err != nil { require.NoError(t, err) } jsonOutput := readDoc.(func() string)() var jsonDoc interface{} if err := json.Unmarshal([]byte(jsonOutput), &jsonDoc); err != nil { require.NoError(t, err) } expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) if err != nil { require.NoError(t, err) } assert.JSONEq(t, string(expectedJSON), jsonOutput) } func TestGen_jsonIndent(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } gen := New() gen.jsonIndent = func(data interface{}) ([]byte, error) { return nil, errors.New("fail") } assert.Error(t, gen.Build(config)) } func TestGen_jsonToYAML(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } gen := New() gen.jsonToYAML = func(data []byte) ([]byte, error) { return nil, errors.New("fail") } assert.Error(t, gen.Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_SearchDirIsNotExist(t *testing.T) { var swaggerConfDir, propNamingStrategy string config := &Config{ SearchDir: "../isNotExistDir", MainAPIFile: "./main.go", OutputDir: swaggerConfDir, OutputTypes: outputTypes, PropNamingStrategy: propNamingStrategy, } assert.EqualError(t, New().Build(config), "dir: ../isNotExistDir does not exist") } func TestGen_MainAPiNotExist(t *testing.T) { var swaggerConfDir, propNamingStrategy string config := &Config{ SearchDir: searchDir, MainAPIFile: "./notExists.go", OutputDir: swaggerConfDir, OutputTypes: outputTypes, PropNamingStrategy: propNamingStrategy, } assert.Error(t, New().Build(config)) } func TestGen_OutputIsNotExist(t *testing.T) { var propNamingStrategy string config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "/dev/null", OutputTypes: outputTypes, PropNamingStrategy: propNamingStrategy, } assert.Error(t, New().Build(config)) } func TestGen_FailToWrite(t *testing.T) { outputDir := filepath.Join(os.TempDir(), "swagg", "test") outputTypes := []string{"go", "json", "yaml"} var propNamingStrategy string config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: outputDir, OutputTypes: outputTypes, PropNamingStrategy: propNamingStrategy, } err := os.MkdirAll(outputDir, 0o755) if err != nil { require.NoError(t, err) } _ = os.RemoveAll(filepath.Join(outputDir, "swagger.yaml")) err = os.Mkdir(filepath.Join(outputDir, "swagger.yaml"), 0o755) if err != nil { require.NoError(t, err) } assert.Error(t, New().Build(config)) _ = os.RemoveAll(filepath.Join(outputDir, "swagger.json")) err = os.Mkdir(filepath.Join(outputDir, "swagger.json"), 0o755) if err != nil { require.NoError(t, err) } assert.Error(t, New().Build(config)) _ = os.RemoveAll(filepath.Join(outputDir, "docs.go")) err = os.Mkdir(filepath.Join(outputDir, "docs.go"), 0o755) if err != nil { require.NoError(t, err) } assert.Error(t, New().Build(config)) err = os.RemoveAll(outputDir) if err != nil { require.NoError(t, err) } } func TestGen_configWithOutputDir(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_configWithOutputTypesAll(t *testing.T) { searchDir := "../testdata/simple" outputTypes := []string{"go", "json", "yaml"} config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ path.Join(config.OutputDir, "docs.go"), path.Join(config.OutputDir, "swagger.json"), path.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { t.Fatal(err) } _ = os.Remove(expectedFile) } } func TestGen_configWithOutputTypesSingle(t *testing.T) { searchDir := "../testdata/simple" outputTypes := []string{"go", "json", "yaml"} for _, outputType := range outputTypes { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: []string{outputType}, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) outFileName := "swagger" if outputType == "go" { outFileName = "docs" } expectedFiles := []string{ path.Join(config.OutputDir, outFileName+"."+outputType), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { t.Fatal(err) } _ = os.Remove(expectedFile) } } } func TestGen_formatSource(t *testing.T) { src := `package main import "net func main() {} ` g := New() res := g.formatSource([]byte(src)) assert.Equal(t, []byte(src), res, "Should return same content due to fmt fail") src2 := `package main import "fmt" func main() { fmt.Print("Hello world") } ` res = g.formatSource([]byte(src2)) assert.NotEqual(t, []byte(src2), res, "Should return fmt code") } type mockWriter struct { hook func([]byte) } func (w *mockWriter) Write(data []byte) (int, error) { if w.hook != nil { w.hook(data) } return len(data), nil } func TestGen_writeGoDoc(t *testing.T) { gen := New() swapTemplate := packageTemplate packageTemplate = `{{{` err := gen.writeGoDoc("docs", nil, nil, &Config{}) assert.Error(t, err) packageTemplate = `{{.Data}}` swagger := &spec.Swagger{ VendorExtensible: spec.VendorExtensible{}, SwaggerProps: spec.SwaggerProps{ Info: &spec.Info{}, }, } err = gen.writeGoDoc("docs", &mockWriter{}, swagger, &Config{}) assert.Error(t, err) packageTemplate = `{{ if .GeneratedTime }}Fake Time{{ end }}` err = gen.writeGoDoc("docs", &mockWriter{ hook: func(data []byte) { assert.Equal(t, "Fake Time", string(data)) }, }, swagger, &Config{GeneratedTime: true}) assert.NoError(t, err) err = gen.writeGoDoc("docs", &mockWriter{ hook: func(data []byte) { assert.Equal(t, "", string(data)) }, }, swagger, &Config{GeneratedTime: false}) assert.NoError(t, err) packageTemplate = swapTemplate } func TestGen_GeneratedDoc(t *testing.T) { config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) goCMD, err := exec.LookPath("go") assert.NoError(t, err) cmd := exec.Command(goCMD, "build", filepath.Join(config.OutputDir, "docs.go")) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr assert.NoError(t, cmd.Run()) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_cgoImports(t *testing.T) { config := &Config{ SearchDir: "../testdata/simple_cgo", MainAPIFile: "./main.go", OutputDir: "../testdata/simple_cgo/docs", OutputTypes: outputTypes, PropNamingStrategy: "", ParseDependency: 1, } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_parseOverrides(t *testing.T) { testCases := []struct { Name string Data string Expected map[string]string ExpectedError error }{ { Name: "replace", Data: `replace github.com/foo/bar baz`, Expected: map[string]string{ "github.com/foo/bar": "baz", }, }, { Name: "skip", Data: `skip github.com/foo/bar`, Expected: map[string]string{ "github.com/foo/bar": "", }, }, { Name: "generic-simple", Data: `replace types.Field[string] string`, Expected: map[string]string{ "types.Field[string]": "string", }, }, { Name: "generic-double", Data: `replace types.Field[string,string] string`, Expected: map[string]string{ "types.Field[string,string]": "string", }, }, { Name: "comment", Data: `// this is a comment replace foo bar`, Expected: map[string]string{ "foo": "bar", }, }, { Name: "ignore whitespace", Data: ` replace foo bar`, Expected: map[string]string{ "foo": "bar", }, }, { Name: "unknown directive", Data: `foo`, ExpectedError: fmt.Errorf("could not parse override: 'foo'"), }, } for _, tc := range testCases { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() overrides, err := parseOverrides(strings.NewReader(tc.Data)) assert.Equal(t, tc.Expected, overrides) assert.Equal(t, tc.ExpectedError, err) }) } } func TestGen_TypeOverridesFile(t *testing.T) { customPath := "/foo/bar/baz" tmp, err := os.CreateTemp("", "") require.NoError(t, err) defer os.Remove(tmp.Name()) config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", PropNamingStrategy: "", } t.Run("Default file is missing", func(t *testing.T) { open = func(path string) (*os.File, error) { assert.Equal(t, DefaultOverridesFile, path) return nil, os.ErrNotExist } defer func() { open = os.Open }() config.OverridesFile = DefaultOverridesFile err := New().Build(config) assert.NoError(t, err) }) t.Run("Default file is present", func(t *testing.T) { open = func(path string) (*os.File, error) { assert.Equal(t, DefaultOverridesFile, path) return tmp, nil } defer func() { open = os.Open }() config.OverridesFile = DefaultOverridesFile err := New().Build(config) assert.NoError(t, err) }) t.Run("Different file is missing", func(t *testing.T) { open = func(path string) (*os.File, error) { assert.Equal(t, customPath, path) return nil, os.ErrNotExist } defer func() { open = os.Open }() config.OverridesFile = customPath err := New().Build(config) assert.EqualError(t, err, "could not open overrides file: file does not exist") }) t.Run("Different file is present", func(t *testing.T) { open = func(path string) (*os.File, error) { assert.Equal(t, customPath, path) return tmp, nil } defer func() { open = os.Open }() config.OverridesFile = customPath err := New().Build(config) assert.NoError(t, err) }) } func TestGen_Debugger(t *testing.T) { var buf bytes.Buffer config := &Config{ SearchDir: searchDir, MainAPIFile: "./main.go", OutputDir: "../testdata/simple/docs", OutputTypes: outputTypes, PropNamingStrategy: "", Debugger: log.New(&buf, "", log.LstdFlags), } assert.True(t, buf.Len() == 0) assert.NoError(t, New().Build(config)) assert.True(t, buf.Len() > 0) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } _ = os.Remove(expectedFile) } } func TestGen_ErrorAndInterface(t *testing.T) { config := &Config{ SearchDir: "../testdata/error", MainAPIFile: "./main.go", OutputDir: "../testdata/error/docs", OutputTypes: outputTypes, PropNamingStrategy: "", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "docs.go"), filepath.Join(config.OutputDir, "swagger.json"), filepath.Join(config.OutputDir, "swagger.yaml"), } t.Cleanup(func() { for _, expectedFile := range expectedFiles { _ = os.Remove(expectedFile) } }) // check files for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } } // check content jsonOutput, err := os.ReadFile(filepath.Join(config.OutputDir, "swagger.json")) if err != nil { require.NoError(t, err) } expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "expected.json")) if err != nil { require.NoError(t, err) } assert.JSONEq(t, string(expectedJSON), string(jsonOutput)) } func TestGen_StateAdmin(t *testing.T) { config := &Config{ SearchDir: "../testdata/state", MainAPIFile: "./main.go", OutputDir: "../testdata/state/docs", OutputTypes: outputTypes, PropNamingStrategy: "", State: "admin", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "admin_docs.go"), filepath.Join(config.OutputDir, "admin_swagger.json"), filepath.Join(config.OutputDir, "admin_swagger.yaml"), } t.Cleanup(func() { for _, expectedFile := range expectedFiles { _ = os.Remove(expectedFile) } }) // check files for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } } // check content jsonOutput, err := os.ReadFile(filepath.Join(config.OutputDir, "admin_swagger.json")) require.NoError(t, err) expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "admin_expected.json")) require.NoError(t, err) assert.JSONEq(t, string(expectedJSON), string(jsonOutput)) } func TestGen_StateUser(t *testing.T) { config := &Config{ SearchDir: "../testdata/state", MainAPIFile: "./main.go", OutputDir: "../testdata/state/docs", OutputTypes: outputTypes, PropNamingStrategy: "", State: "user", } assert.NoError(t, New().Build(config)) expectedFiles := []string{ filepath.Join(config.OutputDir, "user_docs.go"), filepath.Join(config.OutputDir, "user_swagger.json"), filepath.Join(config.OutputDir, "user_swagger.yaml"), } t.Cleanup(func() { for _, expectedFile := range expectedFiles { _ = os.Remove(expectedFile) } }) // check files for _, expectedFile := range expectedFiles { if _, err := os.Stat(expectedFile); os.IsNotExist(err) { require.NoError(t, err) } } // check content jsonOutput, err := os.ReadFile(filepath.Join(config.OutputDir, "user_swagger.json")) require.NoError(t, err) expectedJSON, err := os.ReadFile(filepath.Join(config.SearchDir, "user_expected.json")) require.NoError(t, err) assert.JSONEq(t, string(expectedJSON), string(jsonOutput)) }