goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/router_test.go (about)

     1  package goyave
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/fs"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  	"goyave.dev/goyave/v5/config"
    15  	"goyave.dev/goyave/v5/cors"
    16  	"goyave.dev/goyave/v5/util/fsutil"
    17  	"goyave.dev/goyave/v5/util/fsutil/osfs"
    18  )
    19  
    20  type testStatusHandler struct {
    21  	Component
    22  }
    23  
    24  func (*testStatusHandler) Handle(response *Response, _ *Request) {
    25  	message := map[string]string{
    26  		"status": http.StatusText(response.GetStatus()),
    27  	}
    28  	response.JSON(response.GetStatus(), message)
    29  }
    30  
    31  type extraMiddlewareOrder struct{}
    32  
    33  type testMiddleware struct {
    34  	Component
    35  	key string
    36  }
    37  
    38  func (m *testMiddleware) Handle(next Handler) Handler {
    39  	return func(r *Response, req *Request) {
    40  		var slice []string
    41  		if s, ok := req.Extra[extraMiddlewareOrder{}]; !ok {
    42  			slice = []string{}
    43  		} else {
    44  			slice = s.([]string)
    45  		}
    46  		slice = append(slice, m.key)
    47  		req.Extra[extraMiddlewareOrder{}] = slice
    48  		next(r, req)
    49  	}
    50  }
    51  
    52  func prepareRouterTest() *Router {
    53  	server, err := New(Options{Config: config.LoadDefault()})
    54  	if err != nil {
    55  		panic(err)
    56  	}
    57  	return NewRouter(server)
    58  }
    59  
    60  type testController struct {
    61  	Component
    62  	registered bool
    63  }
    64  
    65  func (c *testController) RegisterRoutes(_ *Router) {
    66  	c.registered = true
    67  }
    68  
    69  func TestRouter(t *testing.T) {
    70  
    71  	t.Run("New", func(t *testing.T) {
    72  		router := prepareRouterTest()
    73  		if !assert.NotNil(t, router) {
    74  			return
    75  		}
    76  		assert.NotNil(t, router.server)
    77  		assert.Nil(t, router.parent)
    78  		assert.Empty(t, router.prefix)
    79  		assert.Len(t, router.statusHandlers, 41)
    80  		assert.NotNil(t, router.namedRoutes)
    81  		assert.NotNil(t, router.Meta)
    82  
    83  		recoveryMiddleware := findMiddleware[*recoveryMiddleware](router.globalMiddleware.middleware)
    84  		langMiddleware := findMiddleware[*languageMiddleware](router.globalMiddleware.middleware)
    85  		if assert.NotNil(t, recoveryMiddleware) {
    86  			assert.Equal(t, router.server, recoveryMiddleware.server)
    87  		}
    88  		if assert.NotNil(t, langMiddleware) {
    89  			assert.Equal(t, router.server, langMiddleware.server)
    90  		}
    91  	})
    92  
    93  	t.Run("ClearRegexCache", func(t *testing.T) {
    94  		router := prepareRouterTest()
    95  		subrouter := router.Subrouter("/subrouter")
    96  
    97  		assert.NotNil(t, router.regexCache)
    98  
    99  		router.ClearRegexCache()
   100  		assert.Nil(t, router.regexCache)
   101  		assert.Nil(t, subrouter.regexCache)
   102  	})
   103  
   104  	t.Run("Accessors", func(t *testing.T) {
   105  		router := prepareRouterTest()
   106  		subrouter := router.Subrouter("/subrouter")
   107  		route := subrouter.Get("/route", func(_ *Response, _ *Request) {}).Name("route-name")
   108  
   109  		assert.Equal(t, router, subrouter.GetParent())
   110  		assert.Equal(t, []*Route{route}, subrouter.GetRoutes())
   111  		assert.Equal(t, []*Router{subrouter}, router.GetSubrouters())
   112  		assert.Equal(t, route, router.GetRoute("route-name"))
   113  		assert.Equal(t, route, subrouter.GetRoute("route-name"))
   114  	})
   115  
   116  	t.Run("Meta", func(t *testing.T) {
   117  		router := prepareRouterTest()
   118  		router.Meta["parent-meta"] = "parent-value"
   119  		subrouter := router.Subrouter("/subrouter")
   120  		subrouter.SetMeta("meta-key", "meta-value")
   121  		assert.Equal(t, map[string]any{"meta-key": "meta-value"}, subrouter.Meta)
   122  
   123  		val, ok := subrouter.LookupMeta("meta-key")
   124  		assert.Equal(t, "meta-value", val)
   125  		assert.True(t, ok)
   126  
   127  		val, ok = subrouter.LookupMeta("parent-meta")
   128  		assert.Equal(t, "parent-value", val)
   129  		assert.True(t, ok)
   130  
   131  		val, ok = subrouter.LookupMeta("nonexistent")
   132  		assert.Nil(t, val)
   133  		assert.False(t, ok)
   134  
   135  		subrouter.RemoveMeta("meta-key")
   136  		assert.Empty(t, subrouter.Meta)
   137  
   138  		subrouter.SetMeta("parent-meta", "override")
   139  		val, ok = subrouter.LookupMeta("parent-meta")
   140  		assert.Equal(t, "override", val)
   141  		assert.True(t, ok)
   142  	})
   143  
   144  	t.Run("GlobalMiddleware", func(t *testing.T) {
   145  		router := prepareRouterTest()
   146  		router.GlobalMiddleware(&corsMiddleware{}, &validateRequestMiddleware{})
   147  		assert.Len(t, router.globalMiddleware.middleware, 4)
   148  		for _, m := range router.globalMiddleware.middleware {
   149  			assert.NotNil(t, m.Server())
   150  		}
   151  	})
   152  
   153  	t.Run("Middleware", func(t *testing.T) {
   154  		router := prepareRouterTest()
   155  		router.Middleware(&corsMiddleware{}, &validateRequestMiddleware{})
   156  		assert.Len(t, router.middleware, 2)
   157  		for _, m := range router.middleware {
   158  			assert.NotNil(t, m.Server())
   159  		}
   160  	})
   161  
   162  	t.Run("CORS", func(t *testing.T) {
   163  		router := prepareRouterTest()
   164  		opts := cors.Default()
   165  
   166  		router.CORS(opts)
   167  
   168  		assert.Equal(t, opts, router.Meta[MetaCORS])
   169  		assert.True(t, hasMiddleware[*corsMiddleware](router.globalMiddleware.middleware))
   170  
   171  		// OPTIONS method is added to routes if the router has CORS
   172  		route := router.Get("/route", func(_ *Response, _ *Request) {})
   173  		assert.Equal(t, []string{http.MethodGet, http.MethodOptions, http.MethodHead}, route.methods)
   174  
   175  		// OPTIONS method is added to routes if one of the parent routes has CORS
   176  		route = router.Subrouter("/subrouter").Get("/route", func(_ *Response, _ *Request) {})
   177  		assert.Equal(t, []string{http.MethodGet, http.MethodOptions, http.MethodHead}, route.methods)
   178  
   179  		// Disable in subrouter
   180  		subrouter := router.Subrouter("/subrouter2")
   181  		subrouter.CORS(nil)
   182  		route = subrouter.Get("/route-2", func(_ *Response, _ *Request) {})
   183  		assert.Equal(t, []string{http.MethodGet, http.MethodHead}, route.methods)
   184  
   185  		// Disable
   186  		router.CORS(nil)
   187  		assert.Contains(t, router.Meta, MetaCORS)
   188  		assert.Nil(t, router.Meta[MetaCORS])
   189  		route = router.Get("/route-2", func(_ *Response, _ *Request) {})
   190  		assert.Equal(t, []string{http.MethodGet, http.MethodHead}, route.methods)
   191  
   192  	})
   193  
   194  	t.Run("StatusHandler", func(t *testing.T) {
   195  		router := prepareRouterTest()
   196  
   197  		statusHandler := &testStatusHandler{}
   198  		router.StatusHandler(statusHandler, 1, 2, 3)
   199  
   200  		assert.Equal(t, router.server, statusHandler.server)
   201  		assert.Equal(t, statusHandler, router.statusHandlers[1])
   202  		assert.Equal(t, statusHandler, router.statusHandlers[2])
   203  		assert.Equal(t, statusHandler, router.statusHandlers[3])
   204  	})
   205  
   206  	t.Run("Subrouter", func(t *testing.T) {
   207  		router := prepareRouterTest()
   208  		router.Get("/named", nil).Name("route-name")
   209  		subrouter := router.Subrouter("/subrouter")
   210  
   211  		assert.Equal(t, router.server, subrouter.server)
   212  		assert.Equal(t, router, subrouter.parent)
   213  		assert.Equal(t, "/subrouter", subrouter.prefix)
   214  		assert.Equal(t, router.statusHandlers, subrouter.statusHandlers)
   215  		assert.NotSame(t, router.statusHandlers, subrouter.statusHandlers)
   216  		assert.Equal(t, router.namedRoutes, subrouter.namedRoutes)
   217  		assert.Equal(t, router.globalMiddleware, subrouter.globalMiddleware)
   218  		assert.Equal(t, router.regexCache, subrouter.regexCache)
   219  		assert.NotNil(t, subrouter.Meta)
   220  		assert.Empty(t, subrouter.Meta)
   221  		assert.Equal(t, []*Router{subrouter}, router.subrouters)
   222  		assert.NotNil(t, subrouter.regex)
   223  
   224  		slash := router.Subrouter("/")
   225  		group := router.Group()
   226  		assert.Empty(t, slash.prefix)
   227  		assert.Equal(t, slash, group)
   228  	})
   229  
   230  	t.Run("Route", func(t *testing.T) {
   231  		router := prepareRouterTest()
   232  
   233  		route := router.Route([]string{http.MethodPost, http.MethodPut}, "/uri/{param}", func(_ *Response, _ *Request) {})
   234  		assert.Empty(t, route.name)
   235  		assert.Equal(t, "/uri/{param}", route.uri)
   236  		assert.Equal(t, []string{http.MethodPost, http.MethodPut}, route.methods)
   237  		assert.Equal(t, router, route.parent)
   238  		assert.NotNil(t, route.handler)
   239  		assert.NotNil(t, route.Meta)
   240  		assert.NotNil(t, route.regex)
   241  
   242  		t.Run("HEAD_added_on_GET_routes", func(t *testing.T) {
   243  			route := router.Route([]string{http.MethodGet}, "/uri", func(_ *Response, _ *Request) {})
   244  			assert.Equal(t, []string{http.MethodGet, http.MethodHead}, route.methods)
   245  		})
   246  
   247  		t.Run("trim_slash", func(t *testing.T) {
   248  			// Not trimmed because no parent
   249  			route := router.Route([]string{http.MethodGet}, "/", func(_ *Response, _ *Request) {})
   250  			assert.Equal(t, "/", route.uri)
   251  
   252  			route = router.Subrouter("/subrouter").Route([]string{http.MethodGet}, "/", func(_ *Response, _ *Request) {})
   253  			assert.Equal(t, "", route.uri)
   254  		})
   255  	})
   256  
   257  	t.Run("Get", func(t *testing.T) {
   258  		router := prepareRouterTest()
   259  		route := router.Get("/uri", func(_ *Response, _ *Request) {})
   260  		assert.Equal(t, []string{http.MethodGet, http.MethodHead}, route.methods)
   261  	})
   262  
   263  	t.Run("Post", func(t *testing.T) {
   264  		router := prepareRouterTest()
   265  		route := router.Post("/uri", func(_ *Response, _ *Request) {})
   266  		assert.Equal(t, []string{http.MethodPost}, route.methods)
   267  	})
   268  
   269  	t.Run("Put", func(t *testing.T) {
   270  		router := prepareRouterTest()
   271  		route := router.Put("/uri", func(_ *Response, _ *Request) {})
   272  		assert.Equal(t, []string{http.MethodPut}, route.methods)
   273  	})
   274  
   275  	t.Run("Patch", func(t *testing.T) {
   276  		router := prepareRouterTest()
   277  		route := router.Patch("/uri", func(_ *Response, _ *Request) {})
   278  		assert.Equal(t, []string{http.MethodPatch}, route.methods)
   279  	})
   280  
   281  	t.Run("Delete", func(t *testing.T) {
   282  		router := prepareRouterTest()
   283  		route := router.Delete("/uri", func(_ *Response, _ *Request) {})
   284  		assert.Equal(t, []string{http.MethodDelete}, route.methods)
   285  	})
   286  
   287  	t.Run("Options", func(t *testing.T) {
   288  		router := prepareRouterTest()
   289  		route := router.Options("/uri", func(_ *Response, _ *Request) {})
   290  		assert.Equal(t, []string{http.MethodOptions}, route.methods)
   291  	})
   292  
   293  	t.Run("Static", func(t *testing.T) {
   294  		router := prepareRouterTest()
   295  		f, err := fs.Sub(&osfs.FS{}, "resources")
   296  		require.NoError(t, err)
   297  		route := router.Static(fsutil.NewEmbed(f.(fs.ReadDirFS)), "/uri", false)
   298  		assert.Equal(t, []string{http.MethodGet, http.MethodHead}, route.methods)
   299  		assert.Equal(t, []string{"resource"}, route.parameters)
   300  		assert.Equal(t, "/uri{resource:.*}", route.uri)
   301  	})
   302  
   303  	t.Run("Controller", func(t *testing.T) {
   304  		router := prepareRouterTest()
   305  		ctrl := &testController{}
   306  		router.Controller(ctrl)
   307  		assert.Equal(t, router.server, ctrl.server)
   308  		assert.True(t, ctrl.registered)
   309  	})
   310  
   311  	t.Run("ServeHTTP", func(t *testing.T) {
   312  		router := prepareRouterTest()
   313  		router.server.config.Set("server.proxy.host", "proxy.io")
   314  		router.server.config.Set("server.proxy.protocol", "http")
   315  		router.server.config.Set("server.proxy.port", 80)
   316  		router.server.config.Set("server.proxy.base", "/base")
   317  
   318  		var route *Route
   319  		route = router.Get("/route/{param}", func(r *Response, req *Request) {
   320  			assert.Equal(t, map[string]string{"param": "value"}, req.RouteParams)
   321  			assert.Equal(t, route, req.Route)
   322  			assert.False(t, req.Now.IsZero())
   323  			r.String(http.StatusOK, "hello world")
   324  		})
   325  		router.Put("/empty", func(_ *Response, _ *Request) {})
   326  		router.Get("/forbidden", func(r *Response, _ *Request) {
   327  			r.Status(http.StatusForbidden)
   328  		})
   329  
   330  		router.Subrouter("/noparam").Get("", func(r *Response, req *Request) {
   331  			assert.Equal(t, map[string]string{}, req.RouteParams)
   332  			r.Status(http.StatusOK)
   333  		})
   334  
   335  		subrouter := router.Subrouter("/subrouter/{param}")
   336  		subrouter.Get("/subroute", func(r *Response, req *Request) {
   337  			assert.Equal(t, map[string]string{"param": "value"}, req.RouteParams)
   338  			r.Status(http.StatusOK)
   339  		})
   340  		subrouter.Get("/subroute/{name}", func(r *Response, req *Request) {
   341  			assert.Equal(t, map[string]string{"param": "value", "name": "johndoe"}, req.RouteParams)
   342  			r.Status(http.StatusOK)
   343  		})
   344  
   345  		router.Middleware(&testMiddleware{key: "router"})
   346  		router.GlobalMiddleware(&testMiddleware{key: "global"})
   347  		router.Get("/middleware", func(r *Response, req *Request) {
   348  			assert.Equal(t, []string{"global", "router", "route"}, req.Extra[extraMiddlewareOrder{}])
   349  			r.Status(http.StatusOK)
   350  		}).Middleware(&testMiddleware{key: "route"})
   351  
   352  		cases := []struct {
   353  			desc           string
   354  			requestMethod  string
   355  			requestURL     string
   356  			expectedBody   string
   357  			expectedStatus int
   358  		}{
   359  			{
   360  				desc:           "simple_param",
   361  				requestMethod:  http.MethodGet,
   362  				requestURL:     "/route/value",
   363  				expectedStatus: http.StatusOK,
   364  				expectedBody:   "hello world",
   365  			},
   366  			{
   367  				desc:           "multiple_param",
   368  				requestMethod:  http.MethodGet,
   369  				requestURL:     "/subrouter/value/subroute/johndoe",
   370  				expectedStatus: http.StatusOK,
   371  				expectedBody:   "",
   372  			},
   373  			{
   374  				desc:           "no_param_in_leaf",
   375  				requestMethod:  http.MethodGet,
   376  				requestURL:     "/subrouter/value/subroute",
   377  				expectedStatus: http.StatusOK,
   378  				expectedBody:   "",
   379  			},
   380  			{
   381  				desc:           "no_param",
   382  				requestMethod:  http.MethodGet,
   383  				requestURL:     "/noparam",
   384  				expectedStatus: http.StatusOK,
   385  				expectedBody:   "",
   386  			},
   387  			{
   388  				desc:           "protocol_rediect",
   389  				requestMethod:  http.MethodGet,
   390  				requestURL:     "https://127.0.0.1:8080/route/value?query=abc",
   391  				expectedStatus: http.StatusPermanentRedirect,
   392  				expectedBody:   "<a href=\"http://proxy.io/base/route/value?query=abc\">Permanent Redirect</a>.\n\n",
   393  			},
   394  			{
   395  				desc:           "empty_response",
   396  				requestMethod:  http.MethodPut,
   397  				requestURL:     "/empty",
   398  				expectedStatus: http.StatusNoContent,
   399  				expectedBody:   "",
   400  			},
   401  			{
   402  				desc:           "status_handler",
   403  				requestMethod:  http.MethodGet,
   404  				requestURL:     "/forbidden",
   405  				expectedStatus: http.StatusForbidden,
   406  				expectedBody:   "{\"error\":\"Forbidden\"}\n",
   407  			},
   408  			{
   409  				desc:           "not_found",
   410  				requestMethod:  http.MethodGet,
   411  				requestURL:     "/not_found",
   412  				expectedStatus: http.StatusNotFound,
   413  				expectedBody:   "{\"error\":\"Not Found\"}\n",
   414  			},
   415  			{
   416  				desc:           "method_not_allowed",
   417  				requestMethod:  http.MethodPatch,
   418  				requestURL:     "/empty",
   419  				expectedStatus: http.StatusMethodNotAllowed,
   420  				expectedBody:   "{\"error\":\"Method Not Allowed\"}\n",
   421  			},
   422  			{
   423  				desc:           "middleware_order",
   424  				requestMethod:  http.MethodGet,
   425  				requestURL:     "/middleware",
   426  				expectedStatus: http.StatusOK,
   427  				expectedBody:   "",
   428  			},
   429  		}
   430  
   431  		for _, c := range cases {
   432  			c := c
   433  			t.Run(c.desc, func(t *testing.T) {
   434  				recorder := httptest.NewRecorder()
   435  				rawRequest := httptest.NewRequest(c.requestMethod, c.requestURL, nil)
   436  				router.ServeHTTP(recorder, rawRequest)
   437  
   438  				res := recorder.Result()
   439  
   440  				assert.Equal(t, c.expectedStatus, res.StatusCode)
   441  
   442  				body, err := io.ReadAll(res.Body)
   443  				assert.NoError(t, res.Body.Close())
   444  				require.NoError(t, err)
   445  				assert.Equal(t, c.expectedBody, string(body))
   446  			})
   447  		}
   448  	})
   449  
   450  	t.Run("match", func(t *testing.T) {
   451  		router := prepareRouterTest()
   452  
   453  		router.Get("/", nil).Name("root")
   454  		router.Get("/first-level", nil).Name("first-level")
   455  
   456  		categories := router.Subrouter("/categories")
   457  		categories.Get("/", nil).Name("categories.index")
   458  		category := categories.Subrouter("/{categoryId:[0-9]+}")
   459  		category.Get("/", nil).Name("categories.show")
   460  		category.Get("/inventory", nil).Name("categories.inventory")
   461  
   462  		// Subrouter has priority over route, this one will never match
   463  		router.Get("/categories/{categoryId:[0-9]+}", nil).Name("never-match")
   464  
   465  		// The first segment in the URI matches the subrouter, so this one will never match neither
   466  		router.Get("/categories/test", nil).Name("never-match-first-segment")
   467  
   468  		products := category.Subrouter("/products")
   469  		products.Get("/", nil).Name("products.index")
   470  		products.Post("/", nil).Name("products.create")
   471  		products.Get("/{id:[0-9]+}", nil).Name("products.show")
   472  
   473  		// Route groups, we should be able to match /profile even with the admins
   474  		// subrouter (because it has an empty prefix)
   475  		users := router.Subrouter("/users")
   476  		admins := users.Group()
   477  		admins.Get("/manage", nil).Name("users.admins.manage")
   478  		admins.Post("/", nil).Name("users.admins.create")
   479  		viewers := users.Group()
   480  		viewers.Get("/profile", nil).Name("users.viewers.profile")
   481  		viewers.Get("/", nil).Name("users.viewers.show")
   482  		users.Put("/", nil).Name("users.update")
   483  
   484  		// Conflicting subrouters
   485  		conflict := router.Subrouter("/conflict")
   486  		conflict.Get("/", nil).Name("conflict.root")
   487  		conflict.Get("/child", nil).Name("conflict.child")
   488  		conflict2 := router.Subrouter("/conflict-2")
   489  		conflict2.Get("/", nil).Name("conflict-2.root")
   490  		conflict2.Get("/child", nil).Name("conflict-2.child")
   491  
   492  		// Multiple segments in subrouter path
   493  		subrouter := router.Subrouter("/subrouter/{param}")
   494  		subrouter.Get("/", nil).Name("multiple-segments.subroute.index")
   495  		subrouter.Get("/subroute", nil).Name("multiple-segments.subroute.show")
   496  		subrouter.Get("/subroute/{name}", nil).Name("multiple-segments.subroute.name")
   497  
   498  		cases := []struct {
   499  			path          string
   500  			method        string
   501  			expectedRoute string
   502  		}{
   503  			{path: "/", method: http.MethodGet, expectedRoute: "root"},
   504  			{path: "/", method: http.MethodPost, expectedRoute: RouteMethodNotAllowed},
   505  			{path: "/first-level", method: http.MethodGet, expectedRoute: "first-level"},
   506  			{path: "/first-level/", method: http.MethodGet, expectedRoute: RouteNotFound}, // Trailing slash
   507  			{path: "/first-level", method: http.MethodPost, expectedRoute: RouteMethodNotAllowed},
   508  			{path: "/categories", method: http.MethodGet, expectedRoute: "categories.index"},
   509  			{path: "/categories/", method: http.MethodGet, expectedRoute: RouteNotFound}, // Trailing slash
   510  			{path: "/categories/123", method: http.MethodGet, expectedRoute: "categories.show"},
   511  			{path: "/categories/123/inventory", method: http.MethodGet, expectedRoute: "categories.inventory"},
   512  			{path: "/categories/test", method: http.MethodGet, expectedRoute: RouteNotFound},
   513  			{path: "/categories/123/products", method: http.MethodGet, expectedRoute: "products.index"},
   514  			{path: "/categories/123/products", method: http.MethodPost, expectedRoute: "products.create"},
   515  			{path: "/categories/123/products/1234567890", method: http.MethodGet, expectedRoute: "products.show"},
   516  			{path: "/users/manage", method: http.MethodGet, expectedRoute: "users.admins.manage"},
   517  			{path: "/users/profile", method: http.MethodGet, expectedRoute: "users.viewers.profile"},
   518  			{path: "/users", method: http.MethodGet, expectedRoute: "users.viewers.show"}, // Method not allowed on users.admins.create
   519  			{path: "/users", method: http.MethodPut, expectedRoute: "users.update"},
   520  			{path: "/conflict", method: http.MethodGet, expectedRoute: "conflict.root"},
   521  			{path: "/conflict/", method: http.MethodGet, expectedRoute: RouteNotFound},
   522  			{path: "/conflict/child", method: http.MethodGet, expectedRoute: "conflict.child"},
   523  			{path: "/conflict-2", method: http.MethodGet, expectedRoute: "conflict-2.root"},
   524  			{path: "/conflict-2/", method: http.MethodGet, expectedRoute: RouteNotFound},
   525  			{path: "/conflict-2/child", method: http.MethodGet, expectedRoute: "conflict-2.child"},
   526  			{path: "/categories/123/not-a-route", method: http.MethodGet, expectedRoute: RouteNotFound},
   527  			{path: "/categories/123/not-a-route/", method: http.MethodGet, expectedRoute: RouteNotFound},
   528  			{path: "/subrouter/value", method: http.MethodGet, expectedRoute: "multiple-segments.subroute.index"},
   529  			{path: "/subrouter/value/", method: http.MethodGet, expectedRoute: RouteNotFound},
   530  			{path: "/subrouter/value/subroute", method: http.MethodGet, expectedRoute: "multiple-segments.subroute.show"},
   531  			{path: "/subrouter/value/subroute/", method: http.MethodGet, expectedRoute: RouteNotFound},
   532  			{path: "/subrouter/value/subroute/johndoe", method: http.MethodGet, expectedRoute: "multiple-segments.subroute.name"},
   533  		}
   534  
   535  		for _, c := range cases {
   536  			c := c
   537  			t.Run(fmt.Sprintf("%s_%s", c.method, strings.ReplaceAll(c.path, "/", "_")), func(t *testing.T) {
   538  				match := routeMatch{currentPath: c.path}
   539  				router.match(c.method, &match)
   540  				assert.Equal(t, c.expectedRoute, match.route.name)
   541  			})
   542  		}
   543  
   544  	})
   545  }