package app_test import ( "context" "encoding/json" "errors" "io" "net" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" omni "git.yuntuo.hk/k2safe/go-omni" "git.yuntuo.hk/k2safe/go-omni/config" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/internal/testutil" "git.yuntuo.hk/k2safe/go-omni/response" ) func corsApp() *omni.App { return omni.New(omni.WithCORS(omni.CORSConfig{Enabled: true, AllowedOrigins: []string{"https://web.example"}, AllowedMethods: []string{"GET", "POST", "PUT"}, AllowedHeaders: []string{"Authorization", "Content-Type"}, ExposedHeaders: []string{"Content-Disposition"}}), omni.WithResponse(response.Config{CodeType: "int32", SuccessCode: 0, ErrorCode: 999, MessageField: "msg", DataMode: "always"})) } func TestAppCORSRoutingAndErrors(t *testing.T) { a := corsApp() defer a.Close() var middlewareCalls int a.Use(func(next omni.Handler) omni.Handler { return func(c *omni.Context) error { middlewareCalls++; return next(c) } }) a.GET("/secure", func(c *omni.Context) error { return c.OK("private") }, func(next omni.Handler) omni.Handler { return func(c *omni.Context) error { return omni.Unauthorized("LOGIN_REQUIRED", "login required") } }) a.GET("/failure", func(c *omni.Context) error { return omni.Internal(errors.New("private detail")) }) a.GET("/panic", func(c *omni.Context) error { panic("private panic") }) a.Handle("OPTIONS", "/options", func(c *omni.Context) error { return c.Text(200, "ordinary options") }) for name, entry := range map[string]http.Handler{"App": a, "Engine": a.Engine(), "NewServer": a.NewServer("").Handler} { t.Run(name, func(t *testing.T) { for _, tc := range []struct { method, path string status int }{{"GET", "/secure", 401}, {"GET", "/failure", 500}, {"GET", "/panic", 500}, {"GET", "/absent", 404}, {"PUT", "/secure", 405}} { r := httptest.NewRequest(tc.method, tc.path, nil) r.Header.Set("Origin", "https://web.example") w := httptest.NewRecorder() entry.ServeHTTP(w, r) var body map[string]any err := json.Unmarshal(w.Body.Bytes(), &body) if err != nil || w.Code != tc.status || w.Header().Get("Access-Control-Allow-Origin") != "https://web.example" || body["code"] != float64(999) || body["data"] != nil || body["msg"] == nil || strings.Contains(w.Body.String(), "private") { t.Fatal(w.Code, w.Header(), w.Body.String(), err) } } for _, path := range []string{"/secure", "/absent"} { before := middlewareCalls r := httptest.NewRequest("OPTIONS", path, nil) r.Header.Set("Origin", "https://web.example") r.Header.Set("Access-Control-Request-Method", "POST") r.Header.Set("Access-Control-Request-Headers", "authorization,content-type") w := httptest.NewRecorder() entry.ServeHTTP(w, r) if w.Code != 204 || w.Body.Len() != 0 || middlewareCalls != before { t.Fatal(w.Code, w.Body.String(), middlewareCalls, before) } } r := httptest.NewRequest("OPTIONS", "/options", nil) w := httptest.NewRecorder() entry.ServeHTTP(w, r) if w.Code != 200 || w.Body.String() != "ordinary options" { t.Fatal(w.Code, w.Body.String()) } }) } } func TestAppCORSConfigIsolation(t *testing.T) { cfg := config.Default() cfg.CORS = omni.CORSConfig{Enabled: true, AllowedOrigins: []string{"https://one.example"}, AllowedHeaders: []string{"Authorization"}} option := omni.WithConfig(cfg) cfg.CORS.AllowedOrigins[0] = "https://evil.example" a := omni.New(option) defer a.Close() b := omni.New(option) defer b.Close() a.Config().CORS.AllowedOrigins[0] = "https://evil.example" for _, app := range []*omni.App{a, b} { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/absent", nil) r.Header.Set("Origin", "https://one.example") app.ServeHTTP(w, r) if w.Header().Get("Access-Control-Allow-Origin") != "https://one.example" { t.Fatal(w.Header()) } } defer func() { if recover() == nil { t.Error("invalid CORS passed direct New") } }() omni.New(omni.WithCORS(omni.CORSConfig{Enabled: true})) } func TestCORSTransports(t *testing.T) { for _, transport := range []string{omni.TransportNetHTTP, omni.TransportNative, omni.TransportHTTP2} { t.Run(transport, func(t *testing.T) { cert, key, tlsClient := testutil.CertificateFiles(t) cfg := omni.CORSConfig{Enabled: true, AllowedOrigins: []string{"https://web.example"}, AllowedMethods: []string{"GET", "POST"}, AllowedHeaders: []string{"Content-Type"}, ExposedHeaders: []string{"Content-Disposition"}} a := omni.New(omni.WithCORS(cfg), omni.WithTransport(transport), omni.WithTLS(cert, key)) var called atomic.Int32 a.Use(func(next omni.Handler) omni.Handler { return func(c *omni.Context) error { called.Add(1); return next(c) } }) a.POST("/upload", func(c *omni.Context) error { b, err := io.ReadAll(c.Request().Body) if err != nil { return err } return c.Text(200, string(b)) }) a.GET("/download", func(c *omni.Context) error { return c.WriteStream(200, "application/octet-stream", &httpx.Stream{Content: io.NopCloser(strings.NewReader("file bytes")), Name: "example.txt"}) }) 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 <- a.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 close") listener.Close() } }() client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{TLSClientConfig: tlsClient, ForceAttemptHTTP2: true}} defer client.CloseIdleConnections() url := "https://" + listener.Addr().String() for _, tc := range []struct { method, path, body string status int }{{"OPTIONS", "/upload", "", 204}, {"POST", "/upload", "raw upload", 200}, {"GET", "/download", "file bytes", 200}, {"GET", "/missing", "", 404}} { var input io.Reader if tc.method == "POST" { input = strings.NewReader(tc.body) } r, _ := http.NewRequest(tc.method, url+tc.path, input) r.Header.Set("Origin", "https://web.example") if tc.method == "OPTIONS" { r.Header.Set("Access-Control-Request-Method", "POST") r.Header.Set("Access-Control-Request-Headers", "content-type") } res, err := client.Do(r) if err != nil { t.Fatal(err) } body, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { t.Fatal(err) } if res.StatusCode != tc.status || res.Header.Get("Access-Control-Allow-Origin") != "https://web.example" { t.Fatal(res.StatusCode, res.Header) } if tc.status == 200 && string(body) != tc.body { t.Fatal(string(body)) } if tc.status == 204 && (len(body) != 0 || called.Load() != 0) { t.Fatal("preflight reached middleware") } if tc.path == "/download" && (!strings.Contains(res.Header.Get("Content-Disposition"), "example.txt") || res.Header.Get("Access-Control-Expose-Headers") != "Content-Disposition") { t.Fatal(res.Header) } if transport == omni.TransportHTTP2 && res.ProtoMajor != 2 { t.Fatal(res.Proto) } } }) } }