mirror of
https://github.com/wweir/sower.git
synced 2026-10-19 16:14:33 +00:00
aconfig parses an empty TOML array (rules = []) as a slice holding one empty string, which the router then treated as a real rule. Empty rules can never match a real domain; Add and Replace now drop them.
233 lines
6.0 KiB
Go
233 lines
6.0 KiB
Go
package router
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
func TestRuleSetAddListCount(t *testing.T) {
|
|
rs := NewRuleSet("example.com", "**.cn")
|
|
|
|
rs.Add("github.com", "example.com") // duplicate example.com must be ignored
|
|
if got := rs.Count(); got != 3 {
|
|
t.Fatalf("unexpected count: %d", got)
|
|
}
|
|
rules := rs.List()
|
|
if len(rules) != 3 {
|
|
t.Fatalf("unexpected rule list: %v", rules)
|
|
}
|
|
seen := make(map[string]bool, len(rules))
|
|
for _, r := range rules {
|
|
seen[r] = true
|
|
}
|
|
for _, want := range []string{"example.com", "**.cn", "github.com"} {
|
|
if !seen[want] {
|
|
t.Fatalf("missing rule %q in %v", want, rules)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRuleSetMatch(t *testing.T) {
|
|
rs := NewRuleSet("example.com", "**.example.org", "*")
|
|
if !rs.Match("example.com") {
|
|
t.Fatal("expected example.com to match")
|
|
}
|
|
if rs.Match("sub.example.com") {
|
|
t.Fatal("expected sub.example.com to not match bare example.com rule")
|
|
}
|
|
if !rs.Match("sub.example.org") {
|
|
t.Fatal("expected sub.example.org to match wildcard rule")
|
|
}
|
|
if !rs.Match("anything") {
|
|
t.Fatal("expected * to match anything")
|
|
}
|
|
if rs.Match("github.com") {
|
|
t.Fatal("expected github.com to not match")
|
|
}
|
|
}
|
|
|
|
func TestRuleSetMatchRule(t *testing.T) {
|
|
rs := NewRuleSet("example.com", "**.example.org", "ads.*.com")
|
|
|
|
cases := []struct {
|
|
item string
|
|
want string
|
|
}{
|
|
{"example.com", "example.com"},
|
|
{"EXAMPLE.COM.", "example.com"}, // normalized like Match
|
|
{"sub.example.com", ""},
|
|
{"sub.example.org", "**.example.org"},
|
|
{"a.b.example.org", "**.example.org"},
|
|
{"example.org", "**.example.org"},
|
|
{"ads.cdn.com", "ads.*.com"},
|
|
{"ads.a.b.com", ""}, // mid-rule * matches one label only
|
|
{"github.com", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
got, ok := rs.MatchRule(tc.item)
|
|
if tc.want == "" {
|
|
if ok {
|
|
t.Fatalf("MatchRule(%q) = %q, want no match", tc.item, got)
|
|
}
|
|
continue
|
|
}
|
|
if !ok || got != tc.want {
|
|
t.Fatalf("MatchRule(%q) = %q, %v; want %q", tc.item, got, ok, tc.want)
|
|
}
|
|
}
|
|
|
|
// MatchRule agrees with Match on every case the tree sees
|
|
for _, item := range []string{"example.com", "sub.example.com", "sub.example.org", "anything"} {
|
|
if got, ok := rs.MatchRule(item); ok != rs.Match(item) {
|
|
t.Fatalf("MatchRule(%q) = %q, %v diverges from Match %v", item, got, ok, rs.Match(item))
|
|
}
|
|
}
|
|
|
|
var nilRS *RuleSet
|
|
if _, ok := nilRS.MatchRule("x"); ok {
|
|
t.Fatal("expected nil RuleSet MatchRule to miss")
|
|
}
|
|
}
|
|
|
|
func TestRuleSetRemoveRebuildsTree(t *testing.T) {
|
|
rs := NewRuleSet("example.com", "github.com", "**.cn")
|
|
|
|
if !rs.Remove("example.com") {
|
|
t.Fatal("expected remove to report present rule")
|
|
}
|
|
if rs.Match("example.com") {
|
|
t.Fatal("expected removed rule to stop matching")
|
|
}
|
|
if !rs.Match("github.com") || !rs.Match("a.cn") {
|
|
t.Fatal("expected remaining rules to keep matching")
|
|
}
|
|
if got := rs.Count(); got != 2 {
|
|
t.Fatalf("unexpected count after remove: %d", got)
|
|
}
|
|
|
|
if rs.Remove("missing.example") {
|
|
t.Fatal("expected remove to report absent rule")
|
|
}
|
|
if got := rs.List(); len(got) != 2 {
|
|
t.Fatalf("unexpected list after absent remove: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestRuleSetNilMatch(t *testing.T) {
|
|
var rs *RuleSet
|
|
if rs.Match("anything") {
|
|
t.Fatal("expected nil RuleSet to never match")
|
|
}
|
|
}
|
|
|
|
func TestRuleSetConcurrentAccess(t *testing.T) {
|
|
rs := NewRuleSet("seed.example.com")
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 16; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
rule := fmt.Sprintf("host%d.example.com", i)
|
|
for j := 0; j < 200; j++ {
|
|
rs.Add(rule)
|
|
_ = rs.Match(rule)
|
|
_ = rs.Count()
|
|
_ = rs.List()
|
|
if j%10 == 0 {
|
|
rs.Remove(fmt.Sprintf("host%d.example.com", (i+1)%16))
|
|
rs.Add(fmt.Sprintf("host%d.example.com", (i+1)%16))
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
if !rs.Match("seed.example.com") {
|
|
t.Fatal("expected seed rule to survive concurrent access")
|
|
}
|
|
}
|
|
|
|
func TestRuleSetReplaceConcurrentWithMatch(t *testing.T) {
|
|
rs := NewRuleSet("seed.example.com")
|
|
var stop atomic.Bool
|
|
var wg sync.WaitGroup
|
|
for range 8 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for !stop.Load() {
|
|
_ = rs.Match("seed.example.com")
|
|
_ = rs.Match("replacement.example.com")
|
|
}
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
rs.Replace(fmt.Sprintf("replacement%d.example.com", i), "replacement.example.com")
|
|
}
|
|
stop.Store(true)
|
|
wg.Wait()
|
|
|
|
if !rs.Match("replacement.example.com") || rs.Match("seed.example.com") {
|
|
t.Fatalf("unexpected final rules: %v", rs.List())
|
|
}
|
|
}
|
|
|
|
func TestListFilteredPaginationAndSearch(t *testing.T) {
|
|
rs := NewRuleSet("example.com", "**.cn", "GitHub.com", "mail.google.com", "a.cn")
|
|
|
|
// empty query: paginated over the full list
|
|
rules, total := rs.ListFiltered("", 1, 2)
|
|
if total != 5 {
|
|
t.Fatalf("expected total 5, got %d", total)
|
|
}
|
|
if len(rules) != 2 || rules[0] != "**.cn" || rules[1] != "GitHub.com" {
|
|
t.Fatalf("unexpected page: %v", rules)
|
|
}
|
|
|
|
// case-insensitive substring search
|
|
rules, total = rs.ListFiltered("GITHUB", 0, 10)
|
|
if total != 1 || len(rules) != 1 || rules[0] != "GitHub.com" {
|
|
t.Fatalf("unexpected search result: %v (total %d)", rules, total)
|
|
}
|
|
|
|
// search with pagination ("cn" matches **.cn and a.cn)
|
|
rules, total = rs.ListFiltered("cn", 0, 1)
|
|
if total != 2 || len(rules) != 1 || rules[0] != "**.cn" {
|
|
t.Fatalf("unexpected search page: %v (total %d)", rules, total)
|
|
}
|
|
rules, _ = rs.ListFiltered("cn", 1, 1)
|
|
if len(rules) != 1 || rules[0] != "a.cn" {
|
|
t.Fatalf("unexpected second page: %v", rules)
|
|
}
|
|
|
|
// offset beyond the end yields an empty page with the full total
|
|
rules, total = rs.ListFiltered("", 99, 10)
|
|
if len(rules) != 0 || total != 5 {
|
|
t.Fatalf("expected empty page with total 5, got %v (total %d)", rules, total)
|
|
}
|
|
}
|
|
|
|
func TestRuleSetDropsEmptyRules(t *testing.T) {
|
|
rs := NewRuleSet("a.com", "", "b.com")
|
|
list := rs.List()
|
|
if len(list) != 2 || list[0] != "a.com" || list[1] != "b.com" {
|
|
t.Fatalf("Add should drop empty rules: %q", list)
|
|
}
|
|
|
|
rs.Add("", "c.com")
|
|
list = rs.List()
|
|
if len(list) != 3 || list[2] != "c.com" {
|
|
t.Fatalf("Add should drop empty rules: %q", list)
|
|
}
|
|
|
|
rs.Replace("", "d.com")
|
|
list = rs.List()
|
|
if len(list) != 1 || list[0] != "d.com" {
|
|
t.Fatalf("Replace should drop empty rules: %q", list)
|
|
}
|
|
}
|