anubis/internal/glob/glob_test.go
Xe Iaso d35e47c655
feat: glob matching for redirect domains (#1084)
* feat: glob matching for redirect domains

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs: update CHANGELOG

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-09-06 15:46:18 +00:00

190 lines
5.3 KiB
Go

package glob
import "testing"
func TestGlob_EqualityAndEmpty(t *testing.T) {
cases := []struct {
name string
pattern string
subj string
want bool
}{
{"exact match", "hello", "hello", true},
{"exact mismatch", "hello", "hell", false},
{"empty pattern and subject", "", "", true},
{"empty pattern with non-empty subject", "", "x", false},
{"pattern star matches empty", "*", "", true},
{"pattern star matches anything", "*", "anything at all", true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
})
}
}
func TestGlob_LeadingAndTrailing(t *testing.T) {
cases := []struct {
name string
pattern string
subj string
want bool
}{
{"prefix match - minimal", "foo*", "foo", true},
{"prefix match - extended", "foo*", "foobar", true},
{"prefix mismatch - not at start", "foo*", "xfoo", false},
{"suffix match - minimal", "*foo", "foo", true},
{"suffix match - extended", "*foo", "xfoo", true},
{"suffix mismatch - not at end", "*foo", "foox", false},
{"contains match", "*foo*", "barfoobaz", true},
{"contains mismatch - missing needle", "*foo*", "f", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
})
}
}
func TestGlob_MiddleAndOrder(t *testing.T) {
cases := []struct {
name string
pattern string
subj string
want bool
}{
{"middle wildcard basic", "f*o", "fo", true},
{"middle wildcard gap", "f*o", "fZZZo", true},
{"middle wildcard requires start f", "f*o", "xfyo", false},
{"order enforced across parts", "a*b*c*d", "axxbxxcxxd", true},
{"order mismatch fails", "a*b*c*d", "abdc", false},
{"must end with last part when no trailing *", "*foo*bar", "zzfooqqbar", true},
{"failing when trailing chars remain", "*foo*bar", "zzfooqqbarzz", false},
{"first part must start when no leading *", "foo*bar", "zzfooqqbar", false},
{"works with overlapping content", "ab*ba", "ababa", true},
{"needle not found fails", "foo*bar", "foobaz", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
})
}
}
func TestGlob_ConsecutiveStarsAndEmptyParts(t *testing.T) {
cases := []struct {
name string
pattern string
subj string
want bool
}{
{"double star matches anything", "**", "", true},
{"double star matches anything non-empty", "**", "abc", true},
{"consecutive stars behave like single", "a**b", "ab", true},
{"consecutive stars with gaps", "a**b", "axxxb", true},
{"consecutive stars + trailing star", "a**b*", "axxbzzz", true},
{"consecutive stars still enforce anchors", "a**b", "xaBy", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
})
}
}
func TestGlob_MaxPartsLimit(t *testing.T) {
// Allowed: up to 4 '*' (5 parts)
allowed := []struct {
pattern string
subj string
want bool
}{
{"a*b*c*d*e", "axxbxxcxxdxxe", true}, // 4 stars -> 5 parts
{"*a*b*c*d", "zzzaaaabbbcccddd", true},
{"a*b*c*d*e", "abcde", true},
{"a*b*c*d*e", "abxdxe", false}, // missing 'c' should fail
}
for _, tc := range allowed {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("allowed pattern Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
}
// Disallowed: 5 '*' (6 parts) -> always false by complexity check
disallowed := []struct {
pattern string
subj string
}{
{"a*b*c*d*e*f", "aXXbYYcZZdQQeRRf"},
{"*a*b*c*d*e*", "abcdef"},
{"******", "anything"}, // 6 stars -> 7 parts
}
for _, tc := range disallowed {
if got := Glob(tc.pattern, tc.subj); got {
t.Fatalf("disallowed pattern should fail Glob(%q,%q) = %v, want false", tc.pattern, tc.subj, got)
}
}
}
func TestGlob_CaseSensitivity(t *testing.T) {
cases := []struct {
pattern string
subj string
want bool
}{
{"FOO*", "foo", false},
{"*Bar", "bar", false},
{"Foo*Bar", "FooZZZBar", true},
}
for _, tc := range cases {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
}
}
func TestGlob_EmptySubjectInteractions(t *testing.T) {
cases := []struct {
pattern string
subj string
want bool
}{
{"*a", "", false},
{"a*", "", false},
{"**", "", true},
{"*", "", true},
}
for _, tc := range cases {
if got := Glob(tc.pattern, tc.subj); got != tc.want {
t.Fatalf("Glob(%q,%q) = %v, want %v", tc.pattern, tc.subj, got, tc.want)
}
}
}
func BenchmarkGlob(b *testing.B) {
patterns := []string{
"*", "*foo*", "foo*bar", "a*b*c*d*e", "a**b*", "*needle*end",
}
subjects := []string{
"", "foo", "barfoo", "foobarbaz", "axxbxxcxxdxxe", "zzfooqqbarzz",
"lorem ipsum dolor sit amet, consectetur adipiscing elit",
}
for _, p := range patterns {
for _, s := range subjects {
b.Run(p+"::"+s, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Glob(p, s)
}
})
}
}
}