From 4439cbd2d5f5d66fa99966601db9855a0e830059 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 9 Feb 2024 18:55:40 +0100 Subject: [PATCH] fix(webconnectivitylte): include network events (#1503) While there, notice that we can increase the coverage in webconnectivityqa. Closes https://github.com/ooni/probe/issues/2674 --- .../webconnectivitylte/cleartextflow.go | 9 +- .../webconnectivitylte/secureflow.go | 9 +- internal/webconnectivityqa/badssl.go | 8 +- internal/webconnectivityqa/checker.go | 61 ++++++++++ internal/webconnectivityqa/checker_test.go | 112 ++++++++++++++++++ internal/webconnectivityqa/cloudflare.go | 24 ++-- internal/webconnectivityqa/control.go | 4 +- internal/webconnectivityqa/dnsblocking.go | 6 +- internal/webconnectivityqa/dnshijacking.go | 8 +- .../webconnectivityqa/dnshijacking_test.go | 47 +++++++- internal/webconnectivityqa/ghost.go | 4 +- internal/webconnectivityqa/ghost_test.go | 56 +++++++++ internal/webconnectivityqa/httpblocking.go | 2 +- internal/webconnectivityqa/httpdiff.go | 4 +- internal/webconnectivityqa/idna.go | 26 ++-- internal/webconnectivityqa/largefile.go | 24 ++-- internal/webconnectivityqa/localhost.go | 4 +- internal/webconnectivityqa/localhost_test.go | 56 +++++++++ internal/webconnectivityqa/redirect.go | 44 +++---- internal/webconnectivityqa/run.go | 8 ++ internal/webconnectivityqa/run_test.go | 44 +++---- internal/webconnectivityqa/success.go | 8 +- internal/webconnectivityqa/tcpblocking.go | 4 +- internal/webconnectivityqa/testcase.go | 7 +- internal/webconnectivityqa/testkeys.go | 16 +-- internal/webconnectivityqa/throttling.go | 4 +- internal/webconnectivityqa/tlsblocking.go | 4 +- internal/webconnectivityqa/websitedown.go | 6 +- .../webconnectivityqa/websitedown_test.go | 43 +++++++ 29 files changed, 512 insertions(+), 140 deletions(-) create mode 100644 internal/webconnectivityqa/checker.go create mode 100644 internal/webconnectivityqa/checker_test.go create mode 100644 internal/webconnectivityqa/ghost_test.go create mode 100644 internal/webconnectivityqa/localhost_test.go create mode 100644 internal/webconnectivityqa/websitedown_test.go diff --git a/internal/experiment/webconnectivitylte/cleartextflow.go b/internal/experiment/webconnectivitylte/cleartextflow.go index 90af528666..142719718a 100644 --- a/internal/experiment/webconnectivitylte/cleartextflow.go +++ b/internal/experiment/webconnectivitylte/cleartextflow.go @@ -124,7 +124,14 @@ func (t *CleartextFlow) Run(parentCtx context.Context, index int64) error { tcpDialer := trace.NewDialerWithoutResolver(t.Logger) tcpConn, err := tcpDialer.DialContext(tcpCtx, "tcp", t.Address) t.TestKeys.AppendTCPConnectResults(trace.TCPConnects()...) - defer t.TestKeys.AppendNetworkEvents(trace.NetworkEvents()...) // here to include "connect" events + defer func() { + // BUGFIX: we must call trace.NetworkEvents()... inside the defer block otherwise + // we miss the read/write network events. See https://github.com/ooni/probe/issues/2674. + // + // Additionally, we must register this defer here because we want to include + // the "connect" event in case connect has failed. + t.TestKeys.AppendNetworkEvents(trace.NetworkEvents()...) + }() if err != nil { ol.Stop(err) return err diff --git a/internal/experiment/webconnectivitylte/secureflow.go b/internal/experiment/webconnectivitylte/secureflow.go index 486b94d24e..658c1e0b59 100644 --- a/internal/experiment/webconnectivitylte/secureflow.go +++ b/internal/experiment/webconnectivitylte/secureflow.go @@ -131,7 +131,14 @@ func (t *SecureFlow) Run(parentCtx context.Context, index int64) error { tcpDialer := trace.NewDialerWithoutResolver(t.Logger) tcpConn, err := tcpDialer.DialContext(tcpCtx, "tcp", t.Address) t.TestKeys.AppendTCPConnectResults(trace.TCPConnects()...) - defer t.TestKeys.AppendNetworkEvents(trace.NetworkEvents()...) // here to include "connect" events + defer func() { + // BUGFIX: we must call trace.NetworkEvents()... inside the defer block otherwise + // we miss the read/write network events. See https://github.com/ooni/probe/issues/2674. + // + // Additionally, we must register this defer here because we want to include + // the "connect" event in case connect has failed. + t.TestKeys.AppendNetworkEvents(trace.NetworkEvents()...) + }() if err != nil { ol.Stop(err) return err diff --git a/internal/webconnectivityqa/badssl.go b/internal/webconnectivityqa/badssl.go index c3a68e9602..14747a8831 100644 --- a/internal/webconnectivityqa/badssl.go +++ b/internal/webconnectivityqa/badssl.go @@ -16,7 +16,7 @@ func badSSLWithExpiredCertificate() *TestCase { // nothing }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "ssl_invalid_certificate", XStatus: 16, // StatusAnomalyControlFailure @@ -38,7 +38,7 @@ func badSSLWithWrongServerName() *TestCase { // nothing }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "ssl_invalid_hostname", XStatus: 16, // StatusAnomalyControlFailure @@ -59,7 +59,7 @@ func badSSLWithUnknownAuthorityWithConsistentDNS() *TestCase { // nothing }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "ssl_unknown_authority", XStatus: 16, // StatusAnomalyControlFailure @@ -88,7 +88,7 @@ func badSSLWithUnknownAuthorityWithInconsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", HTTPExperimentFailure: "ssl_unknown_authority", XStatus: 9248, // StatusExperimentHTTP | StatusAnomalyTLSHandshake | StatusAnomalyDNS diff --git a/internal/webconnectivityqa/checker.go b/internal/webconnectivityqa/checker.go new file mode 100644 index 0000000000..dd878059f2 --- /dev/null +++ b/internal/webconnectivityqa/checker.go @@ -0,0 +1,61 @@ +package webconnectivityqa + +import ( + "errors" + "strings" + + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/must" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/x/dslx" +) + +// Checker checks whether a measurement is correct. +type Checker interface { + Check(mx *model.Measurement) error +} + +// ReadWriteEventsExistentialChecker fails if there are zero network events. +type ReadWriteEventsExistentialChecker struct{} + +var _ Checker = &ReadWriteEventsExistentialChecker{} + +// ErrCheckerNoReadWriteEvents indicates that a checker did not find any read/write events. +var ErrCheckerNoReadWriteEvents = errors.New("no read or write events") + +// ErrCheckerUnexpectedWebConnectivityVersion indicates that the version is unexpected +var ErrCheckerUnexpectedWebConnectivityVersion = errors.New("unexpected Web Connectivity version") + +// Check implements Checker. +func (*ReadWriteEventsExistentialChecker) Check(mx *model.Measurement) error { + // we don't care about v0.4 + if strings.HasPrefix(mx.TestVersion, "0.4.") { + return nil + } + + // make sure it's v0.5 + if !strings.HasPrefix(mx.TestVersion, "0.5.") { + return ErrCheckerUnexpectedWebConnectivityVersion + } + + // serialize and reparse the test keys + var tk *dslx.Observations + must.UnmarshalJSON(must.MarshalJSON(mx.TestKeys), &tk) + + // count the read/write events + var count int + for _, ev := range tk.NetworkEvents { + switch ev.Operation { + case netxlite.ReadOperation, netxlite.WriteOperation: + count++ + default: + // nothing + } + } + + // make sure there's at least one network event + if count <= 0 { + return ErrCheckerNoReadWriteEvents + } + return nil +} diff --git a/internal/webconnectivityqa/checker_test.go b/internal/webconnectivityqa/checker_test.go new file mode 100644 index 0000000000..7d16f23bfc --- /dev/null +++ b/internal/webconnectivityqa/checker_test.go @@ -0,0 +1,112 @@ +package webconnectivityqa_test + +import ( + "context" + "errors" + "testing" + + "github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte" + "github.com/ooni/probe-cli/v3/internal/mocks" + "github.com/ooni/probe-cli/v3/internal/model" + "github.com/ooni/probe-cli/v3/internal/must" + "github.com/ooni/probe-cli/v3/internal/optional" + "github.com/ooni/probe-cli/v3/internal/webconnectivityqa" +) + +func TestConfigureCustomCheckers(t *testing.T) { + tc := &webconnectivityqa.TestCase{ + Name: "", + Input: "", + ExpectErr: false, + ExpectTestKeys: &webconnectivityqa.TestKeys{ + Accessible: true, + Blocking: nil, + }, + Checkers: []webconnectivityqa.Checker{&webconnectivityqa.ReadWriteEventsExistentialChecker{}}, + } + measurer := &mocks.ExperimentMeasurer{ + MockExperimentName: func() string { + return "web_connectivity" + }, + MockExperimentVersion: func() string { + return "0.5.28" + }, + MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { + args.Measurement.TestKeys = &webconnectivitylte.TestKeys{ + Accessible: optional.Some(true), + Blocking: nil, + } + return nil + }, + } + err := webconnectivityqa.RunTestCase(measurer, tc) + if !errors.Is(err, webconnectivityqa.ErrCheckerNoReadWriteEvents) { + t.Fatal("unexpected error", err) + } +} + +func TestReadWriteEventsExistentialChecker(t *testing.T) { + type testcase struct { + name string + version string + tk string + expect error + } + + cases := []testcase{{ + name: "with Web Connectivity v0.4", + version: "0.4.3", + tk: `{}`, + expect: nil, + }, { + name: "with Web Connectivity v0.6", + version: "0.6.0", + tk: `{}`, + expect: webconnectivityqa.ErrCheckerUnexpectedWebConnectivityVersion, + }, { + name: "with read/write network events", + version: "0.5.28", + tk: `{"network_events":[{"operation":"read"},{"operation":"write"}]}`, + expect: nil, + }, { + name: "without network events", + version: "0.5.28", + tk: `{"network_events":[]}`, + expect: webconnectivityqa.ErrCheckerNoReadWriteEvents, + }, { + name: "with no read/write network events", + version: "0.5.28", + tk: `{"network_events":[{"operation":"connect"},{"operation":"close"}]}`, + expect: webconnectivityqa.ErrCheckerNoReadWriteEvents, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var tks map[string]any + must.UnmarshalJSON([]byte(tc.tk), &tks) + + meas := &model.Measurement{ + TestKeys: tks, + TestVersion: tc.version, + } + + err := (&webconnectivityqa.ReadWriteEventsExistentialChecker{}).Check(meas) + + switch { + case tc.expect == nil && err == nil: + return + + case tc.expect == nil && err != nil: + t.Fatal("expected", tc.expect, "got", err) + + case tc.expect != nil && err == nil: + t.Fatal("expected", tc.expect, "got", err) + + case tc.expect != nil && err != nil: + if err.Error() != tc.expect.Error() { + t.Fatal("expected", tc.expect, "got", err) + } + } + }) + } +} diff --git a/internal/webconnectivityqa/cloudflare.go b/internal/webconnectivityqa/cloudflare.go index 51a3f08c1f..5a35d62dd1 100644 --- a/internal/webconnectivityqa/cloudflare.go +++ b/internal/webconnectivityqa/cloudflare.go @@ -1,20 +1,15 @@ package webconnectivityqa -import "github.com/ooni/probe-cli/v3/internal/netemx" - // cloudflareCAPTCHAWithHTTP obtains the cloudflare CAPTCHA using HTTP. func cloudflareCAPTCHAWithHTTP() *TestCase { // See https://github.com/ooni/probe/issues/2661 for an explanation of why // here for now we're forced to declare "http-diff". return &TestCase{ - Name: "cloudflareCAPTCHAWithHTTP", - Flags: TestCaseFlagNoV04, - Input: "http://www.cloudflare-cache.com/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "cloudflareCAPTCHAWithHTTP", + Flags: TestCaseFlagNoV04, + Input: "http://www.cloudflare-cache.com/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", StatusCodeMatch: false, BodyLengthMatch: false, @@ -31,14 +26,11 @@ func cloudflareCAPTCHAWithHTTP() *TestCase { // cloudflareCAPTCHAWithHTTPS obtains the cloudflare CAPTCHA using HTTPS. func cloudflareCAPTCHAWithHTTPS() *TestCase { return &TestCase{ - Name: "cloudflareCAPTCHAWithHTTPS", - Flags: TestCaseFlagNoV04, - Input: "https://www.cloudflare-cache.com/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "cloudflareCAPTCHAWithHTTPS", + Flags: TestCaseFlagNoV04, + Input: "https://www.cloudflare-cache.com/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", StatusCodeMatch: false, BodyLengthMatch: false, diff --git a/internal/webconnectivityqa/control.go b/internal/webconnectivityqa/control.go index 04677784af..1f01cf05c6 100644 --- a/internal/webconnectivityqa/control.go +++ b/internal/webconnectivityqa/control.go @@ -42,7 +42,7 @@ func controlFailureWithSuccessfulHTTPWebsite() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ ControlFailure: "unknown_failure: httpapi: all endpoints failed: [ connection_reset; connection_reset; connection_reset; connection_reset;]", XStatus: 8, // StatusAnomalyControlUnreachable Accessible: nil, @@ -87,7 +87,7 @@ func controlFailureWithSuccessfulHTTPSWebsite() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ ControlFailure: "unknown_failure: httpapi: all endpoints failed: [ connection_reset; connection_reset; connection_reset; connection_reset;]", XStatus: 1, // StatusSuccessSecure XBlockingFlags: 32, // AnalysisBlockingFlagSuccess diff --git a/internal/webconnectivityqa/dnsblocking.go b/internal/webconnectivityqa/dnsblocking.go index 3145ee22ac..ac396ef191 100644 --- a/internal/webconnectivityqa/dnsblocking.go +++ b/internal/webconnectivityqa/dnsblocking.go @@ -20,7 +20,7 @@ func dnsBlockingAndroidDNSCacheNoData() *TestCase { env.ISPResolverConfig().RemoveRecord("www.example.com") }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: "android_dns_cache_no_data", HTTPExperimentFailure: "android_dns_cache_no_data", DNSConsistency: "inconsistent", @@ -52,7 +52,7 @@ func dnsBlockingNXDOMAIN() *TestCase { env.ISPResolverConfig().RemoveRecord("www.example.com") }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: "dns_nxdomain_error", HTTPExperimentFailure: "dns_nxdomain_error", DNSConsistency: "inconsistent", @@ -76,7 +76,7 @@ func dnsBlockingBOGON() *TestCase { env.ISPResolverConfig().AddRecord("www.example.com", "", "10.10.34.35") }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ HTTPExperimentFailure: "generic_timeout_error", DNSExperimentFailure: nil, DNSConsistency: "inconsistent", diff --git a/internal/webconnectivityqa/dnshijacking.go b/internal/webconnectivityqa/dnshijacking.go index b0822fb123..f560b2d27f 100644 --- a/internal/webconnectivityqa/dnshijacking.go +++ b/internal/webconnectivityqa/dnshijacking.go @@ -26,7 +26,7 @@ func dnsHijackingToProxyWithHTTPURL() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", BodyLengthMatch: true, BodyProportion: 1, @@ -63,7 +63,7 @@ func dnsHijackingToProxyWithHTTPSURL() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", BodyLengthMatch: true, BodyProportion: 1, @@ -97,7 +97,7 @@ func dnsHijackingToLocalhostWithHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", XDNSFlags: 5, // AnalysisFlagDNSBogon | AnalysisDNSFlagUnexpectedAddrs XBlockingFlags: 33, // AnalysisBlockingFlagDNSBlocking | AnalysisBlockingFlagSuccess @@ -125,7 +125,7 @@ func dnsHijackingToLocalhostWithHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", XDNSFlags: 5, // AnalysisFlagDNSBogon | AnalysisDNSFlagUnexpectedAddrs XBlockingFlags: 33, // AnalysisBlockingFlagDNSBlocking | AnalysisBlockingFlagSuccess diff --git a/internal/webconnectivityqa/dnshijacking_test.go b/internal/webconnectivityqa/dnshijacking_test.go index 10d21e7b17..1bd44da68d 100644 --- a/internal/webconnectivityqa/dnshijacking_test.go +++ b/internal/webconnectivityqa/dnshijacking_test.go @@ -10,7 +10,7 @@ import ( "github.com/ooni/probe-cli/v3/internal/netxlite" ) -func TestDNSHijackingTestCases(t *testing.T) { +func TestDNSHijackingToProxyTestCases(t *testing.T) { testcases := []*TestCase{ dnsHijackingToProxyWithHTTPURL(), dnsHijackingToProxyWithHTTPSURL(), @@ -54,3 +54,48 @@ func TestDNSHijackingTestCases(t *testing.T) { }) } } + +func TestDNSHijackingToLocalhostTestCases(t *testing.T) { + testcases := []*TestCase{ + dnsHijackingToLocalhostWithHTTP(), + dnsHijackingToLocalhostWithHTTPS(), + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + env := netemx.MustNewScenario(netemx.InternetScenario) + defer env.Close() + + tc.Configure(env) + + env.Do(func() { + expect := []string{"127.0.0.1"} + + t.Run("with stdlib resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + reso := netx.NewStdlibResolver(log.Log) + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("with UDP resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + d := netx.NewDialerWithoutResolver(log.Log) + reso := netx.NewParallelUDPResolver(log.Log, d, "8.8.8.8:53") + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + }) + }) + } +} diff --git a/internal/webconnectivityqa/ghost.go b/internal/webconnectivityqa/ghost.go index 1b1ef0d520..b9e81f5b8e 100644 --- a/internal/webconnectivityqa/ghost.go +++ b/internal/webconnectivityqa/ghost.go @@ -31,7 +31,7 @@ func ghostDNSBlockingWithHTTP() *TestCase { }) }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "inconsistent", XBlockingFlags: 16, // AnalysisBlockingFlagHTTPDiff @@ -68,7 +68,7 @@ func ghostDNSBlockingWithHTTPS() *TestCase { }) }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "inconsistent", HTTPExperimentFailure: "connection_refused", diff --git a/internal/webconnectivityqa/ghost_test.go b/internal/webconnectivityqa/ghost_test.go new file mode 100644 index 0000000000..0b87ca914d --- /dev/null +++ b/internal/webconnectivityqa/ghost_test.go @@ -0,0 +1,56 @@ +package webconnectivityqa + +import ( + "context" + "testing" + + "github.com/apex/log" + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/netemx" + "github.com/ooni/probe-cli/v3/internal/netxlite" +) + +func TestGhostTestCases(t *testing.T) { + testcases := []*TestCase{ + ghostDNSBlockingWithHTTP(), + ghostDNSBlockingWithHTTPS(), + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + env := netemx.MustNewScenario(netemx.InternetScenario) + defer env.Close() + + tc.Configure(env) + + env.Do(func() { + expect := []string{netemx.AddressPublicBlockpage} + + t.Run("with stdlib resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + reso := netx.NewStdlibResolver(log.Log) + addrs, err := reso.LookupHost(context.Background(), "itsat.info") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("with UDP resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + d := netx.NewDialerWithoutResolver(log.Log) + reso := netx.NewParallelUDPResolver(log.Log, d, "8.8.8.8:53") + addrs, err := reso.LookupHost(context.Background(), "itsat.info") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + }) + }) + } +} diff --git a/internal/webconnectivityqa/httpblocking.go b/internal/webconnectivityqa/httpblocking.go index 21e1084101..61d547cecd 100644 --- a/internal/webconnectivityqa/httpblocking.go +++ b/internal/webconnectivityqa/httpblocking.go @@ -23,7 +23,7 @@ func httpBlockingConnectionReset() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", // TODO(bassosimone): it seems LTE QA does not check for the value of // the HTTPExperimentFailure field, why? diff --git a/internal/webconnectivityqa/httpdiff.go b/internal/webconnectivityqa/httpdiff.go index 5090f250a8..a574cca893 100644 --- a/internal/webconnectivityqa/httpdiff.go +++ b/internal/webconnectivityqa/httpdiff.go @@ -26,7 +26,7 @@ func httpDiffWithConsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", BodyLengthMatch: false, @@ -81,7 +81,7 @@ func httpDiffWithInconsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "inconsistent", HTTPExperimentFailure: nil, diff --git a/internal/webconnectivityqa/idna.go b/internal/webconnectivityqa/idna.go index 28f72ba305..8c7479fc22 100644 --- a/internal/webconnectivityqa/idna.go +++ b/internal/webconnectivityqa/idna.go @@ -1,20 +1,13 @@ package webconnectivityqa -import ( - "github.com/ooni/probe-cli/v3/internal/netemx" -) - // idnaWithoutCensorshipLowercase verifies that we can handle IDNA with lowercase. func idnaWithoutCensorshipLowercase() *TestCase { return &TestCase{ - Name: "idnaWithoutCensorshipLowercase", - Flags: TestCaseFlagNoV04, - Input: "http://яндекс.рф/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "idnaWithoutCensorshipLowercase", + Flags: TestCaseFlagNoV04, + Input: "http://яндекс.рф/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: nil, @@ -34,14 +27,11 @@ func idnaWithoutCensorshipLowercase() *TestCase { // with the first letter being uppercase. func idnaWithoutCensorshipWithFirstLetterUppercase() *TestCase { return &TestCase{ - Name: "idnaWithoutCensorshipWithFirstLetterUppercase", - Flags: TestCaseFlagNoV04, - Input: "http://Яндекс.рф/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "idnaWithoutCensorshipWithFirstLetterUppercase", + Flags: TestCaseFlagNoV04, + Input: "http://Яндекс.рф/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: nil, diff --git a/internal/webconnectivityqa/largefile.go b/internal/webconnectivityqa/largefile.go index c2ebfa098b..27b70a85ff 100644 --- a/internal/webconnectivityqa/largefile.go +++ b/internal/webconnectivityqa/largefile.go @@ -1,18 +1,13 @@ package webconnectivityqa -import "github.com/ooni/probe-cli/v3/internal/netemx" - // largeFileWithHTTP is the case where we download a large file. func largeFileWithHTTP() *TestCase { return &TestCase{ - Name: "largeFileWithHTTP", - Flags: TestCaseFlagNoV04, - Input: "http://largefile.com/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "largeFileWithHTTP", + Flags: TestCaseFlagNoV04, + Input: "http://largefile.com/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", StatusCodeMatch: true, HeadersMatch: true, @@ -27,14 +22,11 @@ func largeFileWithHTTP() *TestCase { // largeFileWithHTTPS is the case where we download a large file. func largeFileWithHTTPS() *TestCase { return &TestCase{ - Name: "largeFileWithHTTPS", - Flags: TestCaseFlagNoV04, - Input: "https://largefile.com/", - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "largeFileWithHTTPS", + Flags: TestCaseFlagNoV04, + Input: "https://largefile.com/", ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", StatusCodeMatch: true, HeadersMatch: true, diff --git a/internal/webconnectivityqa/localhost.go b/internal/webconnectivityqa/localhost.go index bfca2eb919..adc0873372 100644 --- a/internal/webconnectivityqa/localhost.go +++ b/internal/webconnectivityqa/localhost.go @@ -16,7 +16,7 @@ func localhostWithHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", XDNSFlags: 1, // AnalysisFlagDNSBogon Accessible: false, @@ -39,7 +39,7 @@ func localhostWithHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", XDNSFlags: 1, // AnalysisFlagDNSBogon Accessible: false, diff --git a/internal/webconnectivityqa/localhost_test.go b/internal/webconnectivityqa/localhost_test.go new file mode 100644 index 0000000000..de00232e1e --- /dev/null +++ b/internal/webconnectivityqa/localhost_test.go @@ -0,0 +1,56 @@ +package webconnectivityqa + +import ( + "context" + "testing" + + "github.com/apex/log" + "github.com/google/go-cmp/cmp" + "github.com/ooni/probe-cli/v3/internal/netemx" + "github.com/ooni/probe-cli/v3/internal/netxlite" +) + +func TestLocalhostTestCases(t *testing.T) { + testcases := []*TestCase{ + localhostWithHTTP(), + localhostWithHTTPS(), + } + + for _, tc := range testcases { + t.Run(tc.Name, func(t *testing.T) { + env := netemx.MustNewScenario(netemx.InternetScenario) + defer env.Close() + + tc.Configure(env) + + env.Do(func() { + expect := []string{"127.0.0.1"} + + t.Run("with stdlib resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + reso := netx.NewStdlibResolver(log.Log) + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("with UDP resolver", func(t *testing.T) { + netx := &netxlite.Netx{} + d := netx.NewDialerWithoutResolver(log.Log) + reso := netx.NewParallelUDPResolver(log.Log, d, "8.8.8.8:53") + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expect, addrs); diff != "" { + t.Fatal(diff) + } + }) + }) + }) + } +} diff --git a/internal/webconnectivityqa/redirect.go b/internal/webconnectivityqa/redirect.go index dc25f8d7c9..bf814b4332 100644 --- a/internal/webconnectivityqa/redirect.go +++ b/internal/webconnectivityqa/redirect.go @@ -32,7 +32,7 @@ func redirectWithConsistentDNSAndThenConnectionRefusedForHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "connection_refused", @@ -70,7 +70,7 @@ func redirectWithConsistentDNSAndThenConnectionRefusedForHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "connection_refused", @@ -108,7 +108,7 @@ func redirectWithConsistentDNSAndThenConnectionResetForHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "connection_reset", @@ -146,7 +146,7 @@ func redirectWithConsistentDNSAndThenConnectionResetForHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "connection_reset", @@ -177,7 +177,7 @@ func redirectWithConsistentDNSAndThenNXDOMAIN() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "dns_nxdomain_error", @@ -215,7 +215,7 @@ func redirectWithConsistentDNSAndThenEOFForHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "eof_error", @@ -253,7 +253,7 @@ func redirectWithConsistentDNSAndThenEOFForHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "eof_error", @@ -292,7 +292,7 @@ func redirectWithConsistentDNSAndThenTimeoutForHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "generic_timeout_error", @@ -331,7 +331,7 @@ func redirectWithConsistentDNSAndThenTimeoutForHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "generic_timeout_error", @@ -350,15 +350,12 @@ func redirectWithConsistentDNSAndThenTimeoutForHTTPS() *TestCase { // See https://github.com/ooni/probe/issues/2628 for more info. func redirectWithBrokenLocationForHTTP() *TestCase { return &TestCase{ - Name: "redirectWithBrokenLocationForHTTP", - Flags: TestCaseFlagNoV04, - Input: "http://httpbin.com/broken-redirect-http", - LongTest: true, - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "redirectWithBrokenLocationForHTTP", + Flags: TestCaseFlagNoV04, + Input: "http://httpbin.com/broken-redirect-http", + LongTest: true, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: netxlite.FailureHTTPInvalidRedirectLocationHost, @@ -377,15 +374,12 @@ func redirectWithBrokenLocationForHTTP() *TestCase { // See https://github.com/ooni/probe/issues/2628 for more info. func redirectWithBrokenLocationForHTTPS() *TestCase { return &TestCase{ - Name: "redirectWithBrokenLocationForHTTPS", - Flags: TestCaseFlagNoV04, - Input: "https://httpbin.com/broken-redirect-https", - LongTest: true, - Configure: func(env *netemx.QAEnv) { - // nothing - }, + Name: "redirectWithBrokenLocationForHTTPS", + Flags: TestCaseFlagNoV04, + Input: "https://httpbin.com/broken-redirect-https", + LongTest: true, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: netxlite.FailureHTTPInvalidRedirectLocationHost, diff --git a/internal/webconnectivityqa/run.go b/internal/webconnectivityqa/run.go index b851e4d4cf..70304d2362 100644 --- a/internal/webconnectivityqa/run.go +++ b/internal/webconnectivityqa/run.go @@ -65,11 +65,19 @@ func MeasureTestCase(measurer model.ExperimentMeasurer, tc *TestCase) (*model.Me // RunTestCase runs a [testCase]. func RunTestCase(measurer model.ExperimentMeasurer, tc *TestCase) error { + // run the test case proper to get a full OONI measurement measurement, err := MeasureTestCase(measurer, tc) if err != nil { return err } + // run each check in the list of checkers + for _, checker := range tc.Checkers { + if err := checker.Check(measurement); err != nil { + return err + } + } + // reduce the test keys to a common format tk := newTestKeys(measurement) diff --git a/internal/webconnectivityqa/run_test.go b/internal/webconnectivityqa/run_test.go index 6adf371394..4110646a39 100644 --- a/internal/webconnectivityqa/run_test.go +++ b/internal/webconnectivityqa/run_test.go @@ -18,7 +18,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{}, + ExpectTestKeys: &TestKeys{}, } measurer := &mocks.ExperimentMeasurer{ MockExperimentName: func() string { @@ -43,7 +43,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: true, - ExpectTestKeys: &testKeys{}, + ExpectTestKeys: &TestKeys{}, } measurer := &mocks.ExperimentMeasurer{ MockExperimentName: func() string { @@ -68,7 +68,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: true, Blocking: nil, }, @@ -81,7 +81,7 @@ func TestRunTestCase(t *testing.T) { return "0.4.3" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{} + args.Measurement.TestKeys = &TestKeys{} return nil }, } @@ -97,7 +97,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: "http-diff", }, @@ -110,7 +110,7 @@ func TestRunTestCase(t *testing.T) { return "0.4.3" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, } return nil @@ -128,7 +128,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XStatus: 100, @@ -142,7 +142,7 @@ func TestRunTestCase(t *testing.T) { return "0.4.3" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XStatus: 101, @@ -162,7 +162,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XStatus: 100, @@ -176,7 +176,7 @@ func TestRunTestCase(t *testing.T) { return "0.4.3" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XStatus: 100, @@ -197,7 +197,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 11, @@ -211,7 +211,7 @@ func TestRunTestCase(t *testing.T) { return "0.5.28" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -232,7 +232,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -247,7 +247,7 @@ func TestRunTestCase(t *testing.T) { return "0.5.28" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -269,7 +269,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -285,7 +285,7 @@ func TestRunTestCase(t *testing.T) { return "0.5.28" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -307,7 +307,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -323,7 +323,7 @@ func TestRunTestCase(t *testing.T) { return "0.5.28" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, XDNSFlags: 10, @@ -345,7 +345,7 @@ func TestRunTestCase(t *testing.T) { Input: "", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: false, Blocking: true, }, @@ -358,7 +358,7 @@ func TestRunTestCase(t *testing.T) { return "0.2.11" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: false, Blocking: true, } @@ -380,7 +380,7 @@ func TestRunTestCase(t *testing.T) { called = true }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ Accessible: true, Blocking: nil, }, @@ -393,7 +393,7 @@ func TestRunTestCase(t *testing.T) { return "0.5.28" }, MockRun: func(ctx context.Context, args *model.ExperimentArgs) error { - args.Measurement.TestKeys = &testKeys{ + args.Measurement.TestKeys = &TestKeys{ Accessible: true, Blocking: nil, } diff --git a/internal/webconnectivityqa/success.go b/internal/webconnectivityqa/success.go index 25906d0df2..44db7a7859 100644 --- a/internal/webconnectivityqa/success.go +++ b/internal/webconnectivityqa/success.go @@ -8,7 +8,7 @@ func successWithHTTP() *TestCase { Input: "http://www.example.com/", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", BodyLengthMatch: true, BodyProportion: 1, @@ -20,6 +20,10 @@ func successWithHTTP() *TestCase { Accessible: true, Blocking: false, }, + Checkers: []Checker{ + // See https://github.com/ooni/probe/issues/2674 + &ReadWriteEventsExistentialChecker{}, + }, } } @@ -31,7 +35,7 @@ func successWithHTTPS() *TestCase { Input: "https://www.example.com/", Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", BodyLengthMatch: true, BodyProportion: 1, diff --git a/internal/webconnectivityqa/tcpblocking.go b/internal/webconnectivityqa/tcpblocking.go index 92e8432067..b0d58b7eff 100644 --- a/internal/webconnectivityqa/tcpblocking.go +++ b/internal/webconnectivityqa/tcpblocking.go @@ -23,7 +23,7 @@ func tcpBlockingConnectTimeout() *TestCase { }) }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "consistent", HTTPExperimentFailure: "generic_timeout_error", @@ -62,7 +62,7 @@ func tcpBlockingConnectionRefusedWithInconsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: nil, DNSConsistency: "inconsistent", HTTPExperimentFailure: "connection_refused", diff --git a/internal/webconnectivityqa/testcase.go b/internal/webconnectivityqa/testcase.go index 078160c573..fbaf991254 100644 --- a/internal/webconnectivityqa/testcase.go +++ b/internal/webconnectivityqa/testcase.go @@ -31,7 +31,12 @@ type TestCase struct { ExpectErr bool // ExpectTestKeys contains the expected test keys - ExpectTestKeys *testKeys + ExpectTestKeys *TestKeys + + // Checkers contains an OPTIONAL list of checkers + // that perform additional parsing of the measurement + // to ensure that specific properties hold. + Checkers []Checker } // AllTestCases returns all the defined test cases. diff --git a/internal/webconnectivityqa/testkeys.go b/internal/webconnectivityqa/testkeys.go index 1fa5138f12..ba5ebfc63d 100644 --- a/internal/webconnectivityqa/testkeys.go +++ b/internal/webconnectivityqa/testkeys.go @@ -10,8 +10,8 @@ import ( "github.com/ooni/probe-cli/v3/internal/runtimex" ) -// testKeys is the test keys structure returned by this package. -type testKeys struct { +// TestKeys is the test keys structure returned by this package. +type TestKeys struct { // XExperimentVersion is the experiment version. XExperimentVersion string `json:"x_experiment_version"` @@ -52,9 +52,9 @@ type testKeys struct { } // newTestKeys constructs the test keys from the measurement. -func newTestKeys(measurement *model.Measurement) *testKeys { +func newTestKeys(measurement *model.Measurement) *TestKeys { rawTk := runtimex.Try1(json.Marshal(measurement.TestKeys)) - var tk testKeys + var tk TestKeys runtimex.Try0(json.Unmarshal(rawTk, &tk)) tk.XExperimentVersion = measurement.TestVersion return &tk @@ -62,20 +62,20 @@ func newTestKeys(measurement *model.Measurement) *testKeys { // compareTestKeys compares two testKeys instances. It returns an error in // case of a mismatch and returns nil otherwise. -func compareTestKeys(expected, got *testKeys) error { +func compareTestKeys(expected, got *TestKeys) error { // always ignore the experiment version because it is not set inside the expected value options := []cmp.Option{ - cmpopts.IgnoreFields(testKeys{}, "XExperimentVersion"), + cmpopts.IgnoreFields(TestKeys{}, "XExperimentVersion"), } switch got.XExperimentVersion { case "0.4.3": // ignore the fields that are specific to LTE - options = append(options, cmpopts.IgnoreFields(testKeys{}, "XDNSFlags", "XBlockingFlags", "XNullNullFlags")) + options = append(options, cmpopts.IgnoreFields(TestKeys{}, "XDNSFlags", "XBlockingFlags", "XNullNullFlags")) case "0.5.28": // ignore the fields that are specific to v0.4 - options = append(options, cmpopts.IgnoreFields(testKeys{}, "XStatus")) + options = append(options, cmpopts.IgnoreFields(TestKeys{}, "XStatus")) default: return fmt.Errorf("unknown experiment version: %s", got.XExperimentVersion) diff --git a/internal/webconnectivityqa/throttling.go b/internal/webconnectivityqa/throttling.go index 4839d71da0..3b0f3ef264 100644 --- a/internal/webconnectivityqa/throttling.go +++ b/internal/webconnectivityqa/throttling.go @@ -34,7 +34,7 @@ func throttlingWithHTTP() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "generic_timeout_error", XBlockingFlags: 8, // AnalysisBlockingFlagHTTPBlocking @@ -61,7 +61,7 @@ func throttlingWithHTTPS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "generic_timeout_error", XBlockingFlags: 8, // AnalysisBlockingFlagHTTPBlocking diff --git a/internal/webconnectivityqa/tlsblocking.go b/internal/webconnectivityqa/tlsblocking.go index d737435b9d..e064057c40 100644 --- a/internal/webconnectivityqa/tlsblocking.go +++ b/internal/webconnectivityqa/tlsblocking.go @@ -25,7 +25,7 @@ func tlsBlockingConnectionResetWithConsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "consistent", HTTPExperimentFailure: "connection_reset", XStatus: 8448, // StatusExperimentHTTP | StatusAnomalyReadWrite @@ -63,7 +63,7 @@ func tlsBlockingConnectionResetWithInconsistentDNS() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSConsistency: "inconsistent", HTTPExperimentFailure: "connection_reset", XStatus: 8480, // StatusExperimentHTTP | StatusAnomalyReadWrite | StatusAnomalyDNS diff --git a/internal/webconnectivityqa/websitedown.go b/internal/webconnectivityqa/websitedown.go index 947954bf4a..048f1069a2 100644 --- a/internal/webconnectivityqa/websitedown.go +++ b/internal/webconnectivityqa/websitedown.go @@ -27,7 +27,7 @@ func websiteDownNXDOMAIN() *TestCase { Input: "http://www.example.xyz/", // domain not defined in the simulation Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: "dns_nxdomain_error", HTTPExperimentFailure: "dns_nxdomain_error", DNSConsistency: "consistent", @@ -51,7 +51,7 @@ func websiteDownTCPConnect() *TestCase { Input: "http://www.example.com:444/", // port where we're not listening. Configure: nil, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ HTTPExperimentFailure: "connection_refused", DNSConsistency: "consistent", XStatus: 2052, // StatusExperimentDNS | StatusSuccessNXDOMAIN @@ -78,7 +78,7 @@ func websiteDownNoAddrs() *TestCase { }, ExpectErr: false, - ExpectTestKeys: &testKeys{ + ExpectTestKeys: &TestKeys{ DNSExperimentFailure: "dns_no_answer", DNSConsistency: "consistent", XBlockingFlags: 0, diff --git a/internal/webconnectivityqa/websitedown_test.go b/internal/webconnectivityqa/websitedown_test.go new file mode 100644 index 0000000000..a60c737330 --- /dev/null +++ b/internal/webconnectivityqa/websitedown_test.go @@ -0,0 +1,43 @@ +package webconnectivityqa + +import ( + "context" + "testing" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/netemx" + "github.com/ooni/probe-cli/v3/internal/netxlite" +) + +func TestWebsiteDownNoAddrs(t *testing.T) { + env := netemx.MustNewScenario(netemx.InternetScenario) + defer env.Close() + + tc := websiteDownNoAddrs() + tc.Configure(env) + + netx := &netxlite.Netx{Underlying: &netxlite.NetemUnderlyingNetworkAdapter{UNet: env.ClientStack}} + + t.Run("for system resolver", func(t *testing.T) { + reso := netx.NewStdlibResolver(log.Log) + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err == nil || err.Error() != netxlite.FailureDNSNoAnswer { + t.Fatal("unexpected error", err) + } + if len(addrs) > 0 { + t.Fatal("expected empty addrs") + } + }) + + t.Run("for UDP resolver", func(t *testing.T) { + d := netx.NewDialerWithoutResolver(log.Log) + reso := netx.NewParallelUDPResolver(log.Log, d, "8.8.8.8:53") + addrs, err := reso.LookupHost(context.Background(), "www.example.com") + if err == nil || err.Error() != netxlite.FailureDNSNoAnswer { + t.Fatal("unexpected error", err) + } + if len(addrs) > 0 { + t.Fatal("expected empty addrs") + } + }) +}