|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors |
| 3 | + |
| 4 | +package echo |
| 5 | + |
| 6 | +import ( |
| 7 | + "net/http" |
| 8 | + "net/http/httptest" |
| 9 | + "testing" |
| 10 | + |
| 11 | + "github.com/stretchr/testify/assert" |
| 12 | +) |
| 13 | + |
| 14 | +// These tests lock in v5's method-handling semantics for routes registered through |
| 15 | +// a Group. v5 resolves method mismatches (405) and OPTIONS at the router level and |
| 16 | +// does NOT register any implicit per-group catch-all route. |
| 17 | +// |
| 18 | +// They double as a regression gate. Registering a group-level catch-all — whether |
| 19 | +// manually via g.RouteNotFound("/*", ...) or automatically (as proposed in #2996 to |
| 20 | +// fix CORS-on-group preflight) — makes that catch-all match every method, which masks |
| 21 | +// both 405 and v5's automatic OPTIONS response as 404. Verified empirically: with such |
| 22 | +// a catch-all in place, "POST /api/users" returns 404 instead of 405 and |
| 23 | +// "OPTIONS /api/users" returns 404 instead of 204. If that behavior reaches this |
| 24 | +// branch, the first two tests below fail. |
| 25 | + |
| 26 | +// A method mismatch on an existing group route must return 405 with the allowed |
| 27 | +// methods, not be masked to 404. |
| 28 | +func TestGroupRoute_methodMismatchReturns405(t *testing.T) { |
| 29 | + e := New() |
| 30 | + g := e.Group("/api") |
| 31 | + g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") }) |
| 32 | + |
| 33 | + req := httptest.NewRequest(http.MethodPost, "/api/users", nil) |
| 34 | + rec := httptest.NewRecorder() |
| 35 | + e.ServeHTTP(rec, req) |
| 36 | + |
| 37 | + assert.Equal(t, http.StatusMethodNotAllowed, rec.Code, |
| 38 | + "POST to a GET-only group route must be 405, not masked to 404") |
| 39 | + assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow), |
| 40 | + "405 response must advertise the allowed methods") |
| 41 | +} |
| 42 | + |
| 43 | +// OPTIONS on an existing group route is answered automatically by Echo (204 + |
| 44 | +// Allow). This is the behavior CORS preflight relies on, so it must not be masked. |
| 45 | +func TestGroupRoute_automaticOPTIONS(t *testing.T) { |
| 46 | + e := New() |
| 47 | + g := e.Group("/api") |
| 48 | + g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") }) |
| 49 | + |
| 50 | + req := httptest.NewRequest(http.MethodOptions, "/api/users", nil) |
| 51 | + rec := httptest.NewRecorder() |
| 52 | + e.ServeHTTP(rec, req) |
| 53 | + |
| 54 | + assert.Equal(t, http.StatusNoContent, rec.Code, |
| 55 | + "OPTIONS on a registered group route must be auto-answered (204), not masked to 404") |
| 56 | + assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow), |
| 57 | + "automatic OPTIONS response must advertise the allowed methods") |
| 58 | +} |
| 59 | + |
| 60 | +// A matched concrete route resolves to its own handler; only a genuinely unmatched |
| 61 | +// path under the prefix is a 404. |
| 62 | +func TestGroupRoute_concreteRoutesResolve(t *testing.T) { |
| 63 | + e := New() |
| 64 | + g := e.Group("/api") |
| 65 | + g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") }) |
| 66 | + |
| 67 | + status, body := request(http.MethodGet, "/api/users", e) |
| 68 | + assert.Equal(t, http.StatusOK, status) |
| 69 | + assert.Equal(t, "users", body) |
| 70 | + |
| 71 | + status, _ = request(http.MethodGet, "/api/nope", e) |
| 72 | + assert.Equal(t, http.StatusNotFound, status) |
| 73 | +} |
| 74 | + |
| 75 | +// A group prefix must not affect routing of routes registered outside the group. |
| 76 | +func TestGroup_doesNotAffectRootRoutes(t *testing.T) { |
| 77 | + e := New() |
| 78 | + e.GET("/health", func(c *Context) error { return c.String(http.StatusOK, "root") }) |
| 79 | + g := e.Group("/api") |
| 80 | + g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") }) |
| 81 | + |
| 82 | + status, body := request(http.MethodGet, "/health", e) |
| 83 | + assert.Equal(t, http.StatusOK, status) |
| 84 | + assert.Equal(t, "root", body) |
| 85 | +} |
0 commit comments