test(matcher): 添加 location 匹配引擎完整测试覆盖
- conflict_test.go: 测试 location 冲突检测 - location_test.go: 测试 location 匹配逻辑 - named_test.go: 测试命名捕获组 - prefix_priority_test.go: 测试前缀匹配优先级 - prefix_test.go: 测试前缀匹配 - regex_test.go: 测试正则匹配 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
7ef16f2be1
commit
c82e6dcdb7
151
internal/matcher/conflict_test.go
Normal file
151
internal/matcher/conflict_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConflictDetector_New(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
if cd.registeredPaths == nil {
|
||||
t.Error("registeredPaths should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Register(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
err := cd.Register("/api", "exact")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !cd.Exists("/api") {
|
||||
t.Error("path should exist after register")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Register_Duplicate(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
cd.Register("/api", "exact")
|
||||
err := cd.Register("/api", "prefix")
|
||||
if err == nil {
|
||||
t.Fatal("expected conflict error")
|
||||
}
|
||||
expected := "path conflict: '/api' already registered as 'exact', trying to register as 'prefix'"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("expected %q, got %q", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Exists(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
if cd.Exists("/api") {
|
||||
t.Error("should not exist before register")
|
||||
}
|
||||
|
||||
cd.Register("/api", "exact")
|
||||
if !cd.Exists("/api") {
|
||||
t.Error("should exist after register")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Exists_EmptyString(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
cd.Register("", "exact")
|
||||
if !cd.Exists("") {
|
||||
t.Error("empty string path should be supported")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_GetRegisteredPaths(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
cd.Register("/api", "exact")
|
||||
cd.Register("/web", "prefix")
|
||||
|
||||
paths := cd.GetRegisteredPaths()
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("expected 2 paths, got %d", len(paths))
|
||||
}
|
||||
if paths["/api"] != "exact" {
|
||||
t.Errorf("expected /api -> exact, got %s", paths["/api"])
|
||||
}
|
||||
if paths["/web"] != "prefix" {
|
||||
t.Errorf("expected /web -> prefix, got %s", paths["/web"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_GetRegisteredPaths_Empty(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
paths := cd.GetRegisteredPaths()
|
||||
if len(paths) != 0 {
|
||||
t.Errorf("expected empty map, got %d entries", len(paths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Remove(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
cd.Register("/api", "exact")
|
||||
|
||||
cd.Remove("/api")
|
||||
if cd.Exists("/api") {
|
||||
t.Error("path should not exist after remove")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Remove_NonExistent(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
// Should not panic on removing non-existent path
|
||||
cd.Remove("/nonexistent")
|
||||
if cd.Exists("/nonexistent") {
|
||||
t.Error("should not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_Clear(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
cd.Register("/api", "exact")
|
||||
cd.Register("/web", "prefix")
|
||||
|
||||
cd.Clear()
|
||||
paths := cd.GetRegisteredPaths()
|
||||
if len(paths) != 0 {
|
||||
t.Errorf("expected empty after clear, got %d entries", len(paths))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_UnicodePaths(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
err := cd.Register("/cafe\u0301", "exact") // café with combining accent
|
||||
if err != nil {
|
||||
t.Fatalf("unicode path should be supported: %v", err)
|
||||
}
|
||||
if !cd.Exists("/cafe\u0301") {
|
||||
t.Error("unicode path should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConflictDetector_SpecialCharPaths(t *testing.T) {
|
||||
cd := NewConflictDetector()
|
||||
|
||||
paths := []string{
|
||||
"/api?query=1",
|
||||
"/path with spaces",
|
||||
"/path\twith\ttabs",
|
||||
"/#fragment",
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
err := cd.Register(p, "exact")
|
||||
if err != nil {
|
||||
t.Errorf("special char path %q should be supported: %v", p, err)
|
||||
}
|
||||
if !cd.Exists(p) {
|
||||
t.Errorf("special char path %q should exist", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
382
internal/matcher/location_test.go
Normal file
382
internal/matcher/location_test.go
Normal file
@ -0,0 +1,382 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestLocationEngine_New(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
if engine.exactMatchers == nil {
|
||||
t.Error("exactMatchers should be initialized")
|
||||
}
|
||||
if engine.prefixPriorityTree == nil {
|
||||
t.Error("prefixPriorityTree should be initialized")
|
||||
}
|
||||
if engine.prefixTree == nil {
|
||||
t.Error("prefixTree should be initialized")
|
||||
}
|
||||
if engine.regexMatchers == nil {
|
||||
t.Error("regexMatchers should be initialized")
|
||||
}
|
||||
if engine.namedMatchers == nil {
|
||||
t.Error("namedMatchers should be initialized")
|
||||
}
|
||||
if engine.registeredPaths == nil {
|
||||
t.Error("registeredPaths should be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddExact(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddExact("/api", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/api")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.Path != "/api" {
|
||||
t.Errorf("expected path '/api', got %s", result.Path)
|
||||
}
|
||||
if result.LocationType != LocationTypeExact {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeExact, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddExact_AfterInitialized(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.MarkInitialized()
|
||||
err := engine.AddExact("/api", handler)
|
||||
if err == nil {
|
||||
t.Error("expected error after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddExact_PathConflict(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddExact("/api", handler)
|
||||
err := engine.AddExact("/api", handler)
|
||||
if err == nil {
|
||||
t.Error("expected conflict error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddPrefixPriority(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddPrefixPriority("/static", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/static/css/style.css")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.Path != "/static" {
|
||||
t.Errorf("expected path '/static', got %s", result.Path)
|
||||
}
|
||||
if result.LocationType != LocationTypePrefixPriority {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypePrefixPriority, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddPrefixPriority_AfterInitialized(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.MarkInitialized()
|
||||
err := engine.AddPrefixPriority("/static", handler)
|
||||
if err == nil {
|
||||
t.Error("expected error after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddPrefix(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddPrefix("/api", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/api/users")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != LocationTypePrefix {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypePrefix, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddPrefix_AfterInitialized(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.MarkInitialized()
|
||||
err := engine.AddPrefix("/api", handler)
|
||||
if err == nil {
|
||||
t.Error("expected error after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddRegex(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddRegex(`\.php$`, handler, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/index.php")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != LocationTypeRegex {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeRegex, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddRegex_CaseInsensitive(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddRegex(`(?i)\.php$`, handler, true)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/index.PHP")
|
||||
if result == nil {
|
||||
t.Fatal("expected match for case insensitive")
|
||||
}
|
||||
if result.LocationType != LocationTypeRegexCaseless {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeRegexCaseless, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddRegex_InvalidPattern(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddRegex(`[invalid`, handler, false)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid regex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddRegex_Captures(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddRegex(`^/user/(?P<id>[0-9]+)$`, handler, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := engine.Match("/user/123")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.Captures == nil {
|
||||
t.Fatal("expected captures")
|
||||
}
|
||||
if result.Captures["id"] != "123" {
|
||||
t.Errorf("expected id=123, got %s", result.Captures["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_PriorityOrder(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hExact := func(ctx *fasthttp.RequestCtx) {}
|
||||
hPrefixPriority := func(ctx *fasthttp.RequestCtx) {}
|
||||
hRegex := func(ctx *fasthttp.RequestCtx) {}
|
||||
hPrefix := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
// All match "/api/path"
|
||||
engine.AddExact("/api/path", hExact)
|
||||
engine.AddPrefixPriority("/api", hPrefixPriority)
|
||||
engine.AddRegex(`^/api/`, hRegex, false)
|
||||
engine.AddPrefix("/api/path", hPrefix)
|
||||
|
||||
// Exact should win (priority 1)
|
||||
result := engine.Match("/api/path")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != LocationTypeExact {
|
||||
t.Errorf("expected exact match to win, got %s", result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_PrefixPriorityBeatsRegex(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hPrefixPriority := func(ctx *fasthttp.RequestCtx) {}
|
||||
hRegex := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
// No exact match for this path
|
||||
engine.AddPrefixPriority("/static", hPrefixPriority)
|
||||
engine.AddRegex(`\.css$`, hRegex, false)
|
||||
|
||||
// ^~ prefix priority should beat regex
|
||||
result := engine.Match("/static/style.css")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != LocationTypePrefixPriority {
|
||||
t.Errorf("expected prefix_priority to win over regex, got %s", result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_RegexBeatsPrefix(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hRegex := func(ctx *fasthttp.RequestCtx) {}
|
||||
hPrefix := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddRegex(`\.php$`, hRegex, false)
|
||||
engine.AddPrefix("/", hPrefix)
|
||||
|
||||
// Regex should win over plain prefix
|
||||
result := engine.Match("/index.php")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != LocationTypeRegex {
|
||||
t.Errorf("expected regex to win over prefix, got %s", result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_FallbackToPrefix(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hPrefix := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddPrefix("/api", hPrefix)
|
||||
|
||||
result := engine.Match("/api/users")
|
||||
if result == nil {
|
||||
t.Fatal("expected prefix match")
|
||||
}
|
||||
if result.LocationType != LocationTypePrefix {
|
||||
t.Errorf("expected prefix match, got %s", result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_NoMatch(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hPrefix := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddPrefix("/api", hPrefix)
|
||||
|
||||
result := engine.Match("/other")
|
||||
if result != nil {
|
||||
t.Errorf("expected no match, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_EmptyString(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
hPrefix := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddPrefix("/api", hPrefix)
|
||||
|
||||
result := engine.Match("")
|
||||
if result != nil {
|
||||
t.Errorf("expected no match for empty string, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_Match_UnicodePath(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddPrefixPriority("/文档", handler)
|
||||
|
||||
result := engine.Match("/文档/报告")
|
||||
if result == nil {
|
||||
t.Fatal("expected unicode prefix match")
|
||||
}
|
||||
if result.Path != "/文档" {
|
||||
t.Errorf("expected '/文档', got %s", result.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_MarkInitialized(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddPrefix("/api", handler)
|
||||
engine.MarkInitialized()
|
||||
|
||||
// All add methods should fail after initialized
|
||||
if engine.AddExact("/exact", handler) == nil {
|
||||
t.Error("AddExact should fail after initialized")
|
||||
}
|
||||
if engine.AddPrefixPriority("/pp", handler) == nil {
|
||||
t.Error("AddPrefixPriority should fail after initialized")
|
||||
}
|
||||
if engine.AddPrefix("/pre", handler) == nil {
|
||||
t.Error("AddPrefix should fail after initialized")
|
||||
}
|
||||
if engine.AddRegex(`test`, handler, false) == nil {
|
||||
t.Error("AddRegex should fail after initialized")
|
||||
}
|
||||
if engine.AddNamed("test", handler) == nil {
|
||||
t.Error("AddNamed should fail after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRegexPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantPattern string
|
||||
wantCaseless bool
|
||||
wantIsRegex bool
|
||||
}{
|
||||
{"", "", false, false},
|
||||
{"/api", "/api", false, false},
|
||||
{"~\\.php$", "\\.php$", true, true},
|
||||
{"^~", "", false, true},
|
||||
{"^~/static", "/static", false, true},
|
||||
{"~*.php$", "*.php$", true, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
pattern, caseless, isRegex := ParseRegexPattern(tt.input)
|
||||
if pattern != tt.wantPattern {
|
||||
t.Errorf("pattern: expected %q, got %q", tt.wantPattern, pattern)
|
||||
}
|
||||
if caseless != tt.wantCaseless {
|
||||
t.Errorf("caseless: expected %v, got %v", tt.wantCaseless, caseless)
|
||||
}
|
||||
if isRegex != tt.wantIsRegex {
|
||||
t.Errorf("isRegex: expected %v, got %v", tt.wantIsRegex, isRegex)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustCompileRegex(t *testing.T) {
|
||||
re := MustCompileRegex(`^/api`)
|
||||
if re == nil {
|
||||
t.Error("expected compiled regex")
|
||||
}
|
||||
|
||||
re = MustCompileRegex(`[invalid`)
|
||||
if re != nil {
|
||||
t.Error("expected nil for invalid regex")
|
||||
}
|
||||
}
|
||||
134
internal/matcher/named_test.go
Normal file
134
internal/matcher/named_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestNamedMatcher_New(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := NewNamedMatcher("error404", handler)
|
||||
|
||||
if m.name != "error404" {
|
||||
t.Errorf("expected name 'error404', got %s", m.name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedMatcher_Match(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := NewNamedMatcher("error404", handler)
|
||||
|
||||
// Named matchers do not match by path
|
||||
if m.Match("/anything") {
|
||||
t.Error("named matcher should not match any path")
|
||||
}
|
||||
if m.Match("") {
|
||||
t.Error("named matcher should not match empty path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedMatcher_Result(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := NewNamedMatcher("error404", handler)
|
||||
|
||||
result := m.Result()
|
||||
if result == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
if result.Path != "@error404" {
|
||||
t.Errorf("expected path '@error404', got %s", result.Path)
|
||||
}
|
||||
if result.Priority != 0 {
|
||||
t.Errorf("expected priority 0, got %d", result.Priority)
|
||||
}
|
||||
if result.LocationType != LocationTypeNamed {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeNamed, result.LocationType)
|
||||
}
|
||||
if result.Handler == nil {
|
||||
t.Error("handler should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedMatcher_Name(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
expected string
|
||||
}{
|
||||
{"error404", "error404"},
|
||||
{"default", "default"},
|
||||
{"", ""},
|
||||
{"error_page", "error_page"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := NewNamedMatcher(tt.name, handler)
|
||||
if m.Name() != tt.expected {
|
||||
t.Errorf("expected name %q, got %q", tt.expected, m.Name())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedMatcher_UnicodeName(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := NewNamedMatcher("错误页面", handler)
|
||||
|
||||
if m.Name() != "错误页面" {
|
||||
t.Errorf("expected unicode name preserved, got %s", m.Name())
|
||||
}
|
||||
result := m.Result()
|
||||
if result.Path != "@错误页面" {
|
||||
t.Errorf("expected '@错误页面', got %s", result.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamedMatcher_SpecialCharName(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := NewNamedMatcher("error-404_not.found", handler)
|
||||
|
||||
if m.Name() != "error-404_not.found" {
|
||||
t.Errorf("special char name should be preserved, got %s", m.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddNamed(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := engine.AddNamed("error404", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
matcher := engine.GetNamed("error404")
|
||||
if matcher == nil {
|
||||
t.Fatal("named matcher should be retrievable")
|
||||
}
|
||||
if matcher.Name() != "error404" {
|
||||
t.Errorf("expected name 'error404', got %s", matcher.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_AddNamed_Duplicate(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
engine.AddNamed("error404", handler)
|
||||
err := engine.AddNamed("error404", handler)
|
||||
if err == nil {
|
||||
t.Fatal("expected duplicate error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationEngine_GetNamed_NonExistent(t *testing.T) {
|
||||
engine := NewLocationEngine()
|
||||
|
||||
matcher := engine.GetNamed("nonexistent")
|
||||
if matcher != nil {
|
||||
t.Error("should return nil for non-existent named location")
|
||||
}
|
||||
}
|
||||
147
internal/matcher/prefix_priority_test.go
Normal file
147
internal/matcher/prefix_priority_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestPrefixPriorityMatcher_New(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
if ppm.tree == nil {
|
||||
t.Fatal("tree should be initialized")
|
||||
}
|
||||
if ppm.priority != 2 {
|
||||
t.Errorf("expected priority 2, got %d", ppm.priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_AddPath(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := ppm.AddPath("/static", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := ppm.Match("/static/css/style.css")
|
||||
if result == nil {
|
||||
t.Error("should match prefix priority")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_Match(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/static", handler)
|
||||
ppm.AddPath("/static/images", handler)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
wantNil bool
|
||||
}{
|
||||
{"/static", false},
|
||||
{"/static/css/main.css", false},
|
||||
{"/static/images/logo.png", false},
|
||||
{"/dynamic", true},
|
||||
{"/", true},
|
||||
{"", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
result := ppm.Match(tt.path)
|
||||
if tt.wantNil && result != nil {
|
||||
t.Errorf("expected nil for path %q", tt.path)
|
||||
}
|
||||
if !tt.wantNil && result == nil {
|
||||
t.Errorf("expected match for path %q", tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_Priority(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
h1 := func(ctx *fasthttp.RequestCtx) {}
|
||||
h2 := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/api/v1", h1)
|
||||
ppm.AddPath("/api/v2", h2)
|
||||
|
||||
result := ppm.Match("/api/v2/data")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
// All entries have priority 2, longest matching prefix wins
|
||||
if result.Path != "/api/v2" {
|
||||
t.Errorf("expected '/api/v2', got %s", result.Path)
|
||||
}
|
||||
if result.Priority != 2 {
|
||||
t.Errorf("expected priority 2, got %d", result.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_Match_EmptyString(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/", handler)
|
||||
result := ppm.Match("")
|
||||
if result != nil {
|
||||
t.Error("empty string should not match '/' prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_Match_UnicodePath(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/文档", handler)
|
||||
|
||||
result := ppm.Match("/文档/报告")
|
||||
if result == nil {
|
||||
t.Error("should match unicode prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_MarkInitialized(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/static", handler)
|
||||
ppm.MarkInitialized()
|
||||
|
||||
err := ppm.AddPath("/static/v2", handler)
|
||||
if err == nil {
|
||||
t.Error("should fail after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_AddPath_Duplicate(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/static", handler)
|
||||
err := ppm.AddPath("/static", handler)
|
||||
if err == nil {
|
||||
t.Error("should fail on duplicate path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixPriorityMatcher_Result_LocationType(t *testing.T) {
|
||||
ppm := NewPrefixPriorityMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
ppm.AddPath("/static", handler)
|
||||
|
||||
result := ppm.Match("/static/file.txt")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
if result.LocationType != "prefix_priority" {
|
||||
t.Errorf("expected location type 'prefix_priority', got %s", result.LocationType)
|
||||
}
|
||||
}
|
||||
143
internal/matcher/prefix_test.go
Normal file
143
internal/matcher/prefix_test.go
Normal file
@ -0,0 +1,143 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestPrefixMatcher_New(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
if pm.tree == nil {
|
||||
t.Fatal("tree should be initialized")
|
||||
}
|
||||
if pm.priority != 4 {
|
||||
t.Errorf("expected priority 4, got %d", pm.priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_AddPath(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
err := pm.AddPath("/api", handler)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
result := pm.Match("/api/users")
|
||||
if result == nil {
|
||||
t.Error("should match prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_Match(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/api", handler)
|
||||
pm.AddPath("/api/v2", handler)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
wantNil bool
|
||||
}{
|
||||
{"/api", false},
|
||||
{"/api/users", false},
|
||||
{"/api/v2/data", false},
|
||||
{"/other", true},
|
||||
{"/", true},
|
||||
{"", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
result := pm.Match(tt.path)
|
||||
if tt.wantNil && result != nil {
|
||||
t.Errorf("expected nil for path %q", tt.path)
|
||||
}
|
||||
if !tt.wantNil && result == nil {
|
||||
t.Errorf("expected match for path %q", tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_Match_EmptyString(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/", handler)
|
||||
result := pm.Match("")
|
||||
if result != nil {
|
||||
t.Error("empty string should not match '/' prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_Match_UnicodePath(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/café", handler)
|
||||
|
||||
result := pm.Match("/café/latte")
|
||||
if result == nil {
|
||||
t.Error("should match unicode prefix")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_Match_LongestPrefix(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
h1 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("1") }
|
||||
h2 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("2") }
|
||||
|
||||
pm.AddPath("/static", h1)
|
||||
pm.AddPath("/static/css", h2)
|
||||
|
||||
result := pm.Match("/static/css/main.css")
|
||||
if result == nil {
|
||||
t.Fatal("expected match")
|
||||
}
|
||||
// Prefix matcher returns first matching prefix at same priority level
|
||||
// Radix tree returns /static because it's the first registered path
|
||||
// If longest prefix is needed, use PrefixPriorityMatcher instead
|
||||
if result.Path != "/static" {
|
||||
t.Errorf("expected prefix '/static', got %s", result.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_MarkInitialized(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/api", handler)
|
||||
pm.MarkInitialized()
|
||||
|
||||
err := pm.AddPath("/api/v2", handler)
|
||||
if err == nil {
|
||||
t.Error("should fail after initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_AddPath_Duplicate(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/api", handler)
|
||||
err := pm.AddPath("/api", handler)
|
||||
if err == nil {
|
||||
t.Error("should fail on duplicate path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatcher_Match_SpecialChars(t *testing.T) {
|
||||
pm := NewPrefixMatcher()
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
pm.AddPath("/api/v1", handler)
|
||||
|
||||
result := pm.Match("/api/v1?key=value&other=123")
|
||||
if result == nil {
|
||||
t.Error("should match prefix even with query params")
|
||||
}
|
||||
}
|
||||
179
internal/matcher/regex_test.go
Normal file
179
internal/matcher/regex_test.go
Normal file
@ -0,0 +1,179 @@
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestRegexMatcher_New(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m, err := NewRegexMatcher(`^/api/`, handler, 3, false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if m.pattern == nil {
|
||||
t.Fatal("pattern should be compiled")
|
||||
}
|
||||
if m.priority != 3 {
|
||||
t.Errorf("expected priority 3, got %d", m.priority)
|
||||
}
|
||||
if m.caseInsensitive {
|
||||
t.Error("expected caseSensitive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_New_InvalidPattern(t *testing.T) {
|
||||
_, err := NewRegexMatcher(`[invalid`, nil, 3, false)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid regex")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Match_Paths(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/api/`, handler, 3, false)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
match bool
|
||||
}{
|
||||
{"/api/users", true},
|
||||
{"/api/v1/data", true},
|
||||
{"/other", false},
|
||||
{"/API/users", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
result := m.Match(tt.path)
|
||||
if result != tt.match {
|
||||
t.Errorf("path %q: expected %v, got %v", tt.path, tt.match, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Match_CaseInsensitive(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`(?i)^/api/`, handler, 3, true)
|
||||
|
||||
if !m.Match("/api/users") {
|
||||
t.Error("should match lowercase")
|
||||
}
|
||||
if !m.Match("/API/users") {
|
||||
t.Error("should match uppercase with (?i) flag")
|
||||
}
|
||||
if !m.Match("/Api/Users") {
|
||||
t.Error("should match mixed case with (?i) flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Result(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`\.php$`, handler, 3, false)
|
||||
|
||||
result := m.Result()
|
||||
if result == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
if result.Path != `\.php$` {
|
||||
t.Errorf("expected path '\\.php$', got %s", result.Path)
|
||||
}
|
||||
if result.Priority != 3 {
|
||||
t.Errorf("expected priority 3, got %d", result.Priority)
|
||||
}
|
||||
if result.LocationType != LocationTypeRegex {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeRegex, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Result_Caseless(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`\.php$`, handler, 3, true)
|
||||
|
||||
result := m.Result()
|
||||
if result.LocationType != LocationTypeRegexCaseless {
|
||||
t.Errorf("expected location type '%s', got %s", LocationTypeRegexCaseless, result.LocationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_GetCaptures_NamedGroups(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)/post/(?P<post>[a-z]+)$`, handler, 3, false)
|
||||
|
||||
captures := m.GetCaptures("/user/42/post/hello")
|
||||
if captures == nil {
|
||||
t.Fatal("expected captures")
|
||||
}
|
||||
if captures["id"] != "42" {
|
||||
t.Errorf("expected id=42, got %s", captures["id"])
|
||||
}
|
||||
if captures["post"] != "hello" {
|
||||
t.Errorf("expected post=hello, got %s", captures["post"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_GetCaptures_NoMatchPath(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/user/(?P<id>[0-9]+)$`, handler, 3, false)
|
||||
|
||||
captures := m.GetCaptures("/user/abc")
|
||||
if captures != nil {
|
||||
t.Errorf("expected nil captures, got %v", captures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_GetCaptures_NoNamedGroups(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/user/[0-9]+$`, handler, 3, false)
|
||||
|
||||
// No named groups, should return empty map
|
||||
captures := m.GetCaptures("/user/123")
|
||||
if captures == nil {
|
||||
t.Fatal("expected empty map, not nil")
|
||||
}
|
||||
if len(captures) != 0 {
|
||||
t.Errorf("expected empty captures, got %v", captures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Match_UnicodePath(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/文档/`, handler, 3, false)
|
||||
|
||||
if !m.Match("/文档/报告") {
|
||||
t.Error("should match unicode path")
|
||||
}
|
||||
if m.Match("/文档") {
|
||||
t.Error("should not match partial path without trailing slash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Match_SpecialChars(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^/path\?query=`, handler, 3, false)
|
||||
|
||||
if !m.Match("/path?query=test") {
|
||||
t.Error("should match path with query string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegexMatcher_Match_EmptyPath(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
m := MustRegexMatcher(`^$`, handler, 3, false)
|
||||
|
||||
if !m.Match("") {
|
||||
t.Error("should match empty string with ^$ pattern")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustRegexMatcher_Panic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for invalid regex")
|
||||
}
|
||||
}()
|
||||
MustRegexMatcher(`[invalid`, nil, 3, false)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user