Skip to content

Commit

Permalink
feat(session): implement CallWebConnectivityTestHelper (#1581)
Browse files Browse the repository at this point in the history
  • Loading branch information
bassosimone committed May 2, 2024
1 parent 66ff45e commit 9a3abfc
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 0 deletions.
32 changes: 32 additions & 0 deletions internal/engine/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import (
"github.com/ooni/probe-cli/v3/internal/enginelocate"
"github.com/ooni/probe-cli/v3/internal/enginenetx"
"github.com/ooni/probe-cli/v3/internal/engineresolver"
"github.com/ooni/probe-cli/v3/internal/httpapi"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/ooapi"
"github.com/ooni/probe-cli/v3/internal/platform"
"github.com/ooni/probe-cli/v3/internal/probeservices"
"github.com/ooni/probe-cli/v3/internal/runtimex"
Expand Down Expand Up @@ -688,4 +690,34 @@ func (s *Session) MaybeLookupLocationContext(ctx context.Context) error {
return nil
}

// CallWebConnectivityTestHelper implements [model.EngineExperimentSession].
func (s *Session) CallWebConnectivityTestHelper(ctx context.Context,
creq *model.THRequest, testhelpers []model.OOAPIService) (*model.THResponse, int, error) {
// handle the case where there are no available web connectivity test helpers
if len(testhelpers) <= 0 {
return nil, 0, model.ErrNoAvailableTestHelpers
}

// initialize a sequence caller for invoking the THs in FIFO order
seqCaller := httpapi.NewSequenceCaller(
ooapi.NewDescriptorTH(creq),
httpapi.NewEndpointList(s.DefaultHTTPClient(), s.Logger(), s.UserAgent(), testhelpers...)...,
)

// issue the composed call proper and obtain a response and an index or an error
cresp, idx, err := seqCaller.Call(ctx)

// handle the case where all test helpers failed
if err != nil {
return nil, 0, err
}

// apply some sanity checks to the results
runtimex.Assert(idx >= 0 && idx < len(testhelpers), "idx out of bounds")
runtimex.Assert(cresp != nil, "out is nil")

// return the results to the web connectivity caller
return cresp, idx, nil
}

var _ model.ExperimentSession = &Session{}
303 changes: 303 additions & 0 deletions internal/engine/session_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ package engine
import (
"context"
"errors"
"net/http"
"net/url"
"sync"
"testing"
"time"

"github.com/apex/log"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/bytecounter"
"github.com/ooni/probe-cli/v3/internal/checkincache"
"github.com/ooni/probe-cli/v3/internal/enginelocate"
"github.com/ooni/probe-cli/v3/internal/enginenetx"
"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivity"
"github.com/ooni/probe-cli/v3/internal/experiment/webconnectivitylte"
"github.com/ooni/probe-cli/v3/internal/kvstore"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/ooni/probe-cli/v3/internal/registry"
"github.com/ooni/probe-cli/v3/internal/testingx"
"github.com/ooni/probe-cli/v3/internal/version"
)

func (s *Session) GetAvailableProbeServices() []model.OOAPIService {
Expand Down Expand Up @@ -405,3 +411,300 @@ func TestSessionNewExperimentBuilder(t *testing.T) {
}
})
}

// This function tests the [*Session.CallWebConnectivityTestHelper] method.
func TestSessionCallWebConnectivityTestHelper(t *testing.T) {
// We start with simple tests that exercise the basic functionality of the method
// without bothering with having more than one available test helper.

t.Run("when there are no available test helpers", func(t *testing.T) {
// create a new session only initializing the fields that
// are going to matter for running this specific test
sess := &Session{
network: enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
(&netxlite.Netx{}).NewStdlibResolver(model.DiscardLogger),
),
logger: model.DiscardLogger,
softwareName: "miniooni",
softwareVersion: version.Version,
}

// create a new background context
ctx := context.Background()

// create a fake request for the test helper
//
// note: no need to fill the request for this test case
creq := &model.THRequest{}

// invoke the API
cresp, idx, err := sess.CallWebConnectivityTestHelper(ctx, creq, nil)

// make sure we get the expected error
if !errors.Is(err, model.ErrNoAvailableTestHelpers) {
t.Fatal("unexpected error", err)
}

// make sure idx is zero
if idx != 0 {
t.Fatal("expected zero, got", idx)
}

// make sure cresp is nil
if cresp != nil {
t.Fatal("expected nil, got", cresp)
}
})

t.Run("when the call fails", func(t *testing.T) {
// create a local test server that always resets the connection
server := testingx.MustNewHTTPServer(testingx.HTTPHandlerReset())
defer server.Close()

// create a new session only initializing the fields that
// are going to matter for running this specific test
sess := &Session{
network: enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
(&netxlite.Netx{}).NewStdlibResolver(model.DiscardLogger),
),
logger: model.DiscardLogger,
softwareName: "miniooni",
softwareVersion: version.Version,
}

// create a new background context
ctx := context.Background()

// create a fake request for the test helper
//
// note: no need to fill the request for this test case
creq := &model.THRequest{}

// create the list of test helpers to use
testhelpers := []model.OOAPIService{{
Address: server.URL,
Type: "https",
Front: "",
}}

// invoke the API
cresp, idx, err := sess.CallWebConnectivityTestHelper(ctx, creq, testhelpers)

// make sure we get the expected error
if !errors.Is(err, netxlite.ECONNRESET) {
t.Fatal("unexpected error", err)
}

// make sure idx is zero
if idx != 0 {
t.Fatal("expected zero, got", idx)
}

// make sure cresp is nil
if cresp != nil {
t.Fatal("expected nil, got", cresp)
}
})

