From 3cb782f0a2c60476006e296c625e59dcf0ae840b Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 9 Sep 2021 21:24:27 +0200 Subject: [PATCH] refactor(netx): move dns transports in netxlite/dnsx (#503) While there, modernize the way in which we run tests to avoid depending on the fake files scattered around the tree and to use some well defined mock structures instead. Part of https://github.com/ooni/probe/issues/1591 --- internal/engine/netx/resolver/legacy.go | 28 ++++ .../resolver => netxlite/dnsx}/decoder.go | 2 +- .../dnsx}/decoder_test.go | 85 +++++++++++-- .../dnsx}/dnsoverhttps.go | 2 +- .../dnsx}/dnsoverhttps_test.go | 2 +- .../resolver => netxlite/dnsx}/dnsovertcp.go | 2 +- .../dnsx}/dnsovertcp_test.go | 120 +++++++++++++++--- .../resolver => netxlite/dnsx}/dnsoverudp.go | 2 +- .../dnsx}/dnsoverudp_test.go | 78 ++++++++++-- .../resolver => netxlite/dnsx}/encoder.go | 2 +- .../dnsx}/encoder_test.go | 2 +- internal/netxlite/dnsx/mocks/decoder.go | 11 ++ internal/netxlite/dnsx/mocks/decoder_test.go | 26 ++++ internal/netxlite/dnsx/mocks/doc.go | 2 + internal/netxlite/dnsx/mocks/encoder.go | 11 ++ internal/netxlite/dnsx/mocks/encoder_test.go | 26 ++++ internal/netxlite/dnsx/mocks/roundtripper.go | 41 ++++++ .../netxlite/dnsx/mocks/roundtripper_test.go | 73 +++++++++++ internal/netxlite/dnsx/roundtripper.go | 21 +++ .../netx/resolver => netxlite/dnsx}/serial.go | 22 +--- .../resolver => netxlite/dnsx}/serial_test.go | 79 +++++++++--- 21 files changed, 546 insertions(+), 91 deletions(-) create mode 100644 internal/engine/netx/resolver/legacy.go rename internal/{engine/netx/resolver => netxlite/dnsx}/decoder.go (98%) rename internal/{engine/netx/resolver => netxlite/dnsx}/decoder_test.go (52%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsoverhttps.go (99%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsoverhttps_test.go (99%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsovertcp.go (99%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsovertcp_test.go (57%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsoverudp.go (99%) rename internal/{engine/netx/resolver => netxlite/dnsx}/dnsoverudp_test.go (52%) rename internal/{engine/netx/resolver => netxlite/dnsx}/encoder.go (98%) rename internal/{engine/netx/resolver => netxlite/dnsx}/encoder_test.go (99%) create mode 100644 internal/netxlite/dnsx/mocks/decoder.go create mode 100644 internal/netxlite/dnsx/mocks/decoder_test.go create mode 100644 internal/netxlite/dnsx/mocks/doc.go create mode 100644 internal/netxlite/dnsx/mocks/encoder.go create mode 100644 internal/netxlite/dnsx/mocks/encoder_test.go create mode 100644 internal/netxlite/dnsx/mocks/roundtripper.go create mode 100644 internal/netxlite/dnsx/mocks/roundtripper_test.go create mode 100644 internal/netxlite/dnsx/roundtripper.go rename internal/{engine/netx/resolver => netxlite/dnsx}/serial.go (82%) rename internal/{engine/netx/resolver => netxlite/dnsx}/serial_test.go (53%) diff --git a/internal/engine/netx/resolver/legacy.go b/internal/engine/netx/resolver/legacy.go new file mode 100644 index 0000000..654e260 --- /dev/null +++ b/internal/engine/netx/resolver/legacy.go @@ -0,0 +1,28 @@ +package resolver + +import "github.com/ooni/probe-cli/v3/internal/netxlite/dnsx" + +// Variables that other packages expect to find here but have been +// moved into the internal/netxlite/dnsx package. +var ( + NewSerialResolver = dnsx.NewSerialResolver + NewDNSOverUDP = dnsx.NewDNSOverUDP + NewDNSOverTCP = dnsx.NewDNSOverTCP + NewDNSOverTLS = dnsx.NewDNSOverTLS + NewDNSOverHTTPS = dnsx.NewDNSOverHTTPS + NewDNSOverHTTPSWithHostOverride = dnsx.NewDNSOverHTTPSWithHostOverride +) + +// Types that other packages expect to find here but have been +// moved into the internal/netxlite/dnsx package. +type ( + DNSOverHTTPS = dnsx.DNSOverHTTPS + DNSOverTCP = dnsx.DNSOverTCP + DNSOverUDP = dnsx.DNSOverUDP + MiekgEncoder = dnsx.MiekgEncoder + MiekgDecoder = dnsx.MiekgDecoder + RoundTripper = dnsx.RoundTripper + SerialResolver = dnsx.SerialResolver + Dialer = dnsx.Dialer + DialContextFunc = dnsx.DialContextFunc +) diff --git a/internal/engine/netx/resolver/decoder.go b/internal/netxlite/dnsx/decoder.go similarity index 98% rename from internal/engine/netx/resolver/decoder.go rename to internal/netxlite/dnsx/decoder.go index db6afe3..47404a3 100644 --- a/internal/engine/netx/resolver/decoder.go +++ b/internal/netxlite/dnsx/decoder.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "errors" diff --git a/internal/engine/netx/resolver/decoder_test.go b/internal/netxlite/dnsx/decoder_test.go similarity index 52% rename from internal/engine/netx/resolver/decoder_test.go rename to internal/netxlite/dnsx/decoder_test.go index d402e13..0de8485 100644 --- a/internal/engine/netx/resolver/decoder_test.go +++ b/internal/netxlite/dnsx/decoder_test.go @@ -1,6 +1,7 @@ -package resolver +package dnsx import ( + "net" "strings" "testing" @@ -20,7 +21,7 @@ func TestDecoderUnpackError(t *testing.T) { func TestDecoderNXDOMAIN(t *testing.T) { d := &MiekgDecoder{} - data, err := d.Decode(dns.TypeA, GenReplyError(t, dns.RcodeNameError)) + data, err := d.Decode(dns.TypeA, genReplyError(t, dns.RcodeNameError)) if err == nil || !strings.HasSuffix(err.Error(), "no such host") { t.Fatal("not the error we expected") } @@ -31,7 +32,7 @@ func TestDecoderNXDOMAIN(t *testing.T) { func TestDecoderOtherError(t *testing.T) { d := &MiekgDecoder{} - data, err := d.Decode(dns.TypeA, GenReplyError(t, dns.RcodeRefused)) + data, err := d.Decode(dns.TypeA, genReplyError(t, dns.RcodeRefused)) if err == nil || !strings.HasSuffix(err.Error(), "query failed") { t.Fatal("not the error we expected") } @@ -42,7 +43,7 @@ func TestDecoderOtherError(t *testing.T) { func TestDecoderNoAddress(t *testing.T) { d := &MiekgDecoder{} - data, err := d.Decode(dns.TypeA, GenReplySuccess(t, dns.TypeA)) + data, err := d.Decode(dns.TypeA, genReplySuccess(t, dns.TypeA)) if err == nil || !strings.HasSuffix(err.Error(), "no response returned") { t.Fatal("not the error we expected") } @@ -54,7 +55,7 @@ func TestDecoderNoAddress(t *testing.T) { func TestDecoderDecodeA(t *testing.T) { d := &MiekgDecoder{} data, err := d.Decode( - dns.TypeA, GenReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.8.8")) + dns.TypeA, genReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.8.8")) if err != nil { t.Fatal(err) } @@ -72,7 +73,7 @@ func TestDecoderDecodeA(t *testing.T) { func TestDecoderDecodeAAAA(t *testing.T) { d := &MiekgDecoder{} data, err := d.Decode( - dns.TypeAAAA, GenReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1")) + dns.TypeAAAA, genReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1")) if err != nil { t.Fatal(err) } @@ -90,7 +91,7 @@ func TestDecoderDecodeAAAA(t *testing.T) { func TestDecoderUnexpectedAReply(t *testing.T) { d := &MiekgDecoder{} data, err := d.Decode( - dns.TypeA, GenReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1")) + dns.TypeA, genReplySuccess(t, dns.TypeAAAA, "::1", "fe80::1")) if err == nil || !strings.HasSuffix(err.Error(), "no response returned") { t.Fatal("not the error we expected") } @@ -102,7 +103,7 @@ func TestDecoderUnexpectedAReply(t *testing.T) { func TestDecoderUnexpectedAAAAReply(t *testing.T) { d := &MiekgDecoder{} data, err := d.Decode( - dns.TypeAAAA, GenReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.4.4.")) + dns.TypeAAAA, genReplySuccess(t, dns.TypeA, "1.1.1.1", "8.8.4.4.")) if err == nil || !strings.HasSuffix(err.Error(), "no response returned") { t.Fatal("not the error we expected") } @@ -110,3 +111,71 @@ func TestDecoderUnexpectedAAAAReply(t *testing.T) { t.Fatal("expected nil data here") } } + +func genReplyError(t *testing.T, code int) []byte { + question := dns.Question{ + Name: dns.Fqdn("x.org"), + Qtype: dns.TypeA, + Qclass: dns.ClassINET, + } + query := new(dns.Msg) + query.Id = dns.Id() + query.RecursionDesired = true + query.Question = make([]dns.Question, 1) + query.Question[0] = question + reply := new(dns.Msg) + reply.Compress = true + reply.MsgHdr.RecursionAvailable = true + reply.SetRcode(query, code) + data, err := reply.Pack() + if err != nil { + t.Fatal(err) + } + return data +} + +func genReplySuccess(t *testing.T, qtype uint16, ips ...string) []byte { + question := dns.Question{ + Name: dns.Fqdn("x.org"), + Qtype: qtype, + Qclass: dns.ClassINET, + } + query := new(dns.Msg) + query.Id = dns.Id() + query.RecursionDesired = true + query.Question = make([]dns.Question, 1) + query.Question[0] = question + reply := new(dns.Msg) + reply.Compress = true + reply.MsgHdr.RecursionAvailable = true + reply.SetReply(query) + for _, ip := range ips { + switch qtype { + case dns.TypeA: + reply.Answer = append(reply.Answer, &dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("x.org"), + Rrtype: qtype, + Class: dns.ClassINET, + Ttl: 0, + }, + A: net.ParseIP(ip), + }) + case dns.TypeAAAA: + reply.Answer = append(reply.Answer, &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("x.org"), + Rrtype: qtype, + Class: dns.ClassINET, + Ttl: 0, + }, + AAAA: net.ParseIP(ip), + }) + } + } + data, err := reply.Pack() + if err != nil { + t.Fatal(err) + } + return data +} diff --git a/internal/engine/netx/resolver/dnsoverhttps.go b/internal/netxlite/dnsx/dnsoverhttps.go similarity index 99% rename from internal/engine/netx/resolver/dnsoverhttps.go rename to internal/netxlite/dnsx/dnsoverhttps.go index d98fc11..749fda4 100644 --- a/internal/engine/netx/resolver/dnsoverhttps.go +++ b/internal/netxlite/dnsx/dnsoverhttps.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "bytes" diff --git a/internal/engine/netx/resolver/dnsoverhttps_test.go b/internal/netxlite/dnsx/dnsoverhttps_test.go similarity index 99% rename from internal/engine/netx/resolver/dnsoverhttps_test.go rename to internal/netxlite/dnsx/dnsoverhttps_test.go index c6e5350..e6bc8c8 100644 --- a/internal/engine/netx/resolver/dnsoverhttps_test.go +++ b/internal/netxlite/dnsx/dnsoverhttps_test.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "bytes" diff --git a/internal/engine/netx/resolver/dnsovertcp.go b/internal/netxlite/dnsx/dnsovertcp.go similarity index 99% rename from internal/engine/netx/resolver/dnsovertcp.go rename to internal/netxlite/dnsx/dnsovertcp.go index 1c7037c..9f16655 100644 --- a/internal/engine/netx/resolver/dnsovertcp.go +++ b/internal/netxlite/dnsx/dnsovertcp.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "context" diff --git a/internal/engine/netx/resolver/dnsovertcp_test.go b/internal/netxlite/dnsx/dnsovertcp_test.go similarity index 57% rename from internal/engine/netx/resolver/dnsovertcp_test.go rename to internal/netxlite/dnsx/dnsovertcp_test.go index 3295a7e..efcbe34 100644 --- a/internal/engine/netx/resolver/dnsovertcp_test.go +++ b/internal/netxlite/dnsx/dnsovertcp_test.go @@ -1,10 +1,16 @@ -package resolver +package dnsx import ( + "bytes" "context" + "crypto/tls" "errors" + "io" "net" "testing" + "time" + + "github.com/ooni/probe-cli/v3/internal/netxlite/mocks" ) func TestDNSOverTCPTransportQueryTooLarge(t *testing.T) { @@ -22,7 +28,11 @@ func TestDNSOverTCPTransportQueryTooLarge(t *testing.T) { func TestDNSOverTCPTransportDialFailure(t *testing.T) { const address = "9.9.9.9:53" mocked := errors.New("mocked error") - fakedialer := FakeDialer{Err: mocked} + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return nil, mocked + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if !errors.Is(err, mocked) { @@ -36,9 +46,18 @@ func TestDNSOverTCPTransportDialFailure(t *testing.T) { func TestDNSOverTCPTransportSetDealineFailure(t *testing.T) { const address = "9.9.9.9:53" mocked := errors.New("mocked error") - fakedialer := FakeDialer{Conn: &FakeConn{ - SetDeadlineError: mocked, - }} + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return mocked + }, + MockClose: func() error { + return nil + }, + }, nil + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if !errors.Is(err, mocked) { @@ -52,9 +71,21 @@ func TestDNSOverTCPTransportSetDealineFailure(t *testing.T) { func TestDNSOverTCPTransportWriteFailure(t *testing.T) { const address = "9.9.9.9:53" mocked := errors.New("mocked error") - fakedialer := FakeDialer{Conn: &FakeConn{ - WriteError: mocked, - }} + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return 0, mocked + }, + MockClose: func() error { + return nil + }, + }, nil + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if !errors.Is(err, mocked) { @@ -68,9 +99,24 @@ func TestDNSOverTCPTransportWriteFailure(t *testing.T) { func TestDNSOverTCPTransportReadFailure(t *testing.T) { const address = "9.9.9.9:53" mocked := errors.New("mocked error") - fakedialer := FakeDialer{Conn: &FakeConn{ - ReadError: mocked, - }} + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return len(b), nil + }, + MockRead: func(b []byte) (int, error) { + return 0, mocked + }, + MockClose: func() error { + return nil + }, + }, nil + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if !errors.Is(err, mocked) { @@ -84,10 +130,30 @@ func TestDNSOverTCPTransportReadFailure(t *testing.T) { func TestDNSOverTCPTransportSecondReadFailure(t *testing.T) { const address = "9.9.9.9:53" mocked := errors.New("mocked error") - fakedialer := FakeDialer{Conn: &FakeConn{ - ReadError: mocked, - ReadData: []byte{byte(0), byte(2)}, - }} + input := io.MultiReader( + bytes.NewReader([]byte{byte(0), byte(2)}), + &mocks.Reader{ + MockRead: func(b []byte) (int, error) { + return 0, mocked + }, + }, + ) + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return len(b), nil + }, + MockRead: input.Read, + MockClose: func() error { + return nil + }, + }, nil + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if !errors.Is(err, mocked) { @@ -100,11 +166,23 @@ func TestDNSOverTCPTransportSecondReadFailure(t *testing.T) { func TestDNSOverTCPTransportAllGood(t *testing.T) { const address = "9.9.9.9:53" - mocked := errors.New("mocked error") - fakedialer := FakeDialer{Conn: &FakeConn{ - ReadError: mocked, - ReadData: []byte{byte(0), byte(1), byte(1)}, - }} + input := bytes.NewReader([]byte{byte(0), byte(1), byte(1)}) + fakedialer := &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return len(b), nil + }, + MockRead: input.Read, + MockClose: func() error { + return nil + }, + }, nil + }, + } txp := NewDNSOverTCP(fakedialer.DialContext, address) reply, err := txp.RoundTrip(context.Background(), make([]byte, 1<<11)) if err != nil { @@ -131,7 +209,7 @@ func TestDNSOverTCPTransportOK(t *testing.T) { func TestDNSOverTLSTransportOK(t *testing.T) { const address = "9.9.9.9:853" - txp := NewDNSOverTLS(DialTLSContext, address) + txp := NewDNSOverTLS((&tls.Dialer{}).DialContext, address) if txp.RequiresPadding() != true { t.Fatal("invalid RequiresPadding") } diff --git a/internal/engine/netx/resolver/dnsoverudp.go b/internal/netxlite/dnsx/dnsoverudp.go similarity index 99% rename from internal/engine/netx/resolver/dnsoverudp.go rename to internal/netxlite/dnsx/dnsoverudp.go index bbd60a5..cae53a7 100644 --- a/internal/engine/netx/resolver/dnsoverudp.go +++ b/internal/netxlite/dnsx/dnsoverudp.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "context" diff --git a/internal/engine/netx/resolver/dnsoverudp_test.go b/internal/netxlite/dnsx/dnsoverudp_test.go similarity index 52% rename from internal/engine/netx/resolver/dnsoverudp_test.go rename to internal/netxlite/dnsx/dnsoverudp_test.go index 04906dd..092ffa3 100644 --- a/internal/engine/netx/resolver/dnsoverudp_test.go +++ b/internal/netxlite/dnsx/dnsoverudp_test.go @@ -1,16 +1,24 @@ -package resolver +package dnsx import ( + "bytes" "context" "errors" "net" "testing" + "time" + + "github.com/ooni/probe-cli/v3/internal/netxlite/mocks" ) func TestDNSOverUDPDialFailure(t *testing.T) { mocked := errors.New("mocked error") const address = "9.9.9.9:53" - txp := NewDNSOverUDP(FakeDialer{Err: mocked}, address) + txp := NewDNSOverUDP(&mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return nil, mocked + }, + }, address) data, err := txp.RoundTrip(context.Background(), nil) if !errors.Is(err, mocked) { t.Fatal("not the error we expected") @@ -23,9 +31,16 @@ func TestDNSOverUDPDialFailure(t *testing.T) { func TestDNSOverUDPSetDeadlineError(t *testing.T) { mocked := errors.New("mocked error") txp := NewDNSOverUDP( - FakeDialer{ - Conn: &FakeConn{ - SetDeadlineError: mocked, + &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return mocked + }, + MockClose: func() error { + return nil + }, + }, nil }, }, "9.9.9.9:53", ) @@ -41,9 +56,19 @@ func TestDNSOverUDPSetDeadlineError(t *testing.T) { func TestDNSOverUDPWriteFailure(t *testing.T) { mocked := errors.New("mocked error") txp := NewDNSOverUDP( - FakeDialer{ - Conn: &FakeConn{ - WriteError: mocked, + &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return 0, mocked + }, + MockClose: func() error { + return nil + }, + }, nil }, }, "9.9.9.9:53", ) @@ -59,9 +84,22 @@ func TestDNSOverUDPWriteFailure(t *testing.T) { func TestDNSOverUDPReadFailure(t *testing.T) { mocked := errors.New("mocked error") txp := NewDNSOverUDP( - FakeDialer{ - Conn: &FakeConn{ - ReadError: mocked, + &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return len(b), nil + }, + MockRead: func(b []byte) (int, error) { + return 0, mocked + }, + MockClose: func() error { + return nil + }, + }, nil }, }, "9.9.9.9:53", ) @@ -76,9 +114,23 @@ func TestDNSOverUDPReadFailure(t *testing.T) { func TestDNSOverUDPReadSuccess(t *testing.T) { const expected = 17 + input := bytes.NewReader(make([]byte, expected)) txp := NewDNSOverUDP( - FakeDialer{ - Conn: &FakeConn{ReadData: make([]byte, 17)}, + &mocks.Dialer{ + MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return &mocks.Conn{ + MockSetDeadline: func(t time.Time) error { + return nil + }, + MockWrite: func(b []byte) (int, error) { + return len(b), nil + }, + MockRead: input.Read, + MockClose: func() error { + return nil + }, + }, nil + }, }, "9.9.9.9:53", ) data, err := txp.RoundTrip(context.Background(), nil) diff --git a/internal/engine/netx/resolver/encoder.go b/internal/netxlite/dnsx/encoder.go similarity index 98% rename from internal/engine/netx/resolver/encoder.go rename to internal/netxlite/dnsx/encoder.go index 15238fe..d0bb217 100644 --- a/internal/engine/netx/resolver/encoder.go +++ b/internal/netxlite/dnsx/encoder.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import "github.com/miekg/dns" diff --git a/internal/engine/netx/resolver/encoder_test.go b/internal/netxlite/dnsx/encoder_test.go similarity index 99% rename from internal/engine/netx/resolver/encoder_test.go rename to internal/netxlite/dnsx/encoder_test.go index 47fdcba..1ce4c70 100644 --- a/internal/engine/netx/resolver/encoder_test.go +++ b/internal/netxlite/dnsx/encoder_test.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "strings" diff --git a/internal/netxlite/dnsx/mocks/decoder.go b/internal/netxlite/dnsx/mocks/decoder.go new file mode 100644 index 0000000..55da00c --- /dev/null +++ b/internal/netxlite/dnsx/mocks/decoder.go @@ -0,0 +1,11 @@ +package mocks + +// Decoder allows mocking dnsx.Decoder. +type Decoder struct { + MockDecode func(qtype uint16, reply []byte) ([]string, error) +} + +// Decode calls MockDecode. +func (e *Decoder) Decode(qtype uint16, reply []byte) ([]string, error) { + return e.MockDecode(qtype, reply) +} diff --git a/internal/netxlite/dnsx/mocks/decoder_test.go b/internal/netxlite/dnsx/mocks/decoder_test.go new file mode 100644 index 0000000..ed6c13c --- /dev/null +++ b/internal/netxlite/dnsx/mocks/decoder_test.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "errors" + "testing" + + "github.com/miekg/dns" +) + +func TestDecoder(t *testing.T) { + t.Run("Decode", func(t *testing.T) { + expected := errors.New("mocked error") + e := &Decoder{ + MockDecode: func(qtype uint16, reply []byte) ([]string, error) { + return nil, expected + }, + } + out, err := e.Decode(dns.TypeA, make([]byte, 17)) + if !errors.Is(err, expected) { + t.Fatal("unexpected err", err) + } + if out != nil { + t.Fatal("unexpected out") + } + }) +} diff --git a/internal/netxlite/dnsx/mocks/doc.go b/internal/netxlite/dnsx/mocks/doc.go new file mode 100644 index 0000000..e594bf9 --- /dev/null +++ b/internal/netxlite/dnsx/mocks/doc.go @@ -0,0 +1,2 @@ +// Package mocks contains mocks for dnsx. +package mocks diff --git a/internal/netxlite/dnsx/mocks/encoder.go b/internal/netxlite/dnsx/mocks/encoder.go new file mode 100644 index 0000000..f110036 --- /dev/null +++ b/internal/netxlite/dnsx/mocks/encoder.go @@ -0,0 +1,11 @@ +package mocks + +// Encoder allows mocking dnsx.Encoder. +type Encoder struct { + MockEncode func(domain string, qtype uint16, padding bool) ([]byte, error) +} + +// Encode calls MockEncode. +func (e *Encoder) Encode(domain string, qtype uint16, padding bool) ([]byte, error) { + return e.MockEncode(domain, qtype, padding) +} diff --git a/internal/netxlite/dnsx/mocks/encoder_test.go b/internal/netxlite/dnsx/mocks/encoder_test.go new file mode 100644 index 0000000..5f2e5ea --- /dev/null +++ b/internal/netxlite/dnsx/mocks/encoder_test.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "errors" + "testing" + + "github.com/miekg/dns" +) + +func TestEncoder(t *testing.T) { + t.Run("Encode", func(t *testing.T) { + expected := errors.New("mocked error") + e := &Encoder{ + MockEncode: func(domain string, qtype uint16, padding bool) ([]byte, error) { + return nil, expected + }, + } + out, err := e.Encode("dns.google", dns.TypeA, true) + if !errors.Is(err, expected) { + t.Fatal("unexpected err", err) + } + if out != nil { + t.Fatal("unexpected out") + } + }) +} diff --git a/internal/netxlite/dnsx/mocks/roundtripper.go b/internal/netxlite/dnsx/mocks/roundtripper.go new file mode 100644 index 0000000..182bd4f --- /dev/null +++ b/internal/netxlite/dnsx/mocks/roundtripper.go @@ -0,0 +1,41 @@ +package mocks + +import "context" + +// RoundTripper allows mocking dnsx.RoundTripper. +type RoundTripper struct { + MockRoundTrip func(ctx context.Context, query []byte) (reply []byte, err error) + + MockRequiresPadding func() bool + + MockNetwork func() string + + MockAddress func() string + + MockCloseIdleConnections func() +} + +// RoundTrip calls MockRoundTrip. +func (txp *RoundTripper) RoundTrip(ctx context.Context, query []byte) (reply []byte, err error) { + return txp.MockRoundTrip(ctx, query) +} + +// RequiresPadding calls MockRequiresPadding. +func (txp *RoundTripper) RequiresPadding() bool { + return txp.MockRequiresPadding() +} + +// Network calls MockNetwork. +func (txp *RoundTripper) Network() string { + return txp.MockNetwork() +} + +// Address calls MockAddress. +func (txp *RoundTripper) Address() string { + return txp.MockAddress() +} + +// CloseIdleConnections calls MockCloseIdleConnections. +func (txp *RoundTripper) CloseIdleConnections() { + txp.MockCloseIdleConnections() +} diff --git a/internal/netxlite/dnsx/mocks/roundtripper_test.go b/internal/netxlite/dnsx/mocks/roundtripper_test.go new file mode 100644 index 0000000..8fd24dc --- /dev/null +++ b/internal/netxlite/dnsx/mocks/roundtripper_test.go @@ -0,0 +1,73 @@ +package mocks + +import ( + "context" + "errors" + "testing" + + "github.com/ooni/probe-cli/v3/internal/atomicx" +) + +func TestRoundTripper(t *testing.T) { + t.Run("RoundTrip", func(t *testing.T) { + expected := errors.New("mocked error") + txp := &RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) { + return nil, expected + }, + } + resp, err := txp.RoundTrip(context.Background(), make([]byte, 16)) + if !errors.Is(err, expected) { + t.Fatal("not the error we expected", err) + } + if resp != nil { + t.Fatal("expected nil response here") + } + }) + + t.Run("RequiresPadding", func(t *testing.T) { + txp := &RoundTripper{ + MockRequiresPadding: func() bool { + return true + }, + } + if txp.RequiresPadding() != true { + t.Fatal("unexpected result") + } + }) + + t.Run("Network", func(t *testing.T) { + txp := &RoundTripper{ + MockNetwork: func() string { + return "antani" + }, + } + if txp.Network() != "antani" { + t.Fatal("unexpected result") + } + }) + + t.Run("Address", func(t *testing.T) { + txp := &RoundTripper{ + MockAddress: func() string { + return "mascetti" + }, + } + if txp.Address() != "mascetti" { + t.Fatal("unexpected result") + } + }) + + t.Run("CloseIdleConnections", func(t *testing.T) { + called := &atomicx.Int64{} + txp := &RoundTripper{ + MockCloseIdleConnections: func() { + called.Add(1) + }, + } + txp.CloseIdleConnections() + if called.Load() != 1 { + t.Fatal("not called") + } + }) +} diff --git a/internal/netxlite/dnsx/roundtripper.go b/internal/netxlite/dnsx/roundtripper.go new file mode 100644 index 0000000..e2440e5 --- /dev/null +++ b/internal/netxlite/dnsx/roundtripper.go @@ -0,0 +1,21 @@ +package dnsx + +import "context" + +// RoundTripper represents an abstract DNS transport. +type RoundTripper interface { + // RoundTrip sends a DNS query and receives the reply. + RoundTrip(ctx context.Context, query []byte) (reply []byte, err error) + + // RequiresPadding return true for DoH and DoT according to RFC8467 + RequiresPadding() bool + + // Network is the network of the round tripper (e.g. "dot") + Network() string + + // Address is the address of the round tripper (e.g. "1.1.1.1:853") + Address() string + + // CloseIdleConnections closes idle connections. + CloseIdleConnections() +} diff --git a/internal/engine/netx/resolver/serial.go b/internal/netxlite/dnsx/serial.go similarity index 82% rename from internal/engine/netx/resolver/serial.go rename to internal/netxlite/dnsx/serial.go index cfaae73..6389c88 100644 --- a/internal/engine/netx/resolver/serial.go +++ b/internal/netxlite/dnsx/serial.go @@ -1,4 +1,4 @@ -package resolver +package dnsx import ( "context" @@ -9,24 +9,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/atomicx" ) -// RoundTripper represents an abstract DNS transport. -type RoundTripper interface { - // RoundTrip sends a DNS query and receives the reply. - RoundTrip(ctx context.Context, query []byte) (reply []byte, err error) - - // RequiresPadding return true for DoH and DoT according to RFC8467 - RequiresPadding() bool - - // Network is the network of the round tripper (e.g. "dot") - Network() string - - // Address is the address of the round tripper (e.g. "1.1.1.1:853") - Address() string - - // CloseIdleConnections closes idle connections. - CloseIdleConnections() -} - // SerialResolver is a resolver that first issues an A query and then // issues an AAAA query for the requested domain. type SerialResolver struct { @@ -117,5 +99,3 @@ func (r *SerialResolver) roundTrip( } return r.Decoder.Decode(qtype, replydata) } - -var _ Resolver = &SerialResolver{} diff --git a/internal/engine/netx/resolver/serial_test.go b/internal/netxlite/dnsx/serial_test.go similarity index 53% rename from internal/engine/netx/resolver/serial_test.go rename to internal/netxlite/dnsx/serial_test.go index e1092e4..f03b938 100644 --- a/internal/engine/netx/resolver/serial_test.go +++ b/internal/netxlite/dnsx/serial_test.go @@ -1,20 +1,21 @@ -package resolver_test +package dnsx import ( "context" + "crypto/tls" "errors" "net" "strings" - "syscall" "testing" "github.com/miekg/dns" - "github.com/ooni/probe-cli/v3/internal/engine/netx/resolver" + "github.com/ooni/probe-cli/v3/internal/netxlite/dnsx/mocks" + "github.com/ooni/probe-cli/v3/internal/netxlite/errorsx" ) func TestOONIGettingTransport(t *testing.T) { - txp := resolver.NewDNSOverTLS(resolver.DialTLSContext, "8.8.8.8:853") - r := resolver.NewSerialResolver(txp) + txp := NewDNSOverTLS((&tls.Dialer{}).DialContext, "8.8.8.8:853") + r := NewSerialResolver(txp) rtx := r.Transport() if rtx.Network() != "dot" || rtx.Address() != "8.8.8.8:853" { t.Fatal("not the transport we expected") @@ -29,8 +30,15 @@ func TestOONIGettingTransport(t *testing.T) { func TestOONIEncodeError(t *testing.T) { mocked := errors.New("mocked error") - txp := resolver.NewDNSOverTLS(resolver.DialTLSContext, "8.8.8.8:853") - r := resolver.SerialResolver{Encoder: resolver.FakeEncoder{Err: mocked}, Txp: txp} + txp := NewDNSOverTLS((&tls.Dialer{}).DialContext, "8.8.8.8:853") + r := SerialResolver{ + Encoder: &mocks.Encoder{ + MockEncode: func(domain string, qtype uint16, padding bool) ([]byte, error) { + return nil, mocked + }, + }, + Txp: txp, + } addrs, err := r.LookupHost(context.Background(), "www.gogle.com") if !errors.Is(err, mocked) { t.Fatal("not the error we expected") @@ -42,8 +50,15 @@ func TestOONIEncodeError(t *testing.T) { func TestOONIRoundTripError(t *testing.T) { mocked := errors.New("mocked error") - txp := resolver.FakeTransport{Err: mocked} - r := resolver.NewSerialResolver(txp) + txp := &mocks.RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) (reply []byte, err error) { + return nil, mocked + }, + MockRequiresPadding: func() bool { + return true + }, + } + r := NewSerialResolver(txp) addrs, err := r.LookupHost(context.Background(), "www.gogle.com") if !errors.Is(err, mocked) { t.Fatal("not the error we expected") @@ -54,8 +69,15 @@ func TestOONIRoundTripError(t *testing.T) { } func TestOONIWithEmptyReply(t *testing.T) { - txp := resolver.FakeTransport{Data: resolver.GenReplySuccess(t, dns.TypeA)} - r := resolver.NewSerialResolver(txp) + txp := &mocks.RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) (reply []byte, err error) { + return genReplySuccess(t, dns.TypeA), nil + }, + MockRequiresPadding: func() bool { + return true + }, + } + r := NewSerialResolver(txp) addrs, err := r.LookupHost(context.Background(), "www.gogle.com") if err == nil || !strings.HasSuffix(err.Error(), "no response returned") { t.Fatal("not the error we expected") @@ -66,10 +88,15 @@ func TestOONIWithEmptyReply(t *testing.T) { } func TestOONIWithAReply(t *testing.T) { - txp := resolver.FakeTransport{ - Data: resolver.GenReplySuccess(t, dns.TypeA, "8.8.8.8"), + txp := &mocks.RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) (reply []byte, err error) { + return genReplySuccess(t, dns.TypeA, "8.8.8.8"), nil + }, + MockRequiresPadding: func() bool { + return true + }, } - r := resolver.NewSerialResolver(txp) + r := NewSerialResolver(txp) addrs, err := r.LookupHost(context.Background(), "www.gogle.com") if err != nil { t.Fatal(err) @@ -80,10 +107,15 @@ func TestOONIWithAReply(t *testing.T) { } func TestOONIWithAAAAReply(t *testing.T) { - txp := resolver.FakeTransport{ - Data: resolver.GenReplySuccess(t, dns.TypeAAAA, "::1"), + txp := &mocks.RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) (reply []byte, err error) { + return genReplySuccess(t, dns.TypeAAAA, "::1"), nil + }, + MockRequiresPadding: func() bool { + return true + }, } - r := resolver.NewSerialResolver(txp) + r := NewSerialResolver(txp) addrs, err := r.LookupHost(context.Background(), "www.gogle.com") if err != nil { t.Fatal(err) @@ -94,12 +126,17 @@ func TestOONIWithAAAAReply(t *testing.T) { } func TestOONIWithTimeout(t *testing.T) { - txp := resolver.FakeTransport{ - Err: &net.OpError{Err: syscall.ETIMEDOUT, Op: "dial"}, + txp := &mocks.RoundTripper{ + MockRoundTrip: func(ctx context.Context, query []byte) (reply []byte, err error) { + return nil, &net.OpError{Err: errorsx.ETIMEDOUT, Op: "dial"} + }, + MockRequiresPadding: func() bool { + return true + }, } - r := resolver.NewSerialResolver(txp) + r := NewSerialResolver(txp) addrs, err := r.LookupHost(context.Background(), "www.gogle.com") - if !errors.Is(err, syscall.ETIMEDOUT) { + if !errors.Is(err, errorsx.ETIMEDOUT) { t.Fatal("not the error we expected") } if addrs != nil {