1 Star 0 Fork 0

youkelike / web

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
route_test.go 13.72 KB
一键复制 编辑 原始数据 按行查看 历史
youkelike 提交于 2024-04-27 18:29 . test: 添加 route 测试
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
package web
import (
"fmt"
"net/http"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRouteAdd(t *testing.T) {
mockHandler := func(ctx *Context) {}
// 非法用例
r := newRouter()
// 空字符串
assert.PanicsWithValue(t, "web: 路由是空字符串", func() {
r.addRoute(http.MethodGet, "", mockHandler)
})
// 前导没有 /
assert.PanicsWithValue(t, "web: 路由必须以 / 开头", func() {
r.addRoute(http.MethodGet, "a/b/c", mockHandler)
})
// 后缀有 /
assert.PanicsWithValue(t, "web: 路由不能以 / 结尾", func() {
r.addRoute(http.MethodGet, "/a/b/c/", mockHandler)
})
// 根节点重复注册
r.addRoute(http.MethodGet, "/", mockHandler)
assert.PanicsWithValue(t, "web: 路由冲突[/]", func() {
r.addRoute(http.MethodGet, "/", mockHandler)
})
// 普通节点重复注册
r.addRoute(http.MethodGet, "/a/b/c", mockHandler)
assert.PanicsWithValue(t, "web: 路由冲突[/a/b/c]", func() {
r.addRoute(http.MethodGet, "/a/b/c", mockHandler)
})
// 多个 /
assert.PanicsWithValue(t, "web: 非法路由。不允许使用 //a/b, /a//b 形式的路由:[/a//b]", func() {
r.addRoute(http.MethodGet, "/a//b", mockHandler)
})
assert.PanicsWithValue(t, "web: 非法路由。不允许使用 //a/b, /a//b 形式的路由:[//a/b]", func() {
r.addRoute(http.MethodGet, "//a/b", mockHandler)
})
// 同时注册通配符路由和参数路由
assert.PanicsWithValue(t, "web: 非法路由,已有通配符路由,不允许同时注册参数路由 [:id]", func() {
r.addRoute(http.MethodGet, "/a/*", mockHandler)
r.addRoute(http.MethodGet, "/a/:id", mockHandler)
})
assert.PanicsWithValue(t, "web: 非法路由。已有路径参数路由,不允许同时注册通配符路由 [*]", func() {
r.addRoute(http.MethodGet, "/a/b/:id", mockHandler)
r.addRoute(http.MethodGet, "/a/b/*", mockHandler)
})
r = newRouter()
assert.PanicsWithValue(t, "web: 非法路由,已有通配符路由,不允许同时注册参数路由 [:id]", func() {
r.addRoute(http.MethodGet, "/*", mockHandler)
r.addRoute(http.MethodGet, "/:id", mockHandler)
})
r = newRouter()
assert.PanicsWithValue(t, "web: 非法路由。已有路径参数路由,不允许同时注册通配符路由 [*]", func() {
r.addRoute(http.MethodGet, "/:id", mockHandler)
r.addRoute(http.MethodGet, "/*", mockHandler)
})
// 参数冲突
assert.PanicsWithValue(t, "web: 参数路由冲突,已有 [:id],新注册 [:name]", func() {
r.addRoute(http.MethodGet, "/a/b/c/:id", mockHandler)
r.addRoute(http.MethodGet, "/a/b/c/:name", mockHandler)
})
}
func Test_router_AddRoute_static(t *testing.T) {
testRoutes := []struct {
method string
path string
}{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodGet,
path: "/user/home",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodGet,
path: "/order/list",
},
{
method: http.MethodPost,
path: "/order/create",
},
{
method: http.MethodPost,
path: "/login",
},
}
mockHandler := func(ctx *Context) {}
r := newRouter()
for _, tr := range testRoutes {
r.addRoute(tr.method, tr.path, mockHandler)
}
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: {
path: "/",
children: map[string]*node{
"user": {path: "user", children: map[string]*node{
"home": {path: "home", handler: mockHandler},
}, handler: mockHandler},
"order": {path: "order", children: map[string]*node{
"list": {path: "list", handler: mockHandler},
"detail": {path: "detail", handler: mockHandler},
}},
},
handler: mockHandler,
},
http.MethodPost: {
path: "/",
children: map[string]*node{
"order": {
path: "order",
children: map[string]*node{
"create": {path: "create", handler: mockHandler},
},
},
"login": {path: "login", handler: mockHandler},
},
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
func Test_router_AddRoute_nonstatic(t *testing.T) {
testRoutes := []struct {
method string
path string
}{
// 通配符测试用例
{
method: http.MethodGet,
path: "/order/*",
},
{
method: http.MethodGet,
path: "/order/detail",
},
{
method: http.MethodGet,
path: "/*",
},
{
method: http.MethodGet,
path: "/*/*",
},
{
method: http.MethodGet,
path: "/*/abc",
},
{
method: http.MethodGet,
path: "/*/abc/*",
},
// 参数路由
{
method: http.MethodGet,
path: "/param/:id",
},
{
method: http.MethodGet,
path: "/param/:id/detail",
},
{
method: http.MethodGet,
path: "/param/:id/*",
},
}
mockHandler := func(ctx *Context) {}
r := newRouter()
for _, tr := range testRoutes {
r.addRoute(tr.method, tr.path, mockHandler)
}
wantRouter := &router{
trees: map[string]*node{
http.MethodGet: {
path: "/",
children: map[string]*node{
"order": {
path: "order",
children: map[string]*node{
"detail": {path: "detail", handler: mockHandler},
},
starChild: &node{path: "*", handler: mockHandler},
},
"param": {
path: "param",
paramChild: &node{
path: ":id",
starChild: &node{
path: "*",
handler: mockHandler,
},
children: map[string]*node{"detail": {path: "detail", handler: mockHandler}},
handler: mockHandler,
},
},
},
starChild: &node{
path: "*",
children: map[string]*node{
"abc": {
path: "abc",
starChild: &node{path: "*", handler: mockHandler},
handler: mockHandler},
},
starChild: &node{path: "*", handler: mockHandler},
handler: mockHandler,
},
},
},
}
msg, ok := wantRouter.equal(r)
assert.True(t, ok, msg)
}
func (r router) equal(y router) (string, bool) {
for k, v := range r.trees {
yv, ok := y.trees[k]
if !ok {
return fmt.Sprintf("目标 router 里面没有方法 %s 的路由树", k), false
}
str, ok := v.equal(yv)
if !ok {
return k + "-" + str, ok
}
}
return "", true
}
func (n *node) equal(y *node) (string, bool) {
if y == nil {
return "目标节点为 nil", false
}
if n.path != y.path {
return fmt.Sprintf("%s 节点 path 不相等 x %s, y %s", n.path, n.path, y.path), false
}
nhv := reflect.ValueOf(n.handler)
yhv := reflect.ValueOf(y.handler)
if nhv != yhv {
return fmt.Sprintf("%s 节点 handler 不相等 x %s, y %s", n.path, nhv.Type().String(), yhv.Type().String()), false
}
if len(n.children) != len(y.children) {
return fmt.Sprintf("%s 子节点长度不等", n.path), false
}
if n.starChild != nil {
str, ok := n.starChild.equal(y.starChild)
if !ok {
return fmt.Sprintf("%s 通配符节点不匹配 %s", n.path, str), false
}
}
if n.paramChild != nil {
str, ok := n.paramChild.equal(y.paramChild)
if !ok {
return fmt.Sprintf("%s param 节点不匹配 %s", n.path, str), false
}
}
if n.regChild != nil {
str, ok := n.regChild.equal(y.regChild)
if !ok {
return fmt.Sprintf("%s 正则节点不匹配 %s", n.path, str), false
}
}
if len(n.children) == 0 {
return "", true
}
for k, v := range n.children {
yv, ok := y.children[k]
if !ok {
return fmt.Sprintf("%s 目标节点缺少子节点 %s", n.path, k), false
}
str, ok := v.equal(yv)
if !ok {
return n.path + "-" + str, ok
}
}
return "", true
}
func Test_router_findRoute(t *testing.T) {
testRoutes := []struct {
method string
path string
}{
{
method: http.MethodGet,
path: "/",
},
{
method: http.MethodGet,
path: "/user",
},
{
method: http.MethodPost,
path: "/order/create",
},
{
method: http.MethodGet,
path: "/user/*/home",
},
{
method: http.MethodPost,
path: "/order/*",
},
// 参数路由
{
method: http.MethodGet,
path: "/param/:id",
},
{
method: http.MethodGet,
path: "/param/:id/detail",
},
{
method: http.MethodGet,
path: "/param/:id/*",
},
}
mockHandler := func(ctx *Context) {}
testCases := []struct {
name string
method string
path string
found bool
mi *matchInfo
}{
{
name: "method not found",
method: http.MethodHead,
},
{
name: "path not found",
method: http.MethodGet,
path: "/abc",
},
{
name: "root",
method: http.MethodGet,
path: "/",
found: true,
mi: &matchInfo{
n: &node{
path: "/",
handler: mockHandler,
},
},
},
{
name: "user",
method: http.MethodGet,
path: "/user",
found: true,
mi: &matchInfo{
n: &node{
path: "user",
handler: mockHandler,
},
},
},
{
name: "no handler",
method: http.MethodPost,
path: "/order",
found: true,
mi: &matchInfo{
n: &node{
path: "order",
},
},
},
{
name: "two layer",
method: http.MethodPost,
path: "/order/create",
found: true,
mi: &matchInfo{
n: &node{
path: "create",
handler: mockHandler,
},
},
},
// 通配符匹配
{
// 命中/order/*
name: "star match",
method: http.MethodPost,
path: "/order/delete",
found: true,
mi: &matchInfo{
n: &node{
path: "*",
handler: mockHandler,
},
},
},
{
// 命中通配符在中间的
// /user/*/home
name: "star in middle",
method: http.MethodGet,
path: "/user/Tom/home",
found: true,
mi: &matchInfo{
n: &node{
path: "home",
handler: mockHandler,
},
},
},
{
// 比 /order/* 多了一段
name: "overflow",
method: http.MethodPost,
path: "/order/delete/123",
found: true,
mi: &matchInfo{
n: &node{
path: "*",
handler: mockHandler,
},
},
},
// 参数匹配
{
// 命中 /param/:id
name: ":id",
method: http.MethodGet,
path: "/param/123",
found: true,
mi: &matchInfo{
n: &node{
path: ":id",
handler: mockHandler,
},
pathParams: map[string]string{"id": "123"},
},
},
{
// 命中 /param/:id/*
name: ":id*",
method: http.MethodGet,
path: "/param/123/abc",
found: true,
mi: &matchInfo{
n: &node{
path: "*",
handler: mockHandler,
},
pathParams: map[string]string{"id": "123"},
},
},
{
// 命中 /param/:id/detail
name: ":id*",
method: http.MethodGet,
path: "/param/123/detail",
found: true,
mi: &matchInfo{
n: &node{
path: "detail",
handler: mockHandler,
},
pathParams: map[string]string{"id": "123"},
},
},
}
r := newRouter()
for _, tr := range testRoutes {
r.addRoute(tr.method, tr.path, mockHandler)
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mi, found := r.findRoute(tc.method, tc.path)
assert.Equal(t, tc.found, found)
if !found {
return
}
assert.Equal(t, tc.mi.pathParams, mi.pathParams)
n := mi.n
wantVal := reflect.ValueOf(tc.mi.n.handler)
nVal := reflect.ValueOf(n.handler)
assert.Equal(t, wantVal, nVal)
})
}
}
func Test_findRoute_Middleware(t *testing.T) {
var mdlBuilder = func(i byte) Middleware {
return func(next HandlerFunc) HandlerFunc {
return func(ctx *Context) {
ctx.RespData = append(ctx.RespData, i)
next(ctx)
}
}
}
mdlsRoute := []struct {
method string
path string
mdls []Middleware
}{
{
method: http.MethodGet,
path: "/a/b",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('b')},
},
{
method: http.MethodGet,
path: "/a/*",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('*')},
},
{
method: http.MethodGet,
path: "/a/b/*",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('b'), mdlBuilder('*')},
},
{
method: http.MethodPost,
path: "/a/b/*",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('b'), mdlBuilder('*')},
},
{
method: http.MethodPost,
path: "/a/*/c",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('*'), mdlBuilder('c')},
},
{
method: http.MethodPost,
path: "/a/b/c",
mdls: []Middleware{mdlBuilder('a'), mdlBuilder('b'), mdlBuilder('c')},
},
{
method: http.MethodDelete,
path: "/*",
mdls: []Middleware{mdlBuilder('*')},
},
{
method: http.MethodDelete,
path: "/",
mdls: []Middleware{mdlBuilder('/')},
},
}
r := newRouter()
for _, mdlRoute := range mdlsRoute {
r.addRoute(mdlRoute.method, mdlRoute.path, nil, mdlRoute.mdls...)
}
testCases := []struct {
name string
method string
path string
// 我们借助 ctx 里面的 RespData 字段来判断 middleware 有没有按照预期执行
wantResp string
}{
{
name: "static, not match",
method: http.MethodGet,
path: "/a",
},
{
name: "static, match",
method: http.MethodGet,
path: "/a/c",
wantResp: "a*",
},
{
name: "static and star",
method: http.MethodGet,
path: "/a/b",
wantResp: "a*ab",
},
{
name: "static and star",
method: http.MethodGet,
path: "/a/b/c",
wantResp: "a*abab*",
},
{
name: "abc",
method: http.MethodPost,
path: "/a/b/c",
wantResp: "a*cab*abc",
},
{
name: "root",
method: http.MethodDelete,
path: "/",
wantResp: "/",
},
{
name: "root star",
method: http.MethodDelete,
path: "/a",
wantResp: "/*",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mi, _ := r.findRoute(tc.method, tc.path)
mdls := mi.mdls
var root HandlerFunc = func(ctx *Context) {
// 使用 string 可读性比较高
assert.Equal(t, tc.wantResp, string(ctx.RespData))
}
for i := len(mdls) - 1; i >= 0; i-- {
root = mdls[i](root)
}
// 开始调度
root(&Context{
RespData: make([]byte, 0, len(tc.wantResp)),
})
})
}
}
Go
1
https://gitee.com/youkelike/web.git
git@gitee.com:youkelike/web.git
youkelike
web
web
master

搜索帮助