ooni-probe-cli/internal/engine/experiment_integration_test.go
Simone Basso 8a0c062844
feat: clearly indicate which resolver we're using (#885)
See what we documented at https://github.com/ooni/spec/pull/257

Reference issue: https://github.com/ooni/probe/issues/2238

See also the related ooni/spec PR: https://github.com/ooni/spec/pull/257

See also https://github.com/ooni/probe/issues/2237

While there, bump webconnectivity@v0.5 version because this change
has an impact onto the generated data format.

The drop in coverage is unavoidable because we've written some
tests for `measurex` to ensure we deal with DNS resolvers and transport
names correctly depending on the splitting policy we use.

(However, `measurex` is only used for the `tor` experiment and, per
the step-by-step design document, new experiments should use
`measurexlite` instead, so this is hopefully fine(TM).)

While there, fix a broken integration test that does not run in `-short` mode.
2022-08-27 15:47:48 +02:00

490 lines
12 KiB
Go

package engine
import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestCreateAll(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
for _, name := range AllExperiments() {
builder, err := sess.NewExperimentBuilder(name)
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment()
good := (exp.Name() == name)
if !good {
// We have introduced the concept of versioned experiments in
// https://github.com/ooni/probe-cli/pull/882. This works like
// in brew: we append @vX.Y to the experiment name. So, here
// we're stripping the version specification and retry.
index := strings.Index(name, "@")
if index >= 0 {
name = name[:index]
if good := (exp.Name() == name); good {
continue
}
}
t.Fatal("unexpected experiment name", exp.Name(), name)
}
}
}
func TestRunDASH(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("dash")
if err != nil {
t.Fatal(err)
}
if !builder.Interruptible() {
t.Fatal("dash not marked as interruptible")
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestRunExample(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestRunNdt7(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("ndt7")
if err != nil {
t.Fatal(err)
}
if !builder.Interruptible() {
t.Fatal("ndt7 not marked as interruptible")
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestRunPsiphon(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("psiphon")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestRunSNIBlocking(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("sni_blocking")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "kernel.org")
}
func TestRunTelegram(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("telegram")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestRunTor(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("tor")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func TestNeedsInput(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("web_connectivity")
if err != nil {
t.Fatal(err)
}
if builder.InputPolicy() != model.InputOrQueryBackend {
t.Fatal("web_connectivity certainly needs input")
}
}
func TestSetCallbacks(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
if err := builder.SetOptionAny("SleepTime", 0); err != nil {
t.Fatal(err)
}
register := &registerCallbacksCalled{}
builder.SetCallbacks(register)
if _, err := builder.NewExperiment().MeasureWithContext(context.Background(), ""); err != nil {
t.Fatal(err)
}
if register.onProgressCalled == false {
t.Fatal("OnProgress not called")
}
}
type registerCallbacksCalled struct {
onProgressCalled bool
}
func (c *registerCallbacksCalled) OnProgress(percentage float64, message string) {
c.onProgressCalled = true
}
func TestCreateInvalidExperiment(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("antani")
if err == nil {
t.Fatal("expected an error here")
}
if builder != nil {
t.Fatal("expected a nil builder here")
}
}
func TestMeasurementFailure(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
if err := builder.SetOptionAny("ReturnError", true); err != nil {
t.Fatal(err)
}
measurement, err := builder.NewExperiment().MeasureWithContext(context.Background(), "")
if err == nil {
t.Fatal("expected an error here")
}
if err.Error() != "mocked error" {
t.Fatal("unexpected error type")
}
if measurement != nil {
t.Fatal("expected nil measurement here")
}
}
func TestRunHHFM(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("http_header_field_manipulation")
if err != nil {
t.Fatal(err)
}
runexperimentflow(t, builder.NewExperiment(), "")
}
func runexperimentflow(t *testing.T, experiment model.Experiment, input string) {
ctx := context.Background()
err := experiment.OpenReportContext(ctx)
if err != nil {
t.Fatal(err)
}
if experiment.ReportID() == "" {
t.Fatal("reportID should not be empty here")
}
measurement, err := experiment.MeasureWithContext(ctx, input)
if err != nil {
t.Fatal(err)
}
measurement.AddAnnotations(map[string]string{
"probe-engine-ci": "yes",
})
data, err := json.Marshal(measurement)
if err != nil {
t.Fatal(err)
}
if data == nil {
t.Fatal("data is nil")
}
err = experiment.SubmitAndUpdateMeasurementContext(ctx, measurement)
if err != nil {
t.Fatal(err)
}
err = experiment.SaveMeasurement(measurement, "/tmp/experiment.jsonl")
if err != nil {
t.Fatal(err)
}
if experiment.KibiBytesSent() <= 0 {
t.Fatal("no data sent?!")
}
if experiment.KibiBytesReceived() <= 0 {
t.Fatal("no data received?!")
}
if _, err := experiment.GetSummaryKeys(measurement); err != nil {
t.Fatal(err)
}
}
func TestSaveMeasurementErrors(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment().(*experiment)
dirname, err := ioutil.TempDir("", "ooniprobe-engine-save-measurement")
if err != nil {
t.Fatal(err)
}
filename := filepath.Join(dirname, "report.jsonl")
m := new(model.Measurement)
err = exp.saveMeasurement(
m, filename, func(v interface{}) ([]byte, error) {
return nil, errors.New("mocked error")
}, os.OpenFile, func(fp *os.File, b []byte) (int, error) {
return fp.Write(b)
},
)
if err == nil {
t.Fatal("expected an error here")
}
err = exp.saveMeasurement(
m, filename, json.Marshal,
func(name string, flag int, perm os.FileMode) (*os.File, error) {
return nil, errors.New("mocked error")
}, func(fp *os.File, b []byte) (int, error) {
return fp.Write(b)
},
)
if err == nil {
t.Fatal("expected an error here")
}
err = exp.saveMeasurement(
m, filename, json.Marshal, os.OpenFile,
func(fp *os.File, b []byte) (int, error) {
return 0, errors.New("mocked error")
},
)
if err == nil {
t.Fatal("expected an error here")
}
}
func TestOpenReportIdempotent(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment()
if exp.ReportID() != "" {
t.Fatal("unexpected initial report ID")
}
ctx := context.Background()
if err := exp.SubmitAndUpdateMeasurementContext(ctx, &model.Measurement{}); err == nil {
t.Fatal("we should not be able to submit before OpenReport")
}
err = exp.OpenReportContext(ctx)
if err != nil {
t.Fatal(err)
}
rid := exp.ReportID()
if rid == "" {
t.Fatal("invalid report ID")
}
err = exp.OpenReportContext(ctx)
if err != nil {
t.Fatal(err)
}
if rid != exp.ReportID() {
t.Fatal("OpenReport is not idempotent")
}
}
func TestOpenReportFailure(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
server := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
},
))
defer server.Close()
sess := newSessionForTestingNoBackendsLookup(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment().(*experiment)
exp.session.selectedProbeService = &model.OOAPIService{
Address: server.URL,
Type: "https",
}
err = exp.OpenReportContext(context.Background())
if !strings.HasPrefix(err.Error(), "httpx: request failed") {
t.Fatal("not the error we expected")
}
}
func TestOpenReportNewClientFailure(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoBackendsLookup(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment().(*experiment)
exp.session.selectedProbeService = &model.OOAPIService{
Address: "antani:///",
Type: "antani",
}
err = exp.OpenReportContext(context.Background())
if err.Error() != "probe services: unsupported endpoint type" {
t.Fatal(err)
}
}
func TestSubmitAndUpdateMeasurementWithClosedReport(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTesting(t)
defer sess.Close()
builder, err := sess.NewExperimentBuilder("example")
if err != nil {
t.Fatal(err)
}
exp := builder.NewExperiment()
m := new(model.Measurement)
err = exp.SubmitAndUpdateMeasurementContext(context.Background(), m)
if err == nil {
t.Fatal("expected an error here")
}
}
func TestMeasureLookupLocationFailure(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
exp := newExperiment(sess, new(antaniMeasurer))
ctx, cancel := context.WithCancel(context.Background())
cancel() // so we fail immediately
if _, err := exp.MeasureWithContext(ctx, "xx"); err == nil {
t.Fatal("expected an error here")
}
}
func TestOpenReportNonHTTPS(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
sess := newSessionForTestingNoLookups(t)
defer sess.Close()
sess.availableProbeServices = []model.OOAPIService{
{
Address: "antani",
Type: "mascetti",
},
}
exp := newExperiment(sess, new(antaniMeasurer))
if err := exp.OpenReportContext(context.Background()); err == nil {
t.Fatal("expected an error here")
}
}
type antaniMeasurer struct{}
func (am *antaniMeasurer) ExperimentName() string {
return "antani"
}
func (am *antaniMeasurer) ExperimentVersion() string {
return "0.1.1"
}
func (am *antaniMeasurer) Run(
ctx context.Context, sess model.ExperimentSession,
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
) error {
return nil
}
func (am *antaniMeasurer) GetSummaryKeys(m *model.Measurement) (interface{}, error) {
return struct {
Failure *string `json:"failure"`
}{}, nil
}