package netxlite import ( "bytes" "context" "errors" "io" "net/http" "strings" "testing" "github.com/ooni/probe-cli/v3/internal/engine/httpheader" "github.com/ooni/probe-cli/v3/internal/netxlite/mocks" ) func TestDNSOverHTTPS(t *testing.T) { t.Run("RoundTrip", func(t *testing.T) { t.Run("NewRequestFailure", func(t *testing.T) { const invalidURL = "\t" txp := NewDNSOverHTTPS(http.DefaultClient, invalidURL) data, err := txp.RoundTrip(context.Background(), nil) if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } }) t.Run("client.Do failure", func(t *testing.T) { expected := errors.New("mocked error") txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(*http.Request) (*http.Response, error) { return nil, expected }, }, URL: "https://cloudflare-dns.com/dns-query", } data, err := txp.RoundTrip(context.Background(), nil) if !errors.Is(err, expected) { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } }) t.Run("server returns 500", func(t *testing.T) { txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(*http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 500, Body: io.NopCloser(strings.NewReader("")), }, nil }, }, URL: "https://cloudflare-dns.com/dns-query", } data, err := txp.RoundTrip(context.Background(), nil) if err == nil || err.Error() != "doh: server returned error" { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } }) t.Run("missing content type", func(t *testing.T) { txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(*http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), }, nil }, }, URL: "https://cloudflare-dns.com/dns-query", } data, err := txp.RoundTrip(context.Background(), nil) if err == nil || err.Error() != "doh: invalid content-type" { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } }) t.Run("success", func(t *testing.T) { body := []byte("AAA") txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(*http.Request) (*http.Response, error) { return &http.Response{ StatusCode: 200, Body: io.NopCloser(bytes.NewReader(body)), Header: http.Header{ "Content-Type": []string{"application/dns-message"}, }, }, nil }, }, URL: "https://cloudflare-dns.com/dns-query", } data, err := txp.RoundTrip(context.Background(), nil) if err != nil { t.Fatal(err) } if !bytes.Equal(data, body) { t.Fatal("not the response we expected") } }) t.Run("sets the correct user-agent", func(t *testing.T) { expected := errors.New("mocked error") var correct bool txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(req *http.Request) (*http.Response, error) { correct = req.Header.Get("User-Agent") == httpheader.UserAgent() return nil, expected }, }, URL: "https://cloudflare-dns.com/dns-query", } data, err := txp.RoundTrip(context.Background(), nil) if !errors.Is(err, expected) { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } if !correct { t.Fatal("did not see correct user agent") } }) t.Run("we can override the Host header", func(t *testing.T) { var correct bool expected := errors.New("mocked error") hostOverride := "test.com" txp := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockDo: func(req *http.Request) (*http.Response, error) { correct = req.Host == hostOverride return nil, expected }, }, URL: "https://cloudflare-dns.com/dns-query", HostOverride: hostOverride, } data, err := txp.RoundTrip(context.Background(), nil) if !errors.Is(err, expected) { t.Fatal("expected an error here") } if data != nil { t.Fatal("expected no response here") } if !correct { t.Fatal("did not see correct host override") } }) }) t.Run("other functions behave correctly", func(t *testing.T) { const queryURL = "https://cloudflare-dns.com/dns-query" txp := NewDNSOverHTTPS(http.DefaultClient, queryURL) if txp.Network() != "doh" { t.Fatal("invalid network") } if txp.RequiresPadding() != true { t.Fatal("should require padding") } if txp.Address() != queryURL { t.Fatal("invalid address") } }) t.Run("CloseIdleConnections", func(t *testing.T) { var called bool doh := &DNSOverHTTPS{ Client: &mocks.HTTPClient{ MockCloseIdleConnections: func() { called = true }, }, } doh.CloseIdleConnections() if !called { t.Fatal("not called") } }) }