diff --git a/application.go b/application.go index 67c529c..bcbf133 100644 --- a/application.go +++ b/application.go @@ -368,13 +368,22 @@ func (app *Application) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := app.createContext(w, req) var middlewares []HandlerFunc + var bestMatch *RouterGroup + // Find the longest matching group prefix for _, group := range app.groups { - if ok := group.matchPath(ctx.Path); ok { - middlewares = append(middlewares, group.middlewares...) + if group.matchPath(ctx.Path) { + if bestMatch == nil || len(group.prefix) > len(bestMatch.prefix) { + bestMatch = group + } } } + // Use the best matching group's middlewares + if bestMatch != nil { + middlewares = bestMatch.getAllMiddlewares() + } + ctx.handlers = middlewares app.router.handle(ctx) } @@ -665,7 +674,7 @@ func (app *Application) serveHTTP(ctx context.Context) error { } go func() { - <-ctx.Done() // 当上下文被取消时,停止服务器 + <-ctx.Done() // When context is canceled, stop the server server.Close() }() @@ -675,7 +684,7 @@ func (app *Application) serveHTTP(ctx context.Context) error { logger.Info("Server started at http://%s", app.AddressForLog()) } - // 等待所有 goroutine 完成 + // Wait for all goroutines to complete return server.Serve(listener) } @@ -702,7 +711,7 @@ func (app *Application) serveHTTPS(ctx context.Context) error { } go func() { - <-ctx.Done() // 当上下文被取消时,停止服务器 + <-ctx.Done() // When context is canceled, stop the server server.Close() }() diff --git a/context.go b/context.go index 0b8c584..5cfaa8b 100644 --- a/context.go +++ b/context.go @@ -196,16 +196,9 @@ func (ctx *Context) Context() context.Context { // Next runs the next handler in the middleware stack func (ctx *Context) Next() { ctx.index++ - s := len(ctx.handlers) - // for ; ctx.index < s; ctx.index ++ { - // ctx.handlers[ctx.index](ctx) - // } - - if ctx.index >= s { - panic("Handler cannot call ctx.Next") + if ctx.index < len(ctx.handlers) { + ctx.handlers[ctx.index](ctx) } - - ctx.handlers[ctx.index](ctx) } // Query returns the query string parameter with the given name. @@ -770,7 +763,7 @@ func (ctx *Context) BindJSON(obj interface{}) (err error) { } if ctx.Env().Get("DEBUG_ZOOX_REQUEST_BODY") != "" { - // refernece: golang复用http.request.body - https://zhuanlan.zhihu.com/p/47313038 + // reference: golang reuse http.request.body - https://zhuanlan.zhihu.com/p/47313038 _, err = ctx.CloneBody() if err != nil { return fmt.Errorf("failed to read request body: %v", err) @@ -803,7 +796,7 @@ func (ctx *Context) BindYAML(obj interface{}) (err error) { } if ctx.Debug().IsDebugMode() { - // refernece: golang复用http.request.body - https://zhuanlan.zhihu.com/p/47313038 + // reference: golang reuse http.request.body - https://zhuanlan.zhihu.com/p/47313038 _, err = ctx.CloneBody() if err != nil { return fmt.Errorf("failed to read request body: %v", err) @@ -1060,7 +1053,7 @@ func (ctx *Context) Proxy(target string, cfg ...*proxy.SingleHostConfig) { // CloneBody clones the body of the request, should be used carefully. func (ctx *Context) CloneBody() (body io.ReadCloser, err error) { if ctx.bodyBytes == nil { - // refernece: golang复用http.request.body - https://zhuanlan.zhihu.com/p/47313038 + // reference: golang reuse http.request.body - https://zhuanlan.zhihu.com/p/47313038 ctx.bodyBytes, err = ioutil.ReadAll(ctx.Request.Body) if err != nil { return nil, fmt.Errorf("failed to read request body: %v", err) diff --git a/group.go b/group.go index 42fb1a1..cbffb70 100644 --- a/group.go +++ b/group.go @@ -6,10 +6,9 @@ import ( "mime" "net/http" "path" + "strings" "time" - "github.com/go-zoox/core-utils/regexp" - "github.com/go-zoox/core-utils/strings" "github.com/go-zoox/fs" "github.com/go-zoox/headers" "github.com/go-zoox/proxy" @@ -40,6 +39,8 @@ func newRouterGroup(app *Application, prefix string) *RouterGroup { func (g *RouterGroup) Group(prefix string, cb ...GroupFunc) *RouterGroup { newGroup := newRouterGroup(g.app, g.prefix+prefix) newGroup.parent = g + + // Simply append to groups list - no need for complex sorting g.app.groups = append(g.app.groups, newGroup) for _, fn := range cb { @@ -49,30 +50,91 @@ func (g *RouterGroup) Group(prefix string, cb ...GroupFunc) *RouterGroup { return newGroup } -func (g *RouterGroup) matchPath(path string) (ok bool) { - // /v1 => /v1 - // /v1/ => /v1 - if ok := strings.HasPrefix(path, g.prefix); ok { - return ok +// matchPath handles both static and dynamic prefix matching +func (g *RouterGroup) matchPath(path string) bool { + // Empty prefix matches all paths + if g.prefix == "" || g.prefix == "/" { + return true + } + + // If prefix has no dynamic parts, use simple prefix matching + if !strings.Contains(g.prefix, ":") && !strings.Contains(g.prefix, "{") && !strings.Contains(g.prefix, "*") { + return strings.HasPrefix(path, g.prefix) + } + + // For dynamic prefixes, use simple pattern matching + return g.matchDynamicPrefix(path, g.prefix) +} + +// matchDynamicPrefix handles dynamic path matching with a simplified approach +func (g *RouterGroup) matchDynamicPrefix(path, prefix string) bool { + pathParts := strings.Split(strings.Trim(path, "/"), "/") + prefixParts := strings.Split(strings.Trim(prefix, "/"), "/") + + // If prefix has more parts than path, it cannot match + if len(prefixParts) > len(pathParts) { + return false + } + + // Check each part + for i, prefixPart := range prefixParts { + pathPart := pathParts[i] + + // Skip dynamic parameters + if strings.HasPrefix(prefixPart, ":") || + (strings.HasPrefix(prefixPart, "{") && strings.HasSuffix(prefixPart, "}")) || + strings.HasPrefix(prefixPart, "*") { + continue + } + + // Static part must match exactly + if prefixPart != pathPart { + return false + } } - // @TODO /v1/containers/123456/terminal => /v1/containers/:id - re := g.prefix - if strings.Contains(re, ":") { - re = strings.ReplaceAllFunc(re, ":\\w+", func(b []byte) []byte { - return []byte("\\w+") - }) - } else if strings.Contains(re, "{") { - re = strings.ReplaceAllFunc(re, "{.*}", func(b []byte) []byte { - return []byte("\\w+") - }) + return true +} + +// getAllMiddlewares gets all middlewares (including parent) +func (g *RouterGroup) getAllMiddlewares() []HandlerFunc { + var middlewares []HandlerFunc + + // Recursively collect parent middlewares + if g.parent != nil { + middlewares = append(middlewares, g.parent.getAllMiddlewares()...) + } + + // Add current level middlewares + middlewares = append(middlewares, g.middlewares...) + + return middlewares +} + +// joinPath correctly joins URL paths +func (g *RouterGroup) joinPath(path string) string { + if g.prefix == "" { + return path + } + + // Handle root path special case + if g.prefix == "/" && path == "/" { + return "/" + } + + // Simple path joining + prefix := strings.TrimSuffix(g.prefix, "/") + path = strings.TrimPrefix(path, "/") + + if path == "" { + return prefix } - return regexp.Match(re, path) + return prefix + "/" + path } func (g *RouterGroup) addRoute(method string, path string, handler ...HandlerFunc) { - pathX := fs.JoinPath(g.prefix, path) + pathX := g.joinPath(path) g.app.router.addRoute(method, pathX, handler...) } @@ -164,7 +226,7 @@ func (g *RouterGroup) Proxy(path, target string, options ...func(cfg *ProxyConfi handler := WrapH(proxy.NewSingleHost(target, &cfg.SingleHostConfig)) g.Use(func(ctx *Context) { - if strings.StartsWith(ctx.Path, path) { + if strings.HasPrefix(ctx.Path, path) { if cfg.OnRequestWithContext != nil { if err := cfg.OnRequestWithContext(ctx); err != nil { ctx.Logger.Errorf("proxy error: %s", err) @@ -349,7 +411,7 @@ func (g *RouterGroup) Static(basePath string, rootDir string, options ...*Static opts = options[0] } - if !strings.StartsWith(basePath, "/") { + if !strings.HasPrefix(basePath, "/") { rootDir = fs.JoinCurrentDir(basePath) } @@ -363,7 +425,7 @@ func (g *RouterGroup) Static(basePath string, rootDir string, options ...*Static return } - if !strings.StartsWith(ctx.Path, absolutePath) { + if !strings.HasPrefix(ctx.Path, absolutePath) { ctx.Next() return } diff --git a/group_test.go b/group_test.go index 14c2ae6..6f95ac4 100644 --- a/group_test.go +++ b/group_test.go @@ -1,38 +1,596 @@ package zoox -import "testing" +import ( + "net/http/httptest" + "regexp" + "testing" +) func TestGroupMatchPath(t *testing.T) { - testcases := []map[string]any{ + tests := []struct { + name string + prefix string + path string + expected bool + }{ + // Basic matching tests + {"root path matches root prefix", "/", "/", true}, + {"empty prefix matches all", "", "/users", true}, + {"exact match", "/api", "/api", true}, + {"prefix match", "/api", "/api/users", true}, + {"prefix match with trailing slash in prefix", "/api/", "/api/users", true}, + {"should not match different prefix", "/api", "/users", false}, + {"should not match partial prefix", "/api", "/ap", false}, + + // Original test cases + {"original test case 1", "/v1", "/v1/users", true}, + {"original test case 2", "/v1", "/v1", true}, + {"original test case 3", "/v1", "/v2", false}, + {"original test case 4", "/v1", "/v1users", true}, // This will match with simple prefix matching + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + group := &RouterGroup{prefix: tt.prefix} + result := group.matchPath(tt.path) + if result != tt.expected { + t.Errorf("matchPath(%q, %q) = %v, want %v", tt.prefix, tt.path, result, tt.expected) + } + }) + } +} + +func TestGroupMiddlewareInheritance(t *testing.T) { + app := New() + + // Record middleware execution order + var executionOrder []string + + // Root level middleware + app.Use(func(ctx *Context) { + executionOrder = append(executionOrder, "root") + ctx.Next() + }) + + // First level group + v1 := app.Group("/v1") + v1.Use(func(ctx *Context) { + executionOrder = append(executionOrder, "v1") + ctx.Next() + }) + + // Second level group + users := v1.Group("/users") + users.Use(func(ctx *Context) { + executionOrder = append(executionOrder, "users") + ctx.Next() + }) + + users.Get("/:id", func(ctx *Context) { + executionOrder = append(executionOrder, "handler") + ctx.String(200, "user") + }) + + // Test request + req := httptest.NewRequest("GET", "/v1/users/123", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + // Verify middleware execution order + expected := []string{"root", "v1", "users", "handler"} + if len(executionOrder) != len(expected) { + t.Errorf("Expected %d middleware executions, got %d", len(expected), len(executionOrder)) + } + + for i, middleware := range expected { + if i >= len(executionOrder) || executionOrder[i] != middleware { + t.Errorf("Expected middleware %s at position %d, got %s", middleware, i, executionOrder[i]) + } + } + + // Verify response + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + + if w.Body.String() != "user" { + t.Errorf("Expected body 'user', got '%s'", w.Body.String()) + } +} + +func TestGroupPathJoining(t *testing.T) { + testcases := []struct { + name string + prefix string + path string + expected string + }{ + { + name: "simple join", + prefix: "/api", + path: "/users", + expected: "/api/users", + }, + { + name: "join with trailing slash in prefix", + prefix: "/api/", + path: "/users", + expected: "/api/users", + }, { - "path": "/", - "prefix": "/", - "expect": true, + name: "join with leading slash in path", + prefix: "/api", + path: "/users", + expected: "/api/users", }, { - "path": "/", - "prefix": "/api", - "expect": false, + name: "join with both slashes", + prefix: "/api/", + path: "/users", + expected: "/api/users", }, { - "path": "/api", - "prefix": "/", - "expect": true, + name: "empty prefix", + prefix: "", + path: "/users", + expected: "/users", }, { - "path": "/v1/containers/d0ac6213f33620362e59cc1b855658f9792377335087c2f3ba1d43639466dd8a/terminal", - "prefix": "/v1/containers/:id", - "expect": true, + name: "empty path", + prefix: "/api", + path: "", + expected: "/api", }, + { + name: "root paths", + prefix: "/", + path: "/", + expected: "/", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + app := New() + group := &RouterGroup{ + app: app, + prefix: tc.prefix, + } + + result := group.joinPath(tc.path) + if result != tc.expected { + t.Errorf("Expected '%s', got '%s'", tc.expected, result) + } + }) + } +} + +func TestGroupNestedRouting(t *testing.T) { + app := New() + + // Create nested routing groups + api := app.Group("/api") + v1 := api.Group("/v1") + users := v1.Group("/users") + + users.Get("/:id", func(ctx *Context) { + ctx.String(200, "user-"+ctx.Param().Get("id").String()) + }) + + // Test request + req := httptest.NewRequest("GET", "/api/v1/users/123", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + + // Verify response content + expected := "user-123" + if w.Body.String() != expected { + t.Errorf("Expected body '%s', got '%s'", expected, w.Body.String()) + } +} + +func TestGroupMiddlewareOrder(t *testing.T) { + app := New() + var order []string + + // Root level middleware + app.Use(func(ctx *Context) { + order = append(order, "global") + ctx.Next() + }) + + // Group middleware + api := app.Group("/api") + api.Use(func(ctx *Context) { + order = append(order, "api") + ctx.Next() + }) + + v1 := api.Group("/v1") + v1.Use(func(ctx *Context) { + order = append(order, "v1") + ctx.Next() + }) + + // Sub-group middleware + v1.Get("/test", func(ctx *Context) { + order = append(order, "handler") + ctx.String(200, "ok") + }) + + // Test request + req := httptest.NewRequest("GET", "/api/v1/test", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + // Verify middleware execution order + expected := []string{"global", "api", "v1", "handler"} + if len(order) != len(expected) { + t.Errorf("Expected %d middleware executions, got %d", len(expected), len(order)) + } + + for i, middleware := range expected { + if i >= len(order) || order[i] != middleware { + t.Errorf("Expected middleware %s at position %d, got %s", middleware, i, order[i]) + } + } +} + +func TestGroupConflictResolution(t *testing.T) { + app := New() + + // Create two potentially conflicting groups + api := app.Group("/api") + v1 := api.Group("/v1") + + // More specific group + users := v1.Group("/users") + users.Get("/list", func(ctx *Context) { + ctx.String(200, "users-list") + }) + + // Less specific group with different path + v1.Get("/info", func(ctx *Context) { + ctx.String(200, "v1-info") + }) + + // Test request - should match more specific path + req := httptest.NewRequest("GET", "/api/v1/users/list", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + + // Verify only the most specific match is executed + expected := "users-list" + if w.Body.String() != expected { + t.Errorf("Expected body '%s', got '%s'", expected, w.Body.String()) + } +} + +func BenchmarkGroupMatchPath(b *testing.B) { + group := &RouterGroup{prefix: "/api/v1/users/:id"} + path := "/api/v1/users/123/profile" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + group.matchPath(path) + } +} + +func BenchmarkGroupMiddlewareCollection(b *testing.B) { + app := New() + + // Create deeply nested groups + api := app.Group("/api") + v1 := api.Group("/v1") + users := v1.Group("/users") + profile := users.Group("/profile") + + // Add middlewares at each level + api.Use(func(ctx *Context) { ctx.Next() }) + v1.Use(func(ctx *Context) { ctx.Next() }) + users.Use(func(ctx *Context) { ctx.Next() }) + profile.Use(func(ctx *Context) { ctx.Next() }) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + profile.getAllMiddlewares() + } +} + +func TestRegexpQuoteMeta(t *testing.T) { + testcases := []string{ + "/users/:id", + "/users/{id}", + "/files/*path", + } + + for _, tc := range testcases { + quoted := regexp.QuoteMeta(tc) + t.Logf("Input: %s, QuoteMeta: %s", tc, quoted) + } +} + +func TestGroupDynamicRouting(t *testing.T) { + app := New() + + // 测试动态路由组 + userGroup := app.Group("/:id/profile") + userGroup.Get("/settings", func(ctx *Context) { + ctx.String(200, "user-settings-"+ctx.Param().Get("id").String()) + }) + + categoryGroup := app.Group("/:id/xxx/:cat") + categoryGroup.Get("/details", func(ctx *Context) { + id := ctx.Param().Get("id").String() + cat := ctx.Param().Get("cat").String() + ctx.String(200, "category-"+id+"-"+cat) + }) + + // 测试请求 + tests := []struct { + path string + expected string + status int + }{ + {"/123/profile/settings", "user-settings-123", 200}, + {"/456/xxx/books/details", "category-456-books", 200}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + req := httptest.NewRequest("GET", tt.path, nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + if w.Code != tt.status { + t.Errorf("Expected status %d, got %d", tt.status, w.Code) + } + + if w.Body.String() != tt.expected { + t.Errorf("Expected body '%s', got '%s'", tt.expected, w.Body.String()) + } + }) + } +} + +func TestGroupDynamicRoutingDebug(t *testing.T) { + app := New() + + // 测试动态路由组 + userGroup := app.Group("/:id/profile") + userGroup.Get("/settings", func(ctx *Context) { + ctx.String(200, "user-settings-"+ctx.Param().Get("id").String()) + }) + + categoryGroup := app.Group("/:id/xxx/:cat") + categoryGroup.Get("/details", func(ctx *Context) { + id := ctx.Param().Get("id").String() + cat := ctx.Param().Get("cat").String() + ctx.String(200, "category-"+id+"-"+cat) + }) + + // 打印所有路由组 + t.Logf("Total groups: %d", len(app.groups)) + for i, group := range app.groups { + t.Logf("Group %d: prefix='%s'", i, group.prefix) + } + + // 测试路径匹配 + testPaths := []string{"/123/profile/settings", "/456/xxx/books/details"} + for _, path := range testPaths { + t.Logf("\nTesting path: %s", path) + for i, group := range app.groups { + matches := group.matchPath(path) + t.Logf(" Group %d ('%s') matches: %v", i, group.prefix, matches) + } + } +} + +func TestGroupDynamicPathMatching(t *testing.T) { + tests := []struct { + name string + prefix string + path string + expected bool + }{ + // 静态路径匹配 + {"static exact match", "/api/v1", "/api/v1", true}, + {"static prefix match", "/api/v1", "/api/v1/users", true}, + {"static no match", "/api/v1", "/api/v2", false}, + + // 动态路径匹配 + {"single param", "/:id", "/123", true}, + {"single param with static", "/:id/profile", "/123/profile", true}, + {"single param with static and more", "/:id/profile", "/123/profile/settings", true}, + {"single param no match", "/:id/profile", "/123/settings", false}, + + // 多个参数 + {"multiple params", "/:id/xxx/:cat", "/123/xxx/books", true}, + {"multiple params with more", "/:id/xxx/:cat", "/123/xxx/books/details", true}, + {"multiple params no match", "/:id/xxx/:cat", "/123/yyy/books", false}, + + // 混合格式 + {"bracket format", "/{id}/profile", "/123/profile", true}, + {"bracket format no match", "/{id}/profile", "/123/settings", false}, + + // 通配符 + {"wildcard", "/files/*path", "/files/docs/readme.txt", true}, + {"wildcard root", "/*path", "/anything/goes/here", true}, + + // 边界情况 + {"empty parts", "/:id//profile", "/123//profile", true}, + {"trailing slash in prefix", "/:id/profile/", "/123/profile/settings", true}, + {"insufficient path parts", "/:id/profile/settings", "/123/profile", false}, } - for _, testcase := range testcases { - group := &RouterGroup{ - prefix: testcase["prefix"].(string), + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + group := &RouterGroup{prefix: tt.prefix} + result := group.matchPath(tt.path) + if result != tt.expected { + t.Errorf("matchPath(%q, %q) = %v, want %v", tt.prefix, tt.path, result, tt.expected) + } + }) + } +} + +func TestGroupDynamicRoutingWithMiddleware(t *testing.T) { + app := New() + + var middlewareOrder []string + + // 全局中间件 + app.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "global") + ctx.Next() + }) + + // 动态路由组 - 用户相关 + userGroup := app.Group("/:id/profile") + userGroup.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "user-profile") + ctx.Next() + }) + userGroup.Get("/settings", func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "handler") + ctx.String(200, "user-settings-"+ctx.Param().Get("id").String()) + }) + + // 动态路由组 - 分类相关 + categoryGroup := app.Group("/:id/xxx/:cat") + categoryGroup.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "category") + ctx.Next() + }) + categoryGroup.Get("/details", func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "handler") + id := ctx.Param().Get("id").String() + cat := ctx.Param().Get("cat").String() + ctx.String(200, "category-"+id+"-"+cat) + }) + + // 测试用户路由组中间件 + t.Run("user profile middleware", func(t *testing.T) { + middlewareOrder = nil // 重置 + req := httptest.NewRequest("GET", "/123/profile/settings", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + // 验证中间件执行顺序 + expected := []string{"global", "user-profile", "handler"} + if len(middlewareOrder) != len(expected) { + t.Errorf("Expected %d middleware executions, got %d", len(expected), len(middlewareOrder)) } - if got := group.matchPath(testcase["path"].(string)); got != testcase["expect"] { - t.Fatalf("expected %v, got %v (path: %s, group: %s)", testcase["expect"], got, testcase["path"], testcase["prefix"]) + for i, middleware := range expected { + if i >= len(middlewareOrder) || middlewareOrder[i] != middleware { + t.Errorf("Expected middleware %s at position %d, got %s", middleware, i, middlewareOrder[i]) + } } + + // 验证响应 + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + if w.Body.String() != "user-settings-123" { + t.Errorf("Expected body 'user-settings-123', got '%s'", w.Body.String()) + } + }) + + // 测试分类路由组中间件 + t.Run("category middleware", func(t *testing.T) { + middlewareOrder = nil // 重置 + req := httptest.NewRequest("GET", "/456/xxx/books/details", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + // 验证中间件执行顺序 + expected := []string{"global", "category", "handler"} + if len(middlewareOrder) != len(expected) { + t.Errorf("Expected %d middleware executions, got %d", len(expected), len(middlewareOrder)) + } + + for i, middleware := range expected { + if i >= len(middlewareOrder) || middlewareOrder[i] != middleware { + t.Errorf("Expected middleware %s at position %d, got %s", middleware, i, middlewareOrder[i]) + } + } + + // 验证响应 + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + if w.Body.String() != "category-456-books" { + t.Errorf("Expected body 'category-456-books', got '%s'", w.Body.String()) + } + }) +} + +func TestNestedDynamicGroupMiddleware(t *testing.T) { + app := New() + + var middlewareOrder []string + + // 全局中间件 + app.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "global") + ctx.Next() + }) + + // 一级动态路由组 + userGroup := app.Group("/:userId") + userGroup.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "user-"+ctx.Param().Get("userId").String()) + ctx.Next() + }) + + // 二级动态路由组 + profileGroup := userGroup.Group("/profile/:section") + profileGroup.Use(func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "profile-"+ctx.Param().Get("section").String()) + ctx.Next() + }) + + // 三级路由 + profileGroup.Get("/details", func(ctx *Context) { + middlewareOrder = append(middlewareOrder, "handler") + userId := ctx.Param().Get("userId").String() + section := ctx.Param().Get("section").String() + ctx.String(200, "user-"+userId+"-profile-"+section) + }) + + // 测试嵌套动态路由组中间件 + middlewareOrder = nil // 重置 + req := httptest.NewRequest("GET", "/123/profile/settings/details", nil) + w := httptest.NewRecorder() + app.ServeHTTP(w, req) + + // 验证中间件执行顺序 + expected := []string{"global", "user-123", "profile-settings", "handler"} + if len(middlewareOrder) != len(expected) { + t.Errorf("Expected %d middleware executions, got %d", len(expected), len(middlewareOrder)) + t.Logf("Actual order: %v", middlewareOrder) + } + + for i, middleware := range expected { + if i >= len(middlewareOrder) || middlewareOrder[i] != middleware { + t.Errorf("Expected middleware %s at position %d, got %s", middleware, i, middlewareOrder[i]) + } + } + + // 验证响应 + if w.Code != 200 { + t.Errorf("Expected status 200, got %d", w.Code) + } + if w.Body.String() != "user-123-profile-settings" { + t.Errorf("Expected body 'user-123-profile-settings', got '%s'", w.Body.String()) } }