github.com/cloudwego/kitex@v0.9.0/pkg/generic/descriptor/tree_test.go (about)

     1  /*
     2   * Copyright 2013 Julien Schmidt. All rights reserved.
     3   * Use of this source code is governed by a BSD-style license that can be found
     4   * in the LICENSE file.
     5   *
     6   * This file may have been modified by CloudWeGo authors. All CloudWeGo
     7   * Modifications are Copyright 2021 CloudWeGo Authors.
     8   */
     9  
    10  package descriptor
    11  
    12  import (
    13  	"reflect"
    14  	"strings"
    15  	"testing"
    16  )
    17  
    18  func fakeHandler(val string) *FunctionDescriptor {
    19  	return &FunctionDescriptor{Name: val}
    20  }
    21  
    22  type testRequests []struct {
    23  	path       string
    24  	nilHandler bool
    25  	route      string
    26  	ps         *Params
    27  }
    28  
    29  func getParams() *Params {
    30  	return &Params{
    31  		params: make([]Param, 0, 20),
    32  	}
    33  }
    34  
    35  func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
    36  	unescape := false
    37  	if len(unescapes) >= 1 {
    38  		unescape = unescapes[0]
    39  	}
    40  	for _, request := range requests {
    41  		handler, psp, _ := tree.getValue(request.path, getParams, unescape)
    42  
    43  		switch {
    44  		case handler == nil:
    45  			if !request.nilHandler {
    46  				t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path)
    47  			}
    48  		case request.nilHandler:
    49  			t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path)
    50  		default:
    51  			if handler.Name != request.route {
    52  				t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, handler.Name, request.route)
    53  			}
    54  		}
    55  
    56  		var ps *Params
    57  		if psp != nil {
    58  			ps = psp
    59  		}
    60  
    61  		if !reflect.DeepEqual(ps, request.ps) {
    62  			t.Errorf("Params mismatch for route '%s'", request.path)
    63  		}
    64  	}
    65  }
    66  
    67  func TestCountParams(t *testing.T) {
    68  	if countParams("/path/:param1/static/*catch-all") != 2 {
    69  		t.Fail()
    70  	}
    71  	if countParams(strings.Repeat("/:param", 256)) != 256 {
    72  		t.Fail()
    73  	}
    74  }
    75  
    76  func TestNoFunction(t *testing.T) {
    77  	tree := &node{}
    78  
    79  	route := "/hi"
    80  	recv := catchPanic(func() {
    81  		tree.addRoute(route, nil)
    82  	})
    83  	if recv == nil {
    84  		t.Fatalf("no panic while inserting route with empty function '%s", route)
    85  	}
    86  }
    87  
    88  func TestEmptyPath(t *testing.T) {
    89  	tree := &node{}
    90  
    91  	routes := [...]string{
    92  		"",
    93  		"user",
    94  		":user",
    95  		"*user",
    96  	}
    97  	for _, route := range routes {
    98  		recv := catchPanic(func() {
    99  			tree.addRoute(route, nil)
   100  		})
   101  		if recv == nil {
   102  			t.Fatalf("no panic while inserting route with empty path '%s", route)
   103  		}
   104  	}
   105  }
   106  
   107  func TestTreeAddAndGet(t *testing.T) {
   108  	tree := &node{}
   109  
   110  	routes := [...]string{
   111  		"/hi",
   112  		"/contact",
   113  		"/co",
   114  		"/c",
   115  		"/a",
   116  		"/ab",
   117  		"/doc/",
   118  		"/doc/go_faq.html",
   119  		"/doc/go1.html",
   120  		"/α",
   121  		"/β",
   122  	}
   123  	for _, route := range routes {
   124  		tree.addRoute(route, fakeHandler(route))
   125  	}
   126  
   127  	checkRequests(t, tree, testRequests{
   128  		{"", true, "", nil},
   129  		{"a", true, "", nil},
   130  		{"/a", false, "/a", nil},
   131  		{"/", true, "", nil},
   132  		{"/hi", false, "/hi", nil},
   133  		{"/contact", false, "/contact", nil},
   134  		{"/co", false, "/co", nil},
   135  		{"/con", true, "", nil},  // key mismatch
   136  		{"/cona", true, "", nil}, // key mismatch
   137  		{"/no", true, "", nil},   // no matching child
   138  		{"/ab", false, "/ab", nil},
   139  		{"/α", false, "/α", nil},
   140  		{"/β", false, "/β", nil},
   141  	})
   142  }
   143  
   144  func TestTreeWildcard(t *testing.T) {
   145  	tree := &node{}
   146  
   147  	routes := [...]string{
   148  		"/",
   149  		"/cmd/:tool/:sub",
   150  		"/cmd/:tool/",
   151  		"/cmd/xxx/",
   152  		"/src/*filepath",
   153  		"/search/",
   154  		"/search/:query",
   155  		"/user_:name",
   156  		"/user_:name/about",
   157  		"/files/:dir/*filepath",
   158  		"/doc/",
   159  		"/doc/go_faq.html",
   160  		"/doc/go1.html",
   161  		"/info/:user/public",
   162  		"/info/:user/project/:project",
   163  		"/a/b/:c",
   164  		"/a/:b/c/d",
   165  		"/a/*b",
   166  	}
   167  	for _, route := range routes {
   168  		tree.addRoute(route, fakeHandler(route))
   169  	}
   170  
   171  	checkRequests(t, tree, testRequests{
   172  		{"/", false, "/", nil},
   173  		{"/cmd/test/", false, "/cmd/:tool/", &Params{params: []Param{{"tool", "test"}}}},
   174  		{"/cmd/test", true, "", &Params{params: []Param{}}},
   175  		{"/cmd/test/3", false, "/cmd/:tool/:sub", &Params{params: []Param{{"tool", "test"}, {"sub", "3"}}}},
   176  		{"/src/", false, "/src/*filepath", &Params{params: []Param{{"filepath", ""}}}},
   177  		{"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}},
   178  		{"/search/", false, "/search/", nil},
   179  		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng+in+ünìcodé"}}}},
   180  		{"/search/someth!ng+in+ünìcodé/", true, "", &Params{params: []Param{}}},
   181  		{"/user_gopher", false, "/user_:name", &Params{params: []Param{{"name", "gopher"}}}},
   182  		{"/user_gopher/about", false, "/user_:name/about", &Params{params: []Param{{"name", "gopher"}}}},
   183  		{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", &Params{params: []Param{{"dir", "js"}, {"filepath", "inc/framework.js"}}}},
   184  		{"/info/gordon/public", false, "/info/:user/public", &Params{params: []Param{{"user", "gordon"}}}},
   185  		{"/info/gordon/project/go", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "gordon"}, {"project", "go"}}}},
   186  		{"/a/b/c", false, "/a/b/:c", &Params{params: []Param{{Key: "c", Value: "c"}}}},
   187  		{"/a/b/c/d", false, "/a/:b/c/d", &Params{params: []Param{{Key: "b", Value: "b"}}}},
   188  		{"/a/b", false, "/a/*b", &Params{params: []Param{{Key: "b", Value: "b"}}}},
   189  	})
   190  }
   191  
   192  func TestUnescapeParameters(t *testing.T) {
   193  	tree := &node{}
   194  
   195  	routes := [...]string{
   196  		"/",
   197  		"/cmd/:tool/:sub",
   198  		"/cmd/:tool/",
   199  		"/src/*filepath",
   200  		"/search/:query",
   201  		"/files/:dir/*filepath",
   202  		"/info/:user/project/:project",
   203  		"/info/:user",
   204  	}
   205  	for _, route := range routes {
   206  		tree.addRoute(route, fakeHandler(route))
   207  	}
   208  
   209  	unescape := true
   210  	checkRequests(t, tree, testRequests{
   211  		{"/", false, "/", nil},
   212  		{"/cmd/test/", false, "/cmd/:tool/", &Params{params: []Param{{"tool", "test"}}}},
   213  		{"/cmd/test", true, "", &Params{params: []Param{}}},
   214  		{"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}},
   215  		{"/src/some/file+test.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file test.png"}}}},
   216  		{"/src/some/file++++%%%%test.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file++++%%%%test.png"}}}},
   217  		{"/src/some/file%2Ftest.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file/test.png"}}}},
   218  		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng in ünìcodé"}}}},
   219  		{"/info/gordon/project/go", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "gordon"}, {"project", "go"}}}},
   220  		{"/info/slash%2Fgordon", false, "/info/:user", &Params{params: []Param{{"user", "slash/gordon"}}}},
   221  		{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "slash/gordon"}, {"project", "Project #1"}}}},
   222  		{"/info/slash%%%%", false, "/info/:user", &Params{params: []Param{{"user", "slash%%%%"}}}},
   223  		{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", &Params{params: []Param{{"user", "slash%%%%2Fgordon"}, {"project", "Project%%%%20%231"}}}},
   224  	}, unescape)
   225  }
   226  
   227  func catchPanic(testFunc func()) (recv interface{}) {
   228  	defer func() {
   229  		recv = recover()
   230  	}()
   231  
   232  	testFunc()
   233  	return
   234  }
   235  
   236  type testRoute struct {
   237  	path     string
   238  	conflict bool
   239  }
   240  
   241  func testRoutes(t *testing.T, routes []testRoute) {
   242  	tree := &node{}
   243  
   244  	for i := range routes {
   245  		route := routes[i]
   246  		recv := catchPanic(func() {
   247  			tree.addRoute(route.path, fakeHandler(route.path))
   248  		})
   249  
   250  		if route.conflict {
   251  			if recv == nil {
   252  				t.Errorf("no panic for conflicting route '%s'", route.path)
   253  			}
   254  		} else if recv != nil {
   255  			t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
   256  		}
   257  	}
   258  }
   259  
   260  func TestTreeWildcardConflict(t *testing.T) {
   261  	routes := []testRoute{
   262  		{"/cmd/:tool/:sub", false},
   263  		{"/cmd/vet", false},
   264  		{"/src/*filepath", false},
   265  		{"/src/*filepathx", true},
   266  		{"/src/", false},
   267  		{"/src1/", false},
   268  		{"/src1/*filepath", false},
   269  		{"/src2*filepath", true},
   270  		{"/search/:query", false},
   271  		{"/search/invalid", false},
   272  		{"/user_:name", false},
   273  		{"/user_x", false},
   274  		{"/user_:name", true},
   275  		{"/id:id", false},
   276  		{"/id/:id", false},
   277  	}
   278  	testRoutes(t, routes)
   279  }
   280  
   281  func TestTreeChildConflict(t *testing.T) {
   282  	routes := []testRoute{
   283  		{"/cmd/vet", false},
   284  		{"/cmd/:tool/:sub", false},
   285  		{"/src/AUTHORS", false},
   286  		{"/src/*filepath", false},
   287  		{"/user_x", false},
   288  		{"/user_:name", false},
   289  		{"/id/:id", false},
   290  		{"/id:id", false},
   291  		{"/:id", false},
   292  		{"/*filepath", false},
   293  	}
   294  	testRoutes(t, routes)
   295  }
   296  
   297  func TestTreeDuplicatePath(t *testing.T) {
   298  	tree := &node{}
   299  
   300  	routes := [...]string{
   301  		"/",
   302  		"/doc/",
   303  		"/src/*filepath",
   304  		"/search/:query",
   305  		"/user_:name",
   306  	}
   307  	for i := range routes {
   308  		route := routes[i]
   309  		recv := catchPanic(func() {
   310  			tree.addRoute(route, fakeHandler(route))
   311  		})
   312  		if recv != nil {
   313  			t.Fatalf("panic inserting route '%s': %v", route, recv)
   314  		}
   315  
   316  		// Add again
   317  		recv = catchPanic(func() {
   318  			tree.addRoute(route, fakeHandler(route))
   319  		})
   320  		if recv == nil {
   321  			t.Fatalf("no panic while inserting duplicate route '%s", route)
   322  		}
   323  	}
   324  
   325  	checkRequests(t, tree, testRequests{
   326  		{"/", false, "/", nil},
   327  		{"/doc/", false, "/doc/", nil},
   328  		{"/src/some/file.png", false, "/src/*filepath", &Params{params: []Param{{"filepath", "some/file.png"}}}},
   329  		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", &Params{params: []Param{{"query", "someth!ng+in+ünìcodé"}}}},
   330  		{"/user_gopher", false, "/user_:name", &Params{params: []Param{{"name", "gopher"}}}},
   331  	})
   332  }
   333  
   334  func TestEmptyWildcardName(t *testing.T) {
   335  	tree := &node{}
   336  
   337  	routes := [...]string{
   338  		"/user:",
   339  		"/user:/",
   340  		"/cmd/:/",
   341  		"/src/*",
   342  	}
   343  	for i := range routes {
   344  		route := routes[i]
   345  		recv := catchPanic(func() {
   346  			tree.addRoute(route, nil)
   347  		})
   348  		if recv == nil {
   349  			t.Fatalf("no panic while inserting route with empty wildcard name '%s", route)
   350  		}
   351  	}
   352  }
   353  
   354  func TestTreeCatchAllConflict(t *testing.T) {
   355  	routes := []testRoute{
   356  		{"/src/*filepath/x", true},
   357  		{"/src2/", false},
   358  		{"/src2/*filepath/x", true},
   359  		{"/src3/*filepath", false},
   360  		{"/src3/*filepath/x", true},
   361  	}
   362  	testRoutes(t, routes)
   363  }
   364  
   365  func TestTreeCatchMaxParams(t *testing.T) {
   366  	tree := &node{}
   367  	route := "/cmd/*filepath"
   368  	tree.addRoute(route, fakeHandler(route))
   369  }
   370  
   371  func TestTreeDoubleWildcard(t *testing.T) {
   372  	const panicMsg = "only one wildcard per path segment is allowed"
   373  
   374  	routes := [...]string{
   375  		"/:foo:bar",
   376  		"/:foo:bar/",
   377  		"/:foo*bar",
   378  	}
   379  
   380  	for i := range routes {
   381  		route := routes[i]
   382  		tree := &node{}
   383  		recv := catchPanic(func() {
   384  			tree.addRoute(route, nil)
   385  		})
   386  
   387  		if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) {
   388  			t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv)
   389  		}
   390  	}
   391  }
   392  
   393  func TestTreeTrailingSlashRedirect(t *testing.T) {
   394  	tree := &node{}
   395  
   396  	routes := [...]string{
   397  		"/hi",
   398  		"/b/",
   399  		"/search/:query",
   400  		"/cmd/:tool/",
   401  		"/src/*filepath",
   402  		"/x",
   403  		"/x/y",
   404  		"/y/",
   405  		"/y/z",
   406  		"/0/:id",
   407  		"/0/:id/1",
   408  		"/1/:id/",
   409  		"/1/:id/2",
   410  		"/aa",
   411  		"/a/",
   412  		"/admin",
   413  		"/admin/:category",
   414  		"/admin/:category/:page",
   415  		"/doc",
   416  		"/doc/go_faq.html",
   417  		"/doc/go1.html",
   418  		"/no/a",
   419  		"/no/b",
   420  		"/api/hello/:name",
   421  		"/user/:name/*id",
   422  		"/resource",
   423  		"/r/*id",
   424  		"/book/biz/:name",
   425  		"/book/biz/abc",
   426  		"/book/biz/abc/bar",
   427  		"/book/:page/:name",
   428  		"/book/hello/:name/biz/",
   429  	}
   430  	for i := range routes {
   431  		route := routes[i]
   432  		recv := catchPanic(func() {
   433  			tree.addRoute(route, fakeHandler(route))
   434  		})
   435  		if recv != nil {
   436  			t.Fatalf("panic inserting route '%s': %v", route, recv)
   437  		}
   438  	}
   439  
   440  	tsrRoutes := [...]string{
   441  		"/hi/",
   442  		"/b",
   443  		"/search/gopher/",
   444  		"/cmd/vet",
   445  		"/src",
   446  		"/x/",
   447  		"/y",
   448  		"/0/go/",
   449  		"/1/go",
   450  		"/a",
   451  		"/admin/",
   452  		"/admin/config/",
   453  		"/admin/config/permissions/",
   454  		"/doc/",
   455  		"/user/name",
   456  		"/r",
   457  		"/book/hello/a/biz",
   458  		"/book/biz/foo/",
   459  		"/book/biz/abc/bar/",
   460  	}
   461  	for _, route := range tsrRoutes {
   462  		handler, _, tsr := tree.getValue(route, getParams, false)
   463  		if handler != nil {
   464  			t.Fatalf("non-nil handler for TSR route '%s", route)
   465  		} else if !tsr {
   466  			t.Errorf("expected TSR recommendation for route '%s'", route)
   467  		}
   468  	}
   469  
   470  	noTsrRoutes := [...]string{
   471  		"/",
   472  		"/no",
   473  		"/no/",
   474  		"/_",
   475  		"/_/",
   476  		"/api/world/abc",
   477  		"/book",
   478  		"/book/",
   479  		"/book/hello/a/abc",
   480  		"/book/biz/abc/biz",
   481  	}
   482  	for _, route := range noTsrRoutes {
   483  		handler, _, tsr := tree.getValue(route, getParams, false)
   484  		if handler != nil {
   485  			t.Fatalf("non-nil handler for No-TSR route '%s", route)
   486  		} else if tsr {
   487  			t.Errorf("expected no TSR recommendation for route '%s'", route)
   488  		}
   489  	}
   490  }
   491  
   492  func TestTreeTrailingSlashRedirect2(t *testing.T) {
   493  	tree := &node{}
   494  
   495  	routes := [...]string{
   496  		"/api/:version/seller/locales/get",
   497  		"/api/v:version/seller/permissions/get",
   498  		"/api/v:version/seller/university/entrance_knowledge_list/get",
   499  	}
   500  	for _, route := range routes {
   501  		recv := catchPanic(func() {
   502  			tree.addRoute(route, fakeHandler(route))
   503  		})
   504  		if recv != nil {
   505  			t.Fatalf("panic inserting route '%s': %v", route, recv)
   506  		}
   507  	}
   508  
   509  	tsrRoutes := [...]string{
   510  		"/api/v:version/seller/permissions/get/",
   511  		"/api/version/seller/permissions/get/",
   512  	}
   513  
   514  	for _, route := range tsrRoutes {
   515  		handler, _, tsr := tree.getValue(route, getParams, false)
   516  		if handler != nil {
   517  			t.Fatalf("non-nil handler for TSR route '%s", route)
   518  		} else if !tsr {
   519  			t.Errorf("expected TSR recommendation for route '%s'", route)
   520  		}
   521  	}
   522  
   523  	noTsrRoutes := [...]string{
   524  		"/api/v:version/seller/permissions/get/a",
   525  	}
   526  	for _, route := range noTsrRoutes {
   527  		handler, _, tsr := tree.getValue(route, getParams, false)
   528  		if handler != nil {
   529  			t.Fatalf("non-nil handler for No-TSR route '%s", route)
   530  		} else if tsr {
   531  			t.Errorf("expected no TSR recommendation for route '%s'", route)
   532  		}
   533  	}
   534  }
   535  
   536  func TestTreeRootTrailingSlashRedirect(t *testing.T) {
   537  	tree := &node{}
   538  
   539  	recv := catchPanic(func() {
   540  		tree.addRoute("/:test", fakeHandler("/:test"))
   541  	})
   542  	if recv != nil {
   543  		t.Fatalf("panic inserting test route: %v", recv)
   544  	}
   545  
   546  	handler, _, tsr := tree.getValue("/", nil, false)
   547  	if handler != nil {
   548  		t.Fatalf("non-nil handler")
   549  	} else if tsr {
   550  		t.Errorf("expected no TSR recommendation")
   551  	}
   552  }