diff --git a/internal/cmd/apitool/main.go b/internal/cmd/apitool/main.go index 0b24e55f3..fc4ffdbf9 100644 --- a/internal/cmd/apitool/main.go +++ b/internal/cmd/apitool/main.go @@ -17,7 +17,6 @@ import ( "sync/atomic" "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/httpx" "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" @@ -32,16 +31,14 @@ func newclient() probeservices.Client { txp := netx.NewHTTPTransportStdlib(log.Log) ua := fmt.Sprintf("apitool/%s ooniprobe-engine/%s", version.Version, version.Version) return probeservices.Client{ - APIClientTemplate: httpx.APIClientTemplate{ - BaseURL: *backend, - HTTPClient: &http.Client{Transport: txp}, - Logger: log.Log, - UserAgent: ua, - }, + BaseURL: *backend, + HTTPClient: &http.Client{Transport: txp}, KVStore: &kvstore.Memory{}, + Logger: log.Log, LoginCalls: &atomic.Int64{}, RegisterCalls: &atomic.Int64{}, StateFile: probeservices.NewStateFile(&kvstore.Memory{}), + UserAgent: ua, } } diff --git a/internal/probeservices/bouncer.go b/internal/probeservices/bouncer.go index 4320f83ef..b518a1b73 100644 --- a/internal/probeservices/bouncer.go +++ b/internal/probeservices/bouncer.go @@ -23,6 +23,7 @@ func (c *Client) GetTestHelpers(ctx context.Context) (map[string][]model.OOAPISe // get the response return httpclientx.GetJSON[map[string][]model.OOAPIService](ctx, URL, &httpclientx.Config{ Client: c.HTTPClient, + Host: c.Host, Logger: c.Logger, UserAgent: c.UserAgent, }) diff --git a/internal/probeservices/bouncer_test.go b/internal/probeservices/bouncer_test.go index 712e1aa60..63dff4d99 100644 --- a/internal/probeservices/bouncer_test.go +++ b/internal/probeservices/bouncer_test.go @@ -91,6 +91,57 @@ func TestGetTestHelpers(t *testing.T) { } }) + t.Run("we can use cloudfronting", 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.Host == "www.cloudfront.com", "invalid r.Host") + 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() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // 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()) diff --git a/internal/probeservices/checkin_test.go b/internal/probeservices/checkin_test.go index 87b7571b8..9a52d1719 100644 --- a/internal/probeservices/checkin_test.go +++ b/internal/probeservices/checkin_test.go @@ -141,6 +141,81 @@ func TestCheckIn(t *testing.T) { } }) + t.Run("we can use cloudfronting", 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.Host == "www.cloudfront.com", "invalid r.Host") + 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() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // 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()) diff --git a/internal/probeservices/collector.go b/internal/probeservices/collector.go index e9dd3b24b..a00e80b8c 100644 --- a/internal/probeservices/collector.go +++ b/internal/probeservices/collector.go @@ -69,6 +69,7 @@ func (c Client) OpenReport(ctx context.Context, rt model.OOAPIReportTemplate) (R cor, err := httpclientx.PostJSON[model.OOAPIReportTemplate, *model.OOAPICollectorOpenResponse]( ctx, URL, rt, &httpclientx.Config{ Client: c.HTTPClient, + Host: c.Host, Logger: c.Logger, UserAgent: c.UserAgent, }, @@ -118,6 +119,7 @@ func (r reportChan) SubmitMeasurement(ctx context.Context, m *model.Measurement) model.OOAPICollectorUpdateRequest, *model.OOAPICollectorUpdateResponse]( ctx, URL, apiReq, &httpclientx.Config{ Client: r.client.HTTPClient, + Host: r.client.Host, Logger: r.client.Logger, UserAgent: r.client.UserAgent, }, diff --git a/internal/probeservices/collector_test.go b/internal/probeservices/collector_test.go index 03f0e356a..ff814f908 100644 --- a/internal/probeservices/collector_test.go +++ b/internal/probeservices/collector_test.go @@ -179,6 +179,69 @@ func TestReportLifecycle(t *testing.T) { } }) + t.Run("we can use cloudfronting", func(t *testing.T) { + // create state for emulating the OONI backend + state := &testingx.OONICollector{} + + // expose the state via HTTP + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Host == "www.cloudfront.com", "invalid r.Host") + state.ServeHTTP(w, r) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // override the HTTP client so we speak with our local server rather than the true backend + 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() + }, + } + + // create the report template used for testing + template := newReportTemplateForTesting() + + // open the report + report, err := client.OpenReport(context.Background(), template) + + // we expect to be able to open the report + if err != nil { + t.Fatal(err) + } + + // make a measurement out of the report template + measurement := makeMeasurement(template, report.ReportID()) + + // make sure we can submit this measurement within the report, which we really + // expect to succeed since we created the measurement from the template + if report.CanSubmit(&measurement) != true { + t.Fatal("report should be able to submit this measurement") + } + + // attempt to submit the measurement to the backend, which should succeed + // since we've just opened a report for it + if err = report.SubmitMeasurement(context.Background(), &measurement); err != nil { + t.Fatal(err) + } + + // additionally make sure we edited the measurement report ID to + // contain the correct report ID used to submit + if measurement.ReportID != report.ReportID() { + t.Fatal("report ID mismatch") + } + }) + t.Run("opening a report fails with 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()) diff --git a/internal/probeservices/login.go b/internal/probeservices/login.go index 5edfc6d23..6b31e92f3 100644 --- a/internal/probeservices/login.go +++ b/internal/probeservices/login.go @@ -28,6 +28,7 @@ func (c Client) MaybeLogin(ctx context.Context) error { auth, err := httpclientx.PostJSON[*model.OOAPILoginCredentials, *model.OOAPILoginAuth]( ctx, URL, creds, &httpclientx.Config{ Client: c.HTTPClient, + Host: c.Host, Logger: model.DiscardLogger, UserAgent: c.UserAgent, }, diff --git a/internal/probeservices/login_test.go b/internal/probeservices/login_test.go index d5ec74d3b..444fa8d83 100644 --- a/internal/probeservices/login_test.go +++ b/internal/probeservices/login_test.go @@ -97,6 +97,60 @@ func TestMaybeLogin(t *testing.T) { } }) + t.Run("we can use cloudfronting", func(t *testing.T) { + // create state for emulating the OONI backend + state := &testingx.OONIBackendWithLoginFlow{} + mux := state.NewMux() + + // expose the state via HTTP + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Host == "www.cloudfront.com", "invalid r.Host") + mux.ServeHTTP(w, r) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // override the HTTP client so we speak with our local server rather than the true backend + 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() + }, + } + + // we need to register first because we don't have state yet + if err := client.MaybeRegister(context.Background(), MetadataFixture()); err != nil { + t.Fatal(err) + } + + // now we try to login and get a token + if err := client.MaybeLogin(context.Background()); err != nil { + t.Fatal(err) + } + + // do this again, and later on we'll verify that we + // did actually issue just a single login call + if err := client.MaybeLogin(context.Background()); err != nil { + t.Fatal(err) + } + + // make sure we did call login just once: the second call + // should not invoke login because we have good state + if client.LoginCalls.Load() != 1 { + t.Fatal("called login API too many times") + } + }) + 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()) diff --git a/internal/probeservices/measurementmeta.go b/internal/probeservices/measurementmeta.go index a2646657e..bef574391 100644 --- a/internal/probeservices/measurementmeta.go +++ b/internal/probeservices/measurementmeta.go @@ -35,6 +35,7 @@ func (c Client) GetMeasurementMeta( // get the response return httpclientx.GetJSON[*model.OOAPIMeasurementMeta](ctx, URL, &httpclientx.Config{ Client: c.HTTPClient, + Host: c.Host, Logger: c.Logger, UserAgent: c.UserAgent, }) diff --git a/internal/probeservices/measurementmeta_test.go b/internal/probeservices/measurementmeta_test.go index c2774c9f4..dda5df401 100644 --- a/internal/probeservices/measurementmeta_test.go +++ b/internal/probeservices/measurementmeta_test.go @@ -129,6 +129,49 @@ func TestGetMeasurementMeta(t *testing.T) { } }) + t.Run("we can use cloudfronting", 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.Host == "www.cloudfront.com", "invalid r.Host") + 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() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // 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()) diff --git a/internal/probeservices/probeservices.go b/internal/probeservices/probeservices.go index 2aca72315..564c6b53d 100644 --- a/internal/probeservices/probeservices.go +++ b/internal/probeservices/probeservices.go @@ -29,7 +29,6 @@ import ( "sync/atomic" "github.com/ooni/probe-cli/v3/internal/httpapi" - "github.com/ooni/probe-cli/v3/internal/httpx" "github.com/ooni/probe-cli/v3/internal/model" ) @@ -65,11 +64,15 @@ type Session interface { // Client is a client for the OONI probe services API. type Client struct { - httpx.APIClientTemplate + BaseURL string + HTTPClient model.HTTPClient + Host string KVStore model.KeyValueStore + Logger model.Logger LoginCalls *atomic.Int64 RegisterCalls *atomic.Int64 StateFile StateFile + UserAgent string } // GetCredsAndAuth is an utility function that returns the credentials with @@ -92,16 +95,15 @@ func (c Client) GetCredsAndAuth() (*model.OOAPILoginCredentials, *model.OOAPILog // function fails, e.g., we don't support the specified endpoint. func NewClient(sess Session, endpoint model.OOAPIService) (*Client, error) { client := &Client{ - APIClientTemplate: httpx.APIClientTemplate{ - BaseURL: endpoint.Address, - HTTPClient: sess.DefaultHTTPClient(), - Logger: sess.Logger(), - UserAgent: sess.UserAgent(), - }, + BaseURL: endpoint.Address, + HTTPClient: sess.DefaultHTTPClient(), + Host: "", KVStore: sess.KeyValueStore(), + Logger: sess.Logger(), LoginCalls: &atomic.Int64{}, RegisterCalls: &atomic.Int64{}, StateFile: NewStateFile(sess.KeyValueStore()), + UserAgent: sess.UserAgent(), } switch endpoint.Type { case "https": @@ -117,7 +119,7 @@ func NewClient(sess Session, endpoint model.OOAPIService) (*Client, error) { if URL.Scheme != "https" || URL.Host != URL.Hostname() { return nil, ErrUnsupportedCloudFrontAddress } - client.APIClientTemplate.Host = URL.Hostname() + client.Host = URL.Hostname() URL.Host = endpoint.Front client.BaseURL = URL.String() if _, err := url.Parse(client.BaseURL); err != nil { diff --git a/internal/probeservices/psiphon.go b/internal/probeservices/psiphon.go index 1be97de76..e1e616d24 100644 --- a/internal/probeservices/psiphon.go +++ b/internal/probeservices/psiphon.go @@ -32,6 +32,7 @@ func (c Client) FetchPsiphonConfig(ctx context.Context) ([]byte, error) { return httpclientx.GetRaw(ctx, URL, &httpclientx.Config{ Authorization: s, Client: c.HTTPClient, + Host: c.Host, Logger: model.DiscardLogger, UserAgent: c.UserAgent, }) diff --git a/internal/probeservices/psiphon_test.go b/internal/probeservices/psiphon_test.go index 86a6a0b10..00acbcbd9 100644 --- a/internal/probeservices/psiphon_test.go +++ b/internal/probeservices/psiphon_test.go @@ -100,6 +100,55 @@ func TestFetchPsiphonConfig(t *testing.T) { } }) + t.Run("we can use cloudfronting", func(t *testing.T) { + // create state for emulating the OONI backend + state := &testingx.OONIBackendWithLoginFlow{} + mux := state.NewMux() + + // make sure we return something that is JSON parseable + state.SetPsiphonConfig([]byte(`{}`)) + + // expose the state via HTTP + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Host == "www.cloudfront.com", "invalid r.Host") + mux.ServeHTTP(w, r) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // override the HTTP client so we speak with our local server rather than the true backend + 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() + }, + } + + // then we can try to fetch the config + data, err := psiphonflow(t, client) + + // we do not expect an error here + if err != nil { + t.Fatal(err) + } + + // the config is bytes but we want to make sure we can parse it + var config interface{} + if err := json.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + }) + 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()) diff --git a/internal/probeservices/register.go b/internal/probeservices/register.go index 5a4eba128..3ec4f65d1 100644 --- a/internal/probeservices/register.go +++ b/internal/probeservices/register.go @@ -40,6 +40,7 @@ func (c Client) MaybeRegister(ctx context.Context, metadata model.OOAPIProbeMeta resp, err := httpclientx.PostJSON[*model.OOAPIRegisterRequest, *model.OOAPIRegisterResponse]( ctx, URL, req, &httpclientx.Config{ Client: c.HTTPClient, + Host: c.Host, Logger: model.DiscardLogger, UserAgent: c.UserAgent, }, diff --git a/internal/probeservices/register_test.go b/internal/probeservices/register_test.go index f31dfdea8..319721c0d 100644 --- a/internal/probeservices/register_test.go +++ b/internal/probeservices/register_test.go @@ -83,6 +83,53 @@ func TestMaybeRegister(t *testing.T) { } }) + t.Run("we can use cloudfronting", func(t *testing.T) { + // create state for emulating the OONI backend + state := &testingx.OONIBackendWithLoginFlow{} + mux := state.NewMux() + + // expose the state via HTTP + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Host == "www.cloudfront.com", "invalid r.Host") + mux.ServeHTTP(w, r) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // override the HTTP client so we speak with our local server rather than the true backend + 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() + }, + } + + // attempt to register once + if err := client.MaybeRegister(context.Background(), MetadataFixture()); err != nil { + t.Fatal(err) + } + + // try again (we want to make sure it's idempotent once we've registered) + if err := client.MaybeRegister(context.Background(), MetadataFixture()); err != nil { + t.Fatal(err) + } + + // make sure we indeed only called it once + if client.RegisterCalls.Load() != 1 { + t.Fatal("called register API too many times") + } + }) + 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()) diff --git a/internal/probeservices/tor.go b/internal/probeservices/tor.go index 79c4d91a8..3d1eb4b6c 100644 --- a/internal/probeservices/tor.go +++ b/internal/probeservices/tor.go @@ -37,6 +37,7 @@ func (c Client) FetchTorTargets(ctx context.Context, cc string) (map[string]mode return httpclientx.GetJSON[map[string]model.OOAPITorTarget](ctx, URL, &httpclientx.Config{ Authorization: s, Client: c.HTTPClient, + Host: c.Host, Logger: model.DiscardLogger, UserAgent: c.UserAgent, }) diff --git a/internal/probeservices/tor_test.go b/internal/probeservices/tor_test.go index f6a0e9f00..bb91771b2 100644 --- a/internal/probeservices/tor_test.go +++ b/internal/probeservices/tor_test.go @@ -101,6 +101,54 @@ func TestFetchTorTargets(t *testing.T) { } }) + t.Run("we can use cloudfronting", func(t *testing.T) { + // create state for emulating the OONI backend + state := &testingx.OONIBackendWithLoginFlow{} + mux := state.NewMux() + + // make sure we return something that is JSON parseable and non-zero-length + state.SetTorTargets([]byte(`{"foo": {}}`)) + + // expose the state via HTTP + srv := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + runtimex.Assert(r.Host == "www.cloudfront.com", "invalid r.Host") + mux.ServeHTTP(w, r) + })) + defer srv.Close() + + // create a probeservices client + client := newclient() + + // make sure we're using cloudfronting + client.Host = "www.cloudfront.com" + + // override the HTTP client so we speak with our local server rather than the true backend + 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() + }, + } + + // run the tor flow + targets, err := torflow(t, client) + + // we do not expect an error here + if err != nil { + t.Fatal(err) + } + + // we expect non-zero length targets + if len(targets) <= 0 { + t.Fatal("expected non-zero-length targets") + } + }) + 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())