lolly/internal/matcher/location_test.go
xfy ae0bec6c3b feat(internal): 实现 internal 指令
- 新增 IsInternalRedirect 检测内部重定向请求
- static handler 支持 internal 访问限制
- proxy handler 支持 internal 访问限制
- 支持 X-Accel-Redirect 内部重定向

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 18:09:06 +08:00

383 lines
10 KiB
Go

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, false)
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, false)
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, false)
err := engine.AddExact("/api", handler, false)
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, false)
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, false)
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, false)
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, false)
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, 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, false)
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, 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, 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, false)
engine.AddPrefixPriority("/api", hPrefixPriority, false)
engine.AddRegex(`^/api/`, hRegex, false, false)
engine.AddPrefix("/api/path", hPrefix, false)
// 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, false)
engine.AddRegex(`\.css$`, hRegex, false, 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, false)
engine.AddPrefix("/", hPrefix, false)
// 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, false)
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, false)
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, false)
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, false)
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, false)
engine.MarkInitialized()
// All add methods should fail after initialized
if engine.AddExact("/exact", handler, false) == nil {
t.Error("AddExact should fail after initialized")
}
if engine.AddPrefixPriority("/pp", handler, false) == nil {
t.Error("AddPrefixPriority should fail after initialized")
}
if engine.AddPrefix("/pre", handler, false) == nil {
t.Error("AddPrefix should fail after initialized")
}
if engine.AddRegex(`test`, handler, false, 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$", false, true}, // ~ is case-sensitive regex
{"^~", "", false, false}, // ^~ is NOT regex (prefix priority)
{"^~/static", "/static", false, false}, // ^~ is NOT regex
{"~*.php$", ".php$", true, true}, // ~* is case-insensitive regex
}
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")
}
}