From 49a07b04c9ecdedd3508a08a365721fb44fe78d7 Mon Sep 17 00:00:00 2001 From: Will Mann <7892675+Will-Mann-16@users.noreply.github.com> Date: Fri, 29 Dec 2023 14:27:14 +0000 Subject: [PATCH] feat: added integration/snapshot tests (#31) --- astTraversal/packageNode.go | 49 +++++++ astTraversal/packageNode_test.go | 2 +- astTraversal/testUtils.go | 2 +- .../testfiles}/callExpression.go | 2 +- .../testfiles}/declaration.go | 0 .../testfiles}/otherpkg1/bar.go | 0 .../testfiles}/otherpkg1/foo.go | 0 .../testfiles}/runnable/main.go | 0 .../testfiles}/usefulTypes.go | 2 +- .../testfiles}/usefulTypes2.go | 0 astTraversal/type.go | 63 +------- formatTypes.go | 6 +- go.mod | 3 + go.sum | 4 + inputs/gin/parseFunction.go | 56 ++----- inputs/gin/parseRoute.go | 14 +- tests/integration/0-template/.gitignore | 1 + tests/integration/0-template/README.md | 2 + .../integration/0-template/components_test.go | 22 +++ tests/integration/0-template/config_test.go | 22 +++ tests/integration/0-template/handlers.go | 60 ++++++++ tests/integration/0-template/paths_test.go | 22 +++ tests/integration/0-template/router.go | 14 ++ tests/integration/1-basic/.gitignore | 1 + tests/integration/1-basic/README.md | 7 + tests/integration/1-basic/components_test.go | 53 +++++++ tests/integration/1-basic/config_test.go | 73 +++++++++ tests/integration/1-basic/handlers.go | 60 ++++++++ tests/integration/1-basic/paths_test.go | 51 +++++++ tests/integration/1-basic/router.go | 14 ++ .../.gitignore | 1 + .../README.md | 2 + .../components_test.go | 30 ++++ .../handlers.go | 20 +++ .../nested/types/type.go | 5 + .../router.go | 12 ++ .../types/type.go | 5 + .../11-multi-content-types/.gitignore | 1 + .../11-multi-content-types/README.md | 5 + .../11-multi-content-types/components_test.go | 34 +++++ .../11-multi-content-types/handlers.go | 27 ++++ .../11-multi-content-types/router.go | 13 ++ .../12-substitute-types/.gitignore | 1 + .../integration/12-substitute-types/README.md | 3 + .../12-substitute-types/components_test.go | 29 ++++ .../12-substitute-types/handlers.go | 60 ++++++++ .../integration/12-substitute-types/router.go | 14 ++ .../integration/2-struct-embedding/.gitignore | 1 + .../integration/2-struct-embedding/README.md | 4 + .../2-struct-embedding/components_test.go | 46 ++++++ .../2-struct-embedding/handlers.go | 48 ++++++ .../2-struct-embedding/paths_test.go | 35 +++++ .../integration/2-struct-embedding/router.go | 12 ++ tests/integration/2-struct-embedding/types.go | 19 +++ .../integration/3-inline-functions/.gitignore | 1 + .../integration/3-inline-functions/README.md | 6 + .../3-inline-functions/paths_test.go | 44 ++++++ .../integration/3-inline-functions/router.go | 54 +++++++ tests/integration/4-doc-comments/.gitignore | 1 + tests/integration/4-doc-comments/README.md | 5 + .../4-doc-comments/components_test.go | 42 ++++++ tests/integration/4-doc-comments/handlers.go | 75 ++++++++++ .../integration/4-doc-comments/paths_test.go | 36 +++++ tests/integration/4-doc-comments/router.go | 16 ++ .../integration/5-custom-functions/.gitignore | 1 + .../integration/5-custom-functions/README.md | 5 + .../5-custom-functions/customFunctions.go | 7 + .../5-custom-functions/handlers.go | 67 +++++++++ .../5-custom-functions/paths_test.go | 53 +++++++ .../integration/5-custom-functions/router.go | 14 ++ tests/integration/6-openapi-format/.gitignore | 1 + tests/integration/6-openapi-format/README.md | 8 + .../6-openapi-format/components_test.go | 128 ++++++++++++++++ tests/integration/6-openapi-format/router.go | 13 ++ tests/integration/6-openapi-format/type.go | 54 +++++++ tests/integration/7-enums/.gitignore | 1 + tests/integration/7-enums/README.md | 3 + tests/integration/7-enums/components_test.go | 42 ++++++ tests/integration/7-enums/enum.go | 25 ++++ tests/integration/7-enums/handlers.go | 26 ++++ tests/integration/7-enums/router.go | 15 ++ tests/integration/8-headers-abort/.gitignore | 1 + tests/integration/8-headers-abort/README.md | 7 + tests/integration/8-headers-abort/handlers.go | 31 ++++ .../integration/8-headers-abort/paths_test.go | 46 ++++++ tests/integration/8-headers-abort/router.go | 15 ++ tests/integration/9-operation-ids/.gitignore | 1 + tests/integration/9-operation-ids/README.md | 3 + tests/integration/9-operation-ids/handlers.go | 60 ++++++++ .../integration/9-operation-ids/paths_test.go | 32 ++++ tests/integration/9-operation-ids/router.go | 14 ++ tests/integration/helpers/testAstra.go | 41 ++++++ tests/petstore/store.go | 47 ++++++ tests/petstore/types.go | 24 +++ tests/snapshot/README.md | 10 ++ tests/snapshot/comparison/compareSpecs.go | 43 ++++++ tests/snapshot/petstore/.gitignore | 1 + tests/snapshot/petstore/README.md | 2 + tests/snapshot/petstore/handlers.go | 60 ++++++++ tests/snapshot/petstore/snapshot.yaml | 138 ++++++++++++++++++ tests/snapshot/petstore/snapshot_test.go | 52 +++++++ utils/splitHandlerPath.go | 44 ++++++ utils/splitHandlerPath_test.go | 63 ++++++++ 103 files changed, 2396 insertions(+), 114 deletions(-) rename {testfiles => astTraversal/testfiles}/callExpression.go (93%) rename {testfiles => astTraversal/testfiles}/declaration.go (100%) rename {testfiles => astTraversal/testfiles}/otherpkg1/bar.go (100%) rename {testfiles => astTraversal/testfiles}/otherpkg1/foo.go (100%) rename {testfiles => astTraversal/testfiles}/runnable/main.go (100%) rename {testfiles => astTraversal/testfiles}/usefulTypes.go (87%) rename {testfiles => astTraversal/testfiles}/usefulTypes2.go (100%) create mode 100644 tests/integration/0-template/.gitignore create mode 100644 tests/integration/0-template/README.md create mode 100644 tests/integration/0-template/components_test.go create mode 100644 tests/integration/0-template/config_test.go create mode 100644 tests/integration/0-template/handlers.go create mode 100644 tests/integration/0-template/paths_test.go create mode 100644 tests/integration/0-template/router.go create mode 100644 tests/integration/1-basic/.gitignore create mode 100644 tests/integration/1-basic/README.md create mode 100644 tests/integration/1-basic/components_test.go create mode 100644 tests/integration/1-basic/config_test.go create mode 100644 tests/integration/1-basic/handlers.go create mode 100644 tests/integration/1-basic/paths_test.go create mode 100644 tests/integration/1-basic/router.go create mode 100644 tests/integration/10-struct-name-collision-avoidance/.gitignore create mode 100644 tests/integration/10-struct-name-collision-avoidance/README.md create mode 100644 tests/integration/10-struct-name-collision-avoidance/components_test.go create mode 100644 tests/integration/10-struct-name-collision-avoidance/handlers.go create mode 100644 tests/integration/10-struct-name-collision-avoidance/nested/types/type.go create mode 100644 tests/integration/10-struct-name-collision-avoidance/router.go create mode 100644 tests/integration/10-struct-name-collision-avoidance/types/type.go create mode 100644 tests/integration/11-multi-content-types/.gitignore create mode 100644 tests/integration/11-multi-content-types/README.md create mode 100644 tests/integration/11-multi-content-types/components_test.go create mode 100644 tests/integration/11-multi-content-types/handlers.go create mode 100644 tests/integration/11-multi-content-types/router.go create mode 100644 tests/integration/12-substitute-types/.gitignore create mode 100644 tests/integration/12-substitute-types/README.md create mode 100644 tests/integration/12-substitute-types/components_test.go create mode 100644 tests/integration/12-substitute-types/handlers.go create mode 100644 tests/integration/12-substitute-types/router.go create mode 100644 tests/integration/2-struct-embedding/.gitignore create mode 100644 tests/integration/2-struct-embedding/README.md create mode 100644 tests/integration/2-struct-embedding/components_test.go create mode 100644 tests/integration/2-struct-embedding/handlers.go create mode 100644 tests/integration/2-struct-embedding/paths_test.go create mode 100644 tests/integration/2-struct-embedding/router.go create mode 100644 tests/integration/2-struct-embedding/types.go create mode 100644 tests/integration/3-inline-functions/.gitignore create mode 100644 tests/integration/3-inline-functions/README.md create mode 100644 tests/integration/3-inline-functions/paths_test.go create mode 100644 tests/integration/3-inline-functions/router.go create mode 100644 tests/integration/4-doc-comments/.gitignore create mode 100644 tests/integration/4-doc-comments/README.md create mode 100644 tests/integration/4-doc-comments/components_test.go create mode 100644 tests/integration/4-doc-comments/handlers.go create mode 100644 tests/integration/4-doc-comments/paths_test.go create mode 100644 tests/integration/4-doc-comments/router.go create mode 100644 tests/integration/5-custom-functions/.gitignore create mode 100644 tests/integration/5-custom-functions/README.md create mode 100644 tests/integration/5-custom-functions/customFunctions.go create mode 100644 tests/integration/5-custom-functions/handlers.go create mode 100644 tests/integration/5-custom-functions/paths_test.go create mode 100644 tests/integration/5-custom-functions/router.go create mode 100644 tests/integration/6-openapi-format/.gitignore create mode 100644 tests/integration/6-openapi-format/README.md create mode 100644 tests/integration/6-openapi-format/components_test.go create mode 100644 tests/integration/6-openapi-format/router.go create mode 100644 tests/integration/6-openapi-format/type.go create mode 100644 tests/integration/7-enums/.gitignore create mode 100644 tests/integration/7-enums/README.md create mode 100644 tests/integration/7-enums/components_test.go create mode 100644 tests/integration/7-enums/enum.go create mode 100644 tests/integration/7-enums/handlers.go create mode 100644 tests/integration/7-enums/router.go create mode 100644 tests/integration/8-headers-abort/.gitignore create mode 100644 tests/integration/8-headers-abort/README.md create mode 100644 tests/integration/8-headers-abort/handlers.go create mode 100644 tests/integration/8-headers-abort/paths_test.go create mode 100644 tests/integration/8-headers-abort/router.go create mode 100644 tests/integration/9-operation-ids/.gitignore create mode 100644 tests/integration/9-operation-ids/README.md create mode 100644 tests/integration/9-operation-ids/handlers.go create mode 100644 tests/integration/9-operation-ids/paths_test.go create mode 100644 tests/integration/9-operation-ids/router.go create mode 100644 tests/integration/helpers/testAstra.go create mode 100644 tests/petstore/store.go create mode 100644 tests/petstore/types.go create mode 100644 tests/snapshot/README.md create mode 100644 tests/snapshot/comparison/compareSpecs.go create mode 100644 tests/snapshot/petstore/.gitignore create mode 100644 tests/snapshot/petstore/README.md create mode 100644 tests/snapshot/petstore/handlers.go create mode 100644 tests/snapshot/petstore/snapshot.yaml create mode 100644 tests/snapshot/petstore/snapshot_test.go create mode 100644 utils/splitHandlerPath.go create mode 100644 utils/splitHandlerPath_test.go diff --git a/astTraversal/packageNode.go b/astTraversal/packageNode.go index 78afb5b..f67bf72 100644 --- a/astTraversal/packageNode.go +++ b/astTraversal/packageNode.go @@ -14,6 +14,10 @@ type PackageNode struct { Package *packages.Package Edges []*PackageNode Files []*FileNode + + // TypeDocMap is a map of type names to their documentation + // We cache this to save iterating over types every time we need to find the documentation + TypeDocMap map[string]string } func (p *PackageNode) Path() string { @@ -192,3 +196,48 @@ func (p *PackageNode) ASTAtPos(pos token.Pos) (ast.Node, error) { return nil, fmt.Errorf("node at %s not found in package %s", node, p.Path()) } + +// FindDocForType finds the documentation for a type in the package +func (p *PackageNode) FindDocForType(typeName string) (string, bool) { + p.populateTypeDocMap() + + doc, ok := p.TypeDocMap[typeName] + return doc, ok +} + +// populateTypeDocMap populates the TypeDocMap for the package +func (p *PackageNode) populateTypeDocMap() { + // If the map is already populated, we don't need to do anything + if p.TypeDocMap != nil { + return + } + + // Otherwise, we need to populate it + p.TypeDocMap = make(map[string]string) + + // Loop over every file + for _, f := range p.Files { + // Loop over every declaration + for _, decl := range f.AST.Decls { + // If the declaration is a GenDecl, it's a const/var/type declaration (top level) + if genDecl, ok := decl.(*ast.GenDecl); ok { + // Loop over every spec + for _, spec := range genDecl.Specs { + // If the spec is a type spec, it's a type declaration + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + // If the type spec has no documentation, and the overarching declaration has no documentation, we skip it + // TypeSpecs can have documentation, but it's not common + // It's more common for the GenDecl to have the documentation + if typeSpec.Doc == nil && genDecl.Doc == nil { + continue + } else if typeSpec.Doc != nil { // The TypeSpec has priority over the GenDecl + p.TypeDocMap[typeSpec.Name.Name] = typeSpec.Doc.Text() + } else { + p.TypeDocMap[typeSpec.Name.Name] = genDecl.Doc.Text() + } + } + } + } + } + } +} diff --git a/astTraversal/packageNode_test.go b/astTraversal/packageNode_test.go index b5fad11..2b5a4d0 100644 --- a/astTraversal/packageNode_test.go +++ b/astTraversal/packageNode_test.go @@ -126,6 +126,6 @@ func TestPackageNode_FindASTForType(t *testing.T) { // Find the AST for the type "otherpkg1.Foo" _, _, err := traverser.ActiveFile().Package.FindASTForType("otherpkg1.Foo") assert.Error(t, err) - assert.ErrorContains(t, err, "type otherpkg1.Foo not found in package github.com/ls6-events/astra/testfiles") + assert.ErrorContains(t, err, "type otherpkg1.Foo not found in package github.com/ls6-events/astra/astTraversal/testfiles") }) } diff --git a/astTraversal/testUtils.go b/astTraversal/testUtils.go index 962d960..ff04aae 100644 --- a/astTraversal/testUtils.go +++ b/astTraversal/testUtils.go @@ -7,7 +7,7 @@ import ( "path" ) -const relativeTestFilePath = "testfiles" +const relativeTestFilePath = "astTraversal/testfiles" func createTraverserFromTestFile(testFilePath string) (*BaseTraverser, error) { wd, err := os.Getwd() // Will work as tests are only run from the root of the project diff --git a/testfiles/callExpression.go b/astTraversal/testfiles/callExpression.go similarity index 93% rename from testfiles/callExpression.go rename to astTraversal/testfiles/callExpression.go index dab53fe..b6ef236 100644 --- a/testfiles/callExpression.go +++ b/astTraversal/testfiles/callExpression.go @@ -4,7 +4,7 @@ package testfiles import ( "fmt" - "github.com/ls6-events/astra/testfiles/otherpkg1" + "github.com/ls6-events/astra/astTraversal/testfiles/otherpkg1" "strings" ) diff --git a/testfiles/declaration.go b/astTraversal/testfiles/declaration.go similarity index 100% rename from testfiles/declaration.go rename to astTraversal/testfiles/declaration.go diff --git a/testfiles/otherpkg1/bar.go b/astTraversal/testfiles/otherpkg1/bar.go similarity index 100% rename from testfiles/otherpkg1/bar.go rename to astTraversal/testfiles/otherpkg1/bar.go diff --git a/testfiles/otherpkg1/foo.go b/astTraversal/testfiles/otherpkg1/foo.go similarity index 100% rename from testfiles/otherpkg1/foo.go rename to astTraversal/testfiles/otherpkg1/foo.go diff --git a/testfiles/runnable/main.go b/astTraversal/testfiles/runnable/main.go similarity index 100% rename from testfiles/runnable/main.go rename to astTraversal/testfiles/runnable/main.go diff --git a/testfiles/usefulTypes.go b/astTraversal/testfiles/usefulTypes.go similarity index 87% rename from testfiles/usefulTypes.go rename to astTraversal/testfiles/usefulTypes.go index c586710..6c31f9b 100644 --- a/testfiles/usefulTypes.go +++ b/astTraversal/testfiles/usefulTypes.go @@ -2,7 +2,7 @@ package testfiles import ( "fmt" - "github.com/ls6-events/astra/testfiles/otherpkg1" + "github.com/ls6-events/astra/astTraversal/testfiles/otherpkg1" "strings" ) diff --git a/testfiles/usefulTypes2.go b/astTraversal/testfiles/usefulTypes2.go similarity index 100% rename from testfiles/usefulTypes2.go rename to astTraversal/testfiles/usefulTypes2.go diff --git a/astTraversal/type.go b/astTraversal/type.go index 4288c8a..b88e3e7 100644 --- a/astTraversal/type.go +++ b/astTraversal/type.go @@ -233,10 +233,11 @@ func (t *TypeTraverser) Result() (Result, error) { // TODO - this isn't working entirely well for external packages // Needs investigation node, err := structFieldResult.Package.ASTAtPos(pos) - if err != nil || node == nil { - t.Traverser.Log.Warn().Err(err).Msgf("failed to get AST at position %d for field %s", pos, f.Id()) - } else if field, ok := node.(*ast.Field); ok { - structFieldResult.Doc = FormatDoc(field.Doc.Text()) + if err == nil && node != nil { + if field, ok := node.(*ast.Field); ok { + t.Traverser.Log.Debug().Str("field", field.Names[0].Name).Msg("Found doc for field") + structFieldResult.Doc = FormatDoc(field.Doc.Text()) + } } } @@ -280,58 +281,10 @@ func (t *TypeTraverser) Doc() (string, error) { return "", err } - node, err := pkg.ASTAtPos(named.Obj().Pos()) - if err != nil || node == nil { - t.Traverser.Log.Warn().Err(err).Msgf("failed to get AST for type %s", t.Node.String()) - for expr, info := range pkg.Package.TypesInfo.Types { - if info.Type.String() == named.String() { - node = expr - break - } - } - } - - for node != nil { - switch n := node.(type) { - case *ast.TypeSpec: - doc := n.Doc.Text() - if doc != "" { - return FormatDoc(doc), nil - } - - // If the doc doesn't exist, we need to find the declaration of the type - // and get the doc from there - // This is because the doc is attached to the declaration, not the AST type - for _, file := range pkg.Package.Syntax { - if pkg.Package.Fset.Position(file.Pos()).Filename == pkg.Package.Fset.Position(n.Pos()).Filename { - for _, decl := range file.Decls { - if genDecl, ok := decl.(*ast.GenDecl); ok { - for _, spec := range genDecl.Specs { - if typeSpec, ok := spec.(*ast.TypeSpec); ok { - if typeSpec.Name.Name == n.Name.Name { - return FormatDoc(genDecl.Doc.Text()), nil - } - } - } - } - } - } - } - - node = nil - case *ast.CompositeLit: - node = n.Type - case *ast.Ident: - if n.Obj != nil { - node = n.Obj.Decl.(ast.Node) - } else { - node = nil - } - default: - node = nil - } + doc, ok := pkg.FindDocForType(named.Obj().Name()) + if ok { + return FormatDoc(doc), nil } - } return "", nil diff --git a/formatTypes.go b/formatTypes.go index ad05b36..3aeae32 100644 --- a/formatTypes.go +++ b/formatTypes.go @@ -8,7 +8,7 @@ type TypeFormat struct { Format string } -// predefinedTypeMap is the map of the standard go types that are accepted by OpenAPI +// PredefinedTypeMap is the map of the standard go types that are accepted by OpenAPI // It contains the go type as a string and the corresponding OpenAPI type as the value - also including the format var PredefinedTypeMap = map[string]TypeFormat{ "string": { @@ -54,10 +54,6 @@ var PredefinedTypeMap = map[string]TypeFormat{ Type: "integer", Format: "uint64", }, - "float": { - Type: "number", - Format: "float", - }, "float32": { Type: "number", Format: "float32", diff --git a/go.mod b/go.mod index 89e775b..02b7fec 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module github.com/ls6-events/astra go 1.21 require ( + github.com/Jeffail/gabs/v2 v2.7.0 github.com/gin-gonic/gin v1.9.1 + github.com/google/go-cmp v0.5.5 + github.com/google/uuid v1.5.0 github.com/iancoleman/strcase v0.3.0 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index d238bae..854d2e2 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg= +github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= @@ -33,6 +35,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/inputs/gin/parseFunction.go b/inputs/gin/parseFunction.go index 86c3e1a..86b9b45 100644 --- a/inputs/gin/parseFunction.go +++ b/inputs/gin/parseFunction.go @@ -241,9 +241,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers return route, nil }) // Query Param methods - case "GetQuery": - fallthrough - case "Query": + case "GetQuery", "Query": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -261,10 +259,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - - case "GetQueryArray": - fallthrough - case "QueryArray": + case "GetQueryArray", "QueryArray": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -283,10 +278,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - - case "GetQueryMap": - fallthrough - case "QueryMap": + case "GetQueryMap", "QueryMap": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -305,10 +297,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - - case "ShouldBindQuery": - fallthrough - case "BindQuery": + case "ShouldBindQuery", "BindQuery": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -324,9 +313,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers } // Body Param methods - case "ShouldBind": - fallthrough - case "Bind": + case "ShouldBind", "Bind": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -362,9 +349,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "ShouldBindJSON": - fallthrough - case "BindJSON": + case "ShouldBindJSON", "BindJSON": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -379,9 +364,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "ShouldBindXML": - fallthrough - case "BindXML": + case "ShouldBindXML", "BindXML": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -396,9 +379,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "ShouldBindYAML": - fallthrough - case "BindYAML": + case "ShouldBindYAML", "BindYAML": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -413,9 +394,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "GetPostForm": - fallthrough - case "PostForm": + case "GetPostForm", "PostForm": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -434,9 +413,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "GetPostFormArray": - fallthrough - case "PostFormArray": + case "GetPostFormArray", "PostFormArray": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -456,9 +433,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "GetPostFormMap": - fallthrough - case "PostFormMap": + case "GetPostFormMap", "PostFormMap": currRoute, err = funcBuilder.Value().Build(func(route *astra.Route, params []any) (*astra.Route, error) { name := params[0].(string) @@ -496,9 +471,7 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers if err != nil { return false } - case "ShouldBindHeader": - fallthrough - case "BindHeader": + case "ShouldBindHeader", "BindHeader": currRoute, err = funcBuilder.ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { field := astra.ParseResultToField(params[0].(astTraversal.Result)) @@ -569,8 +542,9 @@ func parseFunction(s *astra.Service, funcTraverser *astTraversal.FunctionTravers result := params[1].(astTraversal.Result) returnType := astra.ReturnType{ - StatusCode: statusCode, - Field: astra.ParseResultToField(result), + ContentType: "application/json", + StatusCode: statusCode, + Field: astra.ParseResultToField(result), } route.ReturnTypes = astra.AddReturnType(route.ReturnTypes, returnType) diff --git a/inputs/gin/parseRoute.go b/inputs/gin/parseRoute.go index 314ffba..b570d3f 100644 --- a/inputs/gin/parseRoute.go +++ b/inputs/gin/parseRoute.go @@ -8,7 +8,6 @@ import ( "github.com/ls6-events/astra/utils" "go/ast" "path" - "strings" ) // parseRoute parses a route from a gin routes @@ -29,21 +28,18 @@ func parseRoute(s *astra.Service, baseRoute *astra.Route) error { return path, nil }) - splitHandler := strings.Split(baseRoute.Handler, ".") + handler := utils.SplitHandlerPath(baseRoute.Handler) - pkgPath := splitHandler[0] - pkgParts := strings.Split(pkgPath, "/") - pkgName := pkgParts[len(pkgParts)-1] + pkgPath := handler.PackagePath() + pkgName := handler.PackageName() - funcParts := splitHandler[1:] - - if len(funcParts) < 1 { + if len(handler.HandlerParts) < 1 { err := fmt.Errorf("invalid handler name for file: %s", baseRoute.Handler) log.Error().Err(err).Msg("Failed to parse handler name") return err } - funcName := funcParts[0] + funcName := handler.FuncName() pkgNode := traverser.Packages.AddPackage(pkgPath) diff --git a/tests/integration/0-template/.gitignore b/tests/integration/0-template/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/0-template/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/0-template/README.md b/tests/integration/0-template/README.md new file mode 100644 index 0000000..2c7080c --- /dev/null +++ b/tests/integration/0-template/README.md @@ -0,0 +1,2 @@ +# Template +This is a template for an integration testing framework for Astra. It is meant to be used as a starting point for creating new integration tests. \ No newline at end of file diff --git a/tests/integration/0-template/components_test.go b/tests/integration/0-template/components_test.go new file mode 100644 index 0000000..49f2c1d --- /dev/null +++ b/tests/integration/0-template/components_test.go @@ -0,0 +1,22 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSchemas(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + require.NotNil(t, testAstra) + + // Placeholder for integration test +} diff --git a/tests/integration/0-template/config_test.go b/tests/integration/0-template/config_test.go new file mode 100644 index 0000000..c8974f5 --- /dev/null +++ b/tests/integration/0-template/config_test.go @@ -0,0 +1,22 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestConfig(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + require.NotNil(t, testAstra) + + // Placeholder for integration test +} diff --git a/tests/integration/0-template/handlers.go b/tests/integration/0-template/handlers.go new file mode 100644 index 0000000..b8182a0 --- /dev/null +++ b/tests/integration/0-template/handlers.go @@ -0,0 +1,60 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/integration/0-template/paths_test.go b/tests/integration/0-template/paths_test.go new file mode 100644 index 0000000..343cd32 --- /dev/null +++ b/tests/integration/0-template/paths_test.go @@ -0,0 +1,22 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPaths(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + require.NotNil(t, testAstra) + + // Placeholder for integration test +} diff --git a/tests/integration/0-template/router.go b/tests/integration/0-template/router.go new file mode 100644 index 0000000..0a73435 --- /dev/null +++ b/tests/integration/0-template/router.go @@ -0,0 +1,14 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} diff --git a/tests/integration/1-basic/.gitignore b/tests/integration/1-basic/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/1-basic/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/1-basic/README.md b/tests/integration/1-basic/README.md new file mode 100644 index 0000000..d4acb98 --- /dev/null +++ b/tests/integration/1-basic/README.md @@ -0,0 +1,7 @@ +# Basic +This test is a basic test of the Astra service. It tests the following: + +- Outputting of configuration +- Reading the type definitions from other packages correctly. +- Reading the handler definitions for status codes and return types correctly. +- Generating the correct OpenAPI specification. diff --git a/tests/integration/1-basic/components_test.go b/tests/integration/1-basic/components_test.go new file mode 100644 index 0000000..7a67f40 --- /dev/null +++ b/tests/integration/1-basic/components_test.go @@ -0,0 +1,53 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSchemas(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + components := testAstra.Path("components") + + schemas := components.Path("schemas") + + // gin.H + require.True(t, schemas.Exists("gin.H")) + require.Equal(t, "object", schemas.Search("gin.H", "type").Data().(string)) + + // petstore.Pet + require.True(t, schemas.Exists("petstore.Pet")) + require.Equal(t, "object", schemas.Search("petstore.Pet", "type").Data().(string)) + require.Equal(t, "integer", schemas.Search("petstore.Pet", "properties", "id", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.Pet", "properties", "name", "type").Data().(string)) + require.Equal(t, "array", schemas.Search("petstore.Pet", "properties", "photoUrls", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.Pet", "properties", "photoUrls", "items", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.Pet", "properties", "status", "type").Data().(string)) + require.Equal(t, "array", schemas.Search("petstore.Pet", "properties", "tags", "type").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.Tag", schemas.Search("petstore.Pet", "properties", "tags", "items", "$ref").Data().(string)) + + // petstore.PetDTO + require.True(t, schemas.Exists("petstore.PetDTO")) + require.Equal(t, "object", schemas.Search("petstore.PetDTO", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.PetDTO", "properties", "name", "type").Data().(string)) + require.Equal(t, "array", schemas.Search("petstore.PetDTO", "properties", "photoUrls", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.PetDTO", "properties", "photoUrls", "items", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.PetDTO", "properties", "status", "type").Data().(string)) + require.Equal(t, "array", schemas.Search("petstore.PetDTO", "properties", "tags", "type").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.Tag", schemas.Search("petstore.PetDTO", "properties", "tags", "items", "$ref").Data().(string)) + + // petstore.Tag + require.True(t, schemas.Exists("petstore.Tag")) + require.Equal(t, "object", schemas.Search("petstore.Tag", "type").Data().(string)) + require.Equal(t, "integer", schemas.Search("petstore.Tag", "properties", "id", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("petstore.Tag", "properties", "name", "type").Data().(string)) +} diff --git a/tests/integration/1-basic/config_test.go b/tests/integration/1-basic/config_test.go new file mode 100644 index 0000000..5ca1a31 --- /dev/null +++ b/tests/integration/1-basic/config_test.go @@ -0,0 +1,73 @@ +package petstore + +import ( + "github.com/ls6-events/astra" + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestDefaultConfig(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + t.Run("OpenAPI Version", func(t *testing.T) { + require.Equal(t, "3.0.0", testAstra.Path("openapi").Data().(string)) + }) + + t.Run("OpenAPI Info", func(t *testing.T) { + require.Equal(t, "Generated by astra", testAstra.Path("info.description").Data().(string)) + }) + + t.Run("OpenAPI Servers", func(t *testing.T) { + require.Equal(t, "http://localhost:8000", testAstra.Path("servers.0.url").Data().(string)) + }) +} + +func TestCustomConfig(t *testing.T) { + r := setupRouter() + + config := &astra.Config{ + Title: "Test Title", + Description: "Test Description", + Version: "0.0.1", + Contact: astra.Contact{ + Name: "John Doe", + URL: "https://www.google.com", + Email: "john@doe.com", + }, + License: astra.License{ + Name: "MIT", + URL: "https://opensource.org/licenses/MIT", + }, + Secure: false, + Host: "localhost", + BasePath: "/base-path", + Port: 8000, + } + + testAstra, err := helpers.SetupTestAstra(t, r, config) + require.NoError(t, err) + + t.Run("OpenAPI Version", func(t *testing.T) { + require.Equal(t, "3.0.0", testAstra.Path("openapi").Data().(string)) + }) + + t.Run("OpenAPI Info", func(t *testing.T) { + require.Equal(t, "Test Title", testAstra.Path("info.title").Data().(string)) + require.Equal(t, "0.0.1", testAstra.Path("info.version").Data().(string)) + require.Equal(t, "John Doe", testAstra.Path("info.contact.name").Data().(string)) + require.Equal(t, "https://www.google.com", testAstra.Path("info.contact.url").Data().(string)) + require.Equal(t, "Test Description", testAstra.Path("info.description").Data().(string)) + }) + + t.Run("OpenAPI Servers", func(t *testing.T) { + require.Equal(t, "http://localhost:8000/base-path", testAstra.Path("servers.0.url").Data().(string)) + }) +} diff --git a/tests/integration/1-basic/handlers.go b/tests/integration/1-basic/handlers.go new file mode 100644 index 0000000..b8182a0 --- /dev/null +++ b/tests/integration/1-basic/handlers.go @@ -0,0 +1,60 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/integration/1-basic/paths_test.go b/tests/integration/1-basic/paths_test.go new file mode 100644 index 0000000..07c7c19 --- /dev/null +++ b/tests/integration/1-basic/paths_test.go @@ -0,0 +1,51 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPaths(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // GET /pets + require.True(t, paths.Exists("/pets", "get")) + require.Equal(t, "array", paths.Path("/pets.get.responses.200.content.application/json.schema.type").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.Pet", paths.Path("/pets.get.responses.200.content.application/json.schema.items.$ref").Data().(string)) + + // POST /pets + require.True(t, paths.Exists("/pets", "post")) + require.Equal(t, "#/components/schemas/petstore.PetDTO", paths.Path("/pets.post.requestBody.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.PetDTO", paths.Path("/pets.post.responses.200.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/pets.post.responses.400.content.application/json.schema.$ref").Data().(string)) + + // GET /pets/{id} + require.True(t, paths.Exists("/pets/{id}", "get")) + require.Equal(t, "string", paths.Path("/pets/{id}.get.parameters.0.schema.type").Data().(string)) + require.Equal(t, "id", paths.Path("/pets/{id}.get.parameters.0.name").Data().(string)) + require.Equal(t, "path", paths.Path("/pets/{id}.get.parameters.0.in").Data().(string)) + require.True(t, paths.Path("/pets/{id}.get.parameters.0.required").Data().(bool)) + require.Equal(t, "string", paths.Path("/pets/{id}.get.parameters.0.schema.type").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.Pet", paths.Path("/pets/{id}.get.responses.200.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/pets/{id}.get.responses.400.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/pets/{id}.get.responses.404.content.application/json.schema.$ref").Data().(string)) + + // DELETE /pets/{id} + require.True(t, paths.Exists("/pets/{id}", "delete")) + require.Equal(t, "id", paths.Path("/pets/{id}.get.parameters.0.name").Data().(string)) + require.Equal(t, "path", paths.Path("/pets/{id}.get.parameters.0.in").Data().(string)) + require.True(t, paths.Path("/pets/{id}.get.parameters.0.required").Data().(bool)) + require.Equal(t, "string", paths.Path("/pets/{id}.get.parameters.0.schema.type").Data().(string)) + require.True(t, paths.Exists("/pets/{id}", "delete", "responses", "200")) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/pets/{id}.delete.responses.400.content.application/json.schema.$ref").Data().(string)) +} diff --git a/tests/integration/1-basic/router.go b/tests/integration/1-basic/router.go new file mode 100644 index 0000000..0a73435 --- /dev/null +++ b/tests/integration/1-basic/router.go @@ -0,0 +1,14 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} diff --git a/tests/integration/10-struct-name-collision-avoidance/.gitignore b/tests/integration/10-struct-name-collision-avoidance/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/10-struct-name-collision-avoidance/README.md b/tests/integration/10-struct-name-collision-avoidance/README.md new file mode 100644 index 0000000..2c7080c --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/README.md @@ -0,0 +1,2 @@ +# Template +This is a template for an integration testing framework for Astra. It is meant to be used as a starting point for creating new integration tests. \ No newline at end of file diff --git a/tests/integration/10-struct-name-collision-avoidance/components_test.go b/tests/integration/10-struct-name-collision-avoidance/components_test.go new file mode 100644 index 0000000..a5f68f5 --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/components_test.go @@ -0,0 +1,30 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSchemas(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + schemas := testAstra.Path("components.schemas") + + require.NotNil(t, schemas) + + require.True(t, schemas.Exists("10-struct-name-collision-avoidance.types.TestType")) + require.Equal(t, "object", schemas.Search("10-struct-name-collision-avoidance.types.TestType", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("10-struct-name-collision-avoidance.types.TestType", "properties", "topLevelType", "type").Data().(string)) + + require.True(t, schemas.Exists("nested.types.TestType")) + require.Equal(t, "object", schemas.Search("nested.types.TestType", "type").Data().(string)) + require.Equal(t, "string", schemas.Search("nested.types.TestType", "properties", "nestedField", "type").Data().(string)) +} diff --git a/tests/integration/10-struct-name-collision-avoidance/handlers.go b/tests/integration/10-struct-name-collision-avoidance/handlers.go new file mode 100644 index 0000000..7331f1f --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/handlers.go @@ -0,0 +1,20 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + "github.com/ls6-events/astra/tests/integration/10-struct-name-collision-avoidance/nested/types" + topLevelTypes "github.com/ls6-events/astra/tests/integration/10-struct-name-collision-avoidance/types" + "net/http" +) + +func topLevelHandler(c *gin.Context) { + c.JSON(http.StatusOK, topLevelTypes.TestType{ + TopLevelField: "topLevel", + }) +} + +func nestedHandler(c *gin.Context) { + c.JSON(http.StatusOK, types.TestType{ + NestedField: "nested", + }) +} diff --git a/tests/integration/10-struct-name-collision-avoidance/nested/types/type.go b/tests/integration/10-struct-name-collision-avoidance/nested/types/type.go new file mode 100644 index 0000000..e453bdc --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/nested/types/type.go @@ -0,0 +1,5 @@ +package types + +type TestType struct { + NestedField string `json:"nestedField"` +} diff --git a/tests/integration/10-struct-name-collision-avoidance/router.go b/tests/integration/10-struct-name-collision-avoidance/router.go new file mode 100644 index 0000000..2b9c942 --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/router.go @@ -0,0 +1,12 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/top-level", topLevelHandler) + r.GET("/nested", nestedHandler) + + return r +} diff --git a/tests/integration/10-struct-name-collision-avoidance/types/type.go b/tests/integration/10-struct-name-collision-avoidance/types/type.go new file mode 100644 index 0000000..8e08fe7 --- /dev/null +++ b/tests/integration/10-struct-name-collision-avoidance/types/type.go @@ -0,0 +1,5 @@ +package types + +type TestType struct { + TopLevelField string `json:"topLevelType"` +} diff --git a/tests/integration/11-multi-content-types/.gitignore b/tests/integration/11-multi-content-types/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/11-multi-content-types/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/11-multi-content-types/README.md b/tests/integration/11-multi-content-types/README.md new file mode 100644 index 0000000..306951d --- /dev/null +++ b/tests/integration/11-multi-content-types/README.md @@ -0,0 +1,5 @@ +# Multi-content types +This example shows how to use multiple content types using a single struct type. This tests: +- Multiple content types being set for the endpoints. +- The `Content-Type` header being set correctly for the endpoints. +- The same struct type being used for multiple endpoints with the appropriate type name. \ No newline at end of file diff --git a/tests/integration/11-multi-content-types/components_test.go b/tests/integration/11-multi-content-types/components_test.go new file mode 100644 index 0000000..759650b --- /dev/null +++ b/tests/integration/11-multi-content-types/components_test.go @@ -0,0 +1,34 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestMultiContentTypes(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + schemas := testAstra.Path("components.schemas") + + require.False(t, schemas.Exists("11-multi-content-types.MultiContentType")) + + // JSON + require.True(t, schemas.Exists("11-multi-content-types.json.MultiContentType")) + require.True(t, schemas.Exists("11-multi-content-types.json.MultiContentType", "properties", "json-test")) + + // XML + require.True(t, schemas.Exists("11-multi-content-types.xml.MultiContentType")) + require.True(t, schemas.Exists("11-multi-content-types.xml.MultiContentType", "properties", "xml-test")) + + // YAML + require.True(t, schemas.Exists("11-multi-content-types.yaml.MultiContentType")) + require.True(t, schemas.Exists("11-multi-content-types.yaml.MultiContentType", "properties", "yaml-test")) +} diff --git a/tests/integration/11-multi-content-types/handlers.go b/tests/integration/11-multi-content-types/handlers.go new file mode 100644 index 0000000..0a7df22 --- /dev/null +++ b/tests/integration/11-multi-content-types/handlers.go @@ -0,0 +1,27 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" +) + +type MultiContentType struct { + Test string `json:"json-test" xml:"xml-test" yaml:"yaml-test"` +} + +func getJSON(c *gin.Context) { + c.JSON(200, MultiContentType{ + Test: "json", + }) +} + +func getXML(c *gin.Context) { + c.XML(200, MultiContentType{ + Test: "xml", + }) +} + +func getYAML(c *gin.Context) { + c.YAML(200, MultiContentType{ + Test: "yaml", + }) +} diff --git a/tests/integration/11-multi-content-types/router.go b/tests/integration/11-multi-content-types/router.go new file mode 100644 index 0000000..a1e0610 --- /dev/null +++ b/tests/integration/11-multi-content-types/router.go @@ -0,0 +1,13 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/json", getJSON) + r.GET("/xml", getXML) + r.GET("/yaml", getYAML) + + return r +} diff --git a/tests/integration/12-substitute-types/.gitignore b/tests/integration/12-substitute-types/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/12-substitute-types/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/12-substitute-types/README.md b/tests/integration/12-substitute-types/README.md new file mode 100644 index 0000000..d54ec19 --- /dev/null +++ b/tests/integration/12-substitute-types/README.md @@ -0,0 +1,3 @@ +# Substitute Types +This example shows how to substitute types in the schema. This tests: +- A custom substitute type being used for a component \ No newline at end of file diff --git a/tests/integration/12-substitute-types/components_test.go b/tests/integration/12-substitute-types/components_test.go new file mode 100644 index 0000000..e5973b8 --- /dev/null +++ b/tests/integration/12-substitute-types/components_test.go @@ -0,0 +1,29 @@ +package petstore + +import ( + "github.com/ls6-events/astra" + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestSubstituteTypes(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r, astra.WithCustomTypeMapping(map[string]astra.TypeFormat{ + "github.com/ls6-events/astra/tests/petstore.Tag": astra.TypeFormat{ + Type: "string", + Format: "tag", + }, + })) + require.NoError(t, err) + + schemas := testAstra.Path("components.schemas") + + require.Equal(t, "string", schemas.Search("petstore.Tag", "type").Data().(string)) + require.Equal(t, "tag", schemas.Search("petstore.Tag", "format").Data().(string)) +} diff --git a/tests/integration/12-substitute-types/handlers.go b/tests/integration/12-substitute-types/handlers.go new file mode 100644 index 0000000..b8182a0 --- /dev/null +++ b/tests/integration/12-substitute-types/handlers.go @@ -0,0 +1,60 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/integration/12-substitute-types/router.go b/tests/integration/12-substitute-types/router.go new file mode 100644 index 0000000..0a73435 --- /dev/null +++ b/tests/integration/12-substitute-types/router.go @@ -0,0 +1,14 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} diff --git a/tests/integration/2-struct-embedding/.gitignore b/tests/integration/2-struct-embedding/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/2-struct-embedding/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/2-struct-embedding/README.md b/tests/integration/2-struct-embedding/README.md new file mode 100644 index 0000000..697d806 --- /dev/null +++ b/tests/integration/2-struct-embedding/README.md @@ -0,0 +1,4 @@ +# Struct Embedding +This test is used to test the embedding of structs in the Astra service. It tests the following: +- Struct embedding is read correctly. +- Properties of embedded structs are outputted as `allOf` in the OpenAPI specification. \ No newline at end of file diff --git a/tests/integration/2-struct-embedding/components_test.go b/tests/integration/2-struct-embedding/components_test.go new file mode 100644 index 0000000..88c378c --- /dev/null +++ b/tests/integration/2-struct-embedding/components_test.go @@ -0,0 +1,46 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestEmbeddedStructsAllOf(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + components := testAstra.Path("components") + + schemas := components.Path("schemas") + + // Cat + require.True(t, schemas.Exists("2-struct-embedding.Cat")) + require.Equal(t, "object", schemas.Search("2-struct-embedding.Cat", "type").Data().(string)) + // Does reference Pet + require.Equal(t, "#/components/schemas/petstore.Pet", schemas.Search("2-struct-embedding.Cat", "allOf", "0", "$ref").Data().(string)) + // Breed + require.True(t, schemas.Exists("2-struct-embedding.Cat", "allOf", "1", "properties", "breed")) + require.Equal(t, "string", schemas.Search("2-struct-embedding.Cat", "allOf", "1", "properties", "breed", "type").Data().(string)) + // IsIndependent + require.True(t, schemas.Exists("2-struct-embedding.Cat", "allOf", "1", "properties", "isIndependent")) + require.Equal(t, "boolean", schemas.Search("2-struct-embedding.Cat", "allOf", "1", "properties", "isIndependent", "type").Data().(string)) + + // Dog + require.True(t, schemas.Exists("2-struct-embedding.Dog")) + require.Equal(t, "object", schemas.Search("2-struct-embedding.Dog", "type").Data().(string)) + // Does reference Pet + require.Equal(t, "#/components/schemas/petstore.Pet", schemas.Search("2-struct-embedding.Dog", "allOf", "0", "$ref").Data().(string)) + // Breed + require.True(t, schemas.Exists("2-struct-embedding.Dog", "allOf", "1", "properties", "breed")) + require.Equal(t, "string", schemas.Search("2-struct-embedding.Dog", "allOf", "1", "properties", "breed", "type").Data().(string)) + // IsTrained + require.True(t, schemas.Exists("2-struct-embedding.Dog", "allOf", "1", "properties", "isTrained")) + require.Equal(t, "boolean", schemas.Search("2-struct-embedding.Dog", "allOf", "1", "properties", "isTrained", "type").Data().(string)) +} diff --git a/tests/integration/2-struct-embedding/handlers.go b/tests/integration/2-struct-embedding/handlers.go new file mode 100644 index 0000000..9a79d6d --- /dev/null +++ b/tests/integration/2-struct-embedding/handlers.go @@ -0,0 +1,48 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getCatByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, Cat{ + Pet: *pet, + Breed: "Persian", + IsIndependent: false, + }) +} + +func getDogByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, Dog{ + Pet: *pet, + Breed: "Labrador", + IsTrained: true, + }) +} diff --git a/tests/integration/2-struct-embedding/paths_test.go b/tests/integration/2-struct-embedding/paths_test.go new file mode 100644 index 0000000..3c013f7 --- /dev/null +++ b/tests/integration/2-struct-embedding/paths_test.go @@ -0,0 +1,35 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPaths(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // GET /cats/{id} + require.True(t, paths.Exists("/cats/{id}", "get")) + require.Equal(t, "string", paths.Path("/cats/{id}.get.parameters.0.schema.type").Data().(string)) + require.Equal(t, "#/components/schemas/2-struct-embedding.Cat", paths.Path("/cats/{id}.get.responses.200.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/cats/{id}.get.responses.400.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/cats/{id}.get.responses.404.content.application/json.schema.$ref").Data().(string)) + + // GET /dogs/{id} + require.True(t, paths.Exists("/dogs/{id}", "get")) + require.Equal(t, "string", paths.Path("/dogs/{id}.get.parameters.0.schema.type").Data().(string)) + require.Equal(t, "#/components/schemas/2-struct-embedding.Dog", paths.Path("/dogs/{id}.get.responses.200.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/dogs/{id}.get.responses.400.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/dogs/{id}.get.responses.404.content.application/json.schema.$ref").Data().(string)) + +} diff --git a/tests/integration/2-struct-embedding/router.go b/tests/integration/2-struct-embedding/router.go new file mode 100644 index 0000000..1c29c62 --- /dev/null +++ b/tests/integration/2-struct-embedding/router.go @@ -0,0 +1,12 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/cats/:id", getCatByID) + r.GET("/dogs/:id", getDogByID) + + return r +} diff --git a/tests/integration/2-struct-embedding/types.go b/tests/integration/2-struct-embedding/types.go new file mode 100644 index 0000000..a72f12c --- /dev/null +++ b/tests/integration/2-struct-embedding/types.go @@ -0,0 +1,19 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/petstore" +) + +type Cat struct { + petstore.Pet + + Breed string `json:"breed"` + IsIndependent bool `json:"isIndependent"` +} + +type Dog struct { + petstore.Pet + + Breed string `json:"breed"` + IsTrained bool `json:"isTrained"` +} diff --git a/tests/integration/3-inline-functions/.gitignore b/tests/integration/3-inline-functions/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/3-inline-functions/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/3-inline-functions/README.md b/tests/integration/3-inline-functions/README.md new file mode 100644 index 0000000..e43096a --- /dev/null +++ b/tests/integration/3-inline-functions/README.md @@ -0,0 +1,6 @@ +# Inline functions +This test is used to test the inline functions in the Astra service. It tests the following: +- Inline functions are parsed correctly. +- Inline functions are outputted correctly in the OpenAPI specification. +- They do not clash with other functions with the same name. +- They should not have an operation ID or description. \ No newline at end of file diff --git a/tests/integration/3-inline-functions/paths_test.go b/tests/integration/3-inline-functions/paths_test.go new file mode 100644 index 0000000..2a025cc --- /dev/null +++ b/tests/integration/3-inline-functions/paths_test.go @@ -0,0 +1,44 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestInlineFunctionsAreMapped(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // GET /inline + require.True(t, paths.Exists("/inline", "get")) + require.Equal(t, "#/components/schemas/petstore.Pet", paths.Path("/inline.get.responses.200.content.application/json.schema.$ref").Data().(string)) + + // POST /inline + require.True(t, paths.Exists("/inline", "post")) + require.Equal(t, "#/components/schemas/petstore.PetDTO", paths.Path("/inline.post.requestBody.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/petstore.Pet", paths.Path("/inline.post.responses.200.content.application/json.schema.$ref").Data().(string)) + + // GET /inline/{param} + require.True(t, paths.Exists("/inline/{param}", "get")) + // Path parameter + require.Equal(t, "param", paths.Path("/inline/{param}.get.parameters.0.name").Data().(string)) + require.Equal(t, "path", paths.Path("/inline/{param}.get.parameters.0.in").Data().(string)) + require.True(t, paths.Path("/inline/{param}.get.parameters.0.required").Data().(bool)) + require.Equal(t, "string", paths.Path("/inline/{param}.get.parameters.0.schema.type").Data().(string)) + // Query Parameter + require.Equal(t, "query", paths.Path("/inline/{param}.get.parameters.1.in").Data().(string)) + require.Equal(t, "name", paths.Path("/inline/{param}.get.parameters.1.name").Data().(string)) + require.Equal(t, "string", paths.Path("/inline/{param}.get.parameters.1.schema.type").Data().(string)) + // Response + require.Equal(t, "#/components/schemas/petstore.Pet", paths.Path("/inline/{param}.get.responses.200.content.application/json.schema.$ref").Data().(string)) + require.Equal(t, "#/components/schemas/gin.H", paths.Path("/inline/{param}.get.responses.400.content.application/json.schema.$ref").Data().(string)) +} diff --git a/tests/integration/3-inline-functions/router.go b/tests/integration/3-inline-functions/router.go new file mode 100644 index 0000000..570ebdc --- /dev/null +++ b/tests/integration/3-inline-functions/router.go @@ -0,0 +1,54 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + "github.com/ls6-events/astra/tests/petstore" +) + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/inline", func(c *gin.Context) { + c.JSON(200, petstore.Pet{ + Name: "inline", + PhotoURLs: []string{"inline"}, + Status: "inline", + Tags: []petstore.Tag{{Name: "inline"}}, + }) + }) + + r.GET("/inline/:param", func(c *gin.Context) { + param := c.Param("param") + + name, ok := c.GetQuery("name") + if !ok { + c.JSON(400, gin.H{"error": "name is required"}) + return + } + + c.JSON(200, petstore.Pet{ + Name: name, + PhotoURLs: []string{"inline"}, + Status: param, + Tags: []petstore.Tag{{Name: "inline"}}, + }) + }) + + r.POST("/inline", func(c *gin.Context) { + var pet petstore.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, petstore.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + }) + + return r +} diff --git a/tests/integration/4-doc-comments/.gitignore b/tests/integration/4-doc-comments/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/4-doc-comments/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/4-doc-comments/README.md b/tests/integration/4-doc-comments/README.md new file mode 100644 index 0000000..faba337 --- /dev/null +++ b/tests/integration/4-doc-comments/README.md @@ -0,0 +1,5 @@ +# Doc comments +This test is used to test the doc comments in the Astra service. It tests the following: +- Doc comments are parsed correctly. +- Doc comments are outputted correctly in the OpenAPI specification mapping to the correct property (description). +- It is done for both path functions and types schemas. \ No newline at end of file diff --git a/tests/integration/4-doc-comments/components_test.go b/tests/integration/4-doc-comments/components_test.go new file mode 100644 index 0000000..13fc7fa --- /dev/null +++ b/tests/integration/4-doc-comments/components_test.go @@ -0,0 +1,42 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestComponentsDocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + components := testAstra.Path("components") + + schemas := components.Path("schemas") + + // gin.H + require.True(t, schemas.Exists("gin.H", "description")) + require.Equal(t, "H is a shortcut for map[string]any", schemas.Search("gin.H", "description").Data().(string)) + + // petstore.Pet + require.True(t, schemas.Exists("petstore.Pet", "description")) + require.Equal(t, "Pet the pet model", schemas.Search("petstore.Pet", "description").Data().(string)) + + // petstore.PetDTO + require.True(t, schemas.Exists("petstore.PetDTO", "description")) + require.Equal(t, "PetDTO the pet dto", schemas.Search("petstore.PetDTO", "description").Data().(string)) + + // petstore.Tag + require.True(t, schemas.Exists("petstore.Tag", "description")) + require.Equal(t, "Tag the tag model", schemas.Search("petstore.Tag", "description").Data().(string)) + + // 4-doc-comments.NoDescription + require.True(t, schemas.Exists("4-doc-comments.NoDescription")) + require.False(t, schemas.Exists("4-doc-comments.NoDescription", "description")) +} diff --git a/tests/integration/4-doc-comments/handlers.go b/tests/integration/4-doc-comments/handlers.go new file mode 100644 index 0000000..7b00ef4 --- /dev/null +++ b/tests/integration/4-doc-comments/handlers.go @@ -0,0 +1,75 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +// getAllPets returns all pets +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +// getPetByID returns a pet by its ID +// It takes in the ID as a path parameter +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +// createPet creates a pet +// It takes in a Pet without an ID in the request body +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +// deletePet deletes a pet by its ID +// It takes in the ID as a path parameter +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} + +type NoDescription struct { + Foo string `json:"foo"` +} + +func noDescriptionRoute(c *gin.Context) { + c.JSON(http.StatusOK, NoDescription{Foo: "bar"}) +} diff --git a/tests/integration/4-doc-comments/paths_test.go b/tests/integration/4-doc-comments/paths_test.go new file mode 100644 index 0000000..40baa1a --- /dev/null +++ b/tests/integration/4-doc-comments/paths_test.go @@ -0,0 +1,36 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestPathsDocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // GET /no-docs + require.True(t, paths.Exists("/no-docs", "get")) + require.False(t, paths.Path("/no-docs.get").Exists("description")) + + // GET /pets + require.Equal(t, "getAllPets returns all pets", paths.Path("/pets.get.description").Data().(string)) + + // POST /pets + require.Equal(t, "createPet creates a pet\nIt takes in a Pet without an ID in the request body", paths.Path("/pets.post.description").Data().(string)) + + // GET /pets/{id} + require.Equal(t, "getPetByID returns a pet by its ID\nIt takes in the ID as a path parameter", paths.Path("/pets/{id}.get.description").Data().(string)) + + // DELETE /pets/{id} + require.Equal(t, "deletePet deletes a pet by its ID\nIt takes in the ID as a path parameter", paths.Path("/pets/{id}.delete.description").Data().(string)) +} diff --git a/tests/integration/4-doc-comments/router.go b/tests/integration/4-doc-comments/router.go new file mode 100644 index 0000000..93a7d11 --- /dev/null +++ b/tests/integration/4-doc-comments/router.go @@ -0,0 +1,16 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + r.GET("/no-docs", noDescriptionRoute) + + return r +} diff --git a/tests/integration/5-custom-functions/.gitignore b/tests/integration/5-custom-functions/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/5-custom-functions/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/5-custom-functions/README.md b/tests/integration/5-custom-functions/README.md new file mode 100644 index 0000000..3d3e529 --- /dev/null +++ b/tests/integration/5-custom-functions/README.md @@ -0,0 +1,5 @@ +# Custom functions +This tests the custom functions feature of Astra. It is a simple example of how to use custom functions to create a new function that can be used. This tests: +- Creating a custom `handleError` function +- Using the custom `handleError` function with gin context parameters, status code and error. +- Reporting this correctly in the response to the OpenAPI generator. \ No newline at end of file diff --git a/tests/integration/5-custom-functions/customFunctions.go b/tests/integration/5-custom-functions/customFunctions.go new file mode 100644 index 0000000..6eeff1d --- /dev/null +++ b/tests/integration/5-custom-functions/customFunctions.go @@ -0,0 +1,7 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func handleError(c *gin.Context, statusCode int, err error) { + c.String(statusCode, err.Error()) +} diff --git a/tests/integration/5-custom-functions/handlers.go b/tests/integration/5-custom-functions/handlers.go new file mode 100644 index 0000000..1d02231 --- /dev/null +++ b/tests/integration/5-custom-functions/handlers.go @@ -0,0 +1,67 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +// getAllPets returns all pets +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +// getPetByID returns a pet by its ID +// It takes in the ID as a path parameter +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + handleError(c, http.StatusBadRequest, err) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + handleError(c, http.StatusNotFound, err) + return + } + + c.JSON(http.StatusOK, pet) +} + +// createPet creates a pet +// It takes in a Pet without an ID in the request body +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + handleError(c, http.StatusBadRequest, err) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +// deletePet deletes a pet by its ID +// It takes in the ID as a path parameter +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + handleError(c, http.StatusBadRequest, err) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/integration/5-custom-functions/paths_test.go b/tests/integration/5-custom-functions/paths_test.go new file mode 100644 index 0000000..0885335 --- /dev/null +++ b/tests/integration/5-custom-functions/paths_test.go @@ -0,0 +1,53 @@ +package petstore + +import ( + "github.com/ls6-events/astra" + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCustomFunction(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r, astra.WithCustomFunc(func(contextVarName string, contextFuncBuilder *astra.ContextFuncBuilder) (*astra.Route, error) { + funcType, err := contextFuncBuilder.Traverser.Type() + if err != nil { + return nil, err + } + + if funcType.Name() == "handleError" { + return contextFuncBuilder.Ignored().StatusCode().ExpressionResult().Build(func(route *astra.Route, params []any) (*astra.Route, error) { + statusCode := params[1].(int) + + returnType := astra.ReturnType{ + ContentType: "text/plain", + StatusCode: statusCode, + Field: astra.Field{ + Type: "string", + }, + } + + route.ReturnTypes = astra.AddReturnType(route.ReturnTypes, returnType) + return route, nil + }) + } + return nil, nil + })) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // POST /pets + require.Equal(t, "string", paths.Path("/pets.post.responses.400.content.text/plain.schema.type").Data().(string)) + + // GET /pets/{id} + require.Equal(t, "string", paths.Path("/pets/{id}.get.responses.400.content.text/plain.schema.type").Data().(string)) + + // DELETE /pets/{id} + require.Equal(t, "string", paths.Path("/pets/{id}.delete.responses.400.content.text/plain.schema.type").Data().(string)) +} diff --git a/tests/integration/5-custom-functions/router.go b/tests/integration/5-custom-functions/router.go new file mode 100644 index 0000000..0a73435 --- /dev/null +++ b/tests/integration/5-custom-functions/router.go @@ -0,0 +1,14 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} diff --git a/tests/integration/6-openapi-format/.gitignore b/tests/integration/6-openapi-format/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/6-openapi-format/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/6-openapi-format/README.md b/tests/integration/6-openapi-format/README.md new file mode 100644 index 0000000..834aa40 --- /dev/null +++ b/tests/integration/6-openapi-format/README.md @@ -0,0 +1,8 @@ +# OpenAPI Schema Formats +This tests the OpenAPI schema formats feature of Astra. It is a simple example of how to use OpenAPI schema formats to create a new format that can be used. This tests: +- Creating a custom `date-time` format for the `time.Time` type, and `uuid.UUID` type from google for `uuid` format. +- `int32` is the default format for `int` types +- `int64` is respected for `int64` types +- `float32` is the default format for `float32` types +- `float64` is respected for `float64` types +- Accounted for all other data types in `formatTypes.go` diff --git a/tests/integration/6-openapi-format/components_test.go b/tests/integration/6-openapi-format/components_test.go new file mode 100644 index 0000000..d926544 --- /dev/null +++ b/tests/integration/6-openapi-format/components_test.go @@ -0,0 +1,128 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestFormatSchemas(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + schemas := testAstra.Path("components.schemas") + + properties := schemas.Search("6-openapi-format.TestStructFormatter", "properties") + + t.Run("String", func(t *testing.T) { + require.Equal(t, "string", properties.Path("string.type").Data().(string)) + }) + + t.Run("Int", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("int.type").Data().(string)) + require.Equal(t, "int32", properties.Path("int.format").Data().(string)) + }) + + t.Run("Int8", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("int8.type").Data().(string)) + require.Equal(t, "int8", properties.Path("int8.format").Data().(string)) + }) + + t.Run("Int16", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("int16.type").Data().(string)) + require.Equal(t, "int16", properties.Path("int16.format").Data().(string)) + }) + + t.Run("Int32", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("int32.type").Data().(string)) + require.Equal(t, "int32", properties.Path("int32.format").Data().(string)) + }) + + t.Run("Int64", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("int64.type").Data().(string)) + require.Equal(t, "int64", properties.Path("int64.format").Data().(string)) + }) + + t.Run("Uint", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("uint.type").Data().(string)) + require.Equal(t, "uint", properties.Path("uint.format").Data().(string)) + }) + + t.Run("Uint8", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("uint8.type").Data().(string)) + require.Equal(t, "uint8", properties.Path("uint8.format").Data().(string)) + }) + + t.Run("Uint16", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("uint16.type").Data().(string)) + require.Equal(t, "uint16", properties.Path("uint16.format").Data().(string)) + }) + + t.Run("Uint32", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("uint32.type").Data().(string)) + require.Equal(t, "uint32", properties.Path("uint32.format").Data().(string)) + }) + + t.Run("Uint64", func(t *testing.T) { + require.Equal(t, "integer", properties.Path("uint64.type").Data().(string)) + require.Equal(t, "uint64", properties.Path("uint64.format").Data().(string)) + }) + + t.Run("Float32", func(t *testing.T) { + require.Equal(t, "number", properties.Path("float32.type").Data().(string)) + require.Equal(t, "float32", properties.Path("float32.format").Data().(string)) + }) + + t.Run("Float64", func(t *testing.T) { + require.Equal(t, "number", properties.Path("float64.type").Data().(string)) + require.Equal(t, "float64", properties.Path("float64.format").Data().(string)) + }) + + t.Run("Bool", func(t *testing.T) { + require.Equal(t, "boolean", properties.Path("bool.type").Data().(string)) + }) + + t.Run("Byte", func(t *testing.T) { + require.Equal(t, "string", properties.Path("byte.type").Data().(string)) + require.Equal(t, "byte", properties.Path("byte.format").Data().(string)) + }) + + t.Run("Rune", func(t *testing.T) { + require.Equal(t, "string", properties.Path("rune.type").Data().(string)) + require.Equal(t, "rune", properties.Path("rune.format").Data().(string)) + }) + + t.Run("Struct", func(t *testing.T) { + require.Equal(t, "object", properties.Path("struct.type").Data().(string)) + }) + + t.Run("Map", func(t *testing.T) { + require.Equal(t, "object", properties.Path("map.type").Data().(string)) + }) + + t.Run("Slice", func(t *testing.T) { + require.Equal(t, "array", properties.Path("slice.type").Data().(string)) + }) + + t.Run("Any", func(t *testing.T) { + require.False(t, properties.Exists("any", "type")) + }) + + t.Run("time.Time", func(t *testing.T) { + require.Equal(t, "#/components/schemas/time.Time", properties.Path("time.$ref").Data().(string)) + require.Equal(t, "string", schemas.Search("time.Time", "type").Data().(string)) + require.Equal(t, "date-time", schemas.Search("time.Time", "format").Data().(string)) + }) + + t.Run("github.com/google/uuid.UUID", func(t *testing.T) { + require.Equal(t, "#/components/schemas/uuid.UUID", properties.Path("uuid.$ref").Data().(string)) + require.Equal(t, "string", schemas.Search("uuid.UUID", "type").Data().(string)) + require.Equal(t, "uuid", schemas.Search("uuid.UUID", "format").Data().(string)) + }) +} diff --git a/tests/integration/6-openapi-format/router.go b/tests/integration/6-openapi-format/router.go new file mode 100644 index 0000000..124ac9b --- /dev/null +++ b/tests/integration/6-openapi-format/router.go @@ -0,0 +1,13 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/", func(c *gin.Context) { + c.JSON(200, TestStructFormatter{}) + }) + + return r +} diff --git a/tests/integration/6-openapi-format/type.go b/tests/integration/6-openapi-format/type.go new file mode 100644 index 0000000..ec84497 --- /dev/null +++ b/tests/integration/6-openapi-format/type.go @@ -0,0 +1,54 @@ +package petstore + +import ( + "github.com/google/uuid" + "time" +) + +type TestStructFormatter struct { + // String + String string `json:"string,omitempty"` + // Int (int32) + Int int `json:"int,omitempty"` + // Int8 + Int8 int8 `json:"int8,omitempty"` + // Int16 + Int16 int16 `json:"int16,omitempty"` + // Int32 + Int32 int32 `json:"int32,omitempty"` + // Int64 + Int64 int64 `json:"int64,omitempty"` + // Uint + Uint uint `json:"uint,omitempty"` + // Uint8 + Uint8 uint8 `json:"uint8,omitempty"` + // Uint16 + Uint16 uint16 `json:"uint16,omitempty"` + // Uint32 + Uint32 uint32 `json:"uint32,omitempty"` + // Uint64 + Uint64 uint64 `json:"uint64,omitempty"` + // Float32 + Float32 float32 `json:"float32,omitempty"` + // Float64 + Float64 float64 `json:"float64,omitempty"` + // Bool + Bool bool `json:"bool,omitempty"` + // Byte + Byte byte `json:"byte,omitempty"` + // Rune + Rune rune `json:"rune,omitempty"` + // Struct + Struct struct{} `json:"struct,omitempty"` + // Map + Map map[string]string `json:"map,omitempty"` + // Slice + Slice []string `json:"slice,omitempty"` + // Any + Any any `json:"any,omitempty"` + + // time.Time + Time time.Time `json:"time,omitempty"` + // uuid.UUID + UUID uuid.UUID `json:"uuid,omitempty"` +} diff --git a/tests/integration/7-enums/.gitignore b/tests/integration/7-enums/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/7-enums/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/7-enums/README.md b/tests/integration/7-enums/README.md new file mode 100644 index 0000000..ee83410 --- /dev/null +++ b/tests/integration/7-enums/README.md @@ -0,0 +1,3 @@ +# Enum support +Astra supports enums. Enums are defined as a list of strings. This tests: +- Creating a custom `enum` format for the `string` type. diff --git a/tests/integration/7-enums/components_test.go b/tests/integration/7-enums/components_test.go new file mode 100644 index 0000000..97dc941 --- /dev/null +++ b/tests/integration/7-enums/components_test.go @@ -0,0 +1,42 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestEnums(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + schemas := testAstra.Path("components.schemas") + + t.Run("String Enum", func(t *testing.T) { + stringEnum := schemas.Search("7-enums.TestStringEnum") + + require.Equal(t, "string", stringEnum.Path("type").Data().(string)) + require.Equal(t, []any{"available", "sold"}, stringEnum.Path("enum").Data().([]any)) + + stringStruct := schemas.Search("7-enums.TestStructWithStringEnum") + + require.Equal(t, "#/components/schemas/7-enums.TestStringEnum", stringStruct.Path("properties.enum.$ref").Data().(string)) + }) + + t.Run("Int Enum", func(t *testing.T) { + intEnum := schemas.Search("7-enums.TestIntEnum") + + require.Equal(t, "integer", intEnum.Path("type").Data().(string)) + require.Equal(t, []any{1.0, 2.0}, intEnum.Path("enum").Data().([]any)) + + intStruct := schemas.Search("7-enums.TestStructWithIntEnum") + + require.Equal(t, "#/components/schemas/7-enums.TestIntEnum", intStruct.Path("properties.enum.$ref").Data().(string)) + }) +} diff --git a/tests/integration/7-enums/enum.go b/tests/integration/7-enums/enum.go new file mode 100644 index 0000000..6c7b7d1 --- /dev/null +++ b/tests/integration/7-enums/enum.go @@ -0,0 +1,25 @@ +package petstore + +type TestStringEnum string + +const ( + TestStringEnumAvailable TestStringEnum = "available" + TestStringEnumSold TestStringEnum = "sold" +) + +type TestStructWithStringEnum struct { + // Enum + Enum TestStringEnum `json:"enum,omitempty"` +} + +type TestIntEnum int + +const ( + TestIntEnumAvailable TestIntEnum = 1 + TestIntEnumSold TestIntEnum = 2 +) + +type TestStructWithIntEnum struct { + // Enum + Enum TestIntEnum `json:"enum,omitempty"` +} diff --git a/tests/integration/7-enums/handlers.go b/tests/integration/7-enums/handlers.go new file mode 100644 index 0000000..3c910b5 --- /dev/null +++ b/tests/integration/7-enums/handlers.go @@ -0,0 +1,26 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func getStringEnum(c *gin.Context) { + c.JSON(http.StatusOK, TestStringEnumAvailable) +} + +func getStringStructWithEnum(c *gin.Context) { + c.JSON(http.StatusOK, TestStructWithStringEnum{ + Enum: TestStringEnumSold, + }) +} + +func getIntEnum(c *gin.Context) { + c.JSON(http.StatusOK, TestIntEnumAvailable) +} + +func getIntStructWithEnum(c *gin.Context) { + c.JSON(http.StatusOK, TestStructWithIntEnum{ + Enum: TestIntEnumSold, + }) +} diff --git a/tests/integration/7-enums/router.go b/tests/integration/7-enums/router.go new file mode 100644 index 0000000..b43cb2f --- /dev/null +++ b/tests/integration/7-enums/router.go @@ -0,0 +1,15 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/enums/string", getStringEnum) + r.GET("/enums/string-struct", getStringStructWithEnum) + + r.GET("/enums/int", getIntEnum) + r.GET("/enums/int-struct", getIntStructWithEnum) + + return r +} diff --git a/tests/integration/8-headers-abort/.gitignore b/tests/integration/8-headers-abort/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/8-headers-abort/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/8-headers-abort/README.md b/tests/integration/8-headers-abort/README.md new file mode 100644 index 0000000..1bce258 --- /dev/null +++ b/tests/integration/8-headers-abort/README.md @@ -0,0 +1,7 @@ +# Headers & AbortX methods +This test will test the following: +- `Header` method +- `SetHeader` method +- `AbortWithStatus` method +- `AbortWithError` method +- `AbortWithStatusJSON` method diff --git a/tests/integration/8-headers-abort/handlers.go b/tests/integration/8-headers-abort/handlers.go new file mode 100644 index 0000000..57c82c3 --- /dev/null +++ b/tests/integration/8-headers-abort/handlers.go @@ -0,0 +1,31 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func getHeader(c *gin.Context) { + header := c.GetHeader("X-Test-Header") + c.String(http.StatusOK, header) +} + +func setHeader(c *gin.Context) { + c.Header("X-Test-Header", "test") + c.Status(http.StatusOK) +} + +func abortWithError(c *gin.Context) { + // Ignoring error as it is not used in the test + _ = c.AbortWithError(http.StatusBadRequest, nil) +} + +func abortWithStatus(c *gin.Context) { + c.AbortWithStatus(http.StatusUnauthorized) +} + +func abortWithStatusJSON(c *gin.Context) { + c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{ + "message": "unauthorized", + }) +} diff --git a/tests/integration/8-headers-abort/paths_test.go b/tests/integration/8-headers-abort/paths_test.go new file mode 100644 index 0000000..2d40db6 --- /dev/null +++ b/tests/integration/8-headers-abort/paths_test.go @@ -0,0 +1,46 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestHeadersAndAbort(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + require.NotNil(t, testAstra) + + paths := testAstra.Path("paths") + + t.Run("Headers", func(t *testing.T) { + // Get + require.True(t, paths.Exists("/headers", "get", "parameters", "0")) + require.Equal(t, "X-Test-Header", paths.Path("/headers.get.parameters.0.name").Data().(string)) + require.Equal(t, "header", paths.Path("/headers.get.parameters.0.in").Data().(string)) + require.Equal(t, "string", paths.Path("/headers.get.parameters.0.schema.type").Data().(string)) + + // Set + require.True(t, paths.Exists("/headers", "post", "responses", "200", "headers", "X-Test-Header")) + require.Equal(t, "string", paths.Path("/headers.post.responses.200.headers.X-Test-Header.schema.type").Data().(string)) + }) + + t.Run("Abort", func(t *testing.T) { + // AbortWithError + require.True(t, paths.Exists("/abort-with-error", "get", "responses", "400")) + + // AbortWithStatus + require.True(t, paths.Exists("/abort-with-status", "get", "responses", "401")) + + // AbortWithStatusJSON + require.True(t, paths.Exists("/abort-with-status-json", "get", "responses", "402")) + require.Equal(t, "#/components/schemas/gin.H", paths.Search("/abort-with-status-json", "get", "responses", "402", "content", "application/json", "schema", "$ref").Data().(string)) + }) +} diff --git a/tests/integration/8-headers-abort/router.go b/tests/integration/8-headers-abort/router.go new file mode 100644 index 0000000..040365d --- /dev/null +++ b/tests/integration/8-headers-abort/router.go @@ -0,0 +1,15 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/headers", getHeader) + r.POST("/headers", setHeader) + r.GET("/abort-with-error", abortWithError) + r.GET("/abort-with-status", abortWithStatus) + r.GET("/abort-with-status-json", abortWithStatusJSON) + + return r +} diff --git a/tests/integration/9-operation-ids/.gitignore b/tests/integration/9-operation-ids/.gitignore new file mode 100644 index 0000000..325a564 --- /dev/null +++ b/tests/integration/9-operation-ids/.gitignore @@ -0,0 +1 @@ +output.json \ No newline at end of file diff --git a/tests/integration/9-operation-ids/README.md b/tests/integration/9-operation-ids/README.md new file mode 100644 index 0000000..8cf20aa --- /dev/null +++ b/tests/integration/9-operation-ids/README.md @@ -0,0 +1,3 @@ +# Operation IDs +This test suite is designed to test the operation IDs of the Astra API. This tests: +- `OperationID` being set for the endpoints. \ No newline at end of file diff --git a/tests/integration/9-operation-ids/handlers.go b/tests/integration/9-operation-ids/handlers.go new file mode 100644 index 0000000..b8182a0 --- /dev/null +++ b/tests/integration/9-operation-ids/handlers.go @@ -0,0 +1,60 @@ +package petstore + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/integration/9-operation-ids/paths_test.go b/tests/integration/9-operation-ids/paths_test.go new file mode 100644 index 0000000..2d286ce --- /dev/null +++ b/tests/integration/9-operation-ids/paths_test.go @@ -0,0 +1,32 @@ +package petstore + +import ( + "github.com/ls6-events/astra/tests/integration/helpers" + "github.com/stretchr/testify/require" + "testing" +) + +func TestOperationIDs(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test in short mode") + } + + r := setupRouter() + + testAstra, err := helpers.SetupTestAstraWithDefaultConfig(t, r) + require.NoError(t, err) + + paths := testAstra.Path("paths") + + // GET /pets + require.Equal(t, "getAllPets", paths.Path("/pets.get.operationId").Data().(string)) + + // POST /pets + require.Equal(t, "createPet", paths.Path("/pets.post.operationId").Data().(string)) + + // GET /pets/{id} + require.Equal(t, "getPetById", paths.Path("/pets/{id}.get.operationId").Data().(string)) + + // DELETE /pets/{id} + require.Equal(t, "deletePet", paths.Path("/pets/{id}.delete.operationId").Data().(string)) +} diff --git a/tests/integration/9-operation-ids/router.go b/tests/integration/9-operation-ids/router.go new file mode 100644 index 0000000..0a73435 --- /dev/null +++ b/tests/integration/9-operation-ids/router.go @@ -0,0 +1,14 @@ +package petstore + +import "github.com/gin-gonic/gin" + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} diff --git a/tests/integration/helpers/testAstra.go b/tests/integration/helpers/testAstra.go new file mode 100644 index 0000000..78449a9 --- /dev/null +++ b/tests/integration/helpers/testAstra.go @@ -0,0 +1,41 @@ +package helpers + +import ( + "github.com/Jeffail/gabs/v2" + "github.com/gin-gonic/gin" + "github.com/ls6-events/astra" + "github.com/ls6-events/astra/inputs" + "github.com/ls6-events/astra/outputs" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func SetupTestAstraWithDefaultConfig(t *testing.T, r *gin.Engine, options ...astra.Option) (*gabs.Container, error) { + t.Helper() + + config := &astra.Config{ + Host: "localhost", + Port: 8000, + } + + return SetupTestAstra(t, r, config, options...) +} + +func SetupTestAstra(t *testing.T, r *gin.Engine, config *astra.Config, options ...astra.Option) (*gabs.Container, error) { + t.Helper() + + options = append(options, inputs.WithGinInput(r), outputs.WithOpenAPIOutput("./output.json")) + + gen := astra.New(options...) + + gen.SetConfig(config) + + err := gen.Parse() + require.NoError(t, err) + + fileContents, err := os.ReadFile("./output.json") + require.NoError(t, err) + + return gabs.ParseJSON(fileContents) +} diff --git a/tests/petstore/store.go b/tests/petstore/store.go new file mode 100644 index 0000000..efe160e --- /dev/null +++ b/tests/petstore/store.go @@ -0,0 +1,47 @@ +package petstore + +import ( + "fmt" + "sync" + "sync/atomic" +) + +var Pets = []Pet{ + {ID: 1, Name: "Dog", PhotoURLs: []string{}, Status: "available", Tags: nil}, + {ID: 2, Name: "Cat", PhotoURLs: []string{}, Status: "pending", Tags: nil}, +} + +var petsLock = &sync.Mutex{} +var lastPetID int64 = 2 + +func newPetID() int64 { + return atomic.AddInt64(&lastPetID, 1) +} + +func AddPet(pet Pet) { + petsLock.Lock() + defer petsLock.Unlock() + pet.ID = newPetID() + Pets = append(Pets, pet) +} + +func RemovePet(id int64) { + petsLock.Lock() + defer petsLock.Unlock() + var newPets []Pet + for _, pet := range Pets { + if pet.ID != id { + newPets = append(newPets, pet) + } + } + Pets = newPets +} + +func PetByID(id int64) (*Pet, error) { + for _, pet := range Pets { + if pet.ID == id { + return &pet, nil + } + } + return nil, fmt.Errorf("not found: pet %d", id) +} diff --git a/tests/petstore/types.go b/tests/petstore/types.go new file mode 100644 index 0000000..1631772 --- /dev/null +++ b/tests/petstore/types.go @@ -0,0 +1,24 @@ +package petstore + +// Tag the tag model +type Tag struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +// Pet the pet model +type Pet struct { + ID int64 `json:"id"` + Name string `json:"name"` + PhotoURLs []string `json:"photoUrls,omitempty"` + Status string `json:"status,omitempty"` + Tags []Tag `json:"tags,omitempty"` +} + +// PetDTO the pet dto +type PetDTO struct { + Name string `json:"name"` + PhotoURLs []string `json:"photoUrls,omitempty"` + Status string `json:"status,omitempty"` + Tags []Tag `json:"tags,omitempty"` +} diff --git a/tests/snapshot/README.md b/tests/snapshot/README.md new file mode 100644 index 0000000..b9fa15b --- /dev/null +++ b/tests/snapshot/README.md @@ -0,0 +1,10 @@ +# Snapshot testing +This directory contains snapshot tests for Astra. These tests are used to +ensure that the output of Astra is consistent and to prevent regressions. The tests are fairly slow right now, so the consideration is to run these in `-short` mode in the future but for now we can use them as appropriate. + +## Running the tests +To run the tests, simply run `go test ./...` in this directory. The tests will run and compare the output of Astra to the expected output. If there are any differences, the test will fail and you will need to update the expected output. + +## Updating the snapshots +To do this, simply run `GENERATE_SNAPSHOTS=true go test ./...` and the tests will update the expected output. + diff --git a/tests/snapshot/comparison/compareSpecs.go b/tests/snapshot/comparison/compareSpecs.go new file mode 100644 index 0000000..45de833 --- /dev/null +++ b/tests/snapshot/comparison/compareSpecs.go @@ -0,0 +1,43 @@ +package comparison + +import ( + "github.com/google/go-cmp/cmp" + "gopkg.in/yaml.v3" + "os" + "testing" +) + +func readFile(t *testing.T, path string) []byte { + t.Helper() + + contents, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read file %s: %v", path, err) + } + + return contents +} + +func CompareYAML(t *testing.T, snapshotPath, outputPath string) { + t.Helper() + + outputPathContents := readFile(t, outputPath) + snapshotPathContents := readFile(t, snapshotPath) + + var snapshotMap map[any]any + var outputMap map[any]any + + err := yaml.Unmarshal(snapshotPathContents, &snapshotMap) + if err != nil { + t.Fatalf("failed to unmarshal snapshot yaml: %v", err) + } + + err = yaml.Unmarshal(outputPathContents, &outputMap) + if err != nil { + t.Fatalf("failed to unmarshal output yaml: %v", err) + } + + if !cmp.Equal(outputMap, snapshotMap) { + t.Errorf("snapshots do not match: %s", cmp.Diff(snapshotMap, outputMap)) + } +} diff --git a/tests/snapshot/petstore/.gitignore b/tests/snapshot/petstore/.gitignore new file mode 100644 index 0000000..fce0015 --- /dev/null +++ b/tests/snapshot/petstore/.gitignore @@ -0,0 +1 @@ +output.yaml \ No newline at end of file diff --git a/tests/snapshot/petstore/README.md b/tests/snapshot/petstore/README.md new file mode 100644 index 0000000..63b7904 --- /dev/null +++ b/tests/snapshot/petstore/README.md @@ -0,0 +1,2 @@ +# Petstore snapshot test +This is just a basic test for snapshot testing for the system. The goal for the future is to add more snapshot testing with more complicated APIs. \ No newline at end of file diff --git a/tests/snapshot/petstore/handlers.go b/tests/snapshot/petstore/handlers.go new file mode 100644 index 0000000..fc9b560 --- /dev/null +++ b/tests/snapshot/petstore/handlers.go @@ -0,0 +1,60 @@ +package snapshot + +import ( + "github.com/gin-gonic/gin" + petstore2 "github.com/ls6-events/astra/tests/petstore" + "net/http" + "strconv" +) + +func getAllPets(c *gin.Context) { + allPets := petstore2.Pets + + c.JSON(http.StatusOK, allPets) +} + +func getPetByID(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + pet, err := petstore2.PetByID(int64(id)) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, pet) +} + +func createPet(c *gin.Context) { + var pet petstore2.PetDTO + err := c.BindJSON(&pet) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.AddPet(petstore2.Pet{ + Name: pet.Name, + PhotoURLs: pet.PhotoURLs, + Status: pet.Status, + Tags: pet.Tags, + }) + + c.JSON(http.StatusOK, pet) +} + +func deletePet(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + petstore2.RemovePet(int64(id)) + + c.Status(http.StatusOK) +} diff --git a/tests/snapshot/petstore/snapshot.yaml b/tests/snapshot/petstore/snapshot.yaml new file mode 100644 index 0000000..2f14833 --- /dev/null +++ b/tests/snapshot/petstore/snapshot.yaml @@ -0,0 +1,138 @@ +openapi: 3.0.0 +info: + title: "" + description: Generated by astra + contact: {} + license: + name: "" + version: "" +servers: + - url: http://localhost:8000 +paths: + /pets: + get: + operationId: getAllPets + responses: + "200": + description: "" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/petstore.Pet' + post: + operationId: createPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/petstore.PetDTO' + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/petstore.PetDTO' + "400": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/gin.H' + /pets/{id}: + get: + operationId: getPetById + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/petstore.Pet' + "400": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/gin.H' + "404": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/gin.H' + delete: + operationId: deletePet + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: "" + "400": + description: "" + content: + application/json: + schema: + $ref: '#/components/schemas/gin.H' +components: + schemas: + gin.H: + type: object + additionalProperties: {} + description: H is a shortcut for map[string]any + petstore.Pet: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + photoUrls: + type: array + items: + type: string + status: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/petstore.Tag' + description: Pet the pet model + petstore.PetDTO: + type: object + properties: + name: + type: string + photoUrls: + type: array + items: + type: string + status: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/petstore.Tag' + description: PetDTO the pet dto + petstore.Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: Tag the tag model diff --git a/tests/snapshot/petstore/snapshot_test.go b/tests/snapshot/petstore/snapshot_test.go new file mode 100644 index 0000000..06d6082 --- /dev/null +++ b/tests/snapshot/petstore/snapshot_test.go @@ -0,0 +1,52 @@ +package snapshot + +import ( + "github.com/gin-gonic/gin" + "github.com/ls6-events/astra" + "github.com/ls6-events/astra/inputs" + "github.com/ls6-events/astra/outputs" + "github.com/ls6-events/astra/tests/snapshot/comparison" + "github.com/stretchr/testify/require" + "os" + "testing" +) + +func setupRouter() *gin.Engine { + r := gin.Default() + + r.GET("/pets", getAllPets) + r.GET("/pets/:id", getPetByID) + r.POST("/pets", createPet) + r.DELETE("/pets/:id", deletePet) + + return r +} + +func TestSnapshot(t *testing.T) { + if testing.Short() { + t.Skip("skipping snapshot test in short mode") + } + + r := setupRouter() + + config := &astra.Config{ + Host: "localhost", + Port: 8000, + } + + gen := astra.New(inputs.WithGinInput(r), outputs.WithOpenAPIOutput("./output.yaml")) + + gen.SetConfig(config) + + err := gen.Parse() + require.NoError(t, err) + + if os.Getenv("GENERATE_SNAPSHOT") != "true" { + // Compare the generated snapshot with the existing one + comparison.CompareYAML(t, "./snapshot.yaml", "./output.yaml") + } else { + // Overwrite the existing snapshot with the new one + err = os.Rename("./output.yaml", "./snapshot.yaml") + require.NoError(t, err) + } +} diff --git a/utils/splitHandlerPath.go b/utils/splitHandlerPath.go new file mode 100644 index 0000000..04f8a75 --- /dev/null +++ b/utils/splitHandlerPath.go @@ -0,0 +1,44 @@ +package utils + +import "strings" + +type HandlerPath struct { + PathParts []string + HandlerParts []string +} + +func SplitHandlerPath(handlerPath string) HandlerPath { + // Create path parts by splitting on slashes + pathParts := strings.Split(handlerPath, "/") + + // Create handler parts by splitting on dots from the last path part + handlerParts := strings.Split(pathParts[len(pathParts)-1], ".") + + // Remove the last dot-separated part from the path parts + pathParts = pathParts[:len(pathParts)-1] + pathParts = append(pathParts, handlerParts[0]) + + // Remove the first handler part from the handler parts + handlerParts = handlerParts[1:] + + return HandlerPath{ + PathParts: pathParts, + HandlerParts: handlerParts, + } +} + +func (h HandlerPath) PackagePath() string { + return strings.Join(h.PathParts, "/") +} + +func (h HandlerPath) PackageName() string { + return h.PathParts[len(h.PathParts)-1] +} + +func (h HandlerPath) Handler() string { + return strings.Join(h.HandlerParts, ".") +} + +func (h HandlerPath) FuncName() string { + return h.HandlerParts[0] +} diff --git a/utils/splitHandlerPath_test.go b/utils/splitHandlerPath_test.go new file mode 100644 index 0000000..eda944a --- /dev/null +++ b/utils/splitHandlerPath_test.go @@ -0,0 +1,63 @@ +package utils + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestSplitHandlerPath(t *testing.T) { + testCases := []struct { + name string + path string + result HandlerPath + }{ + { + name: "simple", + path: "hello.world", + result: HandlerPath{ + PathParts: []string{"hello"}, + HandlerParts: []string{"world"}, + }, + }, + { + name: "nested path", + path: "foo/bar.hello", + result: HandlerPath{ + PathParts: []string{"foo", "bar"}, + HandlerParts: []string{"hello"}, + }, + }, + { + name: "nested handler", + path: "foo/bar/hello.world.func1", + result: HandlerPath{ + PathParts: []string{"foo", "bar", "hello"}, + HandlerParts: []string{"world", "func1"}, + }, + }, + { + name: "nested path and handler", + path: "foo/bar/hello.world.func1.func2", + result: HandlerPath{ + PathParts: []string{"foo", "bar", "hello"}, + HandlerParts: []string{"world", "func1", "func2"}, + }, + }, + { + name: "dot in path", + path: "foo.bar/hello.world", + result: HandlerPath{ + PathParts: []string{"foo.bar", "hello"}, + HandlerParts: []string{"world"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := SplitHandlerPath(tc.path) + require.ElementsMatch(t, tc.result.PathParts, result.PathParts) + require.ElementsMatch(t, tc.result.HandlerParts, result.HandlerParts) + }) + } +}