initial commit

This commit is contained in:
2026-08-16 21:18:45 -05:00
commit 1e05a01bcf
122 changed files with 29178 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
package blacklist
import (
"strings"
"testing"
"github.com/owen/vibedns/internal/models"
)
func TestMatchExactAndSubdomains(t *testing.T) {
b := NewBuilder(1, "Test", models.KindBlacklist, 4)
b.Add("example.com", true) // covers subdomains
b.Add("exact.example.net", false) // this name only
b.Add("*.wild.example", false) // wildcard syntax implies subdomains
set := b.Build()
tests := []struct {
name string
query string
want bool
}{
{"exact match on a subdomain entry", "example.com", true},
{"one level down", "www.example.com", true},
{"several levels down", "a.b.c.example.com", true},
{"trailing dot is ignored", "www.example.com.", true},
{"case is ignored", "WWW.Example.COM", true},
{"sibling is not matched", "notexample.com", false},
{"parent is not matched", "com", false},
{"exact-only entry matches itself", "exact.example.net", true},
{"exact-only entry does not cover subdomains", "www.exact.example.net", false},
{"wildcard entry matches the base", "wild.example", true},
{"wildcard entry covers subdomains", "anything.wild.example", true},
{"unlisted name", "example.org", false},
{"empty query", "", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
matched, ok := set.Match(tc.query)
if ok != tc.want {
t.Errorf("Match(%q) = %v (matched %q), want %v", tc.query, ok, matched, tc.want)
}
})
}
}
// TestSubdomainCoverageIsNotStored is the memory property the design depends
// on: covering every subdomain must not cost an entry per subdomain.
func TestSubdomainCoverageIsNotStored(t *testing.T) {
b := NewBuilder(1, "Test", models.KindBlacklist, 1)
b.Add("example.com", true)
set := b.Build()
if set.Len() != 1 {
t.Fatalf("stored %d entries, want exactly 1", set.Len())
}
for _, name := range []string{
"a.example.com", "b.a.example.com", "c.b.a.example.com",
"very.deeply.nested.name.example.com",
} {
if _, ok := set.Match(name); !ok {
t.Errorf("%s should be covered by the single stored entry", name)
}
}
}
func TestEmptySetMatchesNothing(t *testing.T) {
set := NewBuilder(1, "Empty", models.KindBlacklist, 0).Build()
if _, ok := set.Match("example.com"); ok {
t.Error("an empty set must not match")
}
var nilSet *Set
if _, ok := nilSet.Match("example.com"); ok {
t.Error("a nil set must not match")
}
}
func TestParsePlainList(t *testing.T) {
input := `# A comment
example.com
bad.example
tracker.example.net
; another comment style
! adblock comment
`
domains, summary := ParseString(input, ParseOptions{DefaultMatchSubdomains: true})
want := []string{"example.com", "bad.example", "tracker.example.net"}
if len(domains) != len(want) {
t.Fatalf("parsed %d domains, want %d: %v", len(domains), len(want), domains)
}
for i, d := range domains {
if d.Domain != want[i] {
t.Errorf("domain[%d] = %q, want %q", i, d.Domain, want[i])
}
}
if summary.Imported != 3 {
t.Errorf("imported = %d, want 3", summary.Imported)
}
if summary.Ignored != 4 {
t.Errorf("ignored = %d, want 4 comments and blanks", summary.Ignored)
}
}
func TestParseHostsFile(t *testing.T) {
input := `# Hosts-style blocklist
0.0.0.0 example.com
127.0.0.1 tracker.example.net
:: bad.example
0.0.0.0 multi-a.example multi-b.example
127.0.0.1 localhost
::1 ip6-localhost
0.0.0.0
192.168.1.1 printer.local
`
domains, summary := ParseString(input, ParseOptions{DefaultMatchSubdomains: true})
got := map[string]bool{}
for _, d := range domains {
got[d.Domain] = true
}
for _, want := range []string{
"example.com", "tracker.example.net", "bad.example",
"multi-a.example", "multi-b.example", "printer.local",
} {
if !got[want] {
t.Errorf("expected %q to be imported; got %v", want, keys(got))
}
}
// Loopback names are hosts-file boilerplate, not blockable domains.
for _, unwanted := range []string{"localhost", "ip6-localhost"} {
if got[unwanted] {
t.Errorf("%q should not have been imported", unwanted)
}
}
if summary.LinesProcessed != 9 {
t.Errorf("lines processed = %d, want 9", summary.LinesProcessed)
}
}
func TestParseAdblockRules(t *testing.T) {
input := `[Adblock Plus 2.0]
||ads.example.com^
||tracker.example.net^$third-party
@@||allowed.example.com^
||example.org/path/to/thing
##.banner-class
`
domains, _ := ParseString(input, ParseOptions{})
got := map[string]bool{}
for _, d := range domains {
got[d.Domain] = true
}
if !got["ads.example.com"] {
t.Error("a plain ||domain^ rule should be imported")
}
if !got["tracker.example.net"] {
t.Error("a ||domain^ rule with options should import the domain part")
}
if got["allowed.example.com"] {
t.Error("an @@ exception rule must not become a block entry")
}
// An Adblock host rule implies subdomain coverage.
for _, d := range domains {
if d.Domain == "ads.example.com" && !d.MatchSubdomains {
t.Error("an Adblock host rule should cover subdomains")
}
}
}
func TestParseDeduplicatesWithinFile(t *testing.T) {
input := "example.com\nexample.com\nEXAMPLE.COM\nexample.com.\n"
domains, summary := ParseString(input, ParseOptions{})
if len(domains) != 1 {
t.Errorf("parsed %d domains, want 1 after normalisation", len(domains))
}
if summary.Duplicates != 3 {
t.Errorf("duplicates = %d, want 3", summary.Duplicates)
}
}
func TestParseRejectsInvalidEntries(t *testing.T) {
input := "example.com\nnot a domain at all\n-bad-.example\nvalid.example\n"
domains, summary := ParseString(input, ParseOptions{})
got := map[string]bool{}
for _, d := range domains {
got[d.Domain] = true
}
if !got["example.com"] || !got["valid.example"] {
t.Errorf("valid entries were dropped: %v", keys(got))
}
if summary.Invalid == 0 {
t.Error("expected invalid entries to be counted")
}
if len(summary.InvalidSamples) == 0 {
t.Error("expected a sample of the rejected lines for the operator")
}
}
func TestParseIgnoresIPOnlyAndSingleLabel(t *testing.T) {
input := "192.0.2.1\nlocalhost\ncom\nvalid.example\n"
domains, _ := ParseString(input, ParseOptions{})
for _, d := range domains {
if d.Domain != "valid.example" {
t.Errorf("unexpected import %q", d.Domain)
}
}
}
func TestParseLargeInput(t *testing.T) {
// A realistic blocklist shape: confirm parsing scales and counts correctly.
var b strings.Builder
const n = 50000
for i := 0; i < n; i++ {
b.WriteString("0.0.0.0 host")
b.WriteString(itoa(i))
b.WriteString(".example.com\n")
}
domains, summary := ParseString(b.String(), ParseOptions{DefaultMatchSubdomains: true})
if len(domains) != n {
t.Errorf("parsed %d domains, want %d", len(domains), n)
}
if summary.Imported != n {
t.Errorf("imported = %d, want %d", summary.Imported, n)
}
if summary.Invalid != 0 {
t.Errorf("invalid = %d, want 0", summary.Invalid)
}
}
func keys(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
func itoa(i int) string {
if i == 0 {
return "0"
}
var buf [12]byte
pos := len(buf)
for i > 0 {
pos--
buf[pos] = byte('0' + i%10)
i /= 10
}
return string(buf[pos:])
}