t.Run("when the call succeeds", func(t *testing.T) {
// create a local test server that always returns an ~empty response
server := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{}`))
}))
defer server.Close()

// create a new session only initializing the fields that
// are going to matter for running this specific test
sess := &Session{
network: enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
(&netxlite.Netx{}).NewStdlibResolver(model.DiscardLogger),
),
logger: model.DiscardLogger,
softwareName: "miniooni",
softwareVersion: version.Version,
}

// create a new background context
ctx := context.Background()

// create a fake request for the test helper
//
// note: no need to fill the request for this test case
creq := &model.THRequest{}

// create the list of test helpers to use
testhelpers := []model.OOAPIService{{
Address: server.URL,
Type: "https",
Front: "",
}}

// invoke the API
cresp, idx, err := sess.CallWebConnectivityTestHelper(ctx, creq, testhelpers)

// make sure we get the expected error
if err != nil {
t.Fatal("unexpected error", err)
}

// make sure idx is zero
if idx != 0 {
t.Fatal("expected zero, got", idx)
}

// make sure cresp is not nil
if cresp == nil {
t.Fatal("expected not nil, got", cresp)
}
})

t.Run("with two test helpers where the first one resets the connection and the second works", func(t *testing.T) {
// create a local test server1 that always resets the connection
server1 := testingx.MustNewHTTPServer(testingx.HTTPHandlerReset())
defer server1.Close()

// create a local test server2 that always returns an ~empty response
server2 := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{}`))
}))
defer server2.Close()

// create a new session only initializing the fields that
// are going to matter for running this specific test
sess := &Session{
network: enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
(&netxlite.Netx{}).NewStdlibResolver(model.DiscardLogger),
),
logger: model.DiscardLogger,
softwareName: "miniooni",
softwareVersion: version.Version,
}

// create a new background context
ctx := context.Background()

// create a fake request for the test helper
//
// note: no need to fill the request for this test case
creq := &model.THRequest{}

// create the list of test helpers to use
testhelpers := []model.OOAPIService{{
Address: server1.URL,
Type: "https",
Front: "",
}, {
Address: server2.URL,
Type: "https",
Front: "",
}}

// invoke the API
cresp, idx, err := sess.CallWebConnectivityTestHelper(ctx, creq, testhelpers)

// make sure we get the expected error
if err != nil {
t.Fatal("unexpected error", err)
}

// make sure idx is one
if idx != 1 {
t.Fatal("expected one, got", idx)
}

// make sure cresp is not nil
if cresp == nil {
t.Fatal("expected not nil, got", cresp)
}
})

t.Run("with two test helpers where the first one times out the connection and the second works", func(t *testing.T) {
// TODO(bassosimone): the utility of this test will become more obvious
// once we switch this specific test to using httpclientx.

// create a local test server1 that resets the connection after a ~long delay
server1 := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(10 * time.Second):
testingx.HTTPHandlerReset().ServeHTTP(w, r)
case <-r.Context().Done():
return
}
}))
defer server1.Close()

// create a local test server2 that always returns an ~empty response
server2 := testingx.MustNewHTTPServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{}`))
}))
defer server2.Close()

// create a new session only initializing the fields that
// are going to matter for running this specific test
sess := &Session{
network: enginenetx.NewNetwork(
bytecounter.New(),
&kvstore.Memory{},
model.DiscardLogger,
nil,
(&netxlite.Netx{}).NewStdlibResolver(model.DiscardLogger),
),
logger: model.DiscardLogger,
softwareName: "miniooni",
softwareVersion: version.Version,
}

// create a new background context
ctx := context.Background()

// create a fake request for the test helper
//
// note: no need to fill the request for this test case
creq := &model.THRequest{}

// create the list of test helpers to use
testhelpers := []model.OOAPIService{{
Address: server1.URL,
Type: "https",
Front: "",
}, {
Address: server2.URL,
Type: "https",
Front: "",
}}

// invoke the API
cresp, idx, err := sess.CallWebConnectivityTestHelper(ctx, creq, testhelpers)

// make sure we get the expected error
if err != nil {
t.Fatal("unexpected error", err)
}

// make sure idx is one
if idx != 1 {
t.Fatal("expected one, got", idx)
}

// make sure cresp is not nil
if cresp == nil {
t.Fatal("expected not nil, got", cresp)
}
})
}
9 changes: 9 additions & 0 deletions internal/legacy/mockable/mockable.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (
//
// Deprecated: use ./internal/model/mocks.Session instead.
type Session struct {
MocakbleCallWCTHResp *model.THResponse
MockableCallWCTHCount int
MockableCallWCTHErr error
MockableTestHelpers map[string][]model.OOAPIService
MockableHTTPClient model.HTTPClient
MockableLogger model.Logger
Expand All @@ -38,6 +41,12 @@ type Session struct {
MockableUserAgent string
}

// CallWebConnectivityTestHelper implements [model.EngineExperimentSession].
func (sess *Session) CallWebConnectivityTestHelper(
ctx context.Context, request *model.THRequest, ths []model.OOAPIService) (*model.THResponse, int, error) {
return sess.MocakbleCallWCTHResp, sess.MockableCallWCTHCount, sess.MockableCallWCTHErr
}

// GetTestHelpersByName implements ExperimentSession.GetTestHelpersByName
func (sess *Session) GetTestHelpersByName(name string) ([]model.OOAPIService, bool) {
services, okay := sess.MockableTestHelpers[name]
Expand Down
Loading

0 comments on commit 9a3abfc

Please sign in to comment.