Source file src/net/http/routing_tree_test.go

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http
     6  
     7  import (
     8  	"fmt"
     9  	"io"
    10  	"sort"
    11  	"strings"
    12  	"testing"
    13  
    14  	"slices"
    15  )
    16  
    17  func TestRoutingFirstSegment(t *testing.T) {
    18  	for _, test := range []struct {
    19  		in   string
    20  		want []string
    21  	}{
    22  		{"/a/b/c", []string{"a", "b", "c"}},
    23  		{"/a/b/", []string{"a", "b", "/"}},
    24  		{"/", []string{"/"}},
    25  		{"/a/%62/c", []string{"a", "b", "c"}},
    26  		{"/a%2Fb%2fc", []string{"a/b/c"}},
    27  	} {
    28  		var got []string
    29  		rest := test.in
    30  		for len(rest) > 0 {
    31  			var seg string
    32  			seg, rest = firstSegment(rest)
    33  			got = append(got, seg)
    34  		}
    35  		if !slices.Equal(got, test.want) {
    36  			t.Errorf("%q: got %v, want %v", test.in, got, test.want)
    37  		}
    38  	}
    39  }
    40  
    41  // TODO: test host and method
    42  var testTree *routingNode
    43  
    44  func getTestTree() *routingNode {
    45  	if testTree == nil {
    46  		testTree = buildTree("/a", "/a/b", "/a/{x}",
    47  			"/g/h/i", "/g/{x}/j",
    48  			"/a/b/{x...}", "/a/b/{y}", "/a/b/{$}")
    49  	}
    50  	return testTree
    51  }
    52  
    53  func buildTree(pats ...string) *routingNode {
    54  	root := &routingNode{}
    55  	for _, p := range pats {
    56  		pat, err := parsePattern(p)
    57  		if err != nil {
    58  			panic(err)
    59  		}
    60  		root.addPattern(pat, nil)
    61  	}
    62  	return root
    63  }
    64  
    65  func TestRoutingAddPattern(t *testing.T) {
    66  	want := `"":
    67      "":
    68          "a":
    69              "/a"
    70              "":
    71                  "/a/{x}"
    72              "b":
    73                  "/a/b"
    74                  "":
    75                      "/a/b/{y}"
    76                  "*":
    77                      "/a/b/{x...}"
    78                  "/":
    79                      "/a/b/{$}"
    80          "g":
    81              "":
    82                  "j":
    83                      "/g/{x}/j"
    84              "h":
    85                  "i":
    86                      "/g/h/i"
    87  `
    88  
    89  	var b strings.Builder
    90  	getTestTree().print(&b, 0)
    91  	got := b.String()
    92  	if got != want {
    93  		t.Errorf("got\n%s\nwant\n%s", got, want)
    94  	}
    95  }
    96  
    97  type testCase struct {
    98  	method, host, path string
    99  	wantPat            string // "" for nil (no match)
   100  	wantMatches        []string
   101  }
   102  
   103  func TestRoutingNodeMatch(t *testing.T) {
   104  
   105  	test := func(tree *routingNode, tests []testCase) {
   106  		t.Helper()
   107  		for _, test := range tests {
   108  			gotNode, gotMatches := tree.match(test.host, test.method, test.path)
   109  			got := ""
   110  			if gotNode != nil {
   111  				got = gotNode.pattern.String()
   112  			}
   113  			if got != test.wantPat {
   114  				t.Errorf("%s, %s, %s: got %q, want %q", test.host, test.method, test.path, got, test.wantPat)
   115  			}
   116  			if !slices.Equal(gotMatches, test.wantMatches) {
   117  				t.Errorf("%s, %s, %s: got matches %v, want %v", test.host, test.method, test.path, gotMatches, test.wantMatches)
   118  			}
   119  		}
   120  	}
   121  
   122  	test(getTestTree(), []testCase{
   123  		{"GET", "", "/a", "/a", nil},
   124  		{"Get", "", "/b", "", nil},
   125  		{"Get", "", "/a/b", "/a/b", nil},
   126  		{"Get", "", "/a/c", "/a/{x}", []string{"c"}},
   127  		{"Get", "", "/a/b/", "/a/b/{$}", nil},
   128  		{"Get", "", "/a/b/c", "/a/b/{y}", []string{"c"}},
   129  		{"Get", "", "/a/b/c/d", "/a/b/{x...}", []string{"c/d"}},
   130  		{"Get", "", "/g/h/i", "/g/h/i", nil},
   131  		{"Get", "", "/g/h/j", "/g/{x}/j", []string{"h"}},
   132  	})
   133  
   134  	tree := buildTree(
   135  		"/item/",
   136  		"POST /item/{user}",
   137  		"GET /item/{user}",
   138  		"/item/{user}",
   139  		"/item/{user}/{id}",
   140  		"/item/{user}/new",
   141  		"/item/{$}",
   142  		"POST alt.com/item/{user}",
   143  		"GET /headwins",
   144  		"HEAD /headwins",
   145  		"/path/{p...}")
   146  
   147  	test(tree, []testCase{
   148  		{"GET", "", "/item/jba",
   149  			"GET /item/{user}", []string{"jba"}},
   150  		{"POST", "", "/item/jba",
   151  			"POST /item/{user}", []string{"jba"}},
   152  		{"HEAD", "", "/item/jba",
   153  			"GET /item/{user}", []string{"jba"}},
   154  		{"get", "", "/item/jba",
   155  			"/item/{user}", []string{"jba"}}, // method matches are case-sensitive
   156  		{"POST", "", "/item/jba/17",
   157  			"/item/{user}/{id}", []string{"jba", "17"}},
   158  		{"GET", "", "/item/jba/new",
   159  			"/item/{user}/new", []string{"jba"}},
   160  		{"GET", "", "/item/",
   161  			"/item/{$}", []string{}},
   162  		{"GET", "", "/item/jba/17/line2",
   163  			"/item/", nil},
   164  		{"POST", "alt.com", "/item/jba",
   165  			"POST alt.com/item/{user}", []string{"jba"}},
   166  		{"GET", "alt.com", "/item/jba",
   167  			"GET /item/{user}", []string{"jba"}},
   168  		{"GET", "", "/item",
   169  			"", nil}, // does not match
   170  		{"GET", "", "/headwins",
   171  			"GET /headwins", nil},
   172  		{"HEAD", "", "/headwins", // HEAD is more specific than GET
   173  			"HEAD /headwins", nil},
   174  		{"GET", "", "/path/to/file",
   175  			"/path/{p...}", []string{"to/file"}},
   176  	})
   177  
   178  	// A pattern ending in {$} should only match URLS with a trailing slash.
   179  	pat1 := "/a/b/{$}"
   180  	test(buildTree(pat1), []testCase{
   181  		{"GET", "", "/a/b", "", nil},
   182  		{"GET", "", "/a/b/", pat1, nil},
   183  		{"GET", "", "/a/b/c", "", nil},
   184  		{"GET", "", "/a/b/c/d", "", nil},
   185  	})
   186  
   187  	// A pattern ending in a single wildcard should not match a trailing slash URL.
   188  	pat2 := "/a/b/{w}"
   189  	test(buildTree(pat2), []testCase{
   190  		{"GET", "", "/a/b", "", nil},
   191  		{"GET", "", "/a/b/", "", nil},
   192  		{"GET", "", "/a/b/c", pat2, []string{"c"}},
   193  		{"GET", "", "/a/b/c/d", "", nil},
   194  	})
   195  
   196  	// A pattern ending in a multi wildcard should match both URLs.
   197  	pat3 := "/a/b/{w...}"
   198  	test(buildTree(pat3), []testCase{
   199  		{"GET", "", "/a/b", "", nil},
   200  		{"GET", "", "/a/b/", pat3, []string{""}},
   201  		{"GET", "", "/a/b/c", pat3, []string{"c"}},
   202  		{"GET", "", "/a/b/c/d", pat3, []string{"c/d"}},
   203  	})
   204  
   205  	// All three of the above should work together.
   206  	test(buildTree(pat1, pat2, pat3), []testCase{
   207  		{"GET", "", "/a/b", "", nil},
   208  		{"GET", "", "/a/b/", pat1, nil},
   209  		{"GET", "", "/a/b/c", pat2, []string{"c"}},
   210  		{"GET", "", "/a/b/c/d", pat3, []string{"c/d"}},
   211  	})
   212  }
   213  
   214  func TestMatchingMethods(t *testing.T) {
   215  	hostTree := buildTree("GET a.com/", "PUT b.com/", "POST /foo/{x}")
   216  	for _, test := range []struct {
   217  		name       string
   218  		tree       *routingNode
   219  		host, path string
   220  		want       string
   221  	}{
   222  		{
   223  			"post",
   224  			buildTree("POST /"), "", "/foo",
   225  			"POST",
   226  		},
   227  		{
   228  			"get",
   229  			buildTree("GET /"), "", "/foo",
   230  			"GET,HEAD",
   231  		},
   232  		{
   233  			"host",
   234  			hostTree, "", "/foo",
   235  			"",
   236  		},
   237  		{
   238  			"host",
   239  			hostTree, "", "/foo/bar",
   240  			"POST",
   241  		},
   242  		{
   243  			"host2",
   244  			hostTree, "a.com", "/foo/bar",
   245  			"GET,HEAD,POST",
   246  		},
   247  		{
   248  			"host3",
   249  			hostTree, "b.com", "/bar",
   250  			"PUT",
   251  		},
   252  		{
   253  			// This case shouldn't come up because we only call matchingMethods
   254  			// when there was no match, but we include it for completeness.
   255  			"empty",
   256  			buildTree("/"), "", "/",
   257  			"",
   258  		},
   259  	} {
   260  		t.Run(test.name, func(t *testing.T) {
   261  			ms := map[string]bool{}
   262  			test.tree.matchingMethods(test.host, test.path, ms)
   263  			keys := mapKeys(ms)
   264  			sort.Strings(keys)
   265  			got := strings.Join(keys, ",")
   266  			if got != test.want {
   267  				t.Errorf("got %s, want %s", got, test.want)
   268  			}
   269  		})
   270  	}
   271  }
   272  
   273  func (n *routingNode) print(w io.Writer, level int) {
   274  	indent := strings.Repeat("    ", level)
   275  	if n.pattern != nil {
   276  		fmt.Fprintf(w, "%s%q\n", indent, n.pattern)
   277  	}
   278  	if n.emptyChild != nil {
   279  		fmt.Fprintf(w, "%s%q:\n", indent, "")
   280  		n.emptyChild.print(w, level+1)
   281  	}
   282  
   283  	var keys []string
   284  	n.children.eachPair(func(k string, _ *routingNode) bool {
   285  		keys = append(keys, k)
   286  		return true
   287  	})
   288  	sort.Strings(keys)
   289  
   290  	for _, k := range keys {
   291  		fmt.Fprintf(w, "%s%q:\n", indent, k)
   292  		n, _ := n.children.find(k)
   293  		n.print(w, level+1)
   294  	}
   295  }
   296  

View as plain text