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 }