package lazyhandler import ( "fmt" "io" "log" "net/http" "net/http/httptest" "strconv" "strings" "testing" "git.jeffthecoder.xyz/public/lazyhandler/magic" "github.com/gorilla/websocket" ) type StatusPathValues struct { Status int `pathvalue:"status"` } type HelloWorldRequest struct { Name string `json:"name"` } type HelloWorldResponse struct { Greeting string `json:"greeting"` } type HelloWorldParams struct { Name *string `pathvalue:"name,match=" query:"name"` } type MultipleParams struct { Numbers []int Strings []string OptionalNumber *int OptionalString *string } func TestNotAFunc(t *testing.T) { v, err := MagicHandler(1) if err == nil { t.Fatalf("expect not ok, get %v", v) } } func TestEmptyFunc(t *testing.T) { MagicHandler(func() { }) } type SomeType int func (obj SomeType) Func() { } func TestMethod(t *testing.T) { obj := SomeType(0) MagicHandler(obj.Func) } type testCase struct { Path string Pattern string CreateHandler func() http.Handler ExpectPanicOnCreateHandler bool NoCheck bool MakeRequest func(string) *http.Request CheckResponse func(*http.Response) error } var ( cases = []testCase{ { Path: "/", Pattern: "/{$}", CreateHandler: func() http.Handler { return MustMagicHandler(func() (int, string, error) { return 200, "hi", nil }) }, MakeRequest: func(base string) *http.Request { r, err := http.NewRequest("GET", base+"/", nil) if err != nil { panic(err) } return r }, }, { Pattern: "/error-checked-before-other-return-values", CreateHandler: func() http.Handler { return MustMagicHandler(func() (int, string, error) { // it checks errors first return 200, "hi", fmt.Errorf("it is handled before other return values") }) }, MakeRequest: func(base string) *http.Request { r, err := http.NewRequest("GET", base+"/return-error", nil) if err != nil { panic(err) } return r }, }, { Pattern: "/return-nil", CreateHandler: func() http.Handler { return MustMagicHandler(func() (magic.RespondWriter, error) { // no response body, 200 return nil, nil }) }, }, { Pattern: "/no-return", CreateHandler: func() http.Handler { return MustMagicHandler(func() { // return nothing at all }) }, }, { Pattern: "/no-error", CreateHandler: func() http.Handler { return MustMagicHandler(func() int { // well, error is not needed return http.StatusNoContent }) }, }, { Pattern: "/respond-json", CreateHandler: func() http.Handler { return MustMagicHandler(func() magic.Json[map[string]any] { return magic.Json[map[string]any]{ Data: map[string]any{ "message": "hello, world", }, } }) }, }, { Pattern: "/extract-json", CreateHandler: func() http.Handler { return MustMagicHandler(func(body magic.Json[HelloWorldRequest]) magic.Json[HelloWorldResponse] { return magic.Json[HelloWorldResponse]{ Data: HelloWorldResponse{ Greeting: "hello, " + body.Data.Name, }, } }) }, }, { Pattern: "/extract-optional", CreateHandler: func() http.Handler { // the parameter accepts a pointer to extractor, which indicates that it is optional return MustMagicHandler(func(body *magic.Json[HelloWorldRequest]) magic.Json[HelloWorldResponse] { name := "world" if body != nil { name = body.Data.Name } return magic.Json[HelloWorldResponse]{ Data: HelloWorldResponse{ Greeting: "hello, " + name, }, } }) }, }, { Path: "/inject-request-respond-status-from-http-request/418", // yeah...i'm a teapot, lol Pattern: "/inject-request-respond-status-from-http-request/{status}", CreateHandler: func() http.Handler { return MustMagicHandler(func(r *http.Request) (int, error) { // inject *http.Request, respond with status only return strconv.Atoi(r.PathValue("status")) }) }, }, { Path: "/inject-path-value-respond-status/418", // yeah...i'm a teapot, lol Pattern: "/inject-path-value-respond-status/{status}", CreateHandler: func() http.Handler { return MustMagicHandler(func(pathValues magic.PathValue[StatusPathValues]) int { return pathValues.Data.Status }) }, }, { Path: "/inject-optional-path-value/rose", // name extracted Pattern: "/inject-optional-path-value/{name}", CreateHandler: func() http.Handler { return MustMagicHandler(func(pathValues magic.PathValue[HelloWorldParams]) magic.Json[HelloWorldResponse] { name := "world" if pathValues.Data.Name != nil { name = *pathValues.Data.Name } return magic.Json[HelloWorldResponse]{ Data: HelloWorldResponse{ Greeting: "hello, " + name, }, } }) }, }, { Path: "/inject-optional-path-value", // name fallback to world Pattern: "/inject-optional-path-value", CreateHandler: func() http.Handler { return MustMagicHandler(func(pathValues magic.PathValue[HelloWorldParams]) magic.Json[HelloWorldResponse] { name := "world" if pathValues.Data.Name != nil { name = *pathValues.Data.Name } return magic.Json[HelloWorldResponse]{ Data: HelloWorldResponse{ Greeting: "hello, " + name, }, } }) }, }, { Pattern: "/direct-response-writer-extractor", CreateHandler: func() http.Handler { return MustMagicHandler(func(w http.ResponseWriter) { w.WriteHeader(http.StatusNoContent) }) }, }, { Pattern: "/hahaha-it-act-just-like-http-handlerfunc", CreateHandler: func() http.Handler { return MustMagicHandler(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }) }, }, { Pattern: "/websocket", CreateHandler: func() http.Handler { return MustMagicHandler(func(ws *websocket.Conn) { }) }, NoCheck: true, }, { Pattern: "/multiple-respond-extractor-should-panic-on-create", CreateHandler: func() http.Handler { return MustMagicHandler(func(conn *websocket.Conn) magic.Json[HelloWorldResponse] { return magic.Json[HelloWorldResponse]{} }) }, ExpectPanicOnCreateHandler: true, }, // { // Pattern: "/hello-query", // CreateHandler: func() http.Handler { // return MustMagicHandler(func(query magic.Query[HelloWorldParams]) magic.Json[HelloWorldResponse] { // name := "world" // if query.Data.Name != nil { // name = *query.Data.Name // } // return magic.Json[HelloWorldResponse]{ // Data: HelloWorldResponse{ // Greeting: "hello, " + name, // }, // } // }) // }, // }, { Pattern: "/test-query", CreateHandler: func() http.Handler { return MustMagicHandler(func(query magic.Query[MultipleParams]) { log.Println(query.Data) }) }, }, } ) func TestActualFunctions(t *testing.T) { mux := http.NewServeMux() t.Run("register-routes", func(t *testing.T) { for _, c := range cases { path := c.Path if path == "" { path = c.Pattern } t.Run(strings.TrimPrefix(path, "/"), func(t *testing.T) { defer func() { if o := recover(); (o != nil) != c.ExpectPanicOnCreateHandler { if c.ExpectPanicOnCreateHandler { t.Fatal("expect panic but panic did not occur") } else { t.Fatalf("panic not expected: %v", o) } } }() if handler := c.CreateHandler(); handler != nil { mux.Handle(c.Pattern, handler) } }) } }) t.Run("check-actual-result", func(t *testing.T) { server := httptest.NewTLSServer(mux) client := server.Client() for _, c := range cases { if c.ExpectPanicOnCreateHandler || c.NoCheck { continue } path := c.Path if path == "" { path = c.Pattern } t.Run(strings.TrimPrefix(path, "/"), func(t *testing.T) { var req *http.Request if c.MakeRequest != nil { req = c.MakeRequest(server.URL) } else { request, err := http.NewRequest("GET", server.URL+path, nil) if err != nil { t.Fatal(err) } req = request } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() log.Println(resp) if bytes, err := io.ReadAll(resp.Body); err == nil { log.Println(string(bytes)) } }) } }) }