Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: add rewritten group method-handling tests
The previous commit recorded only the deletion of the old file; this adds
the rewritten suite (group_method_handling_test.go).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  • Loading branch information
vishr and claude committed Jun 13, 2026
commit 39dbf3fb6e61f5e187b2fec91b207989ee3ba3df
85 changes: 85 additions & 0 deletions group_method_handling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors

package echo

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

// These tests lock in v5's method-handling semantics for routes registered through
// a Group. v5 resolves method mismatches (405) and OPTIONS at the router level and
// does NOT register any implicit per-group catch-all route.
//
// They double as a regression gate. Registering a group-level catch-all — whether
// manually via g.RouteNotFound("/*", ...) or automatically (as proposed in #2996 to
// fix CORS-on-group preflight) — makes that catch-all match every method, which masks
// both 405 and v5's automatic OPTIONS response as 404. Verified empirically: with such
// a catch-all in place, "POST /api/users" returns 404 instead of 405 and
// "OPTIONS /api/users" returns 404 instead of 204. If that behavior reaches this
// branch, the first two tests below fail.

// A method mismatch on an existing group route must return 405 with the allowed
// methods, not be masked to 404.
func TestGroupRoute_methodMismatchReturns405(t *testing.T) {
e := New()
g := e.Group("/api")
g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") })

req := httptest.NewRequest(http.MethodPost, "/api/users", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusMethodNotAllowed, rec.Code,
"POST to a GET-only group route must be 405, not masked to 404")
assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow),
"405 response must advertise the allowed methods")
}

// OPTIONS on an existing group route is answered automatically by Echo (204 +
// Allow). This is the behavior CORS preflight relies on, so it must not be masked.
func TestGroupRoute_automaticOPTIONS(t *testing.T) {
e := New()
g := e.Group("/api")
g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") })

req := httptest.NewRequest(http.MethodOptions, "/api/users", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusNoContent, rec.Code,
"OPTIONS on a registered group route must be auto-answered (204), not masked to 404")
assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow),
"automatic OPTIONS response must advertise the allowed methods")
}

// A matched concrete route resolves to its own handler; only a genuinely unmatched
// path under the prefix is a 404.
func TestGroupRoute_concreteRoutesResolve(t *testing.T) {
e := New()
g := e.Group("/api")
g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") })

status, body := request(http.MethodGet, "/api/users", e)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "users", body)

status, _ = request(http.MethodGet, "/api/nope", e)
assert.Equal(t, http.StatusNotFound, status)
}

// A group prefix must not affect routing of routes registered outside the group.
func TestGroup_doesNotAffectRootRoutes(t *testing.T) {
e := New()
e.GET("/health", func(c *Context) error { return c.String(http.StatusOK, "root") })
g := e.Group("/api")
g.GET("/users", func(c *Context) error { return c.String(http.StatusOK, "users") })

status, body := request(http.MethodGet, "/health", e)
assert.Equal(t, http.StatusOK, status)
assert.Equal(t, "root", body)
}
Loading