代码拉取完成,页面将自动刷新
package web
import (
"fmt"
"github.com/stretchr/testify/assert"
"net/http"
"reflect"
"testing"
)
func Test_router_AddRoute(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.MethodPost,
path: "/order/create",
},
{
method: http.MethodPost,
path: "/login",
},
// 通配符测试用例
{
method: http.MethodGet,
path: "/order/*",
},
{
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{
"user": {path: "user", children: map[string]*node{
"home": {path: "home", handler: mockHandler},
}, handler: mockHandler},
"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},
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)
// 非法用例
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 (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 len(n.children) == 0 {
return "", true
}
if n.starChild != nil {
str, ok := n.starChild.equal(y.starChild)
if !ok {
return fmt.Sprintf("%s 通配符节点不匹配 %s", n.path, str), false
}
}
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",
},
// 参数匹配
{
// 命中 /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 HandleFunc) HandleFunc {
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 HandleFunc = 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)),
})
})
}
}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。