package hirl_test import ( "context" "errors" "io" "testing" "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/engine/experiment/hirl" "github.com/ooni/probe-cli/v3/internal/engine/mockable" "github.com/ooni/probe-cli/v3/internal/engine/netx" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/netxlite" "github.com/ooni/probe-cli/v3/internal/tracex" ) func TestNewExperimentMeasurer(t *testing.T) { measurer := hirl.NewExperimentMeasurer(hirl.Config{}) if measurer.ExperimentName() != "http_invalid_request_line" { t.Fatal("unexpected name") } if measurer.ExperimentVersion() != "0.2.0" { t.Fatal("unexpected version") } } func TestSuccess(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } measurer := hirl.NewExperimentMeasurer(hirl.Config{}) ctx := context.Background() sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if err != nil { t.Fatal(err) } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != len(tk.Received) { t.Fatal("FailureList and Received have different lengths") } if len(tk.Received) != len(tk.Sent) { t.Fatal("Received and Sent have different lengths") } if len(tk.Sent) != len(tk.TamperingList) { t.Fatal("Sent and TamperingList have different lengths") } for _, failure := range tk.FailureList { if failure != nil { t.Fatal(*failure) } } for idx, received := range tk.Received { if received.Value != tk.Sent[idx] { t.Fatal("mismatch between received and sent") } } for _, entry := range tk.TamperingList { if entry != false { t.Fatal("found entry with tampering") } } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } } func TestCancelledContext(t *testing.T) { measurer := hirl.NewExperimentMeasurer(hirl.Config{}) ctx, cancel := context.WithCancel(context.Background()) cancel() sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if err != nil { t.Fatal(err) } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != 5 { t.Fatal("unexpected FailureList length") } for _, failure := range tk.FailureList { if *failure != netxlite.FailureInterrupted { t.Fatal("unexpected failure") } } if len(tk.Received) != 5 { t.Fatal("unexpected Received length") } for _, entry := range tk.Received { if entry.Value != "" { t.Fatal("unexpected received entry") } } if len(tk.Sent) != 5 { t.Fatal("unexpected Sent length") } for _, entry := range tk.Sent { if entry != "" { t.Fatal("unexpected sent entry") } } if len(tk.TamperingList) != 5 { t.Fatal("unexpected TamperingList length") } for _, entry := range tk.TamperingList { if entry != false { t.Fatal("unexpected tampering entry") } } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } sk, err := measurer.GetSummaryKeys(measurement) if err != nil { t.Fatal(err) } if _, ok := sk.(hirl.SummaryKeys); !ok { t.Fatal("invalid type for summary keys") } } type FakeMethodSuccessful struct{} func (FakeMethodSuccessful) Name() string { return "success" } func (meth FakeMethodSuccessful) Run(ctx context.Context, config hirl.MethodConfig) { config.Out <- hirl.MethodResult{ Name: meth.Name(), Received: tracex.MaybeBinaryValue{Value: "antani"}, Sent: "antani", Tampering: false, } } type FakeMethodFailure struct{} func (FakeMethodFailure) Name() string { return "failure" } func (meth FakeMethodFailure) Run(ctx context.Context, config hirl.MethodConfig) { config.Out <- hirl.MethodResult{ Name: meth.Name(), Received: tracex.MaybeBinaryValue{Value: "antani"}, Sent: "melandri", Tampering: true, } } func TestWithFakeMethods(t *testing.T) { measurer := hirl.Measurer{ Config: hirl.Config{}, Methods: []hirl.Method{ FakeMethodSuccessful{}, FakeMethodFailure{}, FakeMethodSuccessful{}, }, } ctx := context.Background() sess := &mockable.Session{ MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "127.0.0.1", Type: "legacy", }}, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if err != nil { t.Fatal(err) } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != len(tk.Received) { t.Fatal("FailureList and Received have different lengths") } if len(tk.Received) != len(tk.Sent) { t.Fatal("Received and Sent have different lengths") } if len(tk.Sent) != len(tk.TamperingList) { t.Fatal("Sent and TamperingList have different lengths") } for _, failure := range tk.FailureList { if failure != nil { t.Fatal(*failure) } } for _, received := range tk.Received { if received.Value != "antani" { t.Fatal("unexpected received value") } } for _, sent := range tk.Sent { if sent != "antani" && sent != "melandri" { t.Fatal("unexpected sent value") } } var falses, trues int for _, entry := range tk.TamperingList { if entry { trues++ } else { falses++ } } if falses != 2 && trues != 1 { t.Fatal("not the right values in tampering list") } if tk.Tampering != true { t.Fatal("overall there is no tampering?!") } } func TestWithNoMethods(t *testing.T) { measurer := hirl.Measurer{ Config: hirl.Config{}, Methods: []hirl.Method{}, } ctx := context.Background() sess := &mockable.Session{ MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "127.0.0.1", Type: "legacy", }}, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if !errors.Is(err, hirl.ErrNoMeasurementMethod) { t.Fatal("not the error we expected") } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != 0 { t.Fatal("unexpected FailureList length") } if len(tk.Received) != 0 { t.Fatal("unexpected Received length") } if len(tk.Sent) != 0 { t.Fatal("unexpected Sent length") } if len(tk.TamperingList) != 0 { t.Fatal("unexpected TamperingList length") } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } } func TestNoHelpers(t *testing.T) { measurer := hirl.NewExperimentMeasurer(hirl.Config{}) ctx := context.Background() sess := &mockable.Session{} measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) { t.Fatal("not the error we expected") } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != 0 { t.Fatal("expected an empty FailureList") } if len(tk.FailureList) != len(tk.Received) { t.Fatal("FailureList and Received have different lengths") } if len(tk.Received) != len(tk.Sent) { t.Fatal("Received and Sent have different lengths") } if len(tk.Sent) != len(tk.TamperingList) { t.Fatal("Sent and TamperingList have different lengths") } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } } func TestNoActualHelperInList(t *testing.T) { measurer := hirl.NewExperimentMeasurer(hirl.Config{}) ctx := context.Background() sess := &mockable.Session{ MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": nil, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if !errors.Is(err, hirl.ErrNoAvailableTestHelpers) { t.Fatal("not the error we expected") } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != 0 { t.Fatal("expected an empty FailureList") } if len(tk.FailureList) != len(tk.Received) { t.Fatal("FailureList and Received have different lengths") } if len(tk.Received) != len(tk.Sent) { t.Fatal("Received and Sent have different lengths") } if len(tk.Sent) != len(tk.TamperingList) { t.Fatal("Sent and TamperingList have different lengths") } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } } func TestWrongTestHelperType(t *testing.T) { measurer := hirl.NewExperimentMeasurer(hirl.Config{}) ctx := context.Background() sess := &mockable.Session{ MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "127.0.0.1", Type: "antani", }}, }, } measurement := new(model.Measurement) callbacks := model.NewPrinterCallbacks(log.Log) err := measurer.Run(ctx, sess, measurement, callbacks) if !errors.Is(err, hirl.ErrInvalidHelperType) { t.Fatal("not the error we expected") } tk := measurement.TestKeys.(*hirl.TestKeys) if len(tk.FailureList) != 0 { t.Fatal("expected an empty FailureList") } if len(tk.FailureList) != len(tk.Received) { t.Fatal("FailureList and Received have different lengths") } if len(tk.Received) != len(tk.Sent) { t.Fatal("Received and Sent have different lengths") } if len(tk.Sent) != len(tk.TamperingList) { t.Fatal("Sent and TamperingList have different lengths") } if tk.Tampering != false { t.Fatal("overall there is tampering?!") } } func TestRunMethodDialFailure(t *testing.T) { sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } helpers, ok := sess.GetTestHelpersByName("tcp-echo") if len(helpers) < 1 || !ok { t.Fatal("cannot get helper") } expected := errors.New("mocked error") out := make(chan hirl.MethodResult) config := hirl.RunMethodConfig{ MethodConfig: hirl.MethodConfig{ Address: helpers[0].Address, Logger: log.Log, Out: out, }, Name: "random_invalid_version_number", NewDialer: func(config netx.Config) model.Dialer { return FakeDialer{Err: expected} }, RequestLine: "GET / HTTP/ABC", } go hirl.RunMethod(context.Background(), config) result := <-out if !errors.Is(result.Err, expected) { t.Fatal("not the error we expected") } if result.Name != "random_invalid_version_number" { t.Fatal("unexpected Name") } if result.Received.Value != "" { t.Fatal("unexpected Received.Value") } if result.Sent != "" { t.Fatal("unexpected Sent") } if result.Tampering != false { t.Fatal("unexpected Tampering") } } func TestRunMethodSetDeadlineFailure(t *testing.T) { sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } helpers, ok := sess.GetTestHelpersByName("tcp-echo") if len(helpers) < 1 || !ok { t.Fatal("cannot get helper") } expected := errors.New("mocked error") out := make(chan hirl.MethodResult) config := hirl.RunMethodConfig{ MethodConfig: hirl.MethodConfig{ Address: helpers[0].Address, Logger: log.Log, Out: out, }, Name: "random_invalid_version_number", NewDialer: func(config netx.Config) model.Dialer { return FakeDialer{Conn: &FakeConn{ SetDeadlineError: expected, }} }, RequestLine: "GET / HTTP/ABC", } go hirl.RunMethod(context.Background(), config) result := <-out if !errors.Is(result.Err, expected) { t.Fatal("not the error we expected") } if result.Name != "random_invalid_version_number" { t.Fatal("unexpected Name") } if result.Received.Value != "" { t.Fatal("unexpected Received.Value") } if result.Sent != "" { t.Fatal("unexpected Sent") } if result.Tampering != false { t.Fatal("unexpected Tampering") } } func TestRunMethodWriteFailure(t *testing.T) { sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } helpers, ok := sess.GetTestHelpersByName("tcp-echo") if len(helpers) < 1 || !ok { t.Fatal("cannot get helper") } expected := errors.New("mocked error") out := make(chan hirl.MethodResult) config := hirl.RunMethodConfig{ MethodConfig: hirl.MethodConfig{ Address: helpers[0].Address, Logger: log.Log, Out: out, }, Name: "random_invalid_version_number", NewDialer: func(config netx.Config) model.Dialer { return FakeDialer{Conn: &FakeConn{ WriteError: expected, }} }, RequestLine: "GET / HTTP/ABC", } go hirl.RunMethod(context.Background(), config) result := <-out if !errors.Is(result.Err, expected) { t.Fatal("not the error we expected") } if result.Name != "random_invalid_version_number" { t.Fatal("unexpected Name") } if result.Received.Value != "" { t.Fatal("unexpected Received.Value") } if result.Sent != "" { t.Fatal("unexpected Sent") } if result.Tampering != false { t.Fatal("unexpected Tampering") } } func TestRunMethodReadEOFWithWrongData(t *testing.T) { sess := &mockable.Session{ MockableLogger: log.Log, MockableTestHelpers: map[string][]model.OOAPIService{ "tcp-echo": {{ Address: "37.218.241.93", Type: "legacy", }}, }, } helpers, ok := sess.GetTestHelpersByName("tcp-echo") if len(helpers) < 1 || !ok { t.Fatal("cannot get helper") } out := make(chan hirl.MethodResult) config := hirl.RunMethodConfig{ MethodConfig: hirl.MethodConfig{ Address: helpers[0].Address, Logger: log.Log, Out: out, }, Name: "random_invalid_version_number", NewDialer: func(config netx.Config) model.Dialer { return FakeDialer{Conn: &FakeConn{ ReadData: []byte("0xdeadbeef"), }} }, RequestLine: "GET / HTTP/ABC", } go hirl.RunMethod(context.Background(), config) result := <-out if !errors.Is(result.Err, io.EOF) { t.Fatal("not the error we expected") } if result.Name != "random_invalid_version_number" { t.Fatal("unexpected Name") } if result.Received.Value != "0xdeadbeef" { t.Fatal("unexpected Received.Value") } if result.Sent != "GET / HTTP/ABC" { t.Fatal("unexpected Sent") } if result.Tampering != true { t.Fatal("unexpected Tampering") } } func TestSummaryKeysInvalidType(t *testing.T) { measurement := new(model.Measurement) m := &hirl.Measurer{} _, err := m.GetSummaryKeys(measurement) if err.Error() != "invalid test keys type" { t.Fatal("not the error we expected") } } func TestSummaryKeysFalse(t *testing.T) { measurement := &model.Measurement{TestKeys: &hirl.TestKeys{ Tampering: false, }} m := &hirl.Measurer{} osk, err := m.GetSummaryKeys(measurement) if err != nil { t.Fatal(err) } sk := osk.(hirl.SummaryKeys) if sk.IsAnomaly { t.Fatal("invalid isAnomaly") } } func TestSummaryKeysTrue(t *testing.T) { measurement := &model.Measurement{TestKeys: &hirl.TestKeys{ Tampering: true, }} m := &hirl.Measurer{} osk, err := m.GetSummaryKeys(measurement) if err != nil { t.Fatal(err) } sk := osk.(hirl.SummaryKeys) if sk.IsAnomaly == false { t.Fatal("invalid isAnomaly") } }