proxy-pool/internal/domain/routing/routing_test.go
2026-07-29 21:41:17 +08:00

184 lines
5.6 KiB
Go

package routing
import (
"sync"
"testing"
)
func TestRuleSetUsesFirstMatchingRule(t *testing.T) {
rules, err := Compile([]Rule{
{Name: "specific", Match: Match{HostRegex: `(^|\.)jd\.com$`}, Upstreams: []string{"jd"}},
{Name: "default", Match: Match{HostRegex: `.*`}, Action: ActionReject},
})
if err != nil {
t.Fatalf("Compile(): %v", err)
}
got, ok := rules.Match(Request{Host: "api.jd.com", Method: "GET", Path: "/"})
if !ok || got.Name != "specific" {
t.Fatalf("Match() = %q, %v; want specific, true", got.Name, ok)
}
}
func TestRuleSetIsDetachedFromInputAndReturnedRules(t *testing.T) {
input := []Rule{{
Name: "route-a",
Match: Match{
HostRegex: `.*`,
Methods: []string{"GET"},
Headers: map[string]string{"X-Tenant": "a"},
},
Upstreams: []string{"provider-a"},
}}
rules, err := Compile(input)
if err != nil {
t.Fatalf("Compile(): %v", err)
}
input[0].Match.Methods[0] = "POST"
input[0].Match.Headers["X-Tenant"] = "changed"
input[0].Upstreams[0] = "changed"
matched, ok := rules.Match(Request{
Host: "example.com", Method: "GET", Headers: map[string]string{"X-Tenant": "a"},
})
if !ok || matched.Upstreams[0] != "provider-a" {
t.Fatalf("Match() after input mutation = %+v, %v", matched, ok)
}
matched.Match.Methods[0] = "DELETE"
matched.Match.Headers["X-Tenant"] = "returned-change"
matched.Upstreams[0] = "returned-change"
second, ok := rules.Match(Request{
Host: "example.com", Method: "GET", Headers: map[string]string{"X-Tenant": "a"},
})
if !ok || second.Upstreams[0] != "provider-a" || second.Match.Methods[0] != "GET" {
t.Fatalf("Match() after returned-rule mutation = %+v, %v", second, ok)
}
}
func TestSequentialSwitchesOnceAtThreshold(t *testing.T) {
sequence, err := NewSequential([]string{"a", "b", "c"}, 5)
if err != nil {
t.Fatalf("NewSequential(): %v", err)
}
for range 4 {
sequence.ObserveEmpty("a")
}
if got := sequence.Current(); got != "a" {
t.Fatalf("Current() = %q before threshold, want a", got)
}
var wg sync.WaitGroup
for range 100 {
wg.Add(1)
go func() {
defer wg.Done()
sequence.ObserveEmpty("a")
}()
}
wg.Wait()
if got := sequence.Current(); got != "b" {
t.Fatalf("Current() = %q after concurrent threshold, want b", got)
}
}
func TestSequentialValidFetchResetsEmptyCount(t *testing.T) {
sequence, _ := NewSequential([]string{"a", "b"}, 5)
for range 4 {
sequence.ObserveEmpty("a")
}
sequence.ObserveValid("a")
for range 4 {
sequence.ObserveEmpty("a")
}
if got := sequence.Current(); got != "a" {
t.Fatalf("Current() = %q, want a after reset", got)
}
}
func TestSequentialDefaultsToStopAfterLastUpstream(t *testing.T) {
sequence, err := NewSequential([]string{"a", "b"}, 1)
if err != nil {
t.Fatalf("NewSequential(): %v", err)
}
if !sequence.ObserveEmpty("a") || sequence.Current() != "b" {
t.Fatalf("first transition current = %q, want b", sequence.Current())
}
if !sequence.ObserveEmpty("b") {
t.Fatal("ObserveEmpty(b) = false, want transition to stopped")
}
if current, available, _ := sequence.CurrentSelection(); available || current != "" {
t.Fatalf("CurrentSelection() = %q, %v; want stopped", current, available)
}
}
func TestSequentialRejectsSingleUpstream(t *testing.T) {
if _, err := NewSequential([]string{"a"}, 1); err == nil {
t.Fatal("NewSequential(single upstream) error = nil")
}
if _, err := NewSequentialWithState(
[]string{"a"}, 1, EndStop, NewUpstreamEmptyState(),
); err == nil {
t.Fatal("NewSequentialWithState(single upstream) error = nil")
}
}
func TestSequentialSharesUpstreamEmptyStateAcrossRoutingCursors(t *testing.T) {
empty := NewUpstreamEmptyState()
first, err := NewSequentialWithState([]string{"a", "b"}, 5, EndStayLast, empty)
if err != nil {
t.Fatalf("NewSequentialWithState(first): %v", err)
}
second, err := NewSequentialWithState([]string{"a", "c"}, 5, EndStayLast, empty)
if err != nil {
t.Fatalf("NewSequentialWithState(second): %v", err)
}
for range 5 {
first.ObserveEmpty("a")
}
if got := first.Current(); got != "b" {
t.Fatalf("first.Current() = %q, want b", got)
}
if got := second.EmptyCount("a"); got != 5 {
t.Fatalf("second.EmptyCount(a) = %d, want shared count 5", got)
}
if !second.ObserveEmpty("a") || second.Current() != "c" {
t.Fatalf("second did not advance from shared empty state: current=%q", second.Current())
}
}
func TestSequentialStopEndBehaviorHasNoCurrentSelection(t *testing.T) {
sequence, err := NewSequentialWithState([]string{"a", "b"}, 1, EndStop, NewUpstreamEmptyState())
if err != nil {
t.Fatalf("NewSequentialWithState(): %v", err)
}
if !sequence.ObserveEmpty("a") {
t.Fatal("ObserveEmpty(a) = false, want transition to b")
}
if !sequence.ObserveEmpty("b") {
t.Fatal("ObserveEmpty(b) = false, want transition to stopped")
}
if current, ok, version := sequence.CurrentSelection(); ok || current != "" || version != 3 {
t.Fatalf("CurrentSelection() = %q, %v, %d; want stopped version 3", current, ok, version)
}
}
func TestSequentialLoopDoesNotReuseSameEmptyEpisode(t *testing.T) {
empty := NewUpstreamEmptyState()
sequence, err := NewSequentialWithState([]string{"a", "b"}, 1, EndLoop, empty)
if err != nil {
t.Fatalf("NewSequentialWithState(): %v", err)
}
if !sequence.ObserveEmpty("a") || !sequence.ObserveEmpty("b") || sequence.Current() != "a" {
t.Fatalf("sequence did not loop to a: current=%q", sequence.Current())
}
if sequence.ObserveEmpty("a") || sequence.Current() != "a" {
t.Fatal("sequence reused the same a empty episode")
}
sequence.ObserveValid("a")
if !sequence.ObserveEmpty("a") || sequence.Current() != "b" {
t.Fatal("sequence did not advance after a new empty episode")
}
}