Initial commit
This commit is contained in:
commit
542ebdea34
40 changed files with 4592 additions and 0 deletions
244
checker/provider_test.go
Normal file
244
checker/provider_test.go
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2020-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
happydns "git.happydns.org/happyDomain/model"
|
||||
"git.happydns.org/happyDomain/services/abstract"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func mkServer(t *testing.T, name string, ipv4, ipv6 string) *abstract.Server {
|
||||
t.Helper()
|
||||
s := &abstract.Server{}
|
||||
if ipv4 != "" {
|
||||
s.A = &dns.A{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP(ipv4),
|
||||
}
|
||||
}
|
||||
if ipv6 != "" {
|
||||
s.AAAA = &dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 300},
|
||||
AAAA: net.ParseIP(ipv6),
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func mkServiceMessage(t *testing.T, srv *abstract.Server) happydns.ServiceMessage {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(srv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return happydns.ServiceMessage{
|
||||
ServiceMeta: happydns.ServiceMeta{Type: "abstract.Server"},
|
||||
Service: raw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_KeyAndDefinition(t *testing.T) {
|
||||
p := Provider()
|
||||
if p.Key() != ObservationKeyHTTP {
|
||||
t.Errorf("Key() = %q, want %q", p.Key(), ObservationKeyHTTP)
|
||||
}
|
||||
dp, ok := p.(sdk.CheckerDefinitionProvider)
|
||||
if !ok {
|
||||
t.Fatal("provider does not implement CheckerDefinitionProvider")
|
||||
}
|
||||
def := dp.Definition()
|
||||
if def == nil || def.ID != "http" {
|
||||
t.Fatalf("unexpected definition: %+v", def)
|
||||
}
|
||||
if !def.Availability.ApplyToService {
|
||||
t.Errorf("ApplyToService should be true")
|
||||
}
|
||||
if len(def.Availability.LimitToServices) != 1 || def.Availability.LimitToServices[0] != "abstract.Server" {
|
||||
t.Errorf("LimitToServices: %+v", def.Availability.LimitToServices)
|
||||
}
|
||||
if len(def.Rules) == 0 {
|
||||
t.Error("Rules slice empty")
|
||||
}
|
||||
if def.Interval == nil || def.Interval.Default <= 0 {
|
||||
t.Error("Interval default not set")
|
||||
}
|
||||
// User options must include expected keys.
|
||||
idx := map[string]bool{}
|
||||
for _, o := range def.Options.UserOpts {
|
||||
idx[o.Id] = true
|
||||
}
|
||||
for _, want := range []string{OptionProbeTimeoutMs, OptionMaxRedirects, OptionUserAgent, OptionRequireHTTPS, OptionRequireHSTS, OptionMinHSTSMaxAgeDays, OptionRequireCSP} {
|
||||
if !idx[want] {
|
||||
t.Errorf("UserOpts missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveServer_Success(t *testing.T) {
|
||||
srv := mkServer(t, "example.test.", "203.0.113.10", "")
|
||||
opts := sdk.CheckerOptions{OptionService: mkServiceMessage(t, srv)}
|
||||
got, err := resolveServer(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveServer: %v", err)
|
||||
}
|
||||
if got.A == nil || got.A.A.String() != "203.0.113.10" {
|
||||
t.Errorf("unexpected server: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveServer_MissingService(t *testing.T) {
|
||||
if _, err := resolveServer(sdk.CheckerOptions{}); err == nil {
|
||||
t.Fatal("expected error for missing service option")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveServer_WrongType(t *testing.T) {
|
||||
msg := happydns.ServiceMessage{
|
||||
ServiceMeta: happydns.ServiceMeta{Type: "abstract.NotServer"},
|
||||
Service: json.RawMessage(`{}`),
|
||||
}
|
||||
if _, err := resolveServer(sdk.CheckerOptions{OptionService: msg}); err == nil {
|
||||
t.Fatal("expected error for wrong service type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveServer_BadJSON(t *testing.T) {
|
||||
msg := happydns.ServiceMessage{
|
||||
ServiceMeta: happydns.ServiceMeta{Type: "abstract.Server"},
|
||||
Service: json.RawMessage(`{not json`),
|
||||
}
|
||||
if _, err := resolveServer(sdk.CheckerOptions{OptionService: msg}); err == nil {
|
||||
t.Fatal("expected error for malformed service payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverIPs_DedupesAndMerges(t *testing.T) {
|
||||
// Stand up a loopback DNS server that returns multiple A and AAAA
|
||||
// records, then point a custom resolver at it.
|
||||
mux := dns.NewServeMux()
|
||||
mux.HandleFunc("multi.test.", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
switch r.Question[0].Qtype {
|
||||
case dns.TypeA:
|
||||
for _, ip := range []string{"203.0.113.10", "203.0.113.11"} {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "multi.test.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||
A: net.ParseIP(ip),
|
||||
})
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{Name: "multi.test.", Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60},
|
||||
AAAA: net.ParseIP("2001:db8::a"),
|
||||
})
|
||||
}
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
|
||||
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
srv := &dns.Server{PacketConn: pc, Handler: mux}
|
||||
go func() { _ = srv.ActivateAndServe() }()
|
||||
defer srv.Shutdown()
|
||||
|
||||
prev := resolver
|
||||
resolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
d := net.Dialer{}
|
||||
return d.DialContext(ctx, "udp", pc.LocalAddr().String())
|
||||
},
|
||||
}
|
||||
defer func() { resolver = prev }()
|
||||
|
||||
seen := map[string]struct{}{"203.0.113.10": {}} // already pinned
|
||||
got := discoverIPs(context.Background(), "multi.test", seen)
|
||||
sort.Strings(got)
|
||||
want := []string{"2001:db8::a", "203.0.113.11"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("ip[%d] = %q, want %q", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscoverIPs_LookupFailureIsNonFatal(t *testing.T) {
|
||||
prev := resolver
|
||||
resolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return nil, net.ErrClosed
|
||||
},
|
||||
}
|
||||
defer func() { resolver = prev }()
|
||||
|
||||
if got := discoverIPs(context.Background(), "nope.test", map[string]struct{}{}); got != nil {
|
||||
t.Errorf("expected nil on resolver failure, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressesFromServer(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
srv *abstract.Server
|
||||
wantHost string
|
||||
wantIPs []string
|
||||
}{
|
||||
{
|
||||
name: "v4 only",
|
||||
srv: mkServer(t, "example.test.", "203.0.113.1", ""),
|
||||
wantHost: "example.test",
|
||||
wantIPs: []string{"203.0.113.1"},
|
||||
},
|
||||
{
|
||||
name: "v6 only",
|
||||
srv: mkServer(t, "v6.example.test.", "", "2001:db8::1"),
|
||||
wantHost: "v6.example.test",
|
||||
wantIPs: []string{"2001:db8::1"},
|
||||
},
|
||||
{
|
||||
name: "dual stack",
|
||||
srv: mkServer(t, "dual.example.test.", "203.0.113.2", "2001:db8::2"),
|
||||
wantHost: "dual.example.test",
|
||||
wantIPs: []string{"203.0.113.2", "2001:db8::2"},
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
srv: &abstract.Server{},
|
||||
wantHost: "",
|
||||
wantIPs: nil,
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
host, ips := addressesFromServer(c.srv)
|
||||
if host != c.wantHost {
|
||||
t.Errorf("host = %q, want %q", host, c.wantHost)
|
||||
}
|
||||
if len(ips) != len(c.wantIPs) {
|
||||
t.Fatalf("ips = %+v, want %+v", ips, c.wantIPs)
|
||||
}
|
||||
for i, ip := range ips {
|
||||
if ip != c.wantIPs[i] {
|
||||
t.Errorf("ip[%d] = %q, want %q", i, ip, c.wantIPs[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue