Initial commit
This commit is contained in:
commit
1d93a25983
23 changed files with 2654 additions and 0 deletions
245
checker/collect_test.go
Normal file
245
checker/collect_test.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
if !contains([]string{"a", "b", "c"}, "b") {
|
||||
t.Error("contains: expected true for present element")
|
||||
}
|
||||
if contains([]string{"a", "b"}, "z") {
|
||||
t.Error("contains: expected false for missing element")
|
||||
}
|
||||
if contains(nil, "x") {
|
||||
t.Error("contains: expected false for nil slice")
|
||||
}
|
||||
if contains([]string{}, "") {
|
||||
t.Error("contains: expected false for empty slice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksGeneric_IPv4(t *testing.T) {
|
||||
ip := net.ParseIP("203.0.113.42")
|
||||
cases := []struct {
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{"203.0.113.42.example.net", true}, // dotted IP embedded
|
||||
{"host-203-0-113-42.isp.example", true}, // dashed IP embedded
|
||||
{"dhcp-1-2-3-4.client.example.com", true}, // ISP pattern
|
||||
{"static.203.0.113.42.rev.example", true}, // dotted form
|
||||
{"pool-100-200-1-2.broadband.example", true}, // pool pattern (different IP but matches regex)
|
||||
{"mail.example.com", false}, // clean
|
||||
{"customer.example.com", false}, // clean
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := looksGeneric(c.host, ip); got != c.want {
|
||||
t.Errorf("looksGeneric(%q, %v)=%v, want %v", c.host, ip, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksGeneric_IPv6(t *testing.T) {
|
||||
ip := net.ParseIP("2001:db8::1")
|
||||
if ip == nil {
|
||||
t.Fatal("parse ip")
|
||||
}
|
||||
cases := []struct {
|
||||
host string
|
||||
want bool
|
||||
}{
|
||||
{"20010db8000000000000000000000001.example.com", true}, // flat 32-nibble
|
||||
{"2001-0db8-0000-0000.dyn.example", true}, // dashed group
|
||||
{"2001.0db8.0000.0000.example", true}, // dotted group
|
||||
{"mail.example.com", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := looksGeneric(c.host, ip); got != c.want {
|
||||
t.Errorf("looksGeneric(%q, %v)=%v, want %v", c.host, ip, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOwnerName_Edge(t *testing.T) {
|
||||
// Lowercases sub, strips trailing dot, joins to FQDN.
|
||||
cases := []struct {
|
||||
sub, zone, want string
|
||||
}{
|
||||
{"FOO", "1.168.192.in-addr.arpa", "foo.1.168.192.in-addr.arpa."},
|
||||
{"foo.", "1.168.192.in-addr.arpa", "foo.1.168.192.in-addr.arpa."},
|
||||
{"foo", "", "foo."},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := buildOwnerName(c.sub, c.zone); got != c.want {
|
||||
t.Errorf("buildOwnerName(%q,%q)=%q, want %q", c.sub, c.zone, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollect_NoZoneAutofill verifies that Collect returns a structured
|
||||
// LoadError (not an error) when the host did not provide the zone autofill.
|
||||
func TestCollect_NoZoneAutofill(t *testing.T) {
|
||||
p := &reverseZoneProvider{}
|
||||
opts := sdk.CheckerOptions{"domain_name": "1.168.192.in-addr.arpa"}
|
||||
out, err := p.Collect(context.Background(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Collect: %v", err)
|
||||
}
|
||||
data, ok := out.(*ReverseZoneData)
|
||||
if !ok {
|
||||
t.Fatalf("Collect returned %T, want *ReverseZoneData", out)
|
||||
}
|
||||
if data.LoadError == "" {
|
||||
t.Errorf("expected LoadError to be set, got data=%+v", data)
|
||||
}
|
||||
if !data.IsReverseZone {
|
||||
t.Errorf("IsReverseZone should be true for in-addr.arpa zone")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollect_NotReverseZone exercises the path where a non-arpa zone is
|
||||
// passed: zone metadata is recorded but IsReverseZone stays false.
|
||||
func TestCollect_NotReverseZone(t *testing.T) {
|
||||
p := &reverseZoneProvider{}
|
||||
opts := sdk.CheckerOptions{"domain_name": "example.com"}
|
||||
out, _ := p.Collect(context.Background(), opts)
|
||||
data := out.(*ReverseZoneData)
|
||||
if data.IsReverseZone {
|
||||
t.Errorf("example.com should not be a reverse zone")
|
||||
}
|
||||
if data.IsIPv6 {
|
||||
t.Errorf("example.com should not be IPv6 reverse zone")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollect_PTRDeduplication verifies that multiple PTR entries on the
|
||||
// same owner are merged into a single PTREntry with merged Targets, and
|
||||
// that targets are deduplicated.
|
||||
func TestCollect_PTRDeduplication(t *testing.T) {
|
||||
zone := buildZoneWithPTRs(t, map[string][]string{
|
||||
"42": {"a.example.com.", "a.example.com.", "b.example.com."},
|
||||
"43": {"c.example.com."},
|
||||
})
|
||||
opts := sdk.CheckerOptions{
|
||||
"domain_name": "1.168.192.in-addr.arpa",
|
||||
"zone": zone,
|
||||
"maxPTRsToCheck": float64(1024),
|
||||
}
|
||||
p := &reverseZoneProvider{}
|
||||
out, err := p.Collect(context.Background(), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("Collect: %v", err)
|
||||
}
|
||||
data := out.(*ReverseZoneData)
|
||||
if data.PTRCount != 4 {
|
||||
t.Errorf("PTRCount=%d, want 4", data.PTRCount)
|
||||
}
|
||||
if len(data.Entries) != 2 {
|
||||
t.Fatalf("len(Entries)=%d, want 2", len(data.Entries))
|
||||
}
|
||||
byOwner := map[string]PTREntry{}
|
||||
for _, e := range data.Entries {
|
||||
byOwner[e.OwnerName] = e
|
||||
}
|
||||
e42 := byOwner["42.1.168.192.in-addr.arpa."]
|
||||
if len(e42.Targets) != 2 {
|
||||
t.Errorf("entry 42 Targets=%v, want 2 unique", e42.Targets)
|
||||
}
|
||||
if e42.ReverseIP != "192.168.1.42" {
|
||||
t.Errorf("entry 42 ReverseIP=%q, want 192.168.1.42", e42.ReverseIP)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollect_Truncation ensures the maxPTRsToCheck cap is enforced and
|
||||
// reported via Truncated.
|
||||
func TestCollect_Truncation(t *testing.T) {
|
||||
ptrs := map[string][]string{}
|
||||
for i := 0; i < 5; i++ {
|
||||
ptrs[itoa(i)] = []string{"host.example.com."}
|
||||
}
|
||||
zone := buildZoneWithPTRs(t, ptrs)
|
||||
opts := sdk.CheckerOptions{
|
||||
"domain_name": "1.168.192.in-addr.arpa",
|
||||
"zone": zone,
|
||||
"maxPTRsToCheck": float64(2),
|
||||
}
|
||||
p := &reverseZoneProvider{}
|
||||
out, _ := p.Collect(context.Background(), opts)
|
||||
data := out.(*ReverseZoneData)
|
||||
if !data.Truncated {
|
||||
t.Errorf("expected Truncated=true")
|
||||
}
|
||||
if data.PTRCount != 5 {
|
||||
t.Errorf("PTRCount=%d, want 5", data.PTRCount)
|
||||
}
|
||||
if len(data.Entries) > 2 {
|
||||
t.Errorf("inspected %d entries, want <=2", len(data.Entries))
|
||||
}
|
||||
}
|
||||
|
||||
// buildZoneWithPTRs constructs a zoneMessage round-tripped through JSON so
|
||||
// that GetOption[zoneMessage] picks it up via the same path the host uses.
|
||||
func buildZoneWithPTRs(t *testing.T, ptrs map[string][]string) any {
|
||||
t.Helper()
|
||||
services := map[string][]map[string]any{}
|
||||
for sub, targets := range ptrs {
|
||||
for _, target := range targets {
|
||||
rec := map[string]any{
|
||||
"Hdr": map[string]any{
|
||||
"Name": sub + ".1.168.192.in-addr.arpa.",
|
||||
"Rrtype": 12,
|
||||
"Class": 1,
|
||||
"Ttl": 3600,
|
||||
},
|
||||
"Ptr": target,
|
||||
}
|
||||
svc := map[string]any{"Record": rec}
|
||||
svcRaw, _ := json.Marshal(svc)
|
||||
services[sub] = append(services[sub], map[string]any{
|
||||
"_svctype": "svcs.PTR",
|
||||
"_domain": sub,
|
||||
"Service": json.RawMessage(svcRaw),
|
||||
})
|
||||
}
|
||||
}
|
||||
zone := map[string]any{
|
||||
"default_ttl": 3600,
|
||||
"services": services,
|
||||
}
|
||||
// Round-trip through JSON so the value lives in the options map exactly
|
||||
// as it would when received from the SDK.
|
||||
raw, err := json.Marshal(zone)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal zone: %v", err)
|
||||
}
|
||||
var generic any
|
||||
if err := json.Unmarshal(raw, &generic); err != nil {
|
||||
t.Fatalf("unmarshal zone: %v", err)
|
||||
}
|
||||
return generic
|
||||
}
|
||||
|
||||
func itoa(i int) string {
|
||||
const digits = "0123456789"
|
||||
if i == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buf [3]byte
|
||||
n := 0
|
||||
for i > 0 {
|
||||
buf[n] = digits[i%10]
|
||||
i /= 10
|
||||
n++
|
||||
}
|
||||
out := make([]byte, n)
|
||||
for j := 0; j < n; j++ {
|
||||
out[j] = buf[n-1-j]
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue