263 lines
6.9 KiB
Go
263 lines
6.9 KiB
Go
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:])
|
|
}
|