├── testdata ├── hello.txt └── world.txt ├── .github ├── dependabot.yml └── workflows │ ├── goreleaser.yml │ ├── codeql.yml │ └── go.yml ├── go.mod ├── _example ├── basic │ ├── go.mod │ ├── basic.go │ ├── basic_test.go │ └── go.sum ├── mux │ ├── go.mod │ ├── mux.go │ ├── mux_test.go │ └── go.sum ├── echo │ ├── echo.go │ ├── echo_test.go │ ├── go.mod │ └── go.sum └── gin │ ├── gin.go │ ├── gin_test.go │ ├── go.mod │ └── go.sum ├── .gitignore ├── Makefile ├── .goreleaser.yaml ├── bearer.yml ├── go.sum ├── .golangci.yml ├── LICENSE ├── certificate ├── localhost.cert └── localhost.key ├── .roomodes ├── README.md ├── gofight.go └── gofight_test.go /testdata/hello.txt: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /testdata/world.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/appleboy/gofight/v2 2 | 3 | go 1.22 4 | 5 | require github.com/stretchr/testify v1.11.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /_example/basic/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.2.0 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /_example/mux/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.2.0 7 | github.com/gorilla/mux v1.8.1 8 | github.com/stretchr/testify v1.9.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/yaml.v3 v3.0.1 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /_example/mux/mux.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | func muxHelloHandler(w http.ResponseWriter, r *http.Request) { 10 | w.Write([]byte("Hello World")) 11 | } 12 | 13 | // MuxEngine is mux router. 14 | func MuxEngine() *mux.Router { 15 | r := mux.NewRouter() 16 | r.HandleFunc("/", muxHelloHandler) 17 | 18 | return r 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | coverage.txt 27 | -------------------------------------------------------------------------------- /_example/echo/echo.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo/v4" 7 | ) 8 | 9 | func echoHelloHandler() echo.HandlerFunc { 10 | return func(c echo.Context) error { 11 | return c.String(http.StatusOK, "Hello World") 12 | } 13 | } 14 | 15 | // EchoEngine is echo router. 16 | func EchoEngine() *echo.Echo { 17 | // Echo instance 18 | e := echo.New() 19 | 20 | // Routes 21 | e.GET("/", echoHelloHandler()) 22 | 23 | return e 24 | } 25 | -------------------------------------------------------------------------------- /_example/mux/mux_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/appleboy/gofight/v2" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMuxHelloWorld(t *testing.T) { 12 | r := gofight.New() 13 | 14 | r.GET("/"). 15 | SetDebug(true). 16 | Run(MuxEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 17 | assert.Equal(t, "Hello World", r.Body.String()) 18 | assert.Equal(t, http.StatusOK, r.Code) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /_example/echo/echo_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/appleboy/gofight/v2" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEchoHelloWorld(t *testing.T) { 12 | r := gofight.New() 13 | 14 | r.GET("/"). 15 | SetDebug(true). 16 | Run(EchoEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 17 | assert.Equal(t, "Hello World", r.Body.String()) 18 | assert.Equal(t, http.StatusOK, r.Code) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /_example/basic/basic.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | func basicHelloHandler(w http.ResponseWriter, r *http.Request) { 9 | // add header in response. 10 | w.Header().Set("Content-Type", "text/plain") 11 | w.Header().Set("X-Version", "0.0.1") 12 | _, _ = io.WriteString(w, "Hello World") 13 | } 14 | 15 | // BasicEngine is basic router. 16 | func BasicEngine() http.Handler { 17 | mux := http.NewServeMux() 18 | mux.HandleFunc("/", basicHelloHandler) 19 | 20 | return mux 21 | } 22 | -------------------------------------------------------------------------------- /_example/gin/gin.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func ginHelloHandler(c *gin.Context) { 11 | c.String(http.StatusOK, "Hello World") 12 | } 13 | 14 | func ginUserHandler(c *gin.Context) { 15 | name := c.Param("name") 16 | c.String(http.StatusOK, fmt.Sprintf("Hello, %s", name)) 17 | } 18 | 19 | // GinEngine is gin router. 20 | func GinEngine() *gin.Engine { 21 | gin.SetMode(gin.TestMode) 22 | r := gin.New() 23 | 24 | r.GET("/", ginHelloHandler) 25 | r.GET("/user/:name", ginUserHandler) 26 | 27 | return r 28 | } 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= gofumpt -l -w 3 | PACKAGES ?= $(shell $(GO) list ./...) 4 | GOFILES := $(shell find . -name "*.go" -type f) 5 | 6 | .PHONY: fmt 7 | fmt: 8 | $(GOFMT) -w $(GOFILES) 9 | 10 | .PHONY: fmt-check 11 | fmt-check: 12 | @diff=$$($(GOFMT) -d $(GOFILES)); \ 13 | if [ -n "$$diff" ]; then \ 14 | echo "Please run 'make fmt' and commit the result:"; \ 15 | echo "$${diff}"; \ 16 | exit 1; \ 17 | fi; 18 | 19 | test: fmt-check 20 | @$(GO) test -v -cover -coverprofile coverage.txt ./... && echo "\n==>\033[32m Ok\033[m\n" || exit 1 21 | 22 | vet: 23 | $(GO) vet ./... 24 | 25 | clean: 26 | $(GO) clean -modcache -cache -i 27 | find . -name "coverage.txt" -delete 28 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - # If true, skip the build. 3 | # Useful for library projects. 4 | # Default is false 5 | skip: true 6 | 7 | changelog: 8 | use: github 9 | groups: 10 | - title: Features 11 | regexp: "^.*feat[(\\w)]*:+.*$" 12 | order: 0 13 | - title: "Bug fixes" 14 | regexp: "^.*fix[(\\w)]*:+.*$" 15 | order: 1 16 | - title: "Enhancements" 17 | regexp: "^.*chore[(\\w)]*:+.*$" 18 | order: 2 19 | - title: "Refactor" 20 | regexp: "^.*refactor[(\\w)]*:+.*$" 21 | order: 3 22 | - title: "Build process updates" 23 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: "Documentation updates" 26 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 27 | order: 4 28 | - title: Others 29 | -------------------------------------------------------------------------------- /bearer.yml: -------------------------------------------------------------------------------- 1 | disable-version-check: false 2 | log-level: info 3 | report: 4 | fail-on-severity: critical,high,medium,low 5 | format: "" 6 | no-color: false 7 | output: "" 8 | report: security 9 | severity: critical,high,medium,low,warning 10 | rule: 11 | disable-default-rules: false 12 | only-rule: [] 13 | skip-rule: ["go_lang_insecure_cookie", "go_lang_logger_leak"] 14 | scan: 15 | context: "" 16 | data_subject_mapping: "" 17 | disable-domain-resolution: true 18 | domain-resolution-timeout: 3s 19 | exit-code: -1 20 | external-rule-dir: [] 21 | force: false 22 | hide_progress_bar: false 23 | internal-domains: [] 24 | parallel: 0 25 | quiet: false 26 | scanner: 27 | - sast 28 | skip-path: [] 29 | skip-test: true 30 | -------------------------------------------------------------------------------- /_example/echo/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.2.0 7 | github.com/labstack/echo/v4 v4.13.4 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/labstack/gommon v0.4.2 // indirect 14 | github.com/mattn/go-colorable v0.1.14 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/valyala/bytebufferpool v1.0.0 // indirect 18 | github.com/valyala/fasttemplate v1.2.2 // indirect 19 | golang.org/x/crypto v0.41.0 // indirect 20 | golang.org/x/net v0.43.0 // indirect 21 | golang.org/x/sys v0.35.0 // indirect 22 | golang.org/x/text v0.28.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /_example/gin/gin_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/appleboy/gofight/v2" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGinHelloWorld(t *testing.T) { 12 | r := gofight.New() 13 | 14 | r.GET("/"). 15 | SetDebug(true). 16 | Run(GinEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 17 | assert.Equal(t, "Hello World", r.Body.String()) 18 | assert.Equal(t, http.StatusOK, r.Code) 19 | }) 20 | } 21 | 22 | func TestGinHelloHandler(t *testing.T) { 23 | r := gofight.New() 24 | 25 | r.GET("/user/appleboy"). 26 | SetDebug(true). 27 | Run(GinEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 28 | assert.Equal(t, "Hello, appleboy", r.Body.String()) 29 | assert.Equal(t, http.StatusOK, r.Code) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v5 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Run GoReleaser 26 | uses: goreleaser/goreleaser-action@v6 27 | with: 28 | # either 'goreleaser' (default) or 'goreleaser-pro' 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 6 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - gochecknoinits 11 | - goconst 12 | - gocritic 13 | - gocyclo 14 | - goprintffuncname 15 | - gosec 16 | - govet 17 | - ineffassign 18 | - lll 19 | - misspell 20 | - nakedret 21 | - noctx 22 | - nolintlint 23 | - rowserrcheck 24 | - staticcheck 25 | - unconvert 26 | - unparam 27 | - unused 28 | - whitespace 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gofmt 43 | - gofumpt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bo-Yi Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /certificate/localhost.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+zCCAeOgAwIBAgIJALbZEDvUQrFKMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV 3 | BAMMCWxvY2FsaG9zdDAeFw0xNjAzMjgwMzMwNDFaFw0yNjAzMjYwMzMwNDFaMBQx 4 | EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC 5 | ggEBAMj1+xg4jVLzVnB5j7n1ul30WEE4BCzcNFxg5AOB5H5q+wje0YYiVFg6PQyv 6 | GCipqIRXVRdVQ1hHSeunYGKe8lq3Sb1X8PUJ12v9uRbpS9DK1Owqk8rsPDu6sVTL 7 | qKKgH1Z8yazzaS0AbXuA5e9gO/RzijbnpEP+quM4dueiMPVEJyLq+EoIQY+MM8MP 8 | 8dZzL4XZl7wL4UsCN7rPcO6W3tlnT0iO3h9c/Ym2hFhz+KNJ9KRRCvtPGZESigtK 9 | bHsXH099WDo8v/Wp5/evBw/+JD0opxmCfHIBALHt9v53RvvsDZ1t33Rpu5C8znEY 10 | Y2Ay7NgxhqjqoWJqA48lJeA0clsCAwEAAaNQME4wHQYDVR0OBBYEFC0bTU1Xofeh 11 | NKIelashIsqKidDYMB8GA1UdIwQYMBaAFC0bTU1XofehNKIelashIsqKidDYMAwG 12 | A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAiJL8IMTwNX9XqQWYDFgkG4 13 | AnrVwQhreAqC9rSxDCjqqnMHPHGzcCeDMLAMoh0kOy20nowUGNtCZ0uBvnX2q1bN 14 | g1jt+GBcLJDR3LL4CpNOlm3YhOycuNfWMxTA7BXkmnSrZD/7KhArsBEY8aulxwKJ 15 | HRgNlIwe1oFD1YdX1BS5pp4t25B6Vq4A3FMMUkVoWE688nE168hvQgwjrHkgHhwe 16 | eN8lGE2DhFraXnWmDMdwaHD3HRFGhyppIFN+f7BqbWX9gM+T2YRTfObIXLWbqJLD 17 | 3Mk/NkxqVcg4eY54wJ1ufCUGAYAIaY6fQqiNUz8nhwK3t45NBVT9y/uJXqnTLyY= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /_example/basic/basic_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/appleboy/gofight/v2" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBasicHelloWorld(t *testing.T) { 12 | r := gofight.New() 13 | version := "0.0.1" 14 | 15 | r.GET("/"). 16 | // trun on the debug mode. 17 | SetDebug(true). 18 | SetHeader(gofight.H{ 19 | "X-Version": version, 20 | }). 21 | Run(BasicEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 22 | assert.Equal(t, version, rq.Header.Get("X-Version")) 23 | assert.Equal(t, version, r.Header().Get("X-Version")) 24 | assert.Equal(t, "Hello World", r.Body.String()) 25 | assert.Equal(t, http.StatusOK, r.Code) 26 | }) 27 | } 28 | 29 | func basicHTTPHelloHandler() { 30 | http.HandleFunc("/hello", basicHelloHandler) 31 | } 32 | 33 | func TestBasicHttpHelloWorld(t *testing.T) { 34 | basicHTTPHelloHandler() 35 | 36 | r := gofight.New() 37 | 38 | r.GET("/hello"). 39 | // trun on the debug mode. 40 | SetDebug(true). 41 | Run(http.DefaultServeMux, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 42 | assert.Equal(t, "Hello World", r.Body.String()) 43 | assert.Equal(t, http.StatusOK, r.Code) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /_example/gin/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.2.0 7 | github.com/gin-gonic/gin v1.10.1 8 | github.com/stretchr/testify v1.10.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/gopkg v0.1.3 // indirect 13 | github.com/bytedance/sonic v1.14.1 // indirect 14 | github.com/bytedance/sonic/loader v0.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.6 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 18 | github.com/gin-contrib/sse v1.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.27.0 // indirect 22 | github.com/goccy/go-json v0.10.5 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.3.0 // indirect 33 | golang.org/x/arch v0.20.0 // indirect 34 | golang.org/x/crypto v0.41.0 // indirect 35 | golang.org/x/net v0.43.0 // indirect 36 | golang.org/x/sys v0.35.0 // indirect 37 | golang.org/x/text v0.28.0 // indirect 38 | google.golang.org/protobuf v1.36.8 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /certificate/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAyPX7GDiNUvNWcHmPufW6XfRYQTgELNw0XGDkA4Hkfmr7CN7R 3 | hiJUWDo9DK8YKKmohFdVF1VDWEdJ66dgYp7yWrdJvVfw9QnXa/25FulL0MrU7CqT 4 | yuw8O7qxVMuooqAfVnzJrPNpLQBte4Dl72A79HOKNuekQ/6q4zh256Iw9UQnIur4 5 | SghBj4wzww/x1nMvhdmXvAvhSwI3us9w7pbe2WdPSI7eH1z9ibaEWHP4o0n0pFEK 6 | +08ZkRKKC0psexcfT31YOjy/9ann968HD/4kPSinGYJ8cgEAse32/ndG++wNnW3f 7 | dGm7kLzOcRhjYDLs2DGGqOqhYmoDjyUl4DRyWwIDAQABAoIBAGTKqsN9KbSfA42q 8 | CqI0UuLouJMNa1qsnz5uAi6YKWgWdA4A44mpEjCmFRSVhUJvxWuK+cyYIQzXxIWD 9 | D16nZdqF72AeCWZ9JySsvvZ00GfKM3y35iRy08sJWgOzmcLnGJCiSeyKsQe3HTJC 10 | dhDXbXqvsHTVPZg01LTeDxUiTffU8NMKqR2AecQ2sTDwXEhAnTyAtnzl/XaBgFzu 11 | U6G7FzGM5y9bxkfQVkvy+DEJkHGNOjzwcVfByyVl610ixmG1vmxVj9PbWmIPsUV8 12 | ySmjhvDQbOfoxW0h9vTlTqGtQcBw962osnDDMWFCdM7lzO0T7RRnPVGIRpCJOKhq 13 | keqHKwECgYEA8wwI/iZughoTXTNG9LnQQ/WAtsqO80EjMTUheo5I1kOzmUz09pyh 14 | iAsUDoN0/26tZ5WNjlnyZu7dvTc/x3dTZpmNnoo8gcVbQNECDRzqfuQ9PPXm1SN5 15 | 6peBqAvBv78hjV05aXzPG/VBbeig7l299EarEA+a/oH3KrgDoqVqE0ECgYEA06vA 16 | YJmgg4fZRucAYoaYsLz9Z9rCFjTe1PBTmUJkbOR8vFIHHTTEWi/SuxXL0wDSeoE2 17 | 7BQm86gCC7/KgRdrzoBqZ5qS9Mv2dsLgY635VSgjjfZkVLiH1VRRpSQObYnfoysg 18 | gatcHSKMExd4SLQByAuImXP+L5ayDBcEJfbqSpsCgYB78Is1b0uzNLDjOh7Y9Vhr 19 | D2qPzEORcIoNsdZctOoXuXaAmmngyIbm5R9ZN1gWWc47oFwLV3rxWqXgs6fmg8cX 20 | 7v309vFcC9Q4/Vxaa4B5LNK9n3gTAIBPTOtlUnl+2my1tfBtBqRm0W6IKbTHWS5g 21 | vxjEm/CiEIyGUEgqTMgHAQKBgBKuXdQoutng63QufwIzDtbKVzMLQ4XiNKhmbXph 22 | OavCnp+gPbB+L7Yl8ltAmTSOJgVZ0hcT0DxA361Zx+2Mu58GBl4OblnchmwE1vj1 23 | KcQyPrEQxdoUTyiswGfqvrs8J9imvb+z9/U6T1KAB8Wi3WViXzPr4MsiaaRXg642 24 | FIdxAoGAZ7/735dkhJcyOfs+LKsLr68JSstoorXOYvdMu1+JGa9iLuhnHEcMVWC8 25 | IuihzPfloZtMbGYkZJn8l3BeGd8hmfFtgTgZGPoVRetft2LDFLnPxp2sEH5OFLsQ 26 | R+K/kAOul8eStWuMXOFA9pMzGkGEgIFJMJOyaJON3kedQI8deCM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /_example/basic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= 2 | github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 11 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 12 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 16 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 21 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v5 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /_example/mux/go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= 2 | github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 7 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 14 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 16 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 17 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 18 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 22 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 23 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v5 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v8 27 | with: 28 | args: --verbose 29 | 30 | # This step uses the Bearer GitHub Action to scan for sensitive data in the codebase. 31 | # The 'uses' keyword specifies the action to be used, in this case, 'bearer/bearer-action' at version 'v2'. 32 | # The 'with' keyword provides input parameters for the action: 33 | # - 'diff: true' indicates that the action should only scan the changes in the current pull request or commit. 34 | - name: Bearer 35 | uses: bearer/bearer-action@v2 36 | with: 37 | diff: true 38 | test: 39 | strategy: 40 | matrix: 41 | os: [ubuntu-latest, macos-latest] 42 | go: [1.22, 1.23, 1.24, 1.25] 43 | include: 44 | - os: ubuntu-latest 45 | go-build: ~/.cache/go-build 46 | - os: macos-latest 47 | go-build: ~/Library/Caches/go-build 48 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 49 | runs-on: ${{ matrix.os }} 50 | env: 51 | GO111MODULE: on 52 | GOPROXY: https://proxy.golang.org 53 | steps: 54 | - name: Set up Go ${{ matrix.go }} 55 | uses: actions/setup-go@v5 56 | with: 57 | go-version: ${{ matrix.go }} 58 | 59 | - name: Checkout Code 60 | uses: actions/checkout@v5 61 | with: 62 | ref: ${{ github.ref }} 63 | 64 | - uses: actions/cache@v4 65 | with: 66 | path: | 67 | ${{ matrix.go-build }} 68 | ~/go/pkg/mod 69 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 70 | restore-keys: | 71 | ${{ runner.os }}-go- 72 | - name: Run Tests 73 | run: | 74 | go test -v -covermode=atomic -coverprofile=coverage.out 75 | 76 | - name: Upload coverage to Codecov 77 | uses: codecov/codecov-action@v5 78 | with: 79 | flags: ${{ matrix.os }},go-${{ matrix.go }} 80 | -------------------------------------------------------------------------------- /_example/echo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= 2 | github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= 7 | github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= 8 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 9 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 10 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 11 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 12 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 13 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 18 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 19 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 20 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 24 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 25 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 27 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 28 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 29 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 30 | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 31 | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 32 | golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 33 | golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 34 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 36 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 37 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 38 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 39 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 43 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 44 | -------------------------------------------------------------------------------- /_example/gin/go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.2.0 h1:uqQ3wzTlF1ma+r4jRCQ4cygCjrGZyZEBMBCjT/t9zRw= 2 | github.com/appleboy/gofight/v2 v2.2.0/go.mod h1:USTV3UbA5kHBs4I91EsPi+6PIVZAx3KLorYjvtON91A= 3 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 4 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 5 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 6 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 7 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 8 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 9 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 10 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= 15 | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 16 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 17 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 18 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 19 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 20 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 21 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 22 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 23 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 24 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 25 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 26 | github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= 27 | github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 28 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 29 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 30 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 31 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 34 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 35 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 36 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 37 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 38 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 39 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 40 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 46 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 47 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 50 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 52 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 53 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 54 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 55 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 56 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 57 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 58 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 59 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 60 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 61 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 62 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 63 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 64 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 65 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 66 | golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= 67 | golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 68 | golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= 69 | golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= 70 | golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= 71 | golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= 72 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= 74 | golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 75 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 76 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 77 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 78 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 83 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | -------------------------------------------------------------------------------- /.roomodes: -------------------------------------------------------------------------------- 1 | customModes: 2 | - slug: go-code-tester 3 | name: 🧪 Go Code Tester 4 | description: Go testing and quality expert 5 | roleDefinition: >- 6 | You are Roo, a Golang testing and quality assurance expert specializing in Go testing ecosystem. Your expertise includes: 7 | - Writing Go unit tests using the standard testing package 8 | - Table-driven tests and subtests in Go 9 | - Go benchmarks and performance testing 10 | - Test coverage analysis with go test -cover 11 | - Mock generation and testing with gomock, testify/mock 12 | - Integration testing for Go web services and APIs 13 | - Testing Go HTTP handlers and middleware 14 | - Go race condition detection with go test -race 15 | - Testing Go concurrency and goroutines 16 | - Go fuzz testing (go test -fuzz) 17 | - Testcontainers for Go integration testing 18 | - Go testing best practices and conventions 19 | whenToUse: >- 20 | Use this mode when you need to write Go tests, improve test coverage, debug test failures, 21 | set up Go testing frameworks, create test automation for Go projects, or ensure code quality through 22 | comprehensive Go testing strategies. Perfect for TDD workflows in Go, bug hunting, and 23 | establishing robust testing pipelines for Go applications. 24 | groups: 25 | - read 26 | - edit 27 | - command 28 | - mcp 29 | customInstructions: >- 30 | Focus on creating comprehensive, maintainable Go tests that follow Go testing conventions. 31 | Always consider edge cases, error conditions, and boundary value testing. 32 | When writing Go tests, ensure they: 33 | - Follow Go naming conventions (TestXxx functions) 34 | - Use table-driven tests for multiple test cases 35 | - Leverage t.Run() for subtests when appropriate 36 | - Include proper error handling and assertions 37 | - Use testify/assert or require for cleaner assertions 38 | - Follow the AAA pattern (Arrange, Act, Assert) 39 | - Include benchmarks for performance-critical code 40 | - Use build tags for integration tests when needed 41 | 42 | Prefer Go standard library testing package with minimal dependencies. 43 | Use descriptive test function names that clearly explain the scenario being tested. 44 | Always run tests with go test -v -race -cover for comprehensive validation. 45 | 46 | - slug: go-code-reviewer 47 | name: 🔍 Go Code Reviewer 48 | description: Go code review and quality expert 49 | roleDefinition: >- 50 | You are Roo, a Go code review expert specializing in code quality, performance, and best practices. Your expertise includes: 51 | - Go code style and formatting analysis (gofmt, golint, golangci-lint) 52 | - Performance optimization and memory efficiency review 53 | - Concurrency and goroutine safety analysis 54 | - Error handling patterns and best practices 55 | - Code security vulnerability assessment 56 | - Go idioms and design patterns evaluation 57 | - API design and interface recommendations 58 | - Dependency management and module structure review 59 | - Code maintainability and readability assessment 60 | - Go standard library usage optimization 61 | - Race condition detection and prevention 62 | - Memory leak identification and prevention 63 | - Code complexity analysis and refactoring suggestions 64 | - Documentation and comment quality evaluation 65 | whenToUse: >- 66 | Use this mode when you need to review Go code for quality, performance, security, or maintainability issues. 67 | Perfect for code reviews, pull request analysis, refactoring guidance, performance optimization, 68 | security audits, and ensuring Go best practices compliance. Ideal for identifying potential bugs, 69 | improving code structure, and mentoring developers on Go coding standards. 70 | groups: 71 | - read 72 | - - edit 73 | - fileRegex: \.go$ 74 | description: Go source files only 75 | - command 76 | - mcp 77 | customInstructions: >- 78 | When reviewing Go code, focus on: 79 | 80 | CODE QUALITY: 81 | - Follow Go coding conventions and style guidelines 82 | - Check proper error handling patterns (avoid ignoring errors) 83 | - Ensure proper variable and function naming (camelCase, exported vs unexported) 84 | - Verify correct use of Go idioms and patterns 85 | - Assess code readability and maintainability 86 | 87 | PERFORMANCE: 88 | - Identify unnecessary memory allocations 89 | - Review string concatenation patterns (prefer strings.Builder for multiple concatenations) 90 | - Check for efficient slice and map usage 91 | - Analyze goroutine usage and potential leaks 92 | - Review context usage in long-running operations 93 | 94 | SECURITY: 95 | - Check for SQL injection vulnerabilities 96 | - Review input validation and sanitization 97 | - Identify potential race conditions 98 | - Check for proper secrets handling 99 | - Review error message information leakage 100 | 101 | CONCURRENCY: 102 | - Verify proper channel usage and closing 103 | - Check for goroutine leaks and proper cleanup 104 | - Review mutex usage and deadlock prevention 105 | - Analyze shared state access patterns 106 | - Ensure proper context propagation 107 | 108 | ARCHITECTURE: 109 | - Review package structure and dependencies 110 | - Check interface usage and abstraction levels 111 | - Assess separation of concerns 112 | - Review error types and custom error handling 113 | - Evaluate API design and backwards compatibility 114 | 115 | Always provide specific, actionable feedback with code examples when suggesting improvements. 116 | Prioritize critical issues (security, correctness) over style preferences. 117 | Use go vet, golangci-lint, and other static analysis tools when available. 118 | 119 | - slug: go-code-developer 120 | name: 🚀 Go Code Developer 121 | description: Go development and implementation expert 122 | roleDefinition: >- 123 | You are Roo, a Go development expert specializing in writing high-quality, idiomatic Go code. Your expertise includes: 124 | - Go syntax, language features, and standard library mastery 125 | - Writing clean, readable, and maintainable Go code 126 | - Go modules and dependency management 127 | - Implementing Go interfaces and struct design 128 | - Goroutines, channels, and concurrent programming patterns 129 | - Error handling best practices and custom error types 130 | - Go HTTP server development with net/http 131 | - JSON/XML marshaling and unmarshaling 132 | - Database integration with SQL drivers and ORMs 133 | - Command-line application development with flag package 134 | - Go build system, cross-compilation, and deployment 135 | - Context usage for cancellation and timeouts 136 | - Go generics and type parameters (Go 1.18+) 137 | - File I/O and system programming in Go 138 | - Go toolchain usage (go fmt, go vet, go mod, etc.) 139 | whenToUse: >- 140 | Use this mode when you need to write, implement, or refactor Go code. Perfect for creating new Go applications, 141 | implementing features, building APIs, developing CLI tools, or any Go programming task. Ideal for code implementation, 142 | algorithm development, data structure design, and building complete Go solutions from scratch. 143 | groups: 144 | - read 145 | - edit 146 | - command 147 | - mcp 148 | customInstructions: >- 149 | When writing Go code, always follow these principles: 150 | 151 | CODE STYLE: 152 | - Follow Go conventions: use gofmt for formatting, follow naming conventions 153 | - Use descriptive variable and function names (camelCase for unexported, PascalCase for exported) 154 | - Keep functions small and focused on single responsibility 155 | - Prefer composition over inheritance 156 | - Use interfaces to define behavior, not data 157 | - Write comments for exported functions and types using proper GoDoc format 158 | - Use error wrapping with fmt.Errorf and %w verb 159 | - Avoid global variables; prefer dependency injection 160 | - Use slices and maps idiomatically 161 | - Don't use if else patterns unnecessarily; prefer early returns 162 | - Use Constructor Pattern with functional options for functions with many parameters (>3-4) 163 | 164 | ERROR HANDLING: 165 | - Always handle errors explicitly, never ignore them 166 | - Use custom error types when appropriate 167 | - Wrap errors with context using fmt.Errorf with %w verb 168 | - Return errors as the last return value 169 | - Use errors.Is() and errors.As() for error checking 170 | 171 | CONCURRENCY: 172 | - Use goroutines for concurrent operations 173 | - Employ channels for communication between goroutines 174 | - Always close channels when done sending 175 | - Use context.Context for cancellation and timeouts 176 | - Avoid shared mutable state, prefer message passing 177 | 178 | PERFORMANCE: 179 | - Use string builders for multiple string concatenations 180 | - Preallocate slices and maps when size is known 181 | - Avoid unnecessary allocations in hot paths 182 | - Use sync.Pool for object reuse in high-frequency scenarios 183 | - Profile code when performance is critical 184 | 185 | STANDARD PRACTICES: 186 | - Use go modules for dependency management 187 | - Write self-documenting code with clear function signatures 188 | - Leverage the standard library before adding external dependencies 189 | - Use build tags for conditional compilation 190 | - Implement proper logging with structured logging when needed 191 | 192 | Always write idiomatic Go code that is simple, readable, and efficient. 193 | Use Go's built-in tools like go fmt, go vet, and go test to maintain code quality. 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gofight 2 | 3 | [![GoDoc](https://godoc.org/github.com/appleboy/gofight?status.svg)](https://godoc.org/github.com/appleboy/gofight) 4 | [![Run Tests](https://github.com/appleboy/gofight/actions/workflows/go.yml/badge.svg)](https://github.com/appleboy/gofight/actions/workflows/go.yml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/gofight)](https://goreportcard.com/report/github.com/appleboy/gofight) 6 | [![codebeat badge](https://codebeat.co/badges/4d8b58ae-67ec-469e-bde6-be3dd336b30d)](https://codebeat.co/projects/github-com-appleboy-gofight) 7 | [![codecov](https://codecov.io/gh/appleboy/gofight/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/gofight) 8 | [![Sourcegraph](https://sourcegraph.com/github.com/appleboy/gofight/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/gofight?badge) 9 | 10 | API Handler Testing for Golang Web framework. 11 | 12 | ## Support Framework 13 | 14 | * [x] [Http Handler](https://golang.org/pkg/net/http/) Golang package http provides HTTP client and server implementations. 15 | * [x] [Gin](https://github.com/gin-gonic/gin) 16 | * [x] [Echo](https://github.com/labstack/echo) support [v3.0.0](https://github.com/labstack/echo/releases/tag/v3.0.0) up 17 | * [x] [Mux](https://github.com/gorilla/mux) 18 | 19 | ## Install 20 | 21 | Download this package. 22 | 23 | ```bash 24 | go get github.com/appleboy/gofight/v2 25 | ``` 26 | 27 | To import this package, add the following line to your code: 28 | 29 | ```go 30 | import "github.com/appleboy/gofight/v2" 31 | ``` 32 | 33 | ## Usage 34 | 35 | The following is basic testing example. 36 | 37 | Main Program: 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "io" 44 | "net/http" 45 | ) 46 | 47 | func BasicHelloHandler(w http.ResponseWriter, r *http.Request) { 48 | io.WriteString(w, "Hello World") 49 | } 50 | 51 | func BasicEngine() http.Handler { 52 | mux := http.NewServeMux() 53 | mux.HandleFunc("/", BasicHelloHandler) 54 | 55 | return mux 56 | } 57 | ``` 58 | 59 | Testing: 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "net/http" 66 | "testing" 67 | 68 | "github.com/appleboy/gofight/v2" 69 | "github.com/stretchr/testify/assert" 70 | ) 71 | 72 | func TestBasicHelloWorld(t *testing.T) { 73 | r := gofight.New() 74 | 75 | r.GET("/"). 76 | // turn on the debug mode. 77 | SetDebug(true). 78 | Run(BasicEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 79 | 80 | assert.Equal(t, "Hello World", r.Body.String()) 81 | assert.Equal(t, http.StatusOK, r.Code) 82 | }) 83 | } 84 | ``` 85 | 86 | ### Set Header 87 | 88 | You can add custom header via `SetHeader` func. 89 | 90 | ```go 91 | func TestBasicHelloWorld(t *testing.T) { 92 | r := gofight.New() 93 | version := "0.0.1" 94 | 95 | r.GET("/"). 96 | // turn on the debug mode. 97 | SetDebug(true). 98 | SetHeader(gofight.H{ 99 | "X-Version": version, 100 | }). 101 | Run(BasicEngine(), func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 102 | 103 | assert.Equal(t, version, rq.Header.Get("X-Version")) 104 | assert.Equal(t, "Hello World", r.Body.String()) 105 | assert.Equal(t, http.StatusOK, r.Code) 106 | }) 107 | } 108 | ``` 109 | 110 | ### POST FORM Data 111 | 112 | Using `SetForm` to generate form data. 113 | 114 | ```go 115 | func TestPostFormData(t *testing.T) { 116 | r := gofight.New() 117 | 118 | r.POST("/form"). 119 | SetForm(gofight.H{ 120 | "a": "1", 121 | "b": "2", 122 | }). 123 | Run(BasicEngine(), func(r HTTPResponse, rq HTTPRequest) { 124 | data := []byte(r.Body.String()) 125 | 126 | a, _ := jsonparser.GetString(data, "a") 127 | b, _ := jsonparser.GetString(data, "b") 128 | 129 | assert.Equal(t, "1", a) 130 | assert.Equal(t, "2", b) 131 | assert.Equal(t, http.StatusOK, r.Code) 132 | }) 133 | } 134 | ``` 135 | 136 | ### POST JSON Data 137 | 138 | Using `SetJSON` to generate JSON data. 139 | 140 | ```go 141 | func TestPostJSONData(t *testing.T) { 142 | r := gofight.New() 143 | 144 | r.POST("/json"). 145 | SetJSON(gofight.D{ 146 | "a": 1, 147 | "b": 2, 148 | }). 149 | Run(BasicEngine, func(r HTTPResponse, rq HTTPRequest) { 150 | data := []byte(r.Body.String()) 151 | 152 | a, _ := jsonparser.GetInt(data, "a") 153 | b, _ := jsonparser.GetInt(data, "b") 154 | 155 | assert.Equal(t, 1, int(a)) 156 | assert.Equal(t, 2, int(b)) 157 | assert.Equal(t, http.StatusOK, r.Code) 158 | assert.Equal(t, "application/json; charset=utf-8", r.HeaderMap.Get("Content-Type")) 159 | }) 160 | } 161 | ``` 162 | 163 | ### POST RAW Data 164 | 165 | Using `SetBody` to generate raw data. 166 | 167 | ```go 168 | func TestPostRawData(t *testing.T) { 169 | r := gofight.New() 170 | 171 | r.POST("/raw"). 172 | SetBody("a=1&b=1"). 173 | Run(BasicEngine, func(r HTTPResponse, rq HTTPRequest) { 174 | data := []byte(r.Body.String()) 175 | 176 | a, _ := jsonparser.GetString(data, "a") 177 | b, _ := jsonparser.GetString(data, "b") 178 | 179 | assert.Equal(t, "1", a) 180 | assert.Equal(t, "2", b) 181 | assert.Equal(t, http.StatusOK, r.Code) 182 | }) 183 | } 184 | ``` 185 | 186 | ### Set Query String 187 | 188 | Using `SetQuery` to generate raw data. 189 | 190 | ```go 191 | func TestQueryString(t *testing.T) { 192 | r := gofight.New() 193 | 194 | r.GET("/hello"). 195 | SetQuery(gofight.H{ 196 | "a": "1", 197 | "b": "2", 198 | }). 199 | Run(BasicEngine, func(r HTTPResponse, rq HTTPRequest) { 200 | assert.Equal(t, http.StatusOK, r.Code) 201 | }) 202 | } 203 | ``` 204 | 205 | or append exist query parameter. 206 | 207 | ```go 208 | func TestQueryString(t *testing.T) { 209 | r := gofight.New() 210 | 211 | r.GET("/hello?foo=bar"). 212 | SetQuery(gofight.H{ 213 | "a": "1", 214 | "b": "2", 215 | }). 216 | Run(BasicEngine, func(r HTTPResponse, rq HTTPRequest) { 217 | assert.Equal(t, http.StatusOK, r.Code) 218 | }) 219 | } 220 | ``` 221 | 222 | ### Set Cookie String 223 | 224 | Using `SetCookie` to generate raw data. 225 | 226 | ```go 227 | func TestQueryString(t *testing.T) { 228 | r := gofight.New() 229 | 230 | r.GET("/hello"). 231 | SetCookie(gofight.H{ 232 | "foo": "bar", 233 | }). 234 | Run(BasicEngine, func(r HTTPResponse, rq HTTPRequest) { 235 | assert.Equal(t, http.StatusOK, r.Code) 236 | assert.Equal(t, "foo=bar", rq.Header.Get("cookie")) 237 | }) 238 | } 239 | ``` 240 | 241 | ### Set JSON Struct 242 | 243 | ```go 244 | type User struct { 245 | // Username user name 246 | Username string `json:"username"` 247 | // Password account password 248 | Password string `json:"password"` 249 | } 250 | 251 | func TestSetJSONInterface(t *testing.T) { 252 | r := New() 253 | 254 | r.POST("/user"). 255 | SetJSONInterface(User{ 256 | Username: "foo", 257 | Password: "bar", 258 | }). 259 | Run(framework.GinEngine(), func(r HTTPResponse, rq HTTPRequest) { 260 | data := []byte(r.Body.String()) 261 | 262 | username := gjson.GetBytes(data, "username") 263 | password := gjson.GetBytes(data, "password") 264 | 265 | assert.Equal(t, "foo", username.String()) 266 | assert.Equal(t, "bar", password.String()) 267 | assert.Equal(t, http.StatusOK, r.Code) 268 | assert.Equal(t, "application/json; charset=utf-8", r.HeaderMap.Get("Content-Type")) 269 | }) 270 | } 271 | ``` 272 | 273 | ### Upload multiple file with absolute path and parameter 274 | 275 | The following is route using gin 276 | 277 | ```go 278 | func gintFileUploadHandler(c *gin.Context) { 279 | ip := c.ClientIP() 280 | hello, err := c.FormFile("hello") 281 | if err != nil { 282 | c.JSON(http.StatusBadRequest, gin.H{ 283 | "error": err.Error(), 284 | }) 285 | return 286 | } 287 | 288 | helloFile, _ := hello.Open() 289 | helloBytes := make([]byte, 6) 290 | helloFile.Read(helloBytes) 291 | 292 | world, err := c.FormFile("world") 293 | if err != nil { 294 | c.JSON(http.StatusBadRequest, gin.H{ 295 | "error": err.Error(), 296 | }) 297 | return 298 | } 299 | 300 | worldFile, _ := world.Open() 301 | worldBytes := make([]byte, 6) 302 | worldFile.Read(worldBytes) 303 | 304 | foo := c.PostForm("foo") 305 | bar := c.PostForm("bar") 306 | c.JSON(http.StatusOK, gin.H{ 307 | "hello": hello.Filename, 308 | "world": world.Filename, 309 | "foo": foo, 310 | "bar": bar, 311 | "ip": ip, 312 | "helloSize": string(helloBytes), 313 | "worldSize": string(worldBytes), 314 | }) 315 | } 316 | ``` 317 | 318 | Write the testing: 319 | 320 | ```go 321 | func TestUploadFile(t *testing.T) { 322 | r := New() 323 | 324 | r.POST("/upload"). 325 | SetDebug(true). 326 | SetFileFromPath([]UploadFile{ 327 | { 328 | Path: "./testdata/hello.txt", 329 | Name: "hello", 330 | }, 331 | { 332 | Path: "./testdata/world.txt", 333 | Name: "world", 334 | }, 335 | }, H{ 336 | "foo": "bar", 337 | "bar": "foo", 338 | }). 339 | Run(framework.GinEngine(), func(r HTTPResponse, rq HTTPRequest) { 340 | data := []byte(r.Body.String()) 341 | 342 | hello := gjson.GetBytes(data, "hello") 343 | world := gjson.GetBytes(data, "world") 344 | foo := gjson.GetBytes(data, "foo") 345 | bar := gjson.GetBytes(data, "bar") 346 | ip := gjson.GetBytes(data, "ip") 347 | helloSize := gjson.GetBytes(data, "helloSize") 348 | worldSize := gjson.GetBytes(data, "worldSize") 349 | 350 | assert.Equal(t, "world\n", helloSize.String()) 351 | assert.Equal(t, "hello\n", worldSize.String()) 352 | assert.Equal(t, "hello.txt", hello.String()) 353 | assert.Equal(t, "world.txt", world.String()) 354 | assert.Equal(t, "bar", foo.String()) 355 | assert.Equal(t, "foo", bar.String()) 356 | assert.Equal(t, "", ip.String()) 357 | assert.Equal(t, http.StatusOK, r.Code) 358 | assert.Equal(t, "application/json; charset=utf-8", r.HeaderMap.Get("Content-Type")) 359 | }) 360 | } 361 | ``` 362 | 363 | ### Upload multiple file with content `[]byte` path and parameter 364 | 365 | ```go 366 | func TestUploadFileByContent(t *testing.T) { 367 | r := New() 368 | 369 | helloContent, err := ioutil.ReadFile("./testdata/hello.txt") 370 | if err != nil { 371 | log.Fatal(err) 372 | } 373 | 374 | worldContent, err := ioutil.ReadFile("./testdata/world.txt") 375 | if err != nil { 376 | log.Fatal(err) 377 | } 378 | 379 | r.POST("/upload"). 380 | SetDebug(true). 381 | SetFileFromPath([]UploadFile{ 382 | { 383 | Path: "hello.txt", 384 | Name: "hello", 385 | Content: helloContent, 386 | }, 387 | { 388 | Path: "world.txt", 389 | Name: "world", 390 | Content: worldContent, 391 | }, 392 | }, H{ 393 | "foo": "bar", 394 | "bar": "foo", 395 | }). 396 | Run(framework.GinEngine(), func(r HTTPResponse, rq HTTPRequest) { 397 | data := []byte(r.Body.String()) 398 | 399 | hello := gjson.GetBytes(data, "hello") 400 | world := gjson.GetBytes(data, "world") 401 | foo := gjson.GetBytes(data, "foo") 402 | bar := gjson.GetBytes(data, "bar") 403 | ip := gjson.GetBytes(data, "ip") 404 | helloSize := gjson.GetBytes(data, "helloSize") 405 | worldSize := gjson.GetBytes(data, "worldSize") 406 | 407 | assert.Equal(t, "world\n", helloSize.String()) 408 | assert.Equal(t, "hello\n", worldSize.String()) 409 | assert.Equal(t, "hello.txt", hello.String()) 410 | assert.Equal(t, "world.txt", world.String()) 411 | assert.Equal(t, "bar", foo.String()) 412 | assert.Equal(t, "foo", bar.String()) 413 | assert.Equal(t, "", ip.String()) 414 | assert.Equal(t, http.StatusOK, r.Code) 415 | assert.Equal(t, "application/json; charset=utf-8", r.HeaderMap.Get("Content-Type")) 416 | }) 417 | } 418 | ``` 419 | 420 | ## Example 421 | 422 | * Basic HTTP Router: [example](./_example/basic) 423 | * Gin Framework: [example](./_example/gin) 424 | * Echo Framework: [example](./_example/echo) 425 | * Mux Framework: [example](./_example/mux) 426 | 427 | ## License 428 | 429 | Copyright 2019 Bo-Yi Wu [@appleboy](https://twitter.com/appleboy). 430 | 431 | Licensed under the MIT License. 432 | -------------------------------------------------------------------------------- /gofight.go: -------------------------------------------------------------------------------- 1 | // Package gofight offers simple API http handler testing for Golang framework. 2 | // 3 | // Details about the gofight project are found in github page: 4 | // 5 | // https://github.com/appleboy/gofight 6 | // 7 | // Installation: 8 | // 9 | // $ go get -u github.com/appleboy/gofight 10 | // 11 | // Set Header: You can add custom header via SetHeader func. 12 | // 13 | // SetHeader(gofight.H{ 14 | // "X-Version": version, 15 | // }) 16 | // 17 | // Set Cookie: You can add custom cookie via SetCookie func. 18 | // 19 | // SetCookie(gofight.H{ 20 | // "foo": "bar", 21 | // }) 22 | // 23 | // Set query string: Using SetQuery to generate query string data. 24 | // 25 | // SetQuery(gofight.H{ 26 | // "a": "1", 27 | // "b": "2", 28 | // }) 29 | // 30 | // POST FORM Data: Using SetForm to generate form data. 31 | // 32 | // SetForm(gofight.H{ 33 | // "a": "1", 34 | // "b": "2", 35 | // }) 36 | // 37 | // POST JSON Data: Using SetJSON to generate json data. 38 | // 39 | // SetJSON(gofight.H{ 40 | // "a": "1", 41 | // "b": "2", 42 | // }) 43 | // 44 | // POST RAW Data: Using SetBody to generate raw data. 45 | // 46 | // SetBody("a=1&b=1") 47 | // 48 | // For more details, see the documentation and example. 49 | package gofight 50 | 51 | import ( 52 | "bytes" 53 | "context" 54 | "encoding/json" 55 | "fmt" 56 | "io" 57 | "log" 58 | "mime/multipart" 59 | "net/http" 60 | "net/http/httptest" 61 | "net/url" 62 | "os" 63 | "path/filepath" 64 | "strings" 65 | ) 66 | 67 | // Media types 68 | const ( 69 | Version = "1.0" 70 | UserAgent = "User-Agent" 71 | ContentType = "Content-Type" 72 | ApplicationJSON = "application/json" 73 | ApplicationForm = "application/x-www-form-urlencoded" 74 | ) 75 | 76 | // HTTPResponse wraps the httptest.ResponseRecorder to provide additional 77 | // functionality or to simplify the response handling in tests. 78 | type HTTPResponse struct { 79 | *httptest.ResponseRecorder 80 | } 81 | 82 | // HTTPRequest is a wrapper around the standard http.Request. 83 | // It embeds the http.Request struct, allowing you to use all the methods 84 | // and fields of http.Request while also providing the ability to extend 85 | // its functionality with additional methods or fields if needed. 86 | type HTTPRequest struct { 87 | *http.Request 88 | } 89 | 90 | // ResponseFunc is a type alias for a function that takes an HTTPResponse and an HTTPRequest as parameters. 91 | // It is used to define a callback function that can handle or process HTTP responses and requests. 92 | type ResponseFunc func(HTTPResponse, HTTPRequest) 93 | 94 | // H is HTTP Header Type 95 | type H map[string]string 96 | 97 | // D is HTTP Data Type 98 | type D map[string]any 99 | 100 | // RequestConfig provide user input request structure 101 | type RequestConfig struct { 102 | Method string 103 | Path string 104 | Body string 105 | Headers H 106 | Cookies H 107 | Debug bool 108 | ContentType string 109 | Context context.Context 110 | } 111 | 112 | // UploadFile for upload file struct 113 | type UploadFile struct { 114 | Path string 115 | Name string 116 | Content []byte 117 | } 118 | 119 | // New supply initial structure 120 | func New() *RequestConfig { 121 | return &RequestConfig{ 122 | Context: context.Background(), 123 | } 124 | } 125 | 126 | // SetDebug supply enable debug mode. 127 | func (rc *RequestConfig) SetDebug(enable bool) *RequestConfig { 128 | rc.Debug = enable 129 | 130 | return rc 131 | } 132 | 133 | // SetContext sets the context for the RequestConfig. 134 | // This allows the request to be aware of deadlines, cancellation signals, and other request-scoped values. 135 | // It returns the updated RequestConfig instance. 136 | // 137 | // Parameters: 138 | // 139 | // ctx - the context to be set for the RequestConfig 140 | // 141 | // Returns: 142 | // 143 | // *RequestConfig - the updated RequestConfig instance with the new context 144 | func (rc *RequestConfig) SetContext(ctx context.Context) *RequestConfig { 145 | rc.Context = ctx 146 | 147 | return rc 148 | } 149 | 150 | // setHTTPMethod is a helper function to set the HTTP method and path. 151 | func (rc *RequestConfig) setHTTPMethod(method, path string) *RequestConfig { 152 | rc.Method = method 153 | rc.Path = path 154 | return rc 155 | } 156 | 157 | // GET is request method. 158 | func (rc *RequestConfig) GET(path string) *RequestConfig { 159 | return rc.setHTTPMethod("GET", path) 160 | } 161 | 162 | // POST is request method. 163 | func (rc *RequestConfig) POST(path string) *RequestConfig { 164 | return rc.setHTTPMethod("POST", path) 165 | } 166 | 167 | // PUT is request method. 168 | func (rc *RequestConfig) PUT(path string) *RequestConfig { 169 | return rc.setHTTPMethod("PUT", path) 170 | } 171 | 172 | // DELETE is request method. 173 | func (rc *RequestConfig) DELETE(path string) *RequestConfig { 174 | return rc.setHTTPMethod("DELETE", path) 175 | } 176 | 177 | // PATCH is request method. 178 | func (rc *RequestConfig) PATCH(path string) *RequestConfig { 179 | return rc.setHTTPMethod("PATCH", path) 180 | } 181 | 182 | // HEAD is request method. 183 | func (rc *RequestConfig) HEAD(path string) *RequestConfig { 184 | return rc.setHTTPMethod("HEAD", path) 185 | } 186 | 187 | // OPTIONS is request method. 188 | func (rc *RequestConfig) OPTIONS(path string) *RequestConfig { 189 | return rc.setHTTPMethod("OPTIONS", path) 190 | } 191 | 192 | // SetHeader supply http header what you defined. 193 | func (rc *RequestConfig) SetHeader(headers H) *RequestConfig { 194 | if len(headers) > 0 { 195 | rc.Headers = headers 196 | } 197 | 198 | return rc 199 | } 200 | 201 | // SetJSON supply JSON body. 202 | func (rc *RequestConfig) SetJSON(body D) *RequestConfig { 203 | b, err := json.Marshal(body) 204 | if err != nil { 205 | // Log error but continue to maintain backward compatibility 206 | log.Printf("SetJSON: failed to marshal JSON: %v", err) 207 | return rc 208 | } 209 | rc.Body = string(b) 210 | return rc 211 | } 212 | 213 | // SetJSONInterface supply JSON body 214 | func (rc *RequestConfig) SetJSONInterface(body any) *RequestConfig { 215 | b, err := json.Marshal(body) 216 | if err != nil { 217 | // Log error but continue to maintain backward compatibility 218 | log.Printf("SetJSONInterface: failed to marshal JSON: %v", err) 219 | return rc 220 | } 221 | rc.Body = string(b) 222 | return rc 223 | } 224 | 225 | // SetForm sets the form data for the request configuration. 226 | // It takes a map of string keys and values, converts it to url.Values, 227 | // and encodes it as a URL-encoded form string, which is then assigned to the Body field. 228 | // 229 | // Parameters: 230 | // 231 | // body (H): A map containing the form data to be set. 232 | // 233 | // Returns: 234 | // 235 | // *RequestConfig: The updated request configuration. 236 | func (rc *RequestConfig) SetForm(body H) *RequestConfig { 237 | f := make(url.Values) 238 | 239 | for k, v := range body { 240 | f.Set(k, v) 241 | } 242 | 243 | rc.Body = f.Encode() 244 | 245 | return rc 246 | } 247 | 248 | // SetFileFromPath upload new file. 249 | func (rc *RequestConfig) SetFileFromPath(uploads []UploadFile, params ...H) *RequestConfig { 250 | body := new(bytes.Buffer) 251 | writer := multipart.NewWriter(body) 252 | defer func() { 253 | if err := writer.Close(); err != nil { 254 | log.Printf("SetFileFromPath: failed to close writer: %v", err) 255 | } 256 | }() 257 | 258 | for _, f := range uploads { 259 | if err := rc.processUploadFile(writer, f); err != nil { 260 | log.Printf("SetFileFromPath: failed to process file %s: %v", f.Path, err) 261 | continue // Continue processing other files instead of returning 262 | } 263 | } 264 | 265 | if len(params) > 0 { 266 | for key, val := range params[0] { 267 | if err := writer.WriteField(key, val); err != nil { 268 | log.Printf("SetFileFromPath: failed to write field %s: %v", key, err) 269 | } 270 | } 271 | } 272 | 273 | rc.ContentType = writer.FormDataContentType() 274 | rc.Body = body.String() 275 | 276 | return rc 277 | } 278 | 279 | // processUploadFile handles the processing of a single upload file. 280 | func (rc *RequestConfig) processUploadFile(writer *multipart.Writer, f UploadFile) error { 281 | reader := bytes.NewReader(f.Content) 282 | if reader.Size() == 0 { 283 | // Open file and ensure it's closed properly 284 | file, err := os.Open(f.Path) 285 | if err != nil { 286 | return fmt.Errorf("failed to open file %s: %w", f.Path, err) 287 | } 288 | defer file.Close() 289 | 290 | part, err := writer.CreateFormFile(f.Name, filepath.Base(f.Path)) 291 | if err != nil { 292 | return fmt.Errorf("failed to create form file for %s: %w", f.Name, err) 293 | } 294 | 295 | if _, err = io.Copy(part, file); err != nil { 296 | return fmt.Errorf("failed to copy file content: %w", err) 297 | } 298 | } else { 299 | part, err := writer.CreateFormFile(f.Name, filepath.Base(f.Path)) 300 | if err != nil { 301 | return fmt.Errorf("failed to create form file for %s: %w", f.Name, err) 302 | } 303 | 304 | if _, err = reader.WriteTo(part); err != nil { 305 | return fmt.Errorf("failed to write content: %w", err) 306 | } 307 | } 308 | return nil 309 | } 310 | 311 | // isJSONContent checks if the body content appears to be JSON. 312 | func (rc *RequestConfig) isJSONContent(body string) bool { 313 | trimmed := strings.TrimSpace(body) 314 | return (strings.HasPrefix(trimmed, "{") && strings.HasSuffix(trimmed, "}")) || 315 | (strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]")) 316 | } 317 | 318 | // isSecureContext determines if cookies should be set as secure. 319 | // For testing purposes, this returns false, but can be overridden based on context. 320 | func (rc *RequestConfig) isSecureContext() bool { 321 | // In a real application, you might check for HTTPS or environment variables 322 | // For testing framework, we default to false but this can be made configurable 323 | return false 324 | } 325 | 326 | // SetPath supply new request path to deal with path variable request 327 | // ex. /reqpath/:book/:apple , usage: r.POST("/reqpath/").SetPath("book1/apple2")... 328 | func (rc *RequestConfig) SetPath(str string) *RequestConfig { 329 | rc.Path += str 330 | return rc 331 | } 332 | 333 | // SetQueryD supply query string, support query using string array input. 334 | // ex. /reqpath/?Ids[]=E&Ids[]=M usage: 335 | // IDArray:=[]string{"E","M"} r.GET("reqpath").SetQueryD(gofight.D{`Ids[]`: IDArray}) 336 | func (rc *RequestConfig) SetQueryD(query D) *RequestConfig { 337 | if len(query) == 0 { 338 | return rc 339 | } 340 | 341 | var buf strings.Builder 342 | buf.WriteString("?") 343 | first := true 344 | 345 | for k, v := range query { 346 | switch v := v.(type) { 347 | case string: 348 | if !first { 349 | buf.WriteString("&") 350 | } 351 | buf.WriteString(url.QueryEscape(k)) 352 | buf.WriteString("=") 353 | buf.WriteString(url.QueryEscape(v)) 354 | first = false 355 | case []string: 356 | for _, info := range v { 357 | if !first { 358 | buf.WriteString("&") 359 | } 360 | buf.WriteString(url.QueryEscape(k)) 361 | buf.WriteString("=") 362 | buf.WriteString(url.QueryEscape(info)) 363 | first = false 364 | } 365 | } 366 | } 367 | 368 | // Avoid calling buf.String() twice 369 | queryStr := buf.String() 370 | rc.Path += queryStr 371 | return rc 372 | } 373 | 374 | // SetQuery sets the query parameters for the request configuration. 375 | // It takes a map of query parameters and their values, and appends them 376 | // to the existing path of the request configuration. If the path already 377 | // contains query parameters, the new parameters are appended with an '&'. 378 | // Otherwise, they are appended with a '?'. 379 | // 380 | // Parameters: 381 | // 382 | // query (H): A map containing the query parameters and their values. 383 | // 384 | // Returns: 385 | // 386 | // *RequestConfig: The updated request configuration with the query parameters set. 387 | func (rc *RequestConfig) SetQuery(query H) *RequestConfig { 388 | f := make(url.Values) 389 | 390 | for k, v := range query { 391 | f.Set(k, v) 392 | } 393 | 394 | if strings.Contains(rc.Path, "?") { 395 | rc.Path = rc.Path + "&" + f.Encode() 396 | } else { 397 | rc.Path = rc.Path + "?" + f.Encode() 398 | } 399 | 400 | return rc 401 | } 402 | 403 | // SetBody sets the body of the request if the provided body string is not empty. 404 | // It returns the updated RequestConfig instance. 405 | // 406 | // Parameters: 407 | // - body: A string representing the body content to be set. 408 | // 409 | // Returns: 410 | // - *RequestConfig: The updated RequestConfig instance. 411 | func (rc *RequestConfig) SetBody(body string) *RequestConfig { 412 | if len(body) > 0 { 413 | rc.Body = body 414 | } 415 | 416 | return rc 417 | } 418 | 419 | // SetCookie sets the cookies for the request configuration. 420 | // It takes a map of cookies and assigns it to the Cookies field of the RequestConfig 421 | // if the provided map is not empty. 422 | // 423 | // Parameters: 424 | // - cookies: A map of cookies to be set. 425 | // 426 | // Returns: 427 | // - A pointer to the updated RequestConfig. 428 | func (rc *RequestConfig) SetCookie(cookies H) *RequestConfig { 429 | if len(cookies) > 0 { 430 | rc.Cookies = cookies 431 | } 432 | 433 | return rc 434 | } 435 | 436 | func (rc *RequestConfig) initTest() (*http.Request, *httptest.ResponseRecorder) { 437 | qs := "" 438 | if strings.Contains(rc.Path, "?") { 439 | ss := strings.Split(rc.Path, "?") 440 | rc.Path = ss[0] 441 | qs = ss[1] 442 | } 443 | 444 | body := bytes.NewBufferString(rc.Body) 445 | 446 | req, err := http.NewRequestWithContext(rc.Context, rc.Method, rc.Path, body) 447 | if err != nil { 448 | log.Printf("initTest: failed to create HTTP request: %v", err) 449 | // Create minimal request to prevent panic 450 | req, _ = http.NewRequest("GET", "/", nil) 451 | } 452 | req.RequestURI = req.URL.RequestURI() 453 | 454 | if len(qs) > 0 { 455 | req.URL.RawQuery = qs 456 | } 457 | 458 | // Auto add user agent 459 | req.Header.Set(UserAgent, "Gofight-client/"+Version) 460 | 461 | if rc.Method == "POST" || rc.Method == "PUT" || rc.Method == "PATCH" { 462 | if rc.isJSONContent(rc.Body) { 463 | req.Header.Set(ContentType, ApplicationJSON) 464 | } else { 465 | req.Header.Set(ContentType, ApplicationForm) 466 | } 467 | } 468 | 469 | if rc.ContentType != "" { 470 | req.Header.Set(ContentType, rc.ContentType) 471 | } 472 | 473 | if len(rc.Headers) > 0 { 474 | for k, v := range rc.Headers { 475 | req.Header.Set(k, v) 476 | } 477 | } 478 | 479 | if len(rc.Cookies) > 0 { 480 | for k, v := range rc.Cookies { 481 | req.AddCookie(&http.Cookie{ 482 | Name: k, 483 | Value: v, 484 | HttpOnly: true, 485 | Secure: rc.isSecureContext(), // Dynamic secure setting 486 | SameSite: http.SameSiteStrictMode, 487 | }) 488 | } 489 | } 490 | 491 | if rc.Debug { 492 | log.Printf("Request QueryString: %s", qs) 493 | log.Printf("Request Method: %s", rc.Method) 494 | log.Printf("Request Path: %s", rc.Path) 495 | log.Printf("Request Body: %s", rc.Body) 496 | log.Printf("Request Headers: %+v", rc.Headers) 497 | log.Printf("Request Cookies: %+v", rc.Cookies) 498 | log.Printf("Request Header: %+v", req.Header) 499 | } 500 | 501 | w := httptest.NewRecorder() 502 | 503 | return req, w 504 | } 505 | 506 | // Run executes the HTTP request using the provided http.Handler and processes 507 | // the response using the given ResponseFunc. It initializes the test request 508 | // and response writer, serves the HTTP request, and then passes the HTTP 509 | // response and request to the response function. 510 | // 511 | // Parameters: 512 | // - r: The http.Handler that will handle the HTTP request. 513 | // - response: A function that processes the HTTP response and request. 514 | func (rc *RequestConfig) Run(r http.Handler, response ResponseFunc) { 515 | req, w := rc.initTest() 516 | r.ServeHTTP(w, req) 517 | response( 518 | HTTPResponse{ 519 | w, 520 | }, 521 | HTTPRequest{ 522 | req, 523 | }, 524 | ) 525 | } 526 | -------------------------------------------------------------------------------- /gofight_test.go: -------------------------------------------------------------------------------- 1 | package gofight 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | const version = "0.0.1" 19 | 20 | func basicHelloHandler(w http.ResponseWriter, r *http.Request) { 21 | // add header in response. 22 | w.Header().Set("Content-Type", "text/plain") 23 | w.Header().Set("X-Version", version) 24 | _, _ = io.WriteString(w, "Hello World") 25 | } 26 | 27 | func basicCookieHandler(w http.ResponseWriter, r *http.Request) { 28 | // get cookie from request. 29 | foo, err := r.Cookie("foo") 30 | if err != nil { 31 | http.Error(w, err.Error(), http.StatusBadRequest) 32 | return 33 | } 34 | _, _ = io.WriteString(w, foo.Value) 35 | } 36 | 37 | func basicQueryHandler(w http.ResponseWriter, r *http.Request) { 38 | // get query from request. 39 | foo := r.URL.Query().Get("foo") 40 | _, _ = io.WriteString(w, foo) 41 | } 42 | 43 | func basicFormHandler(w http.ResponseWriter, r *http.Request) { 44 | // get form from request. 45 | err := r.ParseForm() 46 | if err != nil { 47 | http.Error(w, err.Error(), http.StatusBadRequest) 48 | return 49 | } 50 | foo := r.Form.Get("foo") 51 | _, _ = io.WriteString(w, foo) 52 | } 53 | 54 | func basicEngine() http.Handler { 55 | mux := http.NewServeMux() 56 | mux.HandleFunc("/", basicHelloHandler) 57 | mux.HandleFunc("/cookie", basicCookieHandler) 58 | mux.HandleFunc("/query", basicQueryHandler) 59 | mux.HandleFunc("/form", basicFormHandler) 60 | 61 | return mux 62 | } 63 | 64 | func TestBasicHelloWorld(t *testing.T) { 65 | r := New() 66 | version := "0.0.1" 67 | 68 | r.GET("/"). 69 | // turn on the debug mode. 70 | SetDebug(true). 71 | SetHeader(H{ 72 | "X-Version": version, 73 | }). 74 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 75 | assert.Equal(t, version, rq.Header.Get("X-Version")) 76 | assert.Equal(t, version, r.Header().Get("X-Version")) 77 | assert.Equal(t, "Hello World", r.Body.String()) 78 | assert.Equal(t, http.StatusOK, r.Code) 79 | }) 80 | } 81 | 82 | func basicHTTPHelloHandler() { 83 | http.HandleFunc("/hello", basicHelloHandler) 84 | } 85 | 86 | func TestBasicHttpHelloWorld(t *testing.T) { 87 | basicHTTPHelloHandler() 88 | 89 | r := New() 90 | 91 | r.GET("/hello"). 92 | // trun on the debug mode. 93 | SetDebug(true). 94 | Run(http.DefaultServeMux, func(r HTTPResponse, rq HTTPRequest) { 95 | assert.Equal(t, "Hello World", r.Body.String()) 96 | assert.Equal(t, http.StatusOK, r.Code) 97 | }) 98 | } 99 | 100 | func TestSetContext(t *testing.T) { 101 | r := New() 102 | type contextKey string 103 | const key contextKey = "key" 104 | ctx := context.WithValue(context.Background(), key, "value") 105 | 106 | r.GET("/"). 107 | SetContext(ctx). 108 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 109 | assert.Equal(t, "value", rq.Context().Value(contextKey("key"))) 110 | assert.Equal(t, "Hello World", r.Body.String()) 111 | assert.Equal(t, http.StatusOK, r.Code) 112 | }) 113 | } 114 | 115 | func TestSetContextWithTimeout(t *testing.T) { 116 | r := New() 117 | ctx, cancel := context.WithTimeout(context.Background(), 0) 118 | defer cancel() 119 | 120 | r.GET("/"). 121 | SetContext(ctx). 122 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 123 | select { 124 | case <-rq.Context().Done(): 125 | assert.Equal(t, context.DeadlineExceeded, rq.Context().Err()) 126 | default: 127 | t.Error("expected context to be done") 128 | } 129 | }) 130 | } 131 | 132 | func TestGetHeaderFromResponse(t *testing.T) { 133 | version := "0.0.1" 134 | r := New() 135 | r.GET("/"). 136 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 137 | assert.Equal(t, version, r.Header().Get("X-Version")) 138 | assert.Equal(t, "Hello World", r.Body.String()) 139 | }) 140 | } 141 | 142 | func TestSetBody(t *testing.T) { 143 | r := New() 144 | body := "a=1&b=2" 145 | 146 | r.POST("/"). 147 | SetBody(body). 148 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 149 | // Read the content of the io.Reader 150 | bodyBytes, err := io.ReadAll(rq.Body) 151 | if err != nil { 152 | t.Fatalf("Failed to read body: %v", err) 153 | } 154 | 155 | // Convert the byte slice to a string 156 | bodyString := string(bodyBytes) 157 | assert.Equal(t, body, bodyString) 158 | assert.Equal(t, "Hello World", r.Body.String()) 159 | assert.Equal(t, http.StatusOK, r.Code) 160 | }) 161 | } 162 | 163 | func TestSetCookie(t *testing.T) { 164 | r := New() 165 | cookies := H{ 166 | "foo": "bar", 167 | "baz": "qux", 168 | } 169 | 170 | r.GET("/cookie"). 171 | SetCookie(cookies). 172 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 173 | cookieFoo, err := rq.Cookie("foo") 174 | assert.NoError(t, err) 175 | assert.Equal(t, "bar", cookieFoo.Value) 176 | 177 | cookieBaz, err := rq.Cookie("baz") 178 | assert.NoError(t, err) 179 | assert.Equal(t, "qux", cookieBaz.Value) 180 | 181 | assert.Equal(t, "bar", r.Body.String()) 182 | assert.Equal(t, http.StatusOK, r.Code) 183 | }) 184 | } 185 | 186 | func TestSetQuery(t *testing.T) { 187 | r := New() 188 | query := H{ 189 | "foo": "bar", 190 | } 191 | 192 | r.GET("/query"). 193 | SetQuery(query). 194 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 195 | assert.Equal(t, "bar", rq.URL.Query().Get("foo")) 196 | assert.Equal(t, "bar", r.Body.String()) 197 | assert.Equal(t, http.StatusOK, r.Code) 198 | }) 199 | } 200 | 201 | func TestSetQueryWithExistingQuery(t *testing.T) { 202 | r := New() 203 | query := H{ 204 | "c": "3", 205 | "d": "4", 206 | } 207 | 208 | r.GET("/query?a=1&b=2&foo=testing"). 209 | SetQuery(query). 210 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 211 | assert.Equal(t, "1", rq.URL.Query().Get("a")) 212 | assert.Equal(t, "2", rq.URL.Query().Get("b")) 213 | assert.Equal(t, "3", rq.URL.Query().Get("c")) 214 | assert.Equal(t, "4", rq.URL.Query().Get("d")) 215 | assert.Equal(t, "testing", rq.URL.Query().Get("foo")) 216 | assert.Equal(t, "testing", r.Body.String()) 217 | assert.Equal(t, http.StatusOK, r.Code) 218 | }) 219 | } 220 | 221 | func TestSetForm(t *testing.T) { 222 | r := New() 223 | formData := H{ 224 | "a": "1", 225 | "b": "2", 226 | "foo": "bar", 227 | } 228 | 229 | r.POST("/form"). 230 | SetForm(formData). 231 | Run(basicEngine(), func(r HTTPResponse, rq HTTPRequest) { 232 | assert.Equal(t, "bar", r.Body.String()) 233 | assert.Equal(t, http.StatusOK, r.Code) 234 | }) 235 | } 236 | 237 | // Additional test handlers for comprehensive testing 238 | func jsonHandler(w http.ResponseWriter, r *http.Request) { 239 | w.Header().Set("Content-Type", "application/json") 240 | body, err := io.ReadAll(r.Body) 241 | if err != nil { 242 | http.Error(w, "Failed to read body", http.StatusBadRequest) 243 | return 244 | } 245 | 246 | var data interface{} 247 | if err := json.Unmarshal(body, &data); err != nil { 248 | http.Error(w, "Invalid JSON", http.StatusBadRequest) 249 | return 250 | } 251 | 252 | response := map[string]interface{}{ 253 | "received": data, 254 | "method": r.Method, 255 | } 256 | json.NewEncoder(w).Encode(response) 257 | } 258 | 259 | func fileUploadHandler(w http.ResponseWriter, r *http.Request) { 260 | if err := r.ParseMultipartForm(10 << 20); err != nil { 261 | http.Error(w, err.Error(), http.StatusBadRequest) 262 | return 263 | } 264 | 265 | // Check for any uploaded files in any field 266 | if r.MultipartForm == nil || len(r.MultipartForm.File) == 0 { 267 | http.Error(w, "No files uploaded", http.StatusBadRequest) 268 | return 269 | } 270 | 271 | // Get the first file from any field 272 | for fieldName, files := range r.MultipartForm.File { 273 | if len(files) > 0 { 274 | file := files[0] 275 | io.WriteString(w, fmt.Sprintf("Uploaded file: %s, Size: %d, Field: %s", 276 | file.Filename, file.Size, fieldName)) 277 | return 278 | } 279 | } 280 | 281 | http.Error(w, "No files found", http.StatusBadRequest) 282 | } 283 | 284 | func methodEchoHandler(w http.ResponseWriter, r *http.Request) { 285 | io.WriteString(w, fmt.Sprintf("Method: %s, Path: %s", r.Method, r.URL.Path)) 286 | } 287 | 288 | func pathVariableHandler(w http.ResponseWriter, r *http.Request) { 289 | path := strings.TrimPrefix(r.URL.Path, "/books/") 290 | io.WriteString(w, fmt.Sprintf("Book path: %s", path)) 291 | } 292 | 293 | func extendedEngine() http.Handler { 294 | mux := http.NewServeMux() 295 | mux.HandleFunc("/", basicHelloHandler) 296 | mux.HandleFunc("/cookie", basicCookieHandler) 297 | mux.HandleFunc("/query", basicQueryHandler) 298 | mux.HandleFunc("/form", basicFormHandler) 299 | mux.HandleFunc("/json", jsonHandler) 300 | mux.HandleFunc("/upload", fileUploadHandler) 301 | mux.HandleFunc("/books/", pathVariableHandler) 302 | mux.HandleFunc("/method", methodEchoHandler) 303 | return mux 304 | } 305 | 306 | // TestHTTPMethods tests all HTTP methods using table-driven tests 307 | func TestHTTPMethods(t *testing.T) { 308 | tests := []struct { 309 | name string 310 | method string 311 | setupRequest func(r *RequestConfig) *RequestConfig 312 | expectedBody string 313 | checkBody bool 314 | }{ 315 | { 316 | name: "GET request", 317 | method: "GET", 318 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.GET("/method") }, 319 | expectedBody: "Method: GET, Path: /method", 320 | checkBody: true, 321 | }, 322 | { 323 | name: "POST request", 324 | method: "POST", 325 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.POST("/method") }, 326 | expectedBody: "Method: POST, Path: /method", 327 | checkBody: true, 328 | }, 329 | { 330 | name: "PUT request", 331 | method: "PUT", 332 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.PUT("/method") }, 333 | expectedBody: "Method: PUT, Path: /method", 334 | checkBody: true, 335 | }, 336 | { 337 | name: "DELETE request", 338 | method: "DELETE", 339 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.DELETE("/method") }, 340 | expectedBody: "Method: DELETE, Path: /method", 341 | checkBody: true, 342 | }, 343 | } 344 | 345 | for _, tt := range tests { 346 | t.Run(tt.name, func(t *testing.T) { 347 | r := New() 348 | tt.setupRequest(r).Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 349 | assert.Equal(t, tt.method, req.Method) 350 | if tt.checkBody { 351 | assert.Equal(t, tt.expectedBody, resp.Body.String()) 352 | } 353 | assert.Equal(t, http.StatusOK, resp.Code) 354 | }) 355 | }) 356 | } 357 | } 358 | 359 | // TestSetJSON tests JSON body setting functionality 360 | func TestSetJSON(t *testing.T) { 361 | tests := []struct { 362 | name string 363 | data D 364 | }{ 365 | { 366 | name: "simple object", 367 | data: D{"name": "test", "value": 123}, 368 | }, 369 | { 370 | name: "nested object", 371 | data: D{"user": D{"name": "john", "age": 30}}, 372 | }, 373 | { 374 | name: "empty object", 375 | data: D{}, 376 | }, 377 | } 378 | 379 | for _, tt := range tests { 380 | t.Run(tt.name, func(t *testing.T) { 381 | r := New() 382 | r.POST("/json"). 383 | SetJSON(tt.data). 384 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 385 | assert.Equal(t, http.StatusOK, resp.Code) 386 | assert.Contains(t, req.Header.Get("Content-Type"), "application/json") 387 | 388 | // Parse response to verify data was processed correctly 389 | var response map[string]interface{} 390 | err := json.Unmarshal(resp.Body.Bytes(), &response) 391 | assert.NoError(t, err) 392 | assert.Equal(t, "POST", response["method"]) 393 | assert.NotNil(t, response["received"]) 394 | }) 395 | }) 396 | } 397 | } 398 | 399 | // TestSetJSONInterface tests JSON interface functionality 400 | func TestSetJSONInterface(t *testing.T) { 401 | type TestStruct struct { 402 | Name string `json:"name"` 403 | Value int `json:"value"` 404 | } 405 | 406 | tests := []struct { 407 | name string 408 | data interface{} 409 | }{ 410 | { 411 | name: "struct", 412 | data: TestStruct{Name: "test", Value: 42}, 413 | }, 414 | { 415 | name: "map", 416 | data: map[string]interface{}{"key": "value", "number": 123}, 417 | }, 418 | { 419 | name: "slice", 420 | data: []string{"item1", "item2", "item3"}, 421 | }, 422 | } 423 | 424 | for _, tt := range tests { 425 | t.Run(tt.name, func(t *testing.T) { 426 | r := New() 427 | r.POST("/json"). 428 | SetJSONInterface(tt.data). 429 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 430 | assert.Equal(t, http.StatusOK, resp.Code) 431 | assert.Contains(t, req.Header.Get("Content-Type"), "application/json") 432 | }) 433 | }) 434 | } 435 | } 436 | 437 | // TestSetQueryD tests query parameter arrays functionality 438 | func TestSetQueryD(t *testing.T) { 439 | tests := []struct { 440 | name string 441 | query D 442 | expected map[string][]string 443 | }{ 444 | { 445 | name: "string query", 446 | query: D{"name": "john", "age": "30"}, 447 | expected: map[string][]string{ 448 | "name": {"john"}, 449 | "age": {"30"}, 450 | }, 451 | }, 452 | { 453 | name: "array query", 454 | query: D{"ids": []string{"1", "2", "3"}}, 455 | expected: map[string][]string{ 456 | "ids": {"1", "2", "3"}, 457 | }, 458 | }, 459 | { 460 | name: "mixed query", 461 | query: D{"name": "john", "ids": []string{"1", "2"}}, 462 | expected: map[string][]string{ 463 | "name": {"john"}, 464 | "ids": {"1", "2"}, 465 | }, 466 | }, 467 | } 468 | 469 | for _, tt := range tests { 470 | t.Run(tt.name, func(t *testing.T) { 471 | r := New() 472 | r.GET("/query"). 473 | SetQueryD(tt.query). 474 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 475 | for key, expectedValues := range tt.expected { 476 | actualValues := req.URL.Query()[key] 477 | assert.Equal(t, expectedValues, actualValues) 478 | } 479 | assert.Equal(t, http.StatusOK, resp.Code) 480 | }) 481 | }) 482 | } 483 | } 484 | 485 | // TestSetPath tests path variable functionality 486 | func TestSetPath(t *testing.T) { 487 | r := New() 488 | r.GET("/books/"). 489 | SetPath("golang/guide"). 490 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 491 | assert.Equal(t, "/books/golang/guide", req.URL.Path) 492 | assert.Equal(t, "Book path: golang/guide", resp.Body.String()) 493 | assert.Equal(t, http.StatusOK, resp.Code) 494 | }) 495 | } 496 | 497 | // TestSetBodyEmpty tests empty body handling 498 | func TestSetBodyEmpty(t *testing.T) { 499 | r := New() 500 | r.POST("/method"). 501 | SetBody(""). 502 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 503 | body, _ := io.ReadAll(req.Body) 504 | assert.Equal(t, "", string(body)) 505 | assert.Equal(t, http.StatusOK, resp.Code) 506 | }) 507 | } 508 | 509 | // TestContentTypeDetection tests automatic content type detection 510 | func TestContentTypeDetection(t *testing.T) { 511 | tests := []struct { 512 | name string 513 | body string 514 | expectedType string 515 | }{ 516 | { 517 | name: "JSON body", 518 | body: `{"name": "test"}`, 519 | expectedType: "application/json", 520 | }, 521 | { 522 | name: "form body", 523 | body: "name=test&value=123", 524 | expectedType: "application/x-www-form-urlencoded", 525 | }, 526 | } 527 | 528 | for _, tt := range tests { 529 | t.Run(tt.name, func(t *testing.T) { 530 | r := New() 531 | r.POST("/method"). 532 | SetBody(tt.body). 533 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 534 | assert.Contains(t, req.Header.Get("Content-Type"), tt.expectedType) 535 | }) 536 | }) 537 | } 538 | } 539 | 540 | // TestCookieSecuritySettings tests cookie security configuration 541 | func TestCookieSecuritySettings(t *testing.T) { 542 | r := New() 543 | r.GET("/cookie"). 544 | SetCookie(H{"foo": "bar"}). // Use "foo" to match basicCookieHandler 545 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 546 | // Test that the cookie was set and handler works 547 | assert.Equal(t, "bar", resp.Body.String()) // Handler returns cookie value 548 | assert.Equal(t, http.StatusOK, resp.Code) 549 | 550 | // Test that cookies were added to the request 551 | cookies := req.Cookies() 552 | assert.NotEmpty(t, cookies) 553 | 554 | // Find the foo cookie and verify its properties 555 | var fooCookie *http.Cookie 556 | for _, cookie := range cookies { 557 | if cookie.Name == "foo" { 558 | fooCookie = cookie 559 | break 560 | } 561 | } 562 | 563 | if fooCookie != nil { 564 | assert.Equal(t, "foo", fooCookie.Name) 565 | assert.Equal(t, "bar", fooCookie.Value) 566 | // Note: HttpOnly and SameSite might not be set in test environment 567 | } 568 | }) 569 | } 570 | 571 | // TestErrorHandling tests error scenarios 572 | func TestErrorHandling(t *testing.T) { 573 | // Test with invalid JSON 574 | r := New() 575 | invalidJSON := `{"invalid": json}` 576 | 577 | r.POST("/json"). 578 | SetBody(invalidJSON). 579 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 580 | // Should handle gracefully 581 | assert.Equal(t, http.StatusBadRequest, resp.Code) 582 | }) 583 | } 584 | 585 | // TestSetFileFromPath tests file upload functionality 586 | func TestSetFileFromPath(t *testing.T) { 587 | // Create a temporary test file 588 | tmpFile := filepath.Join(os.TempDir(), "test.txt") 589 | err := os.WriteFile(tmpFile, []byte("Hello World"), 0o644) 590 | require.NoError(t, err) 591 | defer os.Remove(tmpFile) 592 | 593 | uploadFile := UploadFile{ 594 | Path: tmpFile, 595 | Name: "file", 596 | } 597 | 598 | r := New() 599 | r.POST("/upload"). 600 | SetFileFromPath([]UploadFile{uploadFile}). 601 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 602 | // The request should have multipart content type 603 | contentType := req.Header.Get("Content-Type") 604 | assert.Contains(t, contentType, "multipart/form-data") 605 | 606 | // The body should contain the file upload data 607 | assert.NotEmpty(t, req.Body) 608 | }) 609 | } 610 | 611 | // TestSetFileFromContent tests file upload with content 612 | func TestSetFileFromContent(t *testing.T) { 613 | uploadFile := UploadFile{ 614 | Path: "test.txt", 615 | Name: "file", 616 | Content: []byte("Test file content"), 617 | } 618 | 619 | r := New() 620 | r.POST("/upload"). 621 | SetFileFromPath([]UploadFile{uploadFile}). 622 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 623 | // The request should have multipart content type 624 | contentType := req.Header.Get("Content-Type") 625 | assert.Contains(t, contentType, "multipart/form-data") 626 | 627 | // The body should contain the file upload data 628 | assert.NotEmpty(t, req.Body) 629 | }) 630 | } 631 | 632 | // TestDebugMode tests debug functionality 633 | func TestDebugMode(t *testing.T) { 634 | r := New() 635 | r.GET("/"). 636 | SetDebug(true). 637 | SetHeader(H{"X-Test": "debug"}). 638 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 639 | assert.Equal(t, "debug", req.Header.Get("X-Test")) 640 | assert.Equal(t, http.StatusOK, resp.Code) 641 | }) 642 | } 643 | 644 | // Benchmark tests for performance 645 | func BenchmarkNewRequest(b *testing.B) { 646 | for i := 0; i < b.N; i++ { 647 | r := New() 648 | r.GET("/") 649 | } 650 | } 651 | 652 | func BenchmarkSimpleGETRequest(b *testing.B) { 653 | engine := extendedEngine() 654 | 655 | b.ResetTimer() 656 | for i := 0; i < b.N; i++ { 657 | r := New() 658 | r.GET("/").Run(engine, func(resp HTTPResponse, req HTTPRequest) { 659 | // Simple assertion 660 | _ = resp.Code == http.StatusOK 661 | }) 662 | } 663 | } 664 | 665 | func BenchmarkJSONRequest(b *testing.B) { 666 | engine := extendedEngine() 667 | data := D{"name": "benchmark", "value": 123} 668 | 669 | b.ResetTimer() 670 | for i := 0; i < b.N; i++ { 671 | r := New() 672 | r.POST("/json"). 673 | SetJSON(data). 674 | Run(engine, func(resp HTTPResponse, req HTTPRequest) { 675 | _ = resp.Code == http.StatusOK 676 | }) 677 | } 678 | } 679 | 680 | func BenchmarkFormRequest(b *testing.B) { 681 | engine := extendedEngine() 682 | formData := H{"name": "benchmark", "value": "123"} 683 | 684 | b.ResetTimer() 685 | for i := 0; i < b.N; i++ { 686 | r := New() 687 | r.POST("/form"). 688 | SetForm(formData). 689 | Run(engine, func(resp HTTPResponse, req HTTPRequest) { 690 | _ = resp.Code == http.StatusOK 691 | }) 692 | } 693 | } 694 | 695 | // TestMoreHTTPMethods tests PATCH, HEAD, and OPTIONS methods 696 | func TestMoreHTTPMethods(t *testing.T) { 697 | tests := []struct { 698 | name string 699 | method string 700 | setupRequest func(r *RequestConfig) *RequestConfig 701 | expectedBody string 702 | checkBody bool 703 | }{ 704 | { 705 | name: "PATCH request", 706 | method: "PATCH", 707 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.PATCH("/method") }, 708 | expectedBody: "Method: PATCH, Path: /method", 709 | checkBody: true, 710 | }, 711 | { 712 | name: "HEAD request", 713 | method: "HEAD", 714 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.HEAD("/method") }, 715 | expectedBody: "", 716 | checkBody: false, // HEAD requests don't return body 717 | }, 718 | { 719 | name: "OPTIONS request", 720 | method: "OPTIONS", 721 | setupRequest: func(r *RequestConfig) *RequestConfig { return r.OPTIONS("/method") }, 722 | expectedBody: "Method: OPTIONS, Path: /method", 723 | checkBody: true, 724 | }, 725 | } 726 | 727 | for _, tt := range tests { 728 | t.Run(tt.name, func(t *testing.T) { 729 | r := New() 730 | tt.setupRequest(r).Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 731 | assert.Equal(t, tt.method, req.Method) 732 | if tt.checkBody { 733 | assert.Equal(t, tt.expectedBody, resp.Body.String()) 734 | } 735 | assert.Equal(t, http.StatusOK, resp.Code) 736 | }) 737 | }) 738 | } 739 | } 740 | 741 | // TestEdgeCases tests various edge cases for better coverage 742 | func TestEdgeCases(t *testing.T) { 743 | t.Run("empty headers", func(t *testing.T) { 744 | r := New() 745 | r.GET("/"). 746 | SetHeader(H{}). 747 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 748 | assert.Equal(t, http.StatusOK, resp.Code) 749 | }) 750 | }) 751 | 752 | t.Run("empty cookies", func(t *testing.T) { 753 | r := New() 754 | r.GET("/"). 755 | SetCookie(H{}). 756 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 757 | assert.Equal(t, http.StatusOK, resp.Code) 758 | }) 759 | }) 760 | 761 | t.Run("empty query", func(t *testing.T) { 762 | r := New() 763 | r.GET("/"). 764 | SetQuery(H{}). 765 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 766 | assert.Equal(t, http.StatusOK, resp.Code) 767 | }) 768 | }) 769 | 770 | t.Run("empty form", func(t *testing.T) { 771 | r := New() 772 | r.POST("/"). 773 | SetForm(H{}). 774 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 775 | assert.Equal(t, http.StatusOK, resp.Code) 776 | }) 777 | }) 778 | 779 | t.Run("empty QueryD", func(t *testing.T) { 780 | r := New() 781 | r.GET("/"). 782 | SetQueryD(D{}). 783 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 784 | assert.Equal(t, http.StatusOK, resp.Code) 785 | }) 786 | }) 787 | 788 | t.Run("nil context", func(t *testing.T) { 789 | r := New() 790 | r.SetContext(context.Background()). 791 | GET("/"). 792 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 793 | assert.Equal(t, http.StatusOK, resp.Code) 794 | assert.NotNil(t, req.Context()) 795 | }) 796 | }) 797 | } 798 | 799 | // TestJSONMarshallErrors tests JSON marshalling error handling 800 | func TestJSONMarshallErrors(t *testing.T) { 801 | r := New() 802 | 803 | // Test with channel type that can't be marshalled to JSON 804 | invalidData := make(chan int) 805 | 806 | r.POST("/json"). 807 | SetJSONInterface(invalidData). 808 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 809 | // Should continue execution even with marshal error 810 | body, _ := io.ReadAll(req.Body) 811 | assert.Equal(t, "", string(body)) // Body should be empty due to marshal error 812 | }) 813 | } 814 | 815 | // TestQueryWithSpecialCharacters tests query parameters with special characters 816 | func TestQueryWithSpecialCharacters(t *testing.T) { 817 | r := New() 818 | 819 | specialQuery := H{ 820 | "name": "john doe", 821 | "email": "john@example.com", 822 | "tags": "tag1,tag2,tag3", 823 | } 824 | 825 | r.GET("/query"). 826 | SetQuery(specialQuery). 827 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 828 | assert.Equal(t, "john doe", req.URL.Query().Get("name")) 829 | assert.Equal(t, "john@example.com", req.URL.Query().Get("email")) 830 | assert.Equal(t, "tag1,tag2,tag3", req.URL.Query().Get("tags")) 831 | }) 832 | } 833 | 834 | // TestSecureContextVariations tests different secure context scenarios 835 | func TestSecureContextVariations(t *testing.T) { 836 | r := New() 837 | 838 | // Test isSecureContext method indirectly 839 | r.GET("/"). 840 | SetCookie(H{"secure_test": "value"}). 841 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 842 | // Cookie should be set regardless of secure context in test 843 | cookies := req.Cookies() 844 | assert.NotEmpty(t, cookies) 845 | }) 846 | } 847 | 848 | // TestContentTypeOverride tests content type override functionality 849 | func TestContentTypeOverride(t *testing.T) { 850 | customContentType := "application/xml" 851 | 852 | config := &RequestConfig{ 853 | Method: "POST", 854 | Path: "/method", 855 | Body: "test", 856 | ContentType: customContentType, 857 | Context: context.Background(), 858 | } 859 | 860 | config.Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 861 | assert.Equal(t, customContentType, req.Header.Get("Content-Type")) 862 | }) 863 | } 864 | 865 | // TestUserAgentHeader tests user agent header setting 866 | func TestUserAgentHeader(t *testing.T) { 867 | r := New() 868 | 869 | r.GET("/"). 870 | Run(extendedEngine(), func(resp HTTPResponse, req HTTPRequest) { 871 | userAgent := req.Header.Get("User-Agent") 872 | assert.Contains(t, userAgent, "Gofight-client/") 873 | assert.Contains(t, userAgent, "1.0") 874 | }) 875 | } 876 | --------------------------------------------------------------------------------