Files
fiber/domain_test.go

1932 lines
52 KiB
Go

// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 🤖 GitHub Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
package fiber
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)
func Test_Domain_Basic(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/hello", func(c Ctx) error {
return c.SendString("api hello")
})
// Matching domain
req := httptest.NewRequest(MethodGet, "/hello", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "api hello", string(body))
// Non-matching domain → 404
req = httptest.NewRequest(MethodGet, "/hello", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_Params(t *testing.T) {
t.Parallel()
app := New()
app.Domain(":user.blog.example.com").Get("/", func(c Ctx) error {
user := DomainParam(c, "user")
return c.SendString("blog of " + user)
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "john.blog.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "blog of john", string(body))
}
func Test_Domain_MultipleParams(t *testing.T) {
t.Parallel()
app := New()
app.Domain(":sub.:region.example.com").Get("/", func(c Ctx) error {
sub := DomainParam(c, "sub")
region := DomainParam(c, "region")
return c.SendString(sub + "-" + region)
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.us-east.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "api-us-east", string(body))
}
func Test_Domain_CaseInsensitive(t *testing.T) {
t.Parallel()
app := New()
app.Domain("API.Example.COM").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "ok", string(body))
}
func Test_Domain_ParamNameCasePreserved(t *testing.T) {
t.Parallel()
app := New()
// Use mixed-case param name ":User" — DomainParam should find it by exact name
app.Domain(":User.example.com").Get("/", func(c Ctx) error {
return c.SendString(DomainParam(c, "User"))
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "alice.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "alice", string(body))
}
func Test_Domain_TrailingDot(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Fully-qualified domain name with trailing dot should match
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com."
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "ok", string(body))
}
func Test_Domain_MultipleDomains(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("api")
})
app.Domain("www.example.com").Get("/", func(c Ctx) error {
return c.SendString("www")
})
// First domain
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "api", string(body))
// Second domain
req = httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "www", string(body))
// Unknown domain
req = httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "other.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_WithGroup(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
api := domain.Group("/api")
api.Get("/users", func(c Ctx) error {
return c.SendString("users list")
})
// Matching domain + path
req := httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "users list", string(body))
// Wrong domain
req = httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_WithMiddleware(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
domain.Use(func(c Ctx) error {
c.Set("X-Domain", "api")
return c.Next()
})
domain.Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Matching domain - middleware should set header
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "api", resp.Header.Get("X-Domain"))
// Non-matching domain - middleware should not set header
req = httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
require.Empty(t, resp.Header.Get("X-Domain"))
}
func Test_Domain_HTTPMethods(t *testing.T) {
t.Parallel()
methods := []struct {
reg func(Router, string, any, ...any) Router
method string
}{
{method: MethodGet, reg: func(r Router, p string, h any, hs ...any) Router { return r.Get(p, h, hs...) }},
{method: MethodPost, reg: func(r Router, p string, h any, hs ...any) Router { return r.Post(p, h, hs...) }},
{method: MethodPut, reg: func(r Router, p string, h any, hs ...any) Router { return r.Put(p, h, hs...) }},
{method: MethodDelete, reg: func(r Router, p string, h any, hs ...any) Router { return r.Delete(p, h, hs...) }},
{method: MethodPatch, reg: func(r Router, p string, h any, hs ...any) Router { return r.Patch(p, h, hs...) }},
{method: MethodOptions, reg: func(r Router, p string, h any, hs ...any) Router { return r.Options(p, h, hs...) }},
{method: MethodConnect, reg: func(r Router, p string, h any, hs ...any) Router { return r.Connect(p, h, hs...) }},
{method: MethodTrace, reg: func(r Router, p string, h any, hs ...any) Router { return r.Trace(p, h, hs...) }},
}
for _, m := range methods {
t.Run(m.method, func(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
m.reg(domain, "/test", func(c Ctx) error {
return c.SendString(m.method)
})
req := httptest.NewRequest(m.method, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
})
}
}
func Test_Domain_Head(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Head("/test", func(c Ctx) error {
c.Set("X-Custom", "head")
return c.SendStatus(StatusOK)
})
req := httptest.NewRequest(MethodHead, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
func Test_Domain_All(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").All("/test", func(c Ctx) error {
return c.SendString("all methods")
})
for _, method := range []string{MethodGet, MethodPost, MethodPut, MethodDelete} {
req := httptest.NewRequest(method, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
}
func Test_Domain_Add(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Add([]string{MethodGet, MethodPost}, "/test", func(c Ctx) error {
return c.SendString("ok")
})
// GET should work
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
// POST should work
req = httptest.NewRequest(MethodPost, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
func Test_Domain_DomainParam_DefaultValue(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
// No domain params set, should return default
user := DomainParam(c, "user", "default")
return c.SendString(user)
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "default", string(body))
}
func Test_Domain_DomainParam_NoDefault(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
user := DomainParam(c, "user")
return c.SendString("user=" + user)
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "example.com"
resp, err := app.Test(req)
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "user=", string(body))
}
func Test_Domain_WithHostPort(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Host with port - Hostname() strips the port
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com:8080"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
func Test_Domain_NoMatch_WrongPartCount(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Different number of domain parts
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
req = httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "sub.api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_Route(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Route("/api", func(router Router) {
router.Get("/users", func(c Ctx) error {
return c.SendString("users")
})
router.Get("/posts", func(c Ctx) error {
return c.SendString("posts")
})
})
req := httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "users", string(body))
req = httptest.NewRequest(MethodGet, "/api/posts", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "posts", string(body))
// Wrong domain
req = httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_Name(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(c Ctx) error {
return c.SendString("ok")
}).Name("api-test")
// Verify route was named - by checking routes
var found bool
for _, routes := range app.Stack() {
for _, route := range routes {
if route.Name == "api-test" {
found = true
break
}
}
}
require.True(t, found, "route should be named 'api-test'")
}
func Test_Domain_NameWithGroup(t *testing.T) {
t.Parallel()
app := New()
// When Domain is used with Route(prefix, fn, name), the group Name()
// should apply properly via delegation to the underlying group.
api := app.Domain("api.example.com")
api.Route("/v1", func(r Router) {
r.Get("/items", func(c Ctx) error {
return c.SendString("items")
}).Name("items-list")
}, "v1.")
var found bool
for _, routes := range app.Stack() {
for _, route := range routes {
if route.Name == "v1.items-list" {
found = true
break
}
}
}
require.True(t, found, "route should be named 'v1.items-list'")
}
func Test_Domain_RouteChain(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").RouteChain("/api/users").
Get(func(c Ctx) error {
return c.SendString("get users")
}).
Post(func(c Ctx) error {
return c.SendString("create user")
})
// GET
req := httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "get users", string(body))
// POST
req = httptest.NewRequest(MethodPost, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "create user", string(body))
// Wrong domain
req = httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_GroupFromGroup(t *testing.T) {
t.Parallel()
app := New()
api := app.Group("/api")
domain := api.Domain("api.example.com")
domain.Get("/users", func(c Ctx) error {
return c.SendString("users from group domain")
})
req := httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "users from group domain", string(body))
// Wrong domain
req = httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_UseWithPrefix(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
domain.Use("/api", func(c Ctx) error {
c.Set("X-API", "true")
return c.Next()
})
domain.Get("/api/data", func(c Ctx) error {
return c.SendString("data")
})
req := httptest.NewRequest(MethodGet, "/api/data", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "true", resp.Header.Get("X-API"))
}
func Test_Domain_FallbackToNonDomain(t *testing.T) {
t.Parallel()
app := New()
// Domain-specific route registered first
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("api")
})
// Fallback non-domain route
app.Get("/", func(c Ctx) error {
return c.SendString("fallback")
})
// Domain route matches
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "api", string(body))
// Fallback route matches for other domains
req = httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "fallback", string(body))
}
func Test_Domain_WithPathParams(t *testing.T) {
t.Parallel()
app := New()
app.Domain(":tenant.example.com").Get("/users/:id", func(c Ctx) error {
tenant := DomainParam(c, "tenant")
id := c.Params("id")
return c.SendString(tenant + ":" + id)
})
req := httptest.NewRequest(MethodGet, "/users/42", http.NoBody)
req.Host = "acme.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "acme:42", string(body))
}
func Test_Domain_NestedGroups(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
v1 := domain.Group("/v1")
users := v1.Group("/users")
users.Get("/", func(c Ctx) error {
return c.SendString("v1 users")
})
req := httptest.NewRequest(MethodGet, "/v1/users/", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "v1 users", string(body))
}
func Test_Domain_Chaining(t *testing.T) {
t.Parallel()
app := New()
// Methods should return the domain router for chaining
domain := app.Domain("api.example.com")
domain.
Get("/a", func(c Ctx) error { return c.SendString("a") }).
Post("/b", func(c Ctx) error { return c.SendString("b") })
req := httptest.NewRequest(MethodGet, "/a", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
req = httptest.NewRequest(MethodPost, "/b", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
func Test_Domain_MultipleHandlers(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(c Ctx) error {
c.Set("X-First", "true")
return c.Next()
},
func(c Ctx) error {
return c.SendString("final")
},
)
// Matching host — both handlers should run
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "true", resp.Header.Get("X-First"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "final", string(body))
// Non-matching host — none of the handlers should run
req = httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
require.Empty(t, resp.Header.Get("X-First"))
}
func Test_Domain_NetHTTPHandler(t *testing.T) {
t.Parallel()
app := New()
// Register a net/http handler through domain routing
app.Domain("api.example.com").Get("/http", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("net/http handler"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}))
req := httptest.NewRequest(MethodGet, "/http", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
func Test_Domain_EmptyHostname(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Empty host should not match
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = ""
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_DomainOnDomain(t *testing.T) {
t.Parallel()
app := New()
// Domain created from a domain router (should replace the pattern)
base := app.Domain("api.example.com")
other := base.Domain("www.example.com")
other.Get("/", func(c Ctx) error {
return c.SendString("www")
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "www.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "www", string(body))
}
func Test_Domain_GroupMiddleware(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
api := domain.Group("/api", func(c Ctx) error {
c.Set("X-Group-MW", "yes")
return c.Next()
})
api.Get("/data", func(c Ctx) error {
return c.SendString("data")
})
// Matching domain
req := httptest.NewRequest(MethodGet, "/api/data", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-Group-MW"))
// Non-matching domain
req = httptest.NewRequest(MethodGet, "/api/data", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
require.Empty(t, resp.Header.Get("X-Group-MW"))
}
func Test_Domain_UseMultiplePrefixes(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
domain.Use([]string{"/a", "/b"}, func(c Ctx) error {
c.Set("X-Domain-MW", "true")
return c.Next()
})
domain.Get("/a/test", func(c Ctx) error {
return c.SendString("a")
})
domain.Get("/b/test", func(c Ctx) error {
return c.SendString("b")
})
req := httptest.NewRequest(MethodGet, "/a/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "true", resp.Header.Get("X-Domain-MW"))
req = httptest.NewRequest(MethodGet, "/b/test", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "true", resp.Header.Get("X-Domain-MW"))
}
func Test_Domain_RoutePanic(t *testing.T) {
t.Parallel()
app := New()
require.Panics(t, func() {
app.Domain("api.example.com").Route("/test", nil)
})
}
func Test_Domain_UseMount(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
// Create routes in the sub-app
subApp.Get("/users", func(c Ctx) error {
return c.SendString("users list")
})
subApp.Get("/posts", func(c Ctx) error {
return c.SendString("posts list")
})
// Mount the sub-app on the domain router
app.Domain("api.example.com").Use("/api", subApp)
// Test that sub-app routes work on the correct domain
req := httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "users list", string(body))
// Test second route
req = httptest.NewRequest(MethodGet, "/api/posts", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "posts list", string(body))
// Test that sub-app routes don't work on wrong domain
req = httptest.NewRequest(MethodGet, "/api/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_UseMountNoPrefix(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
// Create a route in the sub-app
subApp.Get("/users", func(c Ctx) error {
return c.SendString("users list")
})
// Mount the sub-app at root on the domain router
app.Domain("api.example.com").Use(subApp)
// Test that sub-app routes work
req := httptest.NewRequest(MethodGet, "/users", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "users list", string(body))
// Wrong domain should 404
req = httptest.NewRequest(MethodGet, "/users", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_UseMountFromGroup(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
// Create a route in the sub-app
subApp.Get("/data", func(c Ctx) error {
return c.SendString("data response")
})
// Mount via a group's domain router
api := app.Group("/api")
api.Domain("api.example.com").Use("/v1", subApp)
// Test that sub-app routes work with group prefix + mount prefix
req := httptest.NewRequest(MethodGet, "/api/v1/data", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "data response", string(body))
// Wrong domain should 404
req = httptest.NewRequest(MethodGet, "/api/v1/data", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
func Test_Domain_StaleParamsCleared(t *testing.T) {
t.Parallel()
app := New()
// First: a domain with a parameter
app.Domain(":tenant.example.com").Use(func(c Ctx) error {
c.Set("X-Tenant", DomainParam(c, "tenant"))
return c.Next()
})
// Second: a static domain (no params) — should clear any stale params
app.Domain("static.example.com").Get("/check", func(c Ctx) error {
// DomainParam should return "" since the static domain has no params
val := DomainParam(c, "tenant")
return c.SendString("tenant=" + val)
})
req := httptest.NewRequest(MethodGet, "/check", http.NoBody)
req.Host = "static.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "tenant=", string(body))
}
func Test_Domain_RouteChainNested(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").RouteChain("/api").RouteChain("/v1").
Get(func(c Ctx) error {
return c.SendString("v1")
})
req := httptest.NewRequest(MethodGet, "/api/v1", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "v1", string(body))
}
func Test_Domain_RouteChainAllMethods(t *testing.T) {
t.Parallel()
app := New()
rc := app.Domain("api.example.com").RouteChain("/test")
rc.All(func(c Ctx) error {
c.Set("X-All", "yes")
return c.Next()
})
rc.Head(func(c Ctx) error {
return c.SendStatus(StatusOK)
})
rc.Put(func(c Ctx) error {
return c.SendString("put")
})
rc.Delete(func(c Ctx) error {
return c.SendString("delete")
})
rc.Connect(func(c Ctx) error {
return c.SendString("connect")
})
rc.Options(func(c Ctx) error {
return c.SendString("options")
})
rc.Trace(func(c Ctx) error {
return c.SendString("trace")
})
rc.Patch(func(c Ctx) error {
return c.SendString("patch")
})
for _, method := range []string{MethodPut, MethodDelete, MethodPatch, MethodOptions} {
req := httptest.NewRequest(method, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
}
}
func Test_parseDomainPattern(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pattern string
paramNames []string
numParts int
}{
{name: "simple", pattern: "example.com", numParts: 2},
{name: "with subdomain", pattern: "api.example.com", numParts: 3},
{name: "single param", pattern: ":sub.example.com", numParts: 3, paramNames: []string{"sub"}},
{name: "multiple params", pattern: ":sub.:region.example.com", numParts: 4, paramNames: []string{"sub", "region"}},
{name: "case insensitive", pattern: "API.Example.COM", numParts: 3},
{name: "single part", pattern: "localhost", numParts: 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m := parseDomainPattern(tt.pattern)
require.Equal(t, tt.numParts, m.numParts)
if tt.paramNames == nil {
require.Empty(t, m.paramNames)
} else {
require.Equal(t, tt.paramNames, m.paramNames)
}
})
}
}
func Test_domainMatcher_match(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pattern string
hostname string
values []string
matched bool
}{
{name: "exact match", pattern: "api.example.com", hostname: "api.example.com", matched: true},
{name: "case mismatch", pattern: "api.example.com", hostname: "API.EXAMPLE.COM", matched: true},
{name: "wrong subdomain", pattern: "api.example.com", hostname: "www.example.com"},
{name: "wrong part count", pattern: "api.example.com", hostname: "example.com"},
{name: "with param", pattern: ":sub.example.com", hostname: "api.example.com", matched: true, values: []string{"api"}},
{name: "multi param", pattern: ":a.:b.com", hostname: "x.y.com", matched: true, values: []string{"x", "y"}},
{name: "param no match const", pattern: ":a.example.com", hostname: "x.other.com"},
{name: "empty hostname", pattern: "example.com", hostname: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
m := parseDomainPattern(tt.pattern)
matched, values := m.match(tt.hostname)
require.Equal(t, tt.matched, matched)
if tt.values == nil {
if matched && len(m.paramIdx) == 0 {
require.Empty(t, values)
}
} else {
require.Equal(t, tt.values, values)
}
})
}
}
// Test_Domain_HandlerTypes verifies that the domain router is compatible with
// all handler types defined in adapter.go.
func Test_Domain_HandlerTypes(t *testing.T) {
t.Parallel()
t.Run("fiber handler", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(c Ctx) error {
return c.SendString("fiber")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "fiber", string(body))
})
t.Run("fiber handler no error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(c Ctx) {
c.Set("X-Handler", "no-error")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "no-error", resp.Header.Get("X-Handler"))
})
t.Run("express req res error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(_ Req, res Res) error {
return res.SendString("express-err")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "express-err", string(body))
})
t.Run("express req res no error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(_ Req, res Res) {
res.Set("X-Express", "ok")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "ok", resp.Header.Get("X-Express"))
})
t.Run("express next-err returns-err", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func() error) error {
res.Set("X-MW", "yes")
return next()
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "chained", string(body))
})
t.Run("express with next error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func() error) {
res.Set("X-MW", "yes")
_ = next() //nolint:errcheck // test
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express with noarg next error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func()) error {
res.Set("X-MW", "yes")
next()
return nil
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express with noarg next", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func()) {
res.Set("X-MW", "yes")
next()
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express with error next", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func(error)) {
res.Set("X-MW", "yes")
next(nil)
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express with error next error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func(error)) error {
res.Set("X-MW", "yes")
next(nil)
return nil
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express errnext-err returns-err", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func(error) error) {
res.Set("X-MW", "yes")
_ = next(nil) //nolint:errcheck // test
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("express errnext-err returns-err err", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(_ Req, res Res, next func(error) error) error {
res.Set("X-MW", "yes")
return next(nil)
},
func(c Ctx) error {
return c.SendString("chained")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "yes", resp.Header.Get("X-MW"))
})
t.Run("net/http HandlerFunc", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
})
t.Run("net/http func handler", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
})
t.Run("fasthttp RequestHandler", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBodyString("fasthttp")
}))
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
})
t.Run("fasthttp handler with error", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test", func(ctx *fasthttp.RequestCtx) error {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBodyString("fasthttp-err")
return nil
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
})
// Verify non-matching domain doesn't execute any handler type
t.Run("non-matching domain skips all handler types", func(t *testing.T) {
t.Parallel()
app := New()
app.Domain("api.example.com").Get("/test",
func(c Ctx) error {
c.Set("X-Handler", "ran")
return c.SendString("should-not-run")
},
)
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "wrong.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
require.Empty(t, resp.Header.Get("X-Handler"))
})
}
// Test_Domain_UseHandlerTypes verifies that Use() is compatible with all handler types.
func Test_Domain_UseHandlerTypes(t *testing.T) {
t.Parallel()
t.Run("fiber handler middleware", func(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
domain.Use(func(c Ctx) error {
c.Set("X-MW", "fiber")
return c.Next()
})
domain.Get("/test", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "fiber", resp.Header.Get("X-MW"))
})
t.Run("express middleware", func(t *testing.T) {
t.Parallel()
app := New()
domain := app.Domain("api.example.com")
domain.Use(func(_ Req, res Res, next func() error) error {
res.Set("X-MW", "express")
return next()
})
domain.Get("/test", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
require.Equal(t, "express", resp.Header.Get("X-MW"))
})
}
func Benchmark_Domain_Match(b *testing.B) {
m := parseDomainPattern(":tenant.api.example.com")
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
m.match("acme.api.example.com")
}
}
func Benchmark_Domain_Route(b *testing.B) {
app := New()
app.Domain("api.example.com").Get("/test", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
req.Host = "api.example.com"
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
resp, err := app.Test(req)
if err != nil {
b.Fatal(err)
}
resp.Body.Close() //nolint:errcheck // benchmark
}
}
func Benchmark_Domain_NoImpact(b *testing.B) {
// Benchmark regular routes to ensure domain feature has zero impact
app := New()
app.Get("/test", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/test", http.NoBody)
b.ResetTimer()
b.ReportAllocs()
for b.Loop() {
resp, err := app.Test(req)
if err != nil {
b.Fatal(err)
}
resp.Body.Close() //nolint:errcheck // benchmark
}
}
// Test_Domain_Security_EmptyPattern tests that empty domain patterns are rejected
func Test_Domain_Security_EmptyPattern(t *testing.T) {
t.Parallel()
require.Panics(t, func() {
parseDomainPattern("")
})
require.Panics(t, func() {
parseDomainPattern(" ")
})
}
// Test_Domain_Security_EmptyLabel tests that domain patterns with empty labels are rejected
func Test_Domain_Security_EmptyLabel(t *testing.T) {
t.Parallel()
require.Panics(t, func() {
parseDomainPattern("example..com")
})
require.Panics(t, func() {
parseDomainPattern(".example.com")
})
}
// Test_Domain_Security_EmptyParamName tests that empty parameter names are rejected
func Test_Domain_Security_EmptyParamName(t *testing.T) {
t.Parallel()
require.Panics(t, func() {
parseDomainPattern(":.example.com")
})
}
// Test_Domain_Security_InvalidParamName tests that invalid parameter names are rejected
func Test_Domain_Security_InvalidParamName(t *testing.T) {
t.Parallel()
require.Panics(t, func() {
parseDomainPattern(":[email protected]")
})
require.Panics(t, func() {
parseDomainPattern(":user$.example.com")
})
require.Panics(t, func() {
parseDomainPattern(":user name.example.com")
})
}
// Test_Domain_Security_InvalidDomainChars tests that invalid domain characters are rejected
func Test_Domain_Security_InvalidDomainChars(t *testing.T) {
t.Parallel()
require.Panics(t, func() {
parseDomainPattern("example$.com")
})
require.Panics(t, func() {
parseDomainPattern("[email protected]")
})
require.Panics(t, func() {
parseDomainPattern("example domain.com")
})
}
// Test_Domain_Security_TooManyParts tests DoS protection against excessive domain labels
func Test_Domain_Security_TooManyParts(t *testing.T) {
t.Parallel()
// Pattern with too many parts should panic
require.Panics(t, func() {
parts := make([]string, 20)
for i := range parts {
parts[i] = "sub"
}
parseDomainPattern(strings.Join(parts, "."))
})
}
// Test_Domain_Security_TooManyPartsRuntime tests DoS protection against excessive hostname labels at runtime
func Test_Domain_Security_TooManyPartsRuntime(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Hostname with too many labels should not match (DoS protection)
parts := make([]string, 20)
for i := range parts {
parts[i] = "sub"
}
maliciousHost := strings.Join(parts, ".")
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = maliciousHost
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_Security_ExcessiveHostnameLength tests DoS protection against very long hostnames
func Test_Domain_Security_ExcessiveHostnameLength(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Hostname exceeding 253 characters should not match (DoS protection)
maliciousHost := strings.Repeat("a", 254) + ".com"
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = maliciousHost
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_Security_ExcessiveLabelLength tests DoS protection against very long labels
func Test_Domain_Security_ExcessiveLabelLength(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Label exceeding 63 characters should not match (DoS protection)
maliciousHost := strings.Repeat("a", 64) + ".example.com"
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = maliciousHost
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_Security_InvalidHostnameChars tests that hostnames with invalid characters are rejected
func Test_Domain_Security_InvalidHostnameChars(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
// Test hostnames with invalid characters that can be tested
// Note: Some invalid chars (like spaces) are rejected by httptest.NewRequest itself
tests := []struct {
host string
canCreate bool
}{
{"example$.com", true},
{"[email protected]", true},
{"example\x00.com", true},
{"example\n.com", true},
{"example;.com", true},
{"example/.com", true},
}
for _, tt := range tests {
if tt.canCreate {
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = tt.host
resp, err := app.Test(req)
if err == nil {
require.Equal(t, StatusNotFound, resp.StatusCode, "Should reject hostname: %s", tt.host)
}
// If there's an error, the validation happened at an earlier layer which is also acceptable
}
}
}
// Test_Domain_Security_EmptyHostnameLabel tests that hostnames with empty labels are rejected
func Test_Domain_Security_EmptyHostnameLabel(t *testing.T) {
t.Parallel()
app := New()
app.Domain("example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "example..com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_Security_ValidParamNames tests that valid parameter names are accepted
func Test_Domain_Security_ValidParamNames(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pattern string
}{
{name: "alphanumeric", pattern: ":user123.example.com"},
{name: "underscore", pattern: ":user_name.example.com"},
{name: "hyphen", pattern: ":user-name.example.com"},
{name: "mixed", pattern: ":user_123-name.example.com"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.NotPanics(t, func() {
parseDomainPattern(tt.pattern)
})
})
}
}
// Test_Domain_Security_NonASCIIRejected tests that non-ASCII characters are rejected
// in both domain labels and parameter names (DNS names are ASCII-only).
func Test_Domain_Security_NonASCIIRejected(t *testing.T) {
t.Parallel()
// Non-ASCII in constant labels
require.Panics(t, func() {
parseDomainPattern("ünïcödé.example.com")
})
// Non-ASCII in parameter names
require.Panics(t, func() {
parseDomainPattern(":üser.example.com")
})
}
// Test_Domain_Security_NonASCIIHostnameRejected tests that non-ASCII hostnames
// are rejected at runtime (DNS names are ASCII-only).
func Test_Domain_Security_NonASCIIHostnameRejected(t *testing.T) {
t.Parallel()
app := New()
app.Domain(":sub.example.com").Get("/", func(c Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest(MethodGet, "/", http.NoBody)
req.Host = "ünïcödé.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_UseMountReusable verifies that mounting the same sub-app on
// multiple domain routers does not double-wrap handlers (the original sub-app
// is not mutated).
func Test_Domain_UseMountReusable(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
subApp.Get("/data", func(c Ctx) error {
return c.SendString("data response")
})
// Mount the same sub-app on two different domains
app.Domain("api.example.com").Use("/v1", subApp)
app.Domain("admin.example.com").Use("/v1", subApp)
// Test first domain works
req := httptest.NewRequest(MethodGet, "/v1/data", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "data response", string(body))
// Test second domain works
req = httptest.NewRequest(MethodGet, "/v1/data", http.NoBody)
req.Host = "admin.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "data response", string(body))
// Test wrong domain is rejected for both
req = httptest.NewRequest(MethodGet, "/v1/data", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_UseMountRoutesAfterMount verifies that routes added to a sub-app
// after it has been mounted on a domain router are NOT domain-filtered (since
// mount clones routes at mount time).
func Test_Domain_UseMountRoutesAfterMount(t *testing.T) {
t.Parallel()
app := New()
subApp := New()
// Register a route BEFORE mounting
subApp.Get("/before", func(c Ctx) error {
return c.SendString("before mount")
})
// Mount on domain router
app.Domain("api.example.com").Use("/api", subApp)
// Register a route AFTER mounting — this will NOT be domain-filtered
// because mount() clones routes at mount time.
subApp.Get("/after", func(c Ctx) error {
return c.SendString("after mount")
})
// Route registered before mount should be domain-filtered
req := httptest.NewRequest(MethodGet, "/api/before", http.NoBody)
req.Host = "api.example.com"
resp, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "before mount", string(body))
// Route registered before mount should be rejected on wrong domain
req = httptest.NewRequest(MethodGet, "/api/before", http.NoBody)
req.Host = "www.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
// Route registered after mount on the original sub-app is NOT included
// in the wrapper. Since the mount group references the wrapper, the
// after-mount route is never expanded into the parent app.
req = httptest.NewRequest(MethodGet, "/api/after", http.NoBody)
req.Host = "api.example.com"
resp, err = app.Test(req)
require.NoError(t, err)
require.Equal(t, StatusNotFound, resp.StatusCode)
}
// Test_Domain_Security_PatternLengthLimits verifies RFC 1035 length limits
// are enforced for domain patterns (253 total, 63 per label).
func Test_Domain_Security_PatternLengthLimits(t *testing.T) {
t.Parallel()
// Pattern exceeding 253 total characters
t.Run("total length exceeds 253", func(t *testing.T) {
t.Parallel()
// 250 chars of 'a' + ".com" = 254 chars total > 253
longPattern := strings.Repeat("a", 250) + ".com"
require.Greater(t, len(longPattern), 253)
require.Panics(t, func() {
parseDomainPattern(longPattern)
})
})
// Single label exceeding 63 characters
t.Run("label exceeds 63 chars", func(t *testing.T) {
t.Parallel()
longLabel := strings.Repeat("a", 64)
require.Panics(t, func() {
parseDomainPattern(longLabel + ".example.com")
})
})
// Pattern at exactly 253 characters should not panic
t.Run("253 chars total is valid", func(t *testing.T) {
t.Parallel()
label63 := strings.Repeat("a", 63)
pattern := label63 + "." + label63 + "." + label63 + "." + strings.Repeat("b", 59)
require.LessOrEqual(t, len(pattern), 253)
require.NotPanics(t, func() {
parseDomainPattern(pattern)
})
})
// Label at exactly 63 characters should not panic
t.Run("63 char label is valid", func(t *testing.T) {
t.Parallel()
label63 := strings.Repeat("a", 63)
require.NotPanics(t, func() {
parseDomainPattern(label63 + ".com")
})
})
}