From ae7d6f866538d6e9c2d5657e6fb73fa4f37b8d5e Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Fri, 26 Apr 2024 11:53:24 +0200 Subject: [PATCH] chore(probeservices): better tests for bouncer, check-in, measurement-meta (#1566) Part of https://github.com/ooni/probe/issues/2718. --- internal/probeservices/bouncer_test.go | 166 +++++++++- internal/probeservices/checkin_test.go | 241 +++++++++++--- .../probeservices/measurementmeta_test.go | 301 ++++++++++++------ 3 files changed, 550 insertions(+), 158 deletions(-) diff --git a/internal/probeservices/bouncer_test.go b/internal/probeservices/bouncer_test.go index 25f23a8519..1fb5c65925 100644 --- a/internal/probeservices/bouncer_test.go +++ b/internal/probeservices/bouncer_test.go @@ -2,18 +2,164 @@ package probeservices import ( "context" + "errors" + "net/http" + "net/url" "testing" + + "github.com/google/go-cmp/cmp" + "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/netxlite" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/testingx" ) func TestGetTestHelpers(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - testhelpers, err := newclient().GetTestHelpers(context.Background()) - if err != nil { - t.Fatal(err) - } - if len(testhelpers) <= 1 { - t.Fatal("no returned test helpers?!") - } + + // First, let's check whether we can get a response from the real OONI backend. + t.Run("is working as intended with the real backend", func(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") + } + + // create client + client := newclient() + + // issue the request + testhelpers, err := client.GetTestHelpers(context.Background()) + + // we do not expect an error here + if err != nil { + t.Fatal(err) + } + + // we expect at least one TH + if len(testhelpers) <= 1 { + t.Fatal("no returned test helpers?!") + } + }) + + // Now let's construct a test server that returns a valid response and try + // to communicate with such a test server successfully and with errors + + t.Run("is working as intended with a local test server", func(t *testing.T) { + // this is what we expect to receive + expect := map[string][]model.OOAPIService{ + "web-connectivity": {{ + Address: "https://0.th.ooni.org/", + Type: "https", + }}, + } + + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Method == http.MethodGet, "invalid method") + runtimex.Assert(r.URL.Path == "/api/v1/test-helpers", "invalid URL path") + w.Write(must.MarshalJSON(expect)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the GET request + testhelpers, err := client.GetTestHelpers(context.Background()) + + // we do not expect an error + if err != nil { + t.Fatal(err) + } + + // we expect to see exactly what the server sent + if diff := cmp.Diff(expect, testhelpers); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("reports an error when the connection is reset", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(testingx.HTTPHandlerReset()) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the GET request + testhelpers, err := client.GetTestHelpers(context.Background()) + + // we do expect an error + if !errors.Is(err, netxlite.ECONNRESET) { + t.Fatal("unexpected error", err) + } + + // we expect to see a zero-length / nil map + if len(testhelpers) != 0 { + t.Fatal("expected result lenght to be zero") + } + }) + + t.Run("reports an error when the response is not JSON parsable", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{`)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the GET request + testhelpers, err := client.GetTestHelpers(context.Background()) + + // we do expect an error + if err == nil || err.Error() != "unexpected end of JSON input" { + t.Fatal("unexpected error", err) + } + + // we expect to see a zero-length / nil map + if len(testhelpers) != 0 { + t.Fatal("expected result lenght to be zero") + } + }) } diff --git a/internal/probeservices/checkin_test.go b/internal/probeservices/checkin_test.go index b9ea1ace94..87b7571b85 100644 --- a/internal/probeservices/checkin_test.go +++ b/internal/probeservices/checkin_test.go @@ -2,19 +2,23 @@ package probeservices import ( "context" - "strings" + "errors" + "net/http" + "net/url" "testing" + "time" + "github.com/google/go-cmp/cmp" + "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/netxlite" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/testingx" ) -func TestCheckInSuccess(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - - client := newclient() - client.BaseURL = "https://ams-pg-test.ooni.org" +func TestCheckIn(t *testing.T) { + // define a common configuration to use across all tests config := model.OOAPICheckInConfig{ Charging: true, OnWiFi: true, @@ -28,49 +32,184 @@ func TestCheckInSuccess(t *testing.T) { CategoryCodes: []string{"NEWS", "CULTR"}, }, } - ctx := context.Background() - result, err := client.CheckIn(ctx, config) - if err != nil { - t.Fatal(err) - } - if result == nil || result.Tests.WebConnectivity == nil { - t.Fatal("got nil result or WebConnectivity") - } - if result.Tests.WebConnectivity.ReportID == "" { - t.Fatal("ReportID is empty") - } - if len(result.Tests.WebConnectivity.URLs) < 1 { - t.Fatal("unexpected number of URLs") - } - for _, entry := range result.Tests.WebConnectivity.URLs { - if entry.CategoryCode != "NEWS" && entry.CategoryCode != "CULTR" { - t.Fatalf("unexpected category code: %+v", entry) + + t.Run("with the real API server", func(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") } - } -} -func TestCheckInFailure(t *testing.T) { - client := newclient() - client.BaseURL = "https://\t\t\t/" // cause test to fail - config := model.OOAPICheckInConfig{ - Charging: true, - OnWiFi: true, - Platform: "android", - ProbeASN: "AS12353", - ProbeCC: "PT", - RunType: model.RunTypeTimed, - SoftwareName: "ooniprobe-android", - SoftwareVersion: "2.7.1", - WebConnectivity: model.OOAPICheckInConfigWebConnectivity{ - CategoryCodes: []string{"NEWS", "CULTR"}, - }, - } - ctx := context.Background() - result, err := client.CheckIn(ctx, config) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected") - } - if result != nil { - t.Fatal("results?!") - } + client := newclient() + client.BaseURL = "https://ams-pg-test.ooni.org" // use the test infra + + ctx := context.Background() + + // call the API + result, err := client.CheckIn(ctx, config) + + // we do not expect to see an error + if err != nil { + t.Fatal(err) + } + + // sanity check the returned response + if result == nil || result.Tests.WebConnectivity == nil { + t.Fatal("got nil result or nil WebConnectivity") + } + if result.Tests.WebConnectivity.ReportID == "" { + t.Fatal("ReportID is empty") + } + if len(result.Tests.WebConnectivity.URLs) < 1 { + t.Fatal("unexpected number of URLs") + } + + // ensure the category codes match our request + for _, entry := range result.Tests.WebConnectivity.URLs { + if entry.CategoryCode != "NEWS" && entry.CategoryCode != "CULTR" { + t.Fatalf("unexpected category code: %+v", entry) + } + } + }) + + t.Run("with a working-as-intended local server", func(t *testing.T) { + // define our expectations + expect := &model.OOAPICheckInResult{ + Conf: model.OOAPICheckInResultConfig{ + Features: map[string]bool{}, + TestHelpers: map[string][]model.OOAPIService{ + "web-connectivity": {{ + Address: "https://0.th.ooni.org/", + Type: "https", + }}, + }, + }, + ProbeASN: "AS30722", + ProbeCC: "US", + Tests: model.OOAPICheckInResultNettests{ + WebConnectivity: &model.OOAPICheckInInfoWebConnectivity{ + ReportID: "20240424T134700Z_webconnectivity_IT_30722_n1_q5N5YSTWEqHYDo9v", + URLs: []model.OOAPIURLInfo{{ + CategoryCode: "NEWS", + CountryCode: "IT", + URL: "https://www.example.com/", + }}, + }, + }, + UTCTime: time.Date(2022, 11, 22, 1, 2, 3, 0, time.UTC), + V: 1, + } + + // create a local server that responds with the expectation + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Method == http.MethodPost, "invalid method") + runtimex.Assert(r.URL.Path == "/api/v1/check-in", "invalid URL path") + rawreqbody := runtimex.Try1(netxlite.ReadAllContext(r.Context(), r.Body)) + var gotrequest model.OOAPICheckInConfig + must.UnmarshalJSON(rawreqbody, &gotrequest) + diff := cmp.Diff(config, gotrequest) + runtimex.Assert(diff == "", "request mismatch:"+diff) + w.Write(must.MarshalJSON(expect)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // call the API + result, err := client.CheckIn(context.Background(), config) + + // we do not expect to see an error + if err != nil { + t.Fatal(err) + } + + // we expect to see exactly what the server sent + if diff := cmp.Diff(expect, result); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("reports an error when the connection is reset", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(testingx.HTTPHandlerReset()) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // call the API + result, err := client.CheckIn(context.Background(), config) + + // we do expect an error + if !errors.Is(err, netxlite.ECONNRESET) { + t.Fatal("unexpected error", err) + } + + // we expect to see a nil pointer + if result != nil { + t.Fatal("expected result to be nil") + } + }) + + t.Run("reports an error when the response is not JSON parsable", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{`)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // call the API + result, err := client.CheckIn(context.Background(), config) + + // we do expect an error + if err == nil || err.Error() != "unexpected end of JSON input" { + t.Fatal("unexpected error", err) + } + + // we expect to see a nil pointer + if result != nil { + t.Fatal("expected result to be nil") + } + }) } diff --git a/internal/probeservices/measurementmeta_test.go b/internal/probeservices/measurementmeta_test.go index 63ed736d73..c2774c9f4c 100644 --- a/internal/probeservices/measurementmeta_test.go +++ b/internal/probeservices/measurementmeta_test.go @@ -2,116 +2,223 @@ package probeservices import ( "context" - "encoding/json" "errors" "net/http" - "sync/atomic" + "net/url" "testing" + "time" - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/httpx" - "github.com/ooni/probe-cli/v3/internal/kvstore" + "github.com/google/go-cmp/cmp" + "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/netxlite" + "github.com/ooni/probe-cli/v3/internal/runtimex" + "github.com/ooni/probe-cli/v3/internal/testingx" ) -func TestGetMeasurementMetaWorkingAsIntended(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } +func TestGetMeasurementMeta(t *testing.T) { - client := Client{ - APIClientTemplate: httpx.APIClientTemplate{ - BaseURL: "https://api.ooni.io/", - HTTPClient: http.DefaultClient, - Logger: log.Log, - UserAgent: "miniooni/0.1.0-dev", - }, - KVStore: &kvstore.Memory{}, - LoginCalls: &atomic.Int64{}, - RegisterCalls: &atomic.Int64{}, - StateFile: NewStateFile(&kvstore.Memory{}), - } + // This is the configuration we use for both testing with the real API server + // and for testing with a local HTTP server for -short tests. config := model.OOAPIMeasurementMetaConfig{ ReportID: `20201209T052225Z_urlgetter_IT_30722_n1_E1VUhMz08SEkgYFU`, Full: true, Input: `https://www.example.org`, } - ctx := context.Background() - mmeta, err := client.GetMeasurementMeta(ctx, config) - if err != nil { - t.Fatal(err) - } - if mmeta.Anomaly != false { - t.Fatal("unexpected anomaly value") - } - if mmeta.CategoryCode != "" { - t.Fatal("unexpected category code value") - } - if mmeta.Confirmed != false { - t.Fatal("unexpected confirmed value") - } - if mmeta.Failure != true { - // TODO(bassosimone): this field seems wrong - t.Fatal("unexpected failure value") - } - if mmeta.Input == nil || *mmeta.Input != config.Input { - t.Fatal("unexpected input value") - } - if mmeta.MeasurementStartTime.String() != "2020-12-09 05:22:25 +0000 UTC" { - t.Fatal("unexpected measurement start time value") - } - if mmeta.ProbeASN != 30722 { - t.Fatal("unexpected probe asn value") - } - if mmeta.ProbeCC != "IT" { - t.Fatal("unexpected probe cc value") - } - if mmeta.ReportID != config.ReportID { - t.Fatal("unexpected report id value") - } - // TODO(bassosimone): we could better this check - var scores interface{} - if err := json.Unmarshal([]byte(mmeta.Scores), &scores); err != nil { - t.Fatalf("cannot parse scores value: %+v", err) - } - if mmeta.TestName != "urlgetter" { - t.Fatal("unexpected test name value") - } - if mmeta.TestStartTime.String() != "2020-12-09 05:22:25 +0000 UTC" { - t.Fatal("unexpected test start time value") - } - // TODO(bassosimone): we could better this check - var rawmeas interface{} - if err := json.Unmarshal([]byte(mmeta.RawMeasurement), &rawmeas); err != nil { - t.Fatalf("cannot parse raw measurement: %+v", err) - } -} -func TestGetMeasurementMetaWorkingWithCancelledContext(t *testing.T) { - client := Client{ - APIClientTemplate: httpx.APIClientTemplate{ - BaseURL: "https://api.ooni.io/", - HTTPClient: http.DefaultClient, - Logger: log.Log, - UserAgent: "miniooni/0.1.0-dev", - }, - KVStore: &kvstore.Memory{}, - LoginCalls: &atomic.Int64{}, - RegisterCalls: &atomic.Int64{}, - StateFile: NewStateFile(&kvstore.Memory{}), - } - config := model.OOAPIMeasurementMetaConfig{ - ReportID: `20201209T052225Z_urlgetter_IT_30722_n1_E1VUhMz08SEkgYFU`, - Full: true, - Input: `https://www.example.org`, - } - ctx, cancel := context.WithCancel(context.Background()) - cancel() // fail immediately - mmeta, err := client.GetMeasurementMeta(ctx, config) - if !errors.Is(err, context.Canceled) { - t.Fatalf("not the error we expected: %+v", err) - } - if mmeta != nil { - t.Fatal("we expected a nil mmeta here") + // This is what we expectMmeta the API to send us. We share this struct + // because we're using it for testing with the real backend as well as + // for testing with a local test server. + // + // The measurement is marked as "failed". This feels wrong but it may + // be that the fastpah marks all urlgetter measurements as failed. + // + // We are not including the raw measurement for simplicity (also, there are + // not tests for the ooni/backend API anyway, and so it's fine). + expectMmeta := &model.OOAPIMeasurementMeta{ + Anomaly: false, + CategoryCode: "", + Confirmed: false, + Failure: true, + Input: &config.Input, + MeasurementStartTime: time.Date(2020, 12, 9, 5, 22, 25, 0, time.UTC), + ProbeASN: 30722, + ProbeCC: "IT", + ReportID: "20201209T052225Z_urlgetter_IT_30722_n1_E1VUhMz08SEkgYFU", + Scores: `{"blocking_general":0.0,"blocking_global":0.0,"blocking_country":0.0,"blocking_isp":0.0,"blocking_local":0.0,"accuracy":0.0}`, + TestName: "urlgetter", + TestStartTime: time.Date(2020, 12, 9, 5, 22, 25, 0, time.UTC), + RawMeasurement: "", } + + // First, let's check whether we can get a response from the real OONI backend. + t.Run("is working as intended with the real backend", func(t *testing.T) { + if testing.Short() { + t.Skip("skip test in short mode") + } + + // construct a client and override the URL to be the production backend + // instead of the testing backend, so we have stable measurements + client := newclient() + client.BaseURL = "https://api.ooni.io/" + + // issue the API call proper + mmeta, err := client.GetMeasurementMeta(context.Background(), config) + + // we do not expect to see errors obviously + if err != nil { + t.Fatal(err) + } + + // the raw measurement must not be empty and must parse as JSON + // + // once we know that, clear it for the subsequent cmp.Diff + if mmeta.RawMeasurement == "" { + t.Fatal("mmeta.RawMeasurement should not be empty") + } + var rawmeas any + must.UnmarshalJSON([]byte(mmeta.RawMeasurement), &rawmeas) + mmeta.RawMeasurement = "" + + // compare with the expectation + if diff := cmp.Diff(expectMmeta, mmeta); diff != "" { + t.Fatal(diff) + } + }) + + // Now let's construct a test server that returns a valid response and try + // to communicate with such a test server successfully and with errors + + t.Run("is working as intended with a local test server", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Method == http.MethodGet, "invalid method") + runtimex.Assert(r.URL.Path == "/api/v1/measurement_meta", "invalid URL path") + w.Write(must.MarshalJSON(expectMmeta)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the API call proper + mmeta, err := client.GetMeasurementMeta(context.Background(), config) + + // we do not expect to see errors obviously + if err != nil { + t.Fatal(err) + } + + // compare with the expectation + if diff := cmp.Diff(expectMmeta, mmeta); diff != "" { + t.Fatal(diff) + } + }) + + t.Run("reports an error when the connection is reset", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(testingx.HTTPHandlerReset()) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the API call proper + mmeta, err := client.GetMeasurementMeta(context.Background(), config) + + // we do expect an error + if !errors.Is(err, netxlite.ECONNRESET) { + t.Fatal("unexpected error", err) + } + + // we expect mmeta to be nil + if mmeta != nil { + t.Fatal("expected nil meta") + } + }) + + t.Run("reports an error when the response is not JSON parsable", func(t *testing.T) { + // create quick and dirty server to serve the response + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{`)) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // override the HTTP client + client.HTTPClient = &mocks.HTTPClient{ + MockDo: func(req *http.Request) (*http.Response, error) { + URL := runtimex.Try1(url.Parse(srv.URL)) + req.URL.Scheme = URL.Scheme + req.URL.Host = URL.Host + return http.DefaultClient.Do(req) + }, + MockCloseIdleConnections: func() { + http.DefaultClient.CloseIdleConnections() + }, + } + + // issue the API call proper + mmeta, err := client.GetMeasurementMeta(context.Background(), config) + + // we do expect an error + if err == nil || err.Error() != "unexpected end of JSON input" { + t.Fatal("unexpected error", err) + } + + // we expect mmeta to be nil + if mmeta != nil { + t.Fatal("expected nil meta") + } + }) + + t.Run("correctly handles the case where the URL is unparseable", func(t *testing.T) { + // create a probeservices client + client := newclient() + + // override the URL to be unparseable + client.BaseURL = "\t\t\t" + + // issue the API call proper + mmeta, err := client.GetMeasurementMeta(context.Background(), config) + + // we do expect an error + if err == nil || err.Error() != `parse "\t\t\t": net/url: invalid control character in URL` { + t.Fatal("unexpected error", err) + } + + // we expect mmeta to be nil + if mmeta != nil { + t.Fatal("expected nil meta") + } + }) }