package omni_test import ( "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" omni "git.yuntuo.hk/k2safe/go-omni" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/middleware" "git.yuntuo.hk/k2safe/go-omni/response" ) func TestResponsePoliciesAndCallbackBoundaries(t *testing.T) { type acknowledgment struct { Message string `xml:"message"` } cfg := response.Config{CodeType: "integer", SuccessCode: 200, ErrorCode: 9999, MessageField: "msg", RequestIDField: "-", DataMode: "always", SuccessMessage: "成功", ErrorMessage: "系统错误", Codes: map[string]any{"DENIED": 403001}} app := omni.New(omni.WithResponse(cfg)) defer app.Close() app.GET("/business", func(c *httpx.Context) error { return c.OK(nil) }) app.GET("/denied", func(c *httpx.Context) error { return omni.Forbidden("DENIED", "denied") }) app.GET("/plain", func(c *httpx.Context) error { return c.Text(200, "success") }) app.GET("/xml", func(c *httpx.Context) error { return c.XML(200, acknowledgment{Message: "accepted"}) }) app.GET("/raw", func(c *httpx.Context) error { return c.JSON(202, map[string]any{"accepted": true}) }) app.GET("/committed", func(c *httpx.Context) error { if err := c.Text(200, "success"); err != nil { return err } return errors.New("late error") }) callback := response.EncoderFunc(func(r response.Result) (response.Payload, error) { if r.Err != nil { return response.Text(200, "fail"), nil } return response.Text(200, "success"), nil }) app.POST("/callback", func(c *httpx.Context) error { return c.OK(map[string]any{"ignored": true}) }, httpx.WithResponse(callback)) app.POST("/callbackError", func(c *httpx.Context) error { return errors.New("private callback error") }, httpx.WithResponse(callback)) app.POST("/callbackPanic", func(c *httpx.Context) error { panic("private callback panic") }, httpx.WithResponse(callback)) broken := response.EncoderFunc(func(response.Result) (response.Payload, error) { panic("broken formatter") }) app.GET("/broken", func(c *httpx.Context) error { return c.OK(nil) }, httpx.WithResponse(broken)) for _, tc := range []struct { method, path string status int body, contentType string }{ {"GET", "/business", 200, `{"code":200,"data":null,"msg":"成功"}` + "\n", "application/json"}, {"GET", "/denied", 403, `{"code":403001,"data":null,"msg":"系统错误"}` + "\n", "application/json"}, {"GET", "/plain", 200, "success", "text/plain"}, {"GET", "/xml", 200, "<", "application/xml"}, {"GET", "/raw", 202, `{"accepted":true}` + "\n", "application/json"}, {"GET", "/committed", 200, "success", "text/plain"}, {"POST", "/callback", 200, "success", "text/plain"}, {"POST", "/callbackError", 200, "fail", "text/plain"}, {"POST", "/callbackPanic", 200, "fail", "text/plain"}, {"GET", "/broken", 500, "", ""}, {"GET", "/missing", 404, `{"code":9999,"data":null,"msg":"系统错误"}` + "\n", "application/json"}, {"PUT", "/business", 405, `{"code":9999,"data":null,"msg":"系统错误"}` + "\n", "application/json"}, } { t.Run(tc.path, func(t *testing.T) { w := httptest.NewRecorder() app.ServeHTTP(w, httptest.NewRequest(tc.method, tc.path, nil)) body := w.Body.String() if w.Code != tc.status || !strings.HasPrefix(w.Header().Get("Content-Type"), tc.contentType) { t.Fatalf("%d %s %s", w.Code, w.Header().Get("Content-Type"), body) } if tc.path == "/xml" { if !strings.Contains(body, "accepted") || strings.Contains(body, "code") { t.Fatal(body) } } else if body != tc.body { t.Fatalf("body %q, want %q", body, tc.body) } }) } } func TestResponsePolicyIsolationAndClones(t *testing.T) { cfg := response.Config{CodeField: "result", MessageField: "notice", DataField: "payload", RequestIDField: "-", Codes: map[string]any{"OK": "SUCCESS"}} app := omni.New(omni.WithResponse(cfg)) defer app.Close() cfg.Codes["OK"] = "MUTATED" app.Config().Response.Codes["OK"] = "CHANGED" app.GET("/clone", func(c *httpx.Context) error { buffer := httptest.NewRecorder() child := c.Clone(context.Background(), buffer) if err := child.OK("value"); err != nil { return err } return c.WriteResponse(buffer.Code, buffer.Header(), buffer.Body.Bytes()) }) other := omni.New() defer other.Close() other.GET("/clone", func(c *httpx.Context) error { return c.OK("value") }) var wg sync.WaitGroup for n := 0; n < 32; n++ { wg.Add(1) go func() { defer wg.Done() for _, item := range []struct { handler http.Handler key, value string }{{app, "result", "SUCCESS"}, {other, "code", "OK"}} { w := httptest.NewRecorder() item.handler.ServeHTTP(w, httptest.NewRequest("GET", "/clone", nil)) var body map[string]any if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || body[item.key] != item.value { t.Errorf("policy leaked: %s %v", w.Body.String(), err) } } }() } wg.Wait() } func TestResponsePolicyPrecedesGlobalMiddleware(t *testing.T) { app := omni.New() defer app.Close() app.Use(func(next httpx.Handler) httpx.Handler { return func(c *httpx.Context) error { switch c.Header("Case") { case "reject": return omni.Forbidden("DENIED", "denied") case "panic": panic("global panic") case "clone": buffer := httptest.NewRecorder() child := c.Clone(c.Context(), buffer) if err := next(child); err != nil { child.WriteError(err) } return c.WriteResponse(buffer.Code, buffer.Header(), buffer.Body.Bytes()) } return next(c) } }) encoder := response.EncoderFunc(func(r response.Result) (response.Payload, error) { if r.Err != nil { return response.Text(200, "fail"), nil } return response.Text(200, "success"), nil }) httpx.HandleWithResponse(app.Group("/hooks"), "POST", "/payment", encoder, func(c *httpx.Context) error { return c.OK(nil) }) for _, tc := range []struct{ mode, want string }{{"", "success"}, {"reject", "fail"}, {"panic", "fail"}, {"clone", "success"}} { r := httptest.NewRequest("POST", "/hooks/payment", nil) r.Header.Set("Case", tc.mode) w := httptest.NewRecorder() app.ServeHTTP(w, r) if w.Code != 200 || w.Body.String() != tc.want { t.Fatalf("%s: %d %s", tc.mode, w.Code, w.Body.String()) } } } func TestResponsePolicyOnRealTransports(t *testing.T) { for _, transport := range []string{"nethttp", "native"} { t.Run(transport, func(t *testing.T) { app := omni.New(omni.WithTransport(transport), omni.WithResponse(response.Config{CodeType: "integer", SuccessCode: 0, ErrorCode: 9999, MessageField: "msg", RequestIDField: "-", DataMode: "always"})) app.GET("/ok", func(c *httpx.Context) error { return c.OK(nil) }) app.GET("/panic", func(c *httpx.Context) error { panic("fault") }, httpx.WithResponse(response.EncoderFunc(func(response.Result) (response.Payload, error) { return response.Text(200, "FAIL"), nil }))) app.GET("/timeout", func(c *httpx.Context) error { <-c.Context().Done(); return c.Context().Err() }, middleware.Timeout(10*time.Millisecond)) listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- app.ServeContext(ctx, listener) }() defer func() { cancel() select { case err := <-done: if err != nil { t.Error(err) } case <-time.After(5 * time.Second): t.Error("server did not stop") } }() client := &http.Client{Timeout: 3 * time.Second} defer client.CloseIdleConnections() for _, path := range []string{"/ok", "/panic", "/timeout"} { r, err := client.Get("http://" + listener.Addr().String() + path) if err != nil { t.Fatal(err) } data, _ := io.ReadAll(r.Body) r.Body.Close() if path == "/panic" { if string(data) != "FAIL" || r.StatusCode != 200 { t.Fatal(string(data)) } continue } var value map[string]any if err = json.Unmarshal(data, &value); err != nil || len(value) != 3 || value["msg"] == nil { t.Fatalf("%s: %s %v", path, data, err) } if path == "/timeout" && fmt.Sprint(value["code"]) != "9999" { t.Fatal(string(data)) } } }) } }