package archival import ( "context" "errors" "io" "testing" "github.com/google/go-cmp/cmp" "github.com/ooni/probe-cli/v3/internal/fakefill" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/model/mocks" "github.com/ooni/probe-cli/v3/internal/netxlite" ) func TestSaverLookupHost(t *testing.T) { // newResolver helps to create a new resolver. newResolver := func(addrs []string, err error) model.Resolver { return &mocks.Resolver{ MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { return addrs, err }, MockAddress: func() string { return "8.8.8.8:53" }, MockNetwork: func() string { return "udp" }, } } t.Run("on success", func(t *testing.T) { const domain = "dns.google" expectAddrs := []string{"8.8.8.8", "8.8.4.4"} saver := NewSaver() v := &SingleDNSLookupValidator{ ExpectALPNs: nil, ExpectAddrs: expectAddrs, ExpectDomain: domain, ExpectLookupType: "getaddrinfo", ExpectFailure: nil, ExpectResolverAddress: "8.8.8.8:53", ExpectResolverNetwork: "udp", Saver: saver, } reso := newResolver(expectAddrs, nil) ctx := context.Background() addrs, err := saver.LookupHost(ctx, reso, domain) if err != nil { t.Fatal(err) } if diff := cmp.Diff(expectAddrs, addrs); diff != "" { t.Fatal(diff) } if err := v.Validate(); err != nil { t.Fatal(err) } }) t.Run("on failure", func(t *testing.T) { mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) const domain = "dns.google" saver := NewSaver() v := &SingleDNSLookupValidator{ ExpectALPNs: nil, ExpectAddrs: nil, ExpectDomain: domain, ExpectLookupType: "getaddrinfo", ExpectFailure: mockedError, ExpectResolverAddress: "8.8.8.8:53", ExpectResolverNetwork: "udp", Saver: saver, } reso := newResolver(nil, mockedError) ctx := context.Background() addrs, err := saver.LookupHost(ctx, reso, domain) if !errors.Is(err, mockedError) { t.Fatal("invalid err", err) } if len(addrs) != 0 { t.Fatal("invalid addrs") } if err := v.Validate(); err != nil { t.Fatal(err) } }) } func TestSaverLookupHTTPS(t *testing.T) { // newResolver helps to create a new resolver. newResolver := func(alpns, ipv4, ipv6 []string, err error) model.Resolver { return &mocks.Resolver{ MockLookupHTTPS: func(ctx context.Context, domain string) (*model.HTTPSSvc, error) { if alpns == nil && ipv4 == nil && ipv6 == nil { return nil, err } return &model.HTTPSSvc{ ALPN: alpns, IPv4: ipv4, IPv6: ipv6, }, err }, MockAddress: func() string { return "8.8.8.8:53" }, MockNetwork: func() string { return "udp" }, } } t.Run("on success", func(t *testing.T) { const domain = "dns.google" expectALPN := []string{"h3", "h2", "http/1.1"} expectA := []string{"8.8.8.8", "8.8.4.4"} expectAAAA := []string{"2001:4860:4860::8844"} expectAddrs := append(expectA, expectAAAA...) saver := NewSaver() v := &SingleDNSLookupValidator{ ExpectALPNs: expectALPN, ExpectAddrs: expectAddrs, ExpectDomain: domain, ExpectLookupType: "https", ExpectFailure: nil, ExpectResolverAddress: "8.8.8.8:53", ExpectResolverNetwork: "udp", Saver: saver, } reso := newResolver(expectALPN, expectA, expectAAAA, nil) ctx := context.Background() https, err := saver.LookupHTTPS(ctx, reso, domain) if err != nil { t.Fatal(err) } if diff := cmp.Diff(expectALPN, https.ALPN); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(expectA, https.IPv4); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(expectAAAA, https.IPv6); diff != "" { t.Fatal(diff) } if err := v.Validate(); err != nil { t.Fatal(err) } }) t.Run("on failure", func(t *testing.T) { mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) const domain = "dns.google" saver := NewSaver() v := &SingleDNSLookupValidator{ ExpectALPNs: nil, ExpectAddrs: nil, ExpectDomain: domain, ExpectLookupType: "https", ExpectFailure: mockedError, ExpectResolverAddress: "8.8.8.8:53", ExpectResolverNetwork: "udp", Saver: saver, } reso := newResolver(nil, nil, nil, mockedError) ctx := context.Background() https, err := saver.LookupHTTPS(ctx, reso, domain) if !errors.Is(err, mockedError) { t.Fatal("unexpected err", err) } if https != nil { t.Fatal("expected nil https") } if err := v.Validate(); err != nil { t.Fatal(err) } }) } type SingleDNSLookupValidator struct { ExpectALPNs []string ExpectAddrs []string ExpectDomain string ExpectLookupType string ExpectFailure error ExpectResolverAddress string ExpectResolverNetwork string Saver *Saver } func (v *SingleDNSLookupValidator) Validate() error { trace := v.Saver.MoveOutTrace() var entries []*DNSLookupEvent switch v.ExpectLookupType { case "getaddrinfo": entries = trace.DNSLookupHost case "https": entries = trace.DNSLookupHTTPS default: return errors.New("invalid v.ExpectLookupType") } if len(entries) != 1 { return errors.New("expected a single entry") } entry := entries[0] if diff := cmp.Diff(v.ExpectALPNs, entry.ALPNs); diff != "" { return errors.New(diff) } if diff := cmp.Diff(v.ExpectAddrs, entry.Addresses); diff != "" { return errors.New(diff) } if v.ExpectDomain != entry.Domain { return errors.New("invalid .Domain value") } if !errors.Is(entry.Failure, v.ExpectFailure) { return errors.New("invalid .Failure value") } if !entry.Finished.After(entry.Started) { return errors.New(".Finished is not after .Started") } if entry.ResolverAddress != v.ExpectResolverAddress { return errors.New("invalid .ResolverAddress value") } if entry.ResolverNetwork != v.ExpectResolverNetwork { return errors.New("invalid .ResolverNetwork value") } return nil } func TestSaverDNSRoundTrip(t *testing.T) { // generateQueryAndReply generates a fake query and reply. generateQueryAndReply := func() (query, reply []byte, err error) { ff := &fakefill.Filler{} ff.Fill(&query) ff.Fill(&reply) if len(query) < 1 || len(reply) < 1 { return nil, nil, errors.New("did not generate query or reply") } return query, reply, nil } // newDNSTransport creates a suitable DNSTransport. newDNSTransport := func(reply []byte, err error) model.DNSTransport { return &mocks.DNSTransport{ MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) { return reply, err }, MockNetwork: func() string { return "udp" }, MockAddress: func() string { return "8.8.8.8:53" }, } } t.Run("on success", func(t *testing.T) { query, expectedReply, err := generateQueryAndReply() if err != nil { t.Fatal(err) } saver := NewSaver() v := &SingleDNSRoundTripValidator{ ExpectAddress: "8.8.8.8:53", ExpectFailure: nil, ExpectNetwork: "udp", ExpectQuery: query, ExpectReply: expectedReply, Saver: saver, } ctx := context.Background() txp := newDNSTransport(expectedReply, nil) reply, err := saver.DNSRoundTrip(ctx, txp, query) if err != nil { t.Fatal(err) } if diff := cmp.Diff(expectedReply, reply); diff != "" { t.Fatal(diff) } if err := v.Validate(); err != nil { t.Fatal(err) } }) t.Run("on failure", func(t *testing.T) { mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) query, _, err := generateQueryAndReply() if err != nil { t.Fatal(err) } saver := NewSaver() v := &SingleDNSRoundTripValidator{ ExpectAddress: "8.8.8.8:53", ExpectFailure: mockedError, ExpectNetwork: "udp", ExpectQuery: query, ExpectReply: nil, Saver: saver, } ctx := context.Background() txp := newDNSTransport(nil, mockedError) reply, err := saver.DNSRoundTrip(ctx, txp, query) if !errors.Is(err, mockedError) { t.Fatal(err) } if len(reply) != 0 { t.Fatal("unexpected reply") } if err := v.Validate(); err != nil { t.Fatal(err) } }) } type SingleDNSRoundTripValidator struct { ExpectAddress string ExpectFailure error ExpectNetwork string ExpectQuery []byte ExpectReply []byte Saver *Saver } func (v *SingleDNSRoundTripValidator) Validate() error { trace := v.Saver.MoveOutTrace() if len(trace.DNSRoundTrip) != 1 { return errors.New("expected a single entry") } entry := trace.DNSRoundTrip[0] if v.ExpectAddress != entry.Address { return errors.New("invalid .Address") } if !errors.Is(entry.Failure, v.ExpectFailure) { return errors.New("invalid .Failure value") } if !entry.Finished.After(entry.Started) { return errors.New(".Finished is not after .Started") } if v.ExpectNetwork != entry.Network { return errors.New("invalid .Network value") } if diff := cmp.Diff(v.ExpectQuery, entry.Query); diff != "" { return errors.New(diff) } if diff := cmp.Diff(v.ExpectReply, entry.Reply); diff != "" { return errors.New(diff) } return nil }