diff --git a/.github/workflows/delete-pr-branch.yml b/.github/workflows/delete-pr-branch.yml new file mode 100644 index 0000000..245785d --- /dev/null +++ b/.github/workflows/delete-pr-branch.yml @@ -0,0 +1,26 @@ +name: Delete Merged Branch + +on: + pull_request: + types: [closed] + +jobs: + run: + if: | + ( + github.event.pull_request.merged == true && + github.event.pull_request.head.ref != 'main' + ) + name: Delete Merged PR Branch + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Delete merged branch + shell: bash + run: | + set -e -o pipefail + headRefName=$(gh pr view ${{ github.event.pull_request.number }} --json headRefName -q ".headRefName") + git push origin --delete ${headRefName} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..13b5f7d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Test + +on: + pull_request: + types: [opened, synchronize, reopened] + + branches: + - 'main' + +jobs: + build-and-test: + name: Lint And Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: 'go.work' + cache-dependency-path: 'go.work.sum' + + - run: make lint + - run: make tests diff --git a/.golangci.yml b/.golangci.yml index da67163..d65ac43 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -284,7 +284,7 @@ linters: - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 - testableexamples # checks if examples are testable (have an expected output) - testifylint # checks usage of github.com/stretchr/testify - - testpackage # makes you use a separate _test package +# - testpackage # makes you use a separate _test package - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - unconvert # removes unnecessary type conversions - unparam # reports unused function parameters @@ -351,4 +351,5 @@ issues: - goconst - gosec - noctx - - wrapcheck \ No newline at end of file + - wrapcheck + - gocognit \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..1bb43a5 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @gemyago diff --git a/Makefile b/Makefile index 2fa9997..21f3fe6 100644 --- a/Makefile +++ b/Makefile @@ -99,10 +99,14 @@ bin/golangci-lint: ./.golangci-version .PHONY: lint/golang lint/golang: bin/golangci-lint cd ./tests/golang && ../../bin/golangci-lint run --config ../../.golangci.yml ./... + cd ./examples/go-apigen-server && ../../bin/golangci-lint run --config ../../.golangci.yml ./... + +.PHONY: lint +lint: lint/golang .PHONY: tests/golang tests/golang: - go test ./tests/golang/... + TZ=US/Alaska go test ./tests/golang/... .PHONY: tests tests: tests/golang \ No newline at end of file diff --git a/examples/go-apigen-server/cmd/service/main.go b/examples/go-apigen-server/cmd/service/main.go index ad058fb..bb19aa6 100644 --- a/examples/go-apigen-server/cmd/service/main.go +++ b/examples/go-apigen-server/cmd/service/main.go @@ -1,27 +1,41 @@ package main import ( - "log/slog" + "fmt" + "log" + "net/http" + "time" - "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/server" + "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/router" "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/v1controllers" "github.com/gemyago/apigen/examples/go-apigen-server/pkg/app" ) func main() { + // Minimal implementation of the http server startup. + // Real world implementation will likely to be more advanced and have + // things like configuration loading, logging setup, some form of DI e.t.c + + port := 8080 + readHeaderTimeoutSec := 2 storage := app.NewStorage() - srv := server.NewHTTPServer(server.HTTPServerParams{ - ServerCfg: server.ServerCfg{Port: 8080}, - Handler: server.NewRouter(server.RoutesDeps{ - PetsController: v1controllers.NewPetsController( - v1controllers.PetsControllerDeps{ - Commands: app.NewCommands(app.CommandsDeps{Storage: storage}), - Queries: app.NewQueries(app.QueriesDeps{Storage: storage}), - }, - ), + + // Generated routes need a controller implementation to process requests + petsController := v1controllers.NewPetsController( + v1controllers.PetsControllerDeps{ + Commands: app.NewCommands(app.CommandsDeps{Storage: storage}), + Queries: app.NewQueries(app.QueriesDeps{Storage: storage}), + }, + ) + + srv := &http.Server{ + Addr: fmt.Sprintf("[::]:%d", port), + ReadHeaderTimeout: time.Duration(readHeaderTimeoutSec) * time.Second, + Handler: router.NewHandler(router.HandlerDeps{ + PetsController: petsController, }), - }) - slog.Info("Starting server on port: 8080") + } + log.Println("Starting server on port:", port) if err := srv.ListenAndServe(); err != nil { panic(err) } diff --git a/examples/go-apigen-server/go.mod b/examples/go-apigen-server/go.mod index 8b3f051..af5f8fe 100644 --- a/examples/go-apigen-server/go.mod +++ b/examples/go-apigen-server/go.mod @@ -2,7 +2,4 @@ module github.com/gemyago/apigen/examples/go-apigen-server go 1.22 -require ( - github.com/go-chi/chi/v5 v5.1.0 - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 -) +require golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 diff --git a/examples/go-apigen-server/go.sum b/examples/go-apigen-server/go.sum index d4d1e1b..dcd770d 100644 --- a/examples/go-apigen-server/go.sum +++ b/examples/go-apigen-server/go.sum @@ -1,4 +1,2 @@ -github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= -github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= diff --git a/examples/go-apigen-server/pkg/api/http/router/handler.go b/examples/go-apigen-server/pkg/api/http/router/handler.go new file mode 100644 index 0000000..6450dd6 --- /dev/null +++ b/examples/go-apigen-server/pkg/api/http/router/handler.go @@ -0,0 +1,83 @@ +package router + +import ( + "errors" + "log" + "net/http" + + "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/v1routes/handlers" + "github.com/gemyago/apigen/examples/go-apigen-server/pkg/app" +) + +type httpRouter struct { + *http.ServeMux +} + +func (httpRouter) PathValue(r *http.Request, paramName string) string { + return r.PathValue(paramName) +} + +func (a httpRouter) HandleRoute(method, pathPattern string, h http.Handler) { + a.ServeMux.Handle(method+" "+pathPattern, h) +} + +func handleActionError(_ *http.Request, w http.ResponseWriter, err error) { + code := 500 + switch { + case errors.Is(err, app.ErrNotFound): + code = 404 + case errors.Is(err, app.ErrConflict): + code = 409 + } + w.WriteHeader(code) + log.Printf("Failed to process request: %v\n", err) +} + +type responseWriterWrapper struct { + http.ResponseWriter + statusCode int +} + +func (lrw *responseWriterWrapper) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} + +// HandlerDeps holds dependencies of the generated routes +// usually controller implementations at least. +type HandlerDeps struct { + PetsController *handlers.PetsController +} + +// NewHandler creates an minimal example implementation of the router handler +// based on the standard http.ServeMux. +func NewHandler(deps HandlerDeps) http.Handler { + mux := http.NewServeMux() + + // Real world instance of the router handler will likely to have a more advanced setup + // below is just some simple access logs on requests processing + muxHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("GET %v %v %v\n", r.URL.String(), r.Proto, r.UserAgent()) + defer func() { + if r := recover(); r != nil { + log.Println("Request panic", r) + } + }() + wrapper := &responseWriterWrapper{ResponseWriter: w, statusCode: http.StatusOK} + mux.ServeHTTP(wrapper, r) + log.Printf("%d %v %v\n", wrapper.statusCode, r.Method, r.URL.String()) + }) + + // The httpApp provides a configuration layer of the generated routes + // and also serves as an adapter that allows using different router implementations + httpApp := handlers.NewHttpApp( + httpRouter{ServeMux: mux}, + handlers.WithActionErrorHandler(handleActionError), + ) + + // Register generated Pets routes. There can be multiple different + // routes registered into the same httpApp instance. + handlers.RegisterPetsRoutes(deps.PetsController, httpApp) + + return muxHandler +} diff --git a/examples/go-apigen-server/pkg/api/http/server/router.go b/examples/go-apigen-server/pkg/api/http/server/router.go deleted file mode 100644 index 48f5f90..0000000 --- a/examples/go-apigen-server/pkg/api/http/server/router.go +++ /dev/null @@ -1,61 +0,0 @@ -package server - -import ( - "errors" - "log/slog" - "net/http" - - "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/v1routes/handlers" - "github.com/gemyago/apigen/examples/go-apigen-server/pkg/app" - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" -) - -type RoutesDeps struct { - PetsController *handlers.PetsController -} - -type httpRouter struct { - chi.Router -} - -func (httpRouter) PathValue(r *http.Request, paramName string) string { - return chi.URLParam(r, paramName) -} - -func (a httpRouter) HandleRoute(method, pathPattern string, h http.Handler) { - a.Router.Method(method, pathPattern, h) -} - -func handleActionError(r *http.Request, w http.ResponseWriter, err error) { - level := slog.LevelWarn - code := 500 - if errors.Is(err, app.ErrNotFound) { - code = 404 - } else if errors.Is(err, app.ErrConflict) { - code = 409 - } else { - level = slog.LevelError - } - w.WriteHeader(code) - slog.Log(r.Context(), level, "Failed to process request", slog.String("err", err.Error())) -} - -func NewRouter(deps RoutesDeps) http.Handler { - router := chi.NewRouter() - - router.Use(middleware.RequestID) - router.Use(middleware.RealIP) - router.Use(middleware.Logger) - router.Use(middleware.Recoverer) - - router.Route("/v1", func(r chi.Router) { - httpApp := handlers.NewHttpApp( - httpRouter{Router: r}, - handlers.WithActionErrorHandler(handleActionError), - ) - handlers.MountPetsRoutes(deps.PetsController, httpApp) - }) - - return router -} diff --git a/examples/go-apigen-server/pkg/api/http/server/server.go b/examples/go-apigen-server/pkg/api/http/server/server.go deleted file mode 100644 index 6fb1a5f..0000000 --- a/examples/go-apigen-server/pkg/api/http/server/server.go +++ /dev/null @@ -1,33 +0,0 @@ -package server - -import ( - "fmt" - "net/http" - "time" -) - -type ServerCfg struct { - Port int - IdleTimeout time.Duration - ReadHeaderTimeout time.Duration - ReadTimeout time.Duration - WriteTimeout time.Duration -} - -type HTTPServerParams struct { - ServerCfg - Handler http.Handler -} - -// NewHTTPServer constructor factory for general use *http.Server -func NewHTTPServer(params HTTPServerParams) *http.Server { - address := fmt.Sprintf("[::]:%d", params.Port) - return &http.Server{ - Addr: address, - IdleTimeout: params.IdleTimeout, - ReadHeaderTimeout: params.ReadHeaderTimeout, - ReadTimeout: params.ReadTimeout, - WriteTimeout: params.WriteTimeout, - Handler: params.Handler, - } -} diff --git a/examples/go-apigen-server/pkg/api/http/v1controllers/pets.go b/examples/go-apigen-server/pkg/api/http/v1controllers/pets.go index f26d2e3..4426a0f 100644 --- a/examples/go-apigen-server/pkg/api/http/v1controllers/pets.go +++ b/examples/go-apigen-server/pkg/api/http/v1controllers/pets.go @@ -14,6 +14,6 @@ func NewPetsController(deps PetsControllerDeps) *handlers.PetsController { return handlers.BuildPetsController(). HandleListPets.With(deps.Queries.ListPets). HandleCreatePet.With(deps.Commands.CreatePet). - HandleGetPetById.With(deps.Queries.GetPetById). + HandleGetPetById.With(deps.Queries.GetPetByID). Finalize() } diff --git a/examples/go-apigen-server/pkg/api/http/v1routes/handlers/handlers.go b/examples/go-apigen-server/pkg/api/http/v1routes/handlers/handlers.go index 623f5f1..d82564e 100644 --- a/examples/go-apigen-server/pkg/api/http/v1routes/handlers/handlers.go +++ b/examples/go-apigen-server/pkg/api/http/v1routes/handlers/handlers.go @@ -239,13 +239,14 @@ func newStringToDateTimeParser(isDateOnly bool) rawValueParser[string, time.Time if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } @@ -256,13 +257,14 @@ func newStringSliceToDateTimeParser(isDateOnly bool) rawValueParser[[]string, ti if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value[0]) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value[0]) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } diff --git a/examples/go-apigen-server/pkg/api/http/v1routes/handlers/pets_controller.go b/examples/go-apigen-server/pkg/api/http/v1routes/handlers/pets_controller.go index 95cb781..6218f5e 100644 --- a/examples/go-apigen-server/pkg/api/http/v1routes/handlers/pets_controller.go +++ b/examples/go-apigen-server/pkg/api/http/v1routes/handlers/pets_controller.go @@ -101,7 +101,7 @@ func BuildPetsController() *PetsControllerBuilder { return controllerBuilder } -func MountPetsRoutes(controller *PetsController, app *httpApp) { +func RegisterPetsRoutes(controller *PetsController, app *httpApp) { app.router.HandleRoute("POST", "/pets", controller.CreatePet(app)) app.router.HandleRoute("GET", "/pets/{petId}", controller.GetPetById(app)) app.router.HandleRoute("GET", "/pets", controller.ListPets(app)) diff --git a/examples/go-apigen-server/pkg/app/commands.go b/examples/go-apigen-server/pkg/app/commands.go index 4ddc6c6..df8d96c 100644 --- a/examples/go-apigen-server/pkg/app/commands.go +++ b/examples/go-apigen-server/pkg/app/commands.go @@ -12,20 +12,20 @@ type Commands interface { } type CommandsDeps struct { - Storage *storage + Storage *Storage } type commandsImpl struct { CommandsDeps } -func (c *commandsImpl) CreatePet(ctx context.Context, req *handlers.PetsCreatePetRequest) error { - if _, ok := c.Storage.petsById[req.Payload.Id]; ok { +func (c *commandsImpl) CreatePet(_ context.Context, req *handlers.PetsCreatePetRequest) error { + if _, ok := c.Storage.petsByID[req.Payload.Id]; ok { return fmt.Errorf("pet %d already exists: %w", req.Payload.Id, ErrConflict) } c.Storage.allPets = append(c.Storage.allPets, req.Payload) - c.Storage.petsById[req.Payload.Id] = req.Payload + c.Storage.petsByID[req.Payload.Id] = req.Payload return nil } diff --git a/examples/go-apigen-server/pkg/app/queries.go b/examples/go-apigen-server/pkg/app/queries.go index ac92868..266d458 100644 --- a/examples/go-apigen-server/pkg/app/queries.go +++ b/examples/go-apigen-server/pkg/app/queries.go @@ -10,18 +10,18 @@ import ( type Queries interface { ListPets(context.Context, *handlers.PetsListPetsRequest) (*models.PetsResponse, error) - GetPetById(context.Context, *handlers.PetsGetPetByIdRequest) (*models.PetResponse, error) + GetPetByID(context.Context, *handlers.PetsGetPetByIdRequest) (*models.PetResponse, error) } type QueriesDeps struct { - Storage *storage + Storage *Storage } type queriesImpl struct { QueriesDeps } -func (q *queriesImpl) ListPets(ctx context.Context, req *handlers.PetsListPetsRequest) (*models.PetsResponse, error) { +func (q *queriesImpl) ListPets(_ context.Context, req *handlers.PetsListPetsRequest) (*models.PetsResponse, error) { allPetsLen := int64(len(q.Storage.allPets)) limit := req.Limit offset := req.Offset @@ -35,8 +35,8 @@ func (q *queriesImpl) ListPets(ctx context.Context, req *handlers.PetsListPetsRe return &models.PetsResponse{Data: result}, nil } -func (q *queriesImpl) GetPetById(ctx context.Context, req *handlers.PetsGetPetByIdRequest) (*models.PetResponse, error) { - pet, ok := q.Storage.petsById[req.PetId] +func (q *queriesImpl) GetPetByID(_ context.Context, req *handlers.PetsGetPetByIdRequest) (*models.PetResponse, error) { + pet, ok := q.Storage.petsByID[req.PetId] if !ok { return nil, fmt.Errorf("pet %d not found: %w", req.PetId, ErrNotFound) } diff --git a/examples/go-apigen-server/pkg/app/storage.go b/examples/go-apigen-server/pkg/app/storage.go index f00bf81..0047b7c 100644 --- a/examples/go-apigen-server/pkg/app/storage.go +++ b/examples/go-apigen-server/pkg/app/storage.go @@ -5,14 +5,14 @@ import "github.com/gemyago/apigen/examples/go-apigen-server/pkg/api/http/v1route // In practice this should be on a service layer (e.g service package) and // should write things to some DB like Postgres or Mongo. But keeping it simple for // this example and storing just in memory. -type storage struct { +type Storage struct { allPets []models.Pet - petsById map[int64]models.Pet + petsByID map[int64]models.Pet } -func NewStorage() *storage { - return &storage{ +func NewStorage() *Storage { + return &Storage{ allPets: []models.Pet{}, - petsById: map[int64]models.Pet{}, + petsByID: map[int64]models.Pet{}, } } diff --git a/examples/petstore.yaml b/examples/petstore.yaml index 4719eb1..159b359 100644 --- a/examples/petstore.yaml +++ b/examples/petstore.yaml @@ -8,7 +8,7 @@ info: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html servers: - - url: http://localhost:8080/v1 + - url: http://localhost:8080 paths: /pets: get: diff --git a/generators/go-apigen-server/src/main/resources/go-apigen-server/controller.mustache b/generators/go-apigen-server/src/main/resources/go-apigen-server/controller.mustache index 20080e1..2764c60 100644 --- a/generators/go-apigen-server/src/main/resources/go-apigen-server/controller.mustache +++ b/generators/go-apigen-server/src/main/resources/go-apigen-server/controller.mustache @@ -75,7 +75,7 @@ func Build{{baseName}}Controller() *{{baseName}}ControllerBuilder { return controllerBuilder } -func Mount{{baseName}}Routes(controller *{{baseName}}Controller, app *httpApp) { +func Register{{baseName}}Routes(controller *{{baseName}}Controller, app *httpApp) { {{#operations}} {{#operation}} app.router.HandleRoute("{{#lambda.uppercase}}{{httpMethod}}{{/lambda.uppercase}}", "{{{path}}}", controller.{{operationId}}(app)) diff --git a/generators/go-apigen-server/src/main/resources/go-apigen-server/handlers.mustache b/generators/go-apigen-server/src/main/resources/go-apigen-server/handlers.mustache index 30e1084..738c3ad 100644 --- a/generators/go-apigen-server/src/main/resources/go-apigen-server/handlers.mustache +++ b/generators/go-apigen-server/src/main/resources/go-apigen-server/handlers.mustache @@ -239,13 +239,14 @@ func newStringToDateTimeParser(isDateOnly bool) rawValueParser[string, time.Time if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } @@ -256,13 +257,14 @@ func newStringSliceToDateTimeParser(isDateOnly bool) rawValueParser[[]string, ti if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value[0]) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value[0]) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } diff --git a/tests/golang/controllers/error_handling.go b/tests/golang/controllers/error_handling.go index 26e7ec7..4da2560 100644 --- a/tests/golang/controllers/error_handling.go +++ b/tests/golang/controllers/error_handling.go @@ -14,7 +14,7 @@ func newErrorHandlingController() *handlers.ErrorHandlingController { return errors.New("not implemented") }). HandleErrorHandlingValidationErrors.With( - func(ctx context.Context, ehver *handlers.ErrorHandlingErrorHandlingValidationErrorsRequest) error { + func(_ context.Context, _ *handlers.ErrorHandlingErrorHandlingValidationErrorsRequest) error { return errors.New("not implemented") }). Finalize() diff --git a/tests/golang/controllers/error_handling_test.go b/tests/golang/controllers/error_handling_test.go index a3ddbe9..a9ac9c4 100644 --- a/tests/golang/controllers/error_handling_test.go +++ b/tests/golang/controllers/error_handling_test.go @@ -18,7 +18,7 @@ func TestErrorHandling(t *testing.T) { router := &routerAdapter{ mux: http.NewServeMux(), } - handlers.MountErrorHandlingRoutes( + handlers.RegisterErrorHandlingRoutes( newErrorHandlingController(), handlers.NewHttpApp(router, handlers.WithLogger(newLogger())), ) @@ -35,7 +35,7 @@ func TestErrorHandling(t *testing.T) { runTestCase := func(t *testing.T, tc testCase) { router := setupRouter() testReq := httptest.NewRequest( - "GET", + http.MethodGet, tc.path, http.NoBody, ) @@ -59,7 +59,10 @@ func TestErrorHandling(t *testing.T) { t.Run("parsing-errors", func(t *testing.T) { runTestCase(t, testCase{ name: "respond with 400 if parsing fails", - path: fmt.Sprintf("/error-handling/parsing-errors/%[1]s/%[1]s?requiredQuery1=%[1]s&requiredQuery2=%[1]s", fake.Lorem().Word()), + path: fmt.Sprintf( + "/error-handling/parsing-errors/%[1]s/%[1]s?requiredQuery1=%[1]s&requiredQuery2=%[1]s", + fake.Lorem().Word(), + ), wantErrors: []handlers.BindingError{ {Field: "pathParam1", Location: "path", Code: "BAD_FORMAT"}, {Field: "pathParam2", Location: "path", Code: "BAD_FORMAT"}, diff --git a/tests/golang/controllers/numeric_types_test.go b/tests/golang/controllers/numeric_types_test.go index 16d4760..a7ac7f3 100644 --- a/tests/golang/controllers/numeric_types_test.go +++ b/tests/golang/controllers/numeric_types_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "strconv" "testing" "github.com/gemyago/apigen/tests/golang/routes/handlers" @@ -21,7 +22,7 @@ func TestNumericTypes(t *testing.T) { router := &routerAdapter{ mux: http.NewServeMux(), } - handlers.MountNumericTypesRoutes(controller, handlers.NewHttpApp(router, handlers.WithLogger(newLogger()))) + handlers.RegisterNumericTypesRoutes(controller, handlers.NewHttpApp(router, handlers.WithLogger(newLogger()))) return testActions, router.mux } @@ -47,25 +48,28 @@ func TestNumericTypes(t *testing.T) { NumberInt64InQuery: fake.Int64(), } } - runRouteTestCase(t, "should parse and bind valid values", setupRouter, func() routeTestCase[*numericTypesControllerTestActions] { - wantReq := randomReq() - query := url.Values{} - query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) - query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) - query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) - - return routeTestCase[*numericTypesControllerTestActions]{ - path: fmt.Sprintf("/numeric-types/parsing/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), - query: query, - expect: func(t *testing.T, testActions *numericTypesControllerTestActions, recorder *httptest.ResponseRecorder) { - assert.Equal(t, 204, recorder.Code) - assert.Equal(t, wantReq, testActions.numericTypesParsing.calls[0].params) - }, - } - }) + runRouteTestCase(t, "should parse and bind valid values", setupRouter, + func() routeTestCase[*numericTypesControllerTestActions] { + wantReq := randomReq() + query := url.Values{} + query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) + query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) + query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) + + return routeTestCase[*numericTypesControllerTestActions]{ + path: fmt.Sprintf("/numeric-types/parsing/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, + wantReq.NumberInt32, wantReq.NumberInt64), + query: query, + expect: func(t *testing.T, testActions *numericTypesControllerTestActions, recorder *httptest.ResponseRecorder) { + assert.Equal(t, 204, recorder.Code) + assert.Equal(t, wantReq, testActions.numericTypesParsing.calls[0].params) + }, + } + }) }) t.Run("range-validation", func(t *testing.T) { @@ -74,9 +78,9 @@ func TestNumericTypes(t *testing.T) { query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) return query } @@ -99,7 +103,10 @@ func TestNumericTypes(t *testing.T) { NumberInt64InQuery: fake.Int64Between(60, 600), } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf( + "/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, + wantReq.NumberInt64), query: buildQuery(wantReq), expect: expectBindingErrors[*numericTypesControllerTestActions]( []handlers.BindingError{ @@ -142,7 +149,10 @@ func TestNumericTypes(t *testing.T) { } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf( + "/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, + wantReq.NumberInt64), query: buildQuery(wantReq), expect: func(t *testing.T, testActions *numericTypesControllerTestActions, recorder *httptest.ResponseRecorder) { assert.Equal(t, 204, recorder.Code, "Got unexpected response: %v", recorder.Body) @@ -170,7 +180,10 @@ func TestNumericTypes(t *testing.T) { NumberInt64InQuery: fake.Int64Between(700, 1000), } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf( + "/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, + wantReq.NumberInt64), query: buildQuery(wantReq), expect: expectBindingErrors[*numericTypesControllerTestActions]( []handlers.BindingError{ @@ -213,7 +226,10 @@ func TestNumericTypes(t *testing.T) { } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf( + "/numeric-types/range-validation/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, + wantReq.NumberInt64), query: buildQuery(wantReq), expect: func(t *testing.T, testActions *numericTypesControllerTestActions, recorder *httptest.ResponseRecorder) { assert.Equal(t, 204, recorder.Code, "Got unexpected response: %v", recorder.Body) @@ -229,9 +245,9 @@ func TestNumericTypes(t *testing.T) { query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) return query } @@ -255,7 +271,9 @@ func TestNumericTypes(t *testing.T) { } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation-exclusive/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf("/numeric-types/range-validation-exclusive/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, + wantReq.NumberInt32, wantReq.NumberInt64), query: buildQuery(wantReq), expect: expectBindingErrors[*numericTypesControllerTestActions]( []handlers.BindingError{ @@ -298,7 +316,9 @@ func TestNumericTypes(t *testing.T) { } return testCase{ - path: fmt.Sprintf("/numeric-types/range-validation-exclusive/%v/%v/%v/%v/%v/%v", wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, wantReq.NumberInt32, wantReq.NumberInt64), + path: fmt.Sprintf("/numeric-types/range-validation-exclusive/%v/%v/%v/%v/%v/%v", + wantReq.NumberAny, wantReq.NumberFloat, wantReq.NumberDouble, wantReq.NumberInt, + wantReq.NumberInt32, wantReq.NumberInt64), query: buildQuery(wantReq), expect: expectBindingErrors[*numericTypesControllerTestActions]( []handlers.BindingError{ @@ -340,9 +360,9 @@ func TestNumericTypes(t *testing.T) { query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) return query } @@ -378,15 +398,15 @@ func TestNumericTypes(t *testing.T) { query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) query.Add("optionalNumberAnyInQuery", fmt.Sprint(wantReq.OptionalNumberAnyInQuery)) query.Add("optionalNumberFloatInQuery", fmt.Sprint(wantReq.OptionalNumberFloatInQuery)) query.Add("optionalNumberDoubleInQuery", fmt.Sprint(wantReq.OptionalNumberDoubleInQuery)) - query.Add("optionalNumberIntInQuery", fmt.Sprint(wantReq.OptionalNumberIntInQuery)) - query.Add("optionalNumberInt32InQuery", fmt.Sprint(wantReq.OptionalNumberInt32InQuery)) - query.Add("optionalNumberInt64InQuery", fmt.Sprint(wantReq.OptionalNumberInt64InQuery)) + query.Add("optionalNumberIntInQuery", strconv.FormatInt(int64(wantReq.OptionalNumberIntInQuery), 10)) + query.Add("optionalNumberInt32InQuery", strconv.FormatInt(int64(wantReq.OptionalNumberInt32InQuery), 10)) + query.Add("optionalNumberInt64InQuery", strconv.FormatInt(wantReq.OptionalNumberInt64InQuery, 10)) return query } @@ -422,15 +442,15 @@ func TestNumericTypes(t *testing.T) { query.Add("numberAnyInQuery", fmt.Sprint(wantReq.NumberAnyInQuery)) query.Add("numberFloatInQuery", fmt.Sprint(wantReq.NumberFloatInQuery)) query.Add("numberDoubleInQuery", fmt.Sprint(wantReq.NumberDoubleInQuery)) - query.Add("numberIntInQuery", fmt.Sprint(wantReq.NumberIntInQuery)) - query.Add("numberInt32InQuery", fmt.Sprint(wantReq.NumberInt32InQuery)) - query.Add("numberInt64InQuery", fmt.Sprint(wantReq.NumberInt64InQuery)) + query.Add("numberIntInQuery", strconv.FormatInt(int64(wantReq.NumberIntInQuery), 10)) + query.Add("numberInt32InQuery", strconv.FormatInt(int64(wantReq.NumberInt32InQuery), 10)) + query.Add("numberInt64InQuery", strconv.FormatInt(wantReq.NumberInt64InQuery, 10)) query.Add("optionalNumberAnyInQuery", fmt.Sprint(wantReq.OptionalNumberAnyInQuery)) query.Add("optionalNumberFloatInQuery", fmt.Sprint(wantReq.OptionalNumberFloatInQuery)) query.Add("optionalNumberDoubleInQuery", fmt.Sprint(wantReq.OptionalNumberDoubleInQuery)) - query.Add("optionalNumberIntInQuery", fmt.Sprint(wantReq.OptionalNumberIntInQuery)) - query.Add("optionalNumberInt32InQuery", fmt.Sprint(wantReq.OptionalNumberInt32InQuery)) - query.Add("optionalNumberInt64InQuery", fmt.Sprint(wantReq.OptionalNumberInt64InQuery)) + query.Add("optionalNumberIntInQuery", strconv.FormatInt(int64(wantReq.OptionalNumberIntInQuery), 10)) + query.Add("optionalNumberInt32InQuery", strconv.FormatInt(int64(wantReq.OptionalNumberInt32InQuery), 10)) + query.Add("optionalNumberInt64InQuery", strconv.FormatInt(wantReq.OptionalNumberInt64InQuery, 10)) return query } diff --git a/tests/golang/controllers/string_types_test.go b/tests/golang/controllers/string_types_test.go index e4ec6e6..4907dc6 100644 --- a/tests/golang/controllers/string_types_test.go +++ b/tests/golang/controllers/string_types_test.go @@ -12,6 +12,7 @@ import ( "github.com/gemyago/apigen/tests/golang/routes/handlers" "github.com/jaswdr/faker" + "github.com/samber/lo" "github.com/stretchr/testify/assert" ) @@ -24,15 +25,17 @@ func TestStringTypes(t *testing.T) { router := &routerAdapter{ mux: http.NewServeMux(), } - handlers.MountStringTypesRoutes(controller, handlers.NewHttpApp(router, handlers.WithLogger(newLogger()))) + handlers.RegisterStringTypesRoutes(controller, handlers.NewHttpApp(router, handlers.WithLogger(newLogger()))) return testActions, router.mux } type testCase = routeTestCase[*stringTypesControllerTestActions] t.Run("parsing", func(t *testing.T) { - randomReq := func() *handlers.StringTypesStringTypesParsingRequest { - return &handlers.StringTypesStringTypesParsingRequest{ + randomReq := func( + opts ...func(*handlers.StringTypesStringTypesParsingRequest), + ) *handlers.StringTypesStringTypesParsingRequest { + req := &handlers.StringTypesStringTypesParsingRequest{ // path UnformattedStr: fake.Lorem().Word(), CustomFormatStr: fake.Lorem().Word(), @@ -47,40 +50,82 @@ func TestStringTypes(t *testing.T) { DateTimeStrInQuery: fake.Time().Time(time.Now()), ByteStrInQuery: fake.BinaryString().BinaryString(10), } + for _, opt := range opts { + opt(req) + } + return req + } + + buildQuery := func(req *handlers.StringTypesStringTypesParsingRequest) url.Values { + query := url.Values{} + query.Add("unformattedStrInQuery", req.UnformattedStrInQuery) + query.Add("customFormatStrInQuery", req.CustomFormatStrInQuery) + query.Add("dateStrInQuery", req.DateStrInQuery.Format(time.DateOnly)) + query.Add("dateTimeStrInQuery", req.DateTimeStrInQuery.Format(time.RFC3339Nano)) + query.Add("byteStrInQuery", req.ByteStrInQuery) + return query } + + // TODO: Should fail if date includes time + runRouteTestCase(t, "should parse and bind valid values", setupRouter, func() testCase { originalReq := randomReq() - query := url.Values{} - query.Add("unformattedStrInQuery", originalReq.UnformattedStrInQuery) - query.Add("customFormatStrInQuery", originalReq.CustomFormatStrInQuery) - query.Add("dateStrInQuery", originalReq.DateStrInQuery.Format(time.RFC3339Nano)) - query.Add("dateTimeStrInQuery", originalReq.DateTimeStrInQuery.Format(time.RFC3339Nano)) - query.Add("byteStrInQuery", originalReq.ByteStrInQuery) + query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/parsing/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/parsing/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr, + ), query: query, expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { assert.Equal(t, 204, recorder.Code) wantReq := *originalReq - wantReq.DateStr = originalReq.DateStr.Truncate(24 * time.Hour) - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStr = lo.Must(time.Parse(time.DateOnly, wantReq.DateStr.Format(time.DateOnly))) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, wantReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.stringTypesParsing.calls[0].params) }, } }) + runRouteTestCase(t, "should parse time with locale", setupRouter, func() testCase { + location := time.FixedZone("", int((time.Duration(fake.IntBetween(2, 30)) * time.Minute).Seconds())) + originalReq := randomReq(func(req *handlers.StringTypesStringTypesParsingRequest) { + req.DateTimeStr = req.DateTimeStr.In(location) + req.DateTimeStrInQuery = req.DateTimeStrInQuery.In(location) + }) + query := buildQuery(originalReq) + + return testCase{ + path: fmt.Sprintf( + "/string-types/parsing/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr, + ), + query: query, + expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { + if !assert.Equal(t, 204, recorder.Code, "Unexpected response", recorder.Body) { + return + } + + wantReq := *originalReq + assert.Equal(t, wantReq.DateTimeStr, testActions.stringTypesParsing.calls[0].params.DateTimeStr) + assert.Equal(t, wantReq.DateTimeStrInQuery, testActions.stringTypesParsing.calls[0].params.DateTimeStrInQuery) + }, + } + }) runRouteTestCase(t, "should fail if bad values", setupRouter, func() testCase { originalReq := randomReq() - query := url.Values{} - query.Add("unformattedStrInQuery", originalReq.UnformattedStrInQuery) - query.Add("customFormatStrInQuery", originalReq.CustomFormatStrInQuery) - query.Add("dateStrInQuery", fake.Lorem().Word()) - query.Add("dateTimeStrInQuery", fake.Lorem().Word()) - query.Add("byteStrInQuery", originalReq.ByteStrInQuery) + query := buildQuery(originalReq) + query.Set("dateStrInQuery", fake.Lorem().Word()) + query.Set("dateTimeStrInQuery", fake.Lorem().Word()) return testCase{ - path: fmt.Sprintf("/string-types/parsing/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, fake.Lorem().Word(), fake.Lorem().Word(), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/parsing/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, fake.Lorem().Word(), + fake.Lorem().Word(), originalReq.ByteStr), query: query, expect: expectBindingErrors[*stringTypesControllerTestActions]( []handlers.BindingError{ @@ -95,6 +140,28 @@ func TestStringTypes(t *testing.T) { ), } }) + runRouteTestCase(t, "should fail if date formatted with time", setupRouter, func() testCase { + originalReq := randomReq() + query := buildQuery(originalReq) + query.Set("dateStrInQuery", originalReq.DateStrInQuery.Format(time.RFC3339)) + + return testCase{ + path: fmt.Sprintf( + "/string-types/parsing/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStrInQuery.Format(time.RFC3339), + originalReq.DateTimeStrInQuery.Format(time.RFC3339Nano), originalReq.ByteStr), + query: query, + expect: expectBindingErrors[*stringTypesControllerTestActions]( + []handlers.BindingError{ + // path + {Field: "dateStr", Location: "path", Code: handlers.ErrBadValueFormat}, + + // query + {Field: "dateStrInQuery", Location: "query", Code: handlers.ErrBadValueFormat}, + }, + ), + } + }) }) t.Run("range-validation", func(t *testing.T) { @@ -126,7 +193,7 @@ func TestStringTypes(t *testing.T) { query := url.Values{} query.Add("unformattedStrInQuery", wantReq.UnformattedStrInQuery) query.Add("customFormatStrInQuery", wantReq.CustomFormatStrInQuery) - query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.RFC3339Nano)) + query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.DateOnly)) query.Add("dateTimeStrInQuery", wantReq.DateTimeStrInQuery.Format(time.RFC3339Nano)) query.Add("byteStrInQuery", wantReq.ByteStrInQuery) return query @@ -137,7 +204,10 @@ func TestStringTypes(t *testing.T) { query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, + originalReq.DateStr.Format(time.DateOnly), originalReq.DateTimeStr.Format(time.RFC3339Nano), + originalReq.ByteStr), query: query, expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { if !assert.Equal(t, 204, recorder.Code) { @@ -145,8 +215,8 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStr = originalReq.DateStr.Truncate(24 * time.Hour) - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStr = lo.Must(time.Parse(time.DateOnly, originalReq.DateStr.Format(time.DateOnly))) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.StringTypesRangeValidation.calls[0].params) }, } @@ -164,7 +234,10 @@ func TestStringTypes(t *testing.T) { query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/range-validation/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), query: query, expect: expectBindingErrors[*stringTypesControllerTestActions]( []handlers.BindingError{ @@ -194,7 +267,10 @@ func TestStringTypes(t *testing.T) { query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/range-validation/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), query: query, expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { if !assert.Equal(t, 204, recorder.Code) { @@ -202,8 +278,8 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStr = originalReq.DateStr.Truncate(24 * time.Hour) - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStr = lo.Must(time.Parse(time.DateOnly, originalReq.DateStr.Format(time.DateOnly))) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.StringTypesRangeValidation.calls[0].params) }, } @@ -221,7 +297,10 @@ func TestStringTypes(t *testing.T) { query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/range-validation/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), query: query, expect: expectBindingErrors[*stringTypesControllerTestActions]( []handlers.BindingError{ @@ -251,7 +330,10 @@ func TestStringTypes(t *testing.T) { query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/range-validation/%v/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), + path: fmt.Sprintf( + "/string-types/range-validation/%v/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano), originalReq.ByteStr), query: query, expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { if !assert.Equal(t, 204, recorder.Code) { @@ -259,8 +341,8 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStr = originalReq.DateStr.Truncate(24 * time.Hour) - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStr = lo.Must(time.Parse(time.DateOnly, originalReq.DateStr.Format(time.DateOnly))) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.StringTypesRangeValidation.calls[0].params) }, } @@ -295,13 +377,13 @@ func TestStringTypes(t *testing.T) { query := url.Values{} query.Add("unformattedStrInQuery", wantReq.UnformattedStrInQuery) query.Add("customFormatStrInQuery", wantReq.CustomFormatStrInQuery) - query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.RFC3339Nano)) + query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.DateOnly)) query.Add("dateTimeStrInQuery", wantReq.DateTimeStrInQuery.Format(time.RFC3339Nano)) query.Add("byteStrInQuery", wantReq.ByteStrInQuery) query.Add("optionalUnformattedStrInQuery", wantReq.OptionalUnformattedStrInQuery) query.Add("optionalCustomFormatStrInQuery", wantReq.OptionalCustomFormatStrInQuery) - query.Add("optionalDateStrInQuery", wantReq.OptionalDateStrInQuery.Format(time.RFC3339Nano)) + query.Add("optionalDateStrInQuery", wantReq.OptionalDateStrInQuery.Format(time.DateOnly)) query.Add("optionalDateTimeStrInQuery", wantReq.OptionalDateTimeStrInQuery.Format(time.RFC3339Nano)) query.Add("optionalByteStrInQuery", wantReq.OptionalByteStrInQuery) return query @@ -319,8 +401,10 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) - wantReq.OptionalDateStrInQuery = originalReq.OptionalDateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) + wantReq.OptionalDateStrInQuery = lo.Must( + time.Parse(time.DateOnly, originalReq.OptionalDateStrInQuery.Format(time.DateOnly)), + ) assert.Equal(t, &wantReq, testActions.StringTypesRequiredValidation.calls[0].params) }, } @@ -351,7 +435,7 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.StringTypesRequiredValidation.calls[0].params) }, } @@ -421,7 +505,7 @@ func TestStringTypes(t *testing.T) { query := url.Values{} query.Add("unformattedStrInQuery", wantReq.UnformattedStrInQuery) query.Add("customFormatStrInQuery", wantReq.CustomFormatStrInQuery) - query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.RFC3339Nano)) + query.Add("dateStrInQuery", wantReq.DateStrInQuery.Format(time.DateOnly)) query.Add("dateTimeStrInQuery", wantReq.DateTimeStrInQuery.Format(time.RFC3339Nano)) return query } @@ -430,7 +514,10 @@ func TestStringTypes(t *testing.T) { originalReq := randomReq() query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/pattern-validation/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano)), + path: fmt.Sprintf( + "/string-types/pattern-validation/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano)), query: query, expect: func(t *testing.T, testActions *stringTypesControllerTestActions, recorder *httptest.ResponseRecorder) { if !assert.Equal(t, 204, recorder.Code) { @@ -438,8 +525,8 @@ func TestStringTypes(t *testing.T) { } wantReq := *originalReq - wantReq.DateStr = originalReq.DateStr.Truncate(24 * time.Hour) - wantReq.DateStrInQuery = originalReq.DateStrInQuery.Truncate(24 * time.Hour) + wantReq.DateStr = lo.Must(time.Parse(time.DateOnly, originalReq.DateStr.Format(time.DateOnly))) + wantReq.DateStrInQuery = lo.Must(time.Parse(time.DateOnly, originalReq.DateStrInQuery.Format(time.DateOnly))) assert.Equal(t, &wantReq, testActions.StringTypesPatternValidation.calls[0].params) }, } @@ -457,7 +544,10 @@ func TestStringTypes(t *testing.T) { }) query := buildQuery(originalReq) return testCase{ - path: fmt.Sprintf("/string-types/pattern-validation/%v/%v/%v/%v", originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.RFC3339Nano), originalReq.DateTimeStr.Format(time.RFC3339Nano)), + path: fmt.Sprintf( + "/string-types/pattern-validation/%v/%v/%v/%v", + originalReq.UnformattedStr, originalReq.CustomFormatStr, originalReq.DateStr.Format(time.DateOnly), + originalReq.DateTimeStr.Format(time.RFC3339Nano)), query: query, expect: expectBindingErrors[*stringTypesControllerTestActions]( []handlers.BindingError{ diff --git a/tests/golang/controllers/testing.go b/tests/golang/controllers/testing.go index 933447a..a80314a 100644 --- a/tests/golang/controllers/testing.go +++ b/tests/golang/controllers/testing.go @@ -31,12 +31,12 @@ func (r *routerAdapter) HandleRoute(method, pathPattern string, h http.Handler) r.mux.Handle(method+" "+pathPattern, h) } -func (r *routerAdapter) HandleError(req *http.Request, w http.ResponseWriter, err error) { +func (r *routerAdapter) HandleError(_ *http.Request, w http.ResponseWriter, err error) { r.handledErrors = append(r.handledErrors, err) w.WriteHeader(http.StatusInternalServerError) } -// openTestLogFile will open a log file in a project root directory +// openTestLogFile will open a log file in a project root directory. func openTestLogFile() *os.File { _, filename, _, _ := runtime.Caller(0) // Will be current file testFilePath := filepath.Join(filename, "..", "..", "..", "..", "golang-test.log") @@ -48,7 +48,7 @@ func openTestLogFile() *os.File { return f } -var testOutput = openTestLogFile() +var testOutput = openTestLogFile() //nolint:gochecknoglobals // we want to open it once for all tests func newLogger() *slog.Logger { return slog.New(slog.NewJSONHandler(testOutput, nil)) @@ -72,7 +72,7 @@ func runRouteTestCase[TActions any]( tc := tc() testActions, router := setupFn() testReq := httptest.NewRequest( - "GET", + http.MethodGet, tc.path, http.NoBody, ) @@ -90,10 +90,10 @@ type routeTestCaseExpectFn[TActions any] func(t *testing.T, testActions TActions func expectBindingErrors[TActions any](wantErrors []handlers.BindingError) routeTestCaseExpectFn[TActions] { return func( t *testing.T, - testActions TActions, + _ TActions, recorder *httptest.ResponseRecorder, ) { - if !assert.Equal(t, 400, recorder.Code, "Unexpected response: %v", recorder.Body) { + if !assert.Equal(t, http.StatusBadRequest, recorder.Code, "Unexpected response: %v", recorder.Body) { return } assert.Equal(t, "application/json; charset=utf-8", recorder.Header().Get("content-type")) @@ -125,14 +125,14 @@ func assertFieldError( location string, field string, code handlers.BindingErrorCode, -) bool { +) { for _, fieldErr := range err.Errors { if fieldErr.Location == location && fieldErr.Field == field { - return assert.Equal(t, code, fieldErr.Code, "field %s: unexpected error code for", field) + assert.Equal(t, code, fieldErr.Code, "field %s: unexpected error code for", field) + return } } assert.Fail(t, fmt.Sprintf("no error found for field %s, code %s", field, code)) - return false } type mockActionCall[TParams any] struct { diff --git a/tests/golang/go.mod b/tests/golang/go.mod index 5090be2..57b483f 100644 --- a/tests/golang/go.mod +++ b/tests/golang/go.mod @@ -4,6 +4,7 @@ go 1.22 require ( github.com/jaswdr/faker v1.19.1 + github.com/samber/lo v1.46.0 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 ) @@ -11,5 +12,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tests/golang/go.sum b/tests/golang/go.sum index c6fc6c0..2782659 100644 --- a/tests/golang/go.sum +++ b/tests/golang/go.sum @@ -4,10 +4,14 @@ github.com/jaswdr/faker v1.19.1 h1:xBoz8/O6r0QAR8eEvKJZMdofxiRH+F0M/7MU9eNKhsM= github.com/jaswdr/faker v1.19.1/go.mod h1:x7ZlyB1AZqwqKZgyQlnqEG8FDptmHlncA5u2zY/yi6w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= +github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/tests/golang/routes/handlers/error_handling_controller.go b/tests/golang/routes/handlers/error_handling_controller.go index e3ee17c..1f1ce43 100644 --- a/tests/golang/routes/handlers/error_handling_controller.go +++ b/tests/golang/routes/handlers/error_handling_controller.go @@ -79,7 +79,7 @@ func BuildErrorHandlingController() *ErrorHandlingControllerBuilder { return controllerBuilder } -func MountErrorHandlingRoutes(controller *ErrorHandlingController, app *httpApp) { +func RegisterErrorHandlingRoutes(controller *ErrorHandlingController, app *httpApp) { app.router.HandleRoute("GET", "/error-handling/parsing-errors/{pathParam1}/{pathParam2}", controller.ErrorHandlingParsingErrors(app)) app.router.HandleRoute("GET", "/error-handling/validation-errors", controller.ErrorHandlingValidationErrors(app)) } diff --git a/tests/golang/routes/handlers/handlers.go b/tests/golang/routes/handlers/handlers.go index 623f5f1..d82564e 100644 --- a/tests/golang/routes/handlers/handlers.go +++ b/tests/golang/routes/handlers/handlers.go @@ -239,13 +239,14 @@ func newStringToDateTimeParser(isDateOnly bool) rawValueParser[string, time.Time if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } @@ -256,13 +257,14 @@ func newStringSliceToDateTimeParser(isDateOnly bool) rawValueParser[[]string, ti if !ov.assigned { return nil } - val, err := time.Parse(time.RFC3339Nano, ov.value[0]) + format := time.RFC3339Nano + if isDateOnly { + format = time.DateOnly + } + val, err := time.Parse(format, ov.value[0]) if err != nil { return err } - if isDateOnly { - val = val.Truncate(24 * time.Hour) - } *t = val return nil } diff --git a/tests/golang/routes/handlers/numeric_types_controller.go b/tests/golang/routes/handlers/numeric_types_controller.go index 6de61d7..3f084a1 100644 --- a/tests/golang/routes/handlers/numeric_types_controller.go +++ b/tests/golang/routes/handlers/numeric_types_controller.go @@ -169,7 +169,7 @@ func BuildNumericTypesController() *NumericTypesControllerBuilder { return controllerBuilder } -func MountNumericTypesRoutes(controller *NumericTypesController, app *httpApp) { +func RegisterNumericTypesRoutes(controller *NumericTypesController, app *httpApp) { app.router.HandleRoute("GET", "/numeric-types/parsing/{numberAny}/{numberFloat}/{numberDouble}/{numberInt}/{numberInt32}/{numberInt64}", controller.NumericTypesParsing(app)) app.router.HandleRoute("GET", "/numeric-types/range-validation/{numberAny}/{numberFloat}/{numberDouble}/{numberInt}/{numberInt32}/{numberInt64}", controller.NumericTypesRangeValidation(app)) app.router.HandleRoute("GET", "/numeric-types/range-validation-exclusive/{numberAny}/{numberFloat}/{numberDouble}/{numberInt}/{numberInt32}/{numberInt64}", controller.NumericTypesRangeValidationExclusive(app)) diff --git a/tests/golang/routes/handlers/string_types_controller.go b/tests/golang/routes/handlers/string_types_controller.go index 398dad3..d079fc4 100644 --- a/tests/golang/routes/handlers/string_types_controller.go +++ b/tests/golang/routes/handlers/string_types_controller.go @@ -159,7 +159,7 @@ func BuildStringTypesController() *StringTypesControllerBuilder { return controllerBuilder } -func MountStringTypesRoutes(controller *StringTypesController, app *httpApp) { +func RegisterStringTypesRoutes(controller *StringTypesController, app *httpApp) { app.router.HandleRoute("GET", "/string-types/parsing/{unformattedStr}/{customFormatStr}/{dateStr}/{dateTimeStr}/{byteStr}", controller.StringTypesParsing(app)) app.router.HandleRoute("GET", "/string-types/pattern-validation/{unformattedStr}/{customFormatStr}/{dateStr}/{dateTimeStr}", controller.StringTypesPatternValidation(app)) app.router.HandleRoute("GET", "/string-types/range-validation/{unformattedStr}/{customFormatStr}/{dateStr}/{dateTimeStr}/{byteStr}", controller.StringTypesRangeValidation(app))