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 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"}, 1, EndStop, NewUpstreamEmptyState()) if err != nil { t.Fatalf("NewSequentialWithState(): %v", err) } if !sequence.ObserveEmpty("a") { t.Fatal("ObserveEmpty() = false, want transition to stopped") } if current, ok, version := sequence.CurrentSelection(); ok || current != "" || version != 2 { t.Fatalf("CurrentSelection() = %q, %v, %d; want stopped version 2", 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") } }