From 6bc76c34048e500c14960b877b8ff8fda5b54e62 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 28 Jun 2026 15:21:27 +0800 Subject: [PATCH] test(router): cover AIP custom verb routes Custom verb support needs coverage beyond the tree insertion changes. Add tree and engine tests for literal colon verbs and params followed by verb suffixes. The assertions cover handler selection, FullPath, Param values, and parameter counting. --- routes_test.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ tree_test.go | 66 ++++++++++++++++++++++++++++--- 2 files changed, 165 insertions(+), 5 deletions(-) diff --git a/routes_test.go b/routes_test.go index 1cae3fce..2cc39446 100644 --- a/routes_test.go +++ b/routes_test.go @@ -789,3 +789,107 @@ func TestEngineHandleMethodNotAllowedCornerCase(t *testing.T) { w := PerformRequest(r, http.MethodGet, "/base/v1/user/groups") assert.Equal(t, http.StatusNotFound, w.Code) } + +func TestRouterGoogleAIPCustomVerbRoutes(t *testing.T) { + router := New() + + router.POST("/users:batchGet", func(c *Context) { + assert.Equal(t, "/users:batchGet", c.FullPath()) + c.String(http.StatusOK, "batch-get") + }) + router.POST("/users:go", func(c *Context) { + assert.Equal(t, "/users:go", c.FullPath()) + assert.Empty(t, c.Param("go")) + c.String(http.StatusOK, "go") + }) + router.POST("/users:batchCreate", func(c *Context) { + assert.Equal(t, "/users:batchCreate", c.FullPath()) + c.String(http.StatusOK, "batch-create") + }) + + w := PerformRequest(router, http.MethodPost, "/users:batchGet") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "batch-get", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/users:batchCreate") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "batch-create", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/users:go") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "go", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/users:anything") + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestRouterGoogleAIPCustomVerbAfterParam(t *testing.T) { + router := New() + + router.POST("/customers/:customer_id", func(c *Context) { + assert.Equal(t, "/customers/:customer_id", c.FullPath()) + assert.Equal(t, "123", c.Param("customer_id")) + c.String(http.StatusOK, "get") + }) + router.POST("/customers/:customer_id:mutate", func(c *Context) { + assert.Equal(t, "/customers/:customer_id:mutate", c.FullPath()) + assert.Equal(t, "123", c.Param("customer_id")) + c.String(http.StatusOK, "mutate") + }) + router.POST("/customers/:customer_id:mutate/static", func(c *Context) { + assert.Equal(t, "/customers/:customer_id:mutate/static", c.FullPath()) + assert.Equal(t, "123", c.Param("customer_id")) + c.String(http.StatusOK, "static") + }) + router.POST("/customers/:customer_id:mutate/:name", func(c *Context) { + assert.Equal(t, "/customers/:customer_id:mutate/:name", c.FullPath()) + assert.Equal(t, "123", c.Param("customer_id")) + c.String(http.StatusOK, c.Param("name")) + }) + router.POST("/customers/:customer_id/devices", func(c *Context) { + assert.Equal(t, "/customers/:customer_id/devices", c.FullPath()) + assert.Equal(t, "123", c.Param("customer_id")) + c.String(http.StatusOK, "devices") + }) + + w := PerformRequest(router, http.MethodPost, "/customers/123") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "get", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/customers/123:mutate") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "mutate", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/customers/123:mutate/static") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "static", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/customers/123:mutate/details") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "details", w.Body.String()) + + w = PerformRequest(router, http.MethodPost, "/customers/123/devices") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "devices", w.Body.String()) +} + +func TestRouterPrefixParamRoutesRemainSupported(t *testing.T) { + router := New() + + router.GET("/id:id", func(c *Context) { + assert.Equal(t, "/id:id", c.FullPath()) + c.String(http.StatusOK, c.Param("id")) + }) + router.GET("/v:version", func(c *Context) { + assert.Equal(t, "/v:version", c.FullPath()) + c.String(http.StatusOK, c.Param("version")) + }) + + w := PerformRequest(router, http.MethodGet, "/id123") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "123", w.Body.String()) + + w = PerformRequest(router, http.MethodGet, "/v1") + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "1", w.Body.String()) +} diff --git a/tree_test.go b/tree_test.go index 23339af4..527b982f 100644 --- a/tree_test.go +++ b/tree_test.go @@ -93,6 +93,18 @@ func TestCountParams(t *testing.T) { if countParams("/path/:param1/static/*catch-all") != 2 { t.Fail() } + if countParams("/users:batchGet") != 0 { + t.Fail() + } + if countParams("/users:go") != 0 { + t.Fail() + } + if countParams("/v:version") != 1 { + t.Fail() + } + if countParams("/customers/:customer_id:mutate") != 1 { + t.Fail() + } if countParams(strings.Repeat("/:param", 256)) != 256 { t.Fail() } @@ -496,8 +508,6 @@ func TestEmptyWildcardName(t *testing.T) { tree := &node{} routes := [...]string{ - "/user:", - "/user:/", "/cmd/:/", "/src/*", } @@ -540,9 +550,9 @@ func TestTreeDoubleWildcard(t *testing.T) { const panicMsg = "only one wildcard per path segment is allowed" routes := [...]string{ - "/:foo:bar", - "/:foo:bar/", "/:foo*bar", + "/:foo*bar:baz", + "/*foo:bar", } for _, route := range routes { @@ -557,6 +567,49 @@ func TestTreeDoubleWildcard(t *testing.T) { } } +func TestTreeCustomVerb(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/user:", + "/user:/", + "/users:batchGet", + "/users:batchCreate", + "/customers/:customer_id", + "/customers/:customer_id:mutate", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + checkRequests(t, tree, testRequests{ + {"/user:", false, "/user:", nil}, + {"/user:/", false, "/user:/", nil}, + {"/users:batchGet", false, "/users:batchGet", nil}, + {"/users:batchCreate", false, "/users:batchCreate", nil}, + {"/customers/123", false, "/customers/:customer_id", Params{Param{Key: "customer_id", Value: "123"}}}, + {"/customers/123:mutate", false, "/customers/:customer_id:mutate", Params{Param{Key: "customer_id", Value: "123"}}}, + {"/customers/123:mutatex", false, "/customers/:customer_id", Params{Param{Key: "customer_id", Value: "123:mutatex"}}}, + }) +} + +func TestTreeCustomVerbBeforeBaseParam(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/customers/:customer_id:mutate", + "/customers/:customer_id", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + checkRequests(t, tree, testRequests{ + {"/customers/123", false, "/customers/:customer_id", Params{Param{Key: "customer_id", Value: "123"}}}, + {"/customers/123:mutate", false, "/customers/:customer_id:mutate", Params{Param{Key: "customer_id", Value: "123"}}}, + }) +} + /*func TestTreeDuplicateWildcard(t *testing.T) { tree := &node{} routes := [...]string{ @@ -721,6 +774,8 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) { "/hi", "/b/", "/ABC/", + "/users:batchGet", + "/customers/:customer_id:mutate", "/search/:query", "/cmd/:tool/", "/src/*filepath", @@ -798,6 +853,8 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) { {"/aBc/", "/ABC/", true, false}, {"/abC", "/ABC/", true, true}, {"/abC/", "/ABC/", true, false}, + {"/USERS:BATCHGET", "/users:batchGet", true, false}, + {"/USERS:BATCHGET/", "/users:batchGet", true, true}, {"/SEARCH/QUERY", "/search/QUERY", true, false}, {"/SEARCH/QUERY/", "/search/QUERY", true, true}, {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, @@ -949,7 +1006,6 @@ func TestTreeWildcardConflictEx(t *testing.T) { {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, - {"/con:nection", ":nection", `/con:tact`, `:tact`}, } for _, conflict := range conflicts {