package oonimkall_test import ( "context" "encoding/json" "errors" "fmt" "os" "runtime" "strings" "testing" "time" "github.com/ooni/probe-cli/v3/internal/engine/geolocate" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/pkg/oonimkall" ) func NewSessionForTestingWithAssetsDir(assetsDir string) (*oonimkall.Session, error) { return oonimkall.NewSession(&oonimkall.SessionConfig{ AssetsDir: assetsDir, ProbeServicesURL: "https://ams-pg-test.ooni.org/", SoftwareName: "oonimkall-test", SoftwareVersion: "0.1.0", StateDir: "../testdata/oonimkall/state", TempDir: "../testdata/", }) } func NewSessionForTesting() (*oonimkall.Session, error) { return NewSessionForTestingWithAssetsDir("../testdata/oonimkall/assets") } func TestNewSessionWithInvalidStateDir(t *testing.T) { sess, err := oonimkall.NewSession(&oonimkall.SessionConfig{ StateDir: "", }) if err == nil || !strings.HasSuffix(err.Error(), "no such file or directory") { t.Fatal("not the error we expected") } if sess != nil { t.Fatal("expected a nil Session here") } } func ReduceErrorForGeolocate(err error) error { if err == nil { return errors.New("we expected an error here") } if errors.Is(err, context.Canceled) { return nil // when we have not downloaded the resources yet } if !errors.Is(err, geolocate.ErrAllIPLookuppersFailed) { return nil // otherwise } return fmt.Errorf("not the error we expected: %w", err) } func TestGeolocateWithCancelledContext(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() ctx.Cancel() // cause immediate failure location, err := sess.Geolocate(ctx) if err := ReduceErrorForGeolocate(err); err != nil { t.Fatal(err) } if location != nil { t.Fatal("expected nil location here") } } func TestGeolocateGood(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() location, err := sess.Geolocate(ctx) if err != nil { t.Fatal(err) } if location.ASN == "" { t.Fatal("location.ASN is empty") } if location.Country == "" { t.Fatal("location.Country is empty") } if location.IP == "" { t.Fatal("location.IP is empty") } if location.Org == "" { t.Fatal("location.Org is empty") } } func ReduceErrorForSubmitter(err error) error { if err == nil { return errors.New("we expected an error here") } if errors.Is(err, context.Canceled) { return nil // when we have not downloaded the resources yet } if err.Error() == "all available probe services failed" { return nil // otherwise } return fmt.Errorf("not the error we expected: %w", err) } func TestSubmitWithCancelledContext(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() ctx.Cancel() // cause immediate failure result, err := sess.Submit(ctx, "{}") if err := ReduceErrorForSubmitter(err); err != nil { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("expected nil result here") } } func TestSubmitWithInvalidJSON(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() result, err := sess.Submit(ctx, "{") if err == nil || err.Error() != "unexpected end of JSON input" { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("expected nil result here") } } func DoSubmission(ctx *oonimkall.Context, sess *oonimkall.Session) error { inputm := model.Measurement{ DataFormatVersion: "0.2.0", MeasurementStartTime: "2019-10-28 12:51:07", MeasurementRuntime: 1.71, ProbeASN: "AS30722", ProbeCC: "IT", ProbeIP: "127.0.0.1", ReportID: "", ResolverIP: "172.217.33.129", SoftwareName: "miniooni", SoftwareVersion: "0.1.0-dev", TestKeys: map[string]bool{"success": true}, TestName: "example", TestVersion: "0.1.0", } inputd, err := json.Marshal(inputm) if err != nil { return err } result, err := sess.Submit(ctx, string(inputd)) if err != nil { return fmt.Errorf("session_test.go: submit failed: %w", err) } if result.UpdatedMeasurement == "" { return errors.New("expected non empty measurement") } if result.UpdatedReportID == "" { return errors.New("expected non empty report ID") } var outputm model.Measurement return json.Unmarshal([]byte(result.UpdatedMeasurement), &outputm) } func TestSubmitMeasurementGood(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() if err := DoSubmission(ctx, sess); err != nil { t.Fatal(err) } } func TestSubmitCancelContextAfterFirstSubmission(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() if err := DoSubmission(ctx, sess); err != nil { t.Fatal(err) } ctx.Cancel() // fail second submission err = DoSubmission(ctx, sess) if err == nil || !strings.HasPrefix(err.Error(), "session_test.go: submit failed") { t.Fatalf("not the error we expected: %+v", err) } if !errors.Is(err, context.Canceled) { t.Fatalf("not the error we expected: %+v", err) } } func TestCheckInSuccess(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() config := oonimkall.CheckInConfig{ Charging: true, OnWiFi: true, Platform: "android", RunType: string(model.RunTypeTimed), SoftwareName: "ooniprobe-android", SoftwareVersion: "2.7.1", WebConnectivity: &oonimkall.CheckInConfigWebConnectivity{}, } config.WebConnectivity.Add("NEWS") config.WebConnectivity.Add("CULTR") result, err := sess.CheckIn(ctx, &config) if err != nil { t.Fatalf("unexpected error: %+v", err) } if result == nil || result.WebConnectivity == nil { t.Fatal("got nil result or WebConnectivity") } if len(result.WebConnectivity.URLs) < 1 { t.Fatal("unexpected number of URLs") } if result.WebConnectivity.ReportID == "" { t.Fatal("got empty report ID") } siz := result.WebConnectivity.Size() if siz <= 0 { t.Fatal("unexpected number of URLs") } for idx := int64(0); idx < siz; idx++ { entry := result.WebConnectivity.At(idx) if entry.CategoryCode != "NEWS" && entry.CategoryCode != "CULTR" { t.Fatalf("unexpected category code: %+v", entry) } } if result.WebConnectivity.At(-1) != nil { t.Fatal("expected nil here") } if result.WebConnectivity.At(siz) != nil { t.Fatal("expected nil here") } } func TestCheckInLookupLocationFailure(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() config := oonimkall.CheckInConfig{ Charging: true, OnWiFi: true, Platform: "android", RunType: string(model.RunTypeTimed), SoftwareName: "ooniprobe-android", SoftwareVersion: "2.7.1", WebConnectivity: &oonimkall.CheckInConfigWebConnectivity{}, } config.WebConnectivity.Add("NEWS") config.WebConnectivity.Add("CULTR") ctx.Cancel() // immediate failure result, err := sess.CheckIn(ctx, &config) if !errors.Is(err, geolocate.ErrAllIPLookuppersFailed) { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("expected nil result here") } } func TestCheckInNewProbeServicesFailure(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } sess.TestingCheckInBeforeNewProbeServicesClient = func(ctx *oonimkall.Context) { ctx.Cancel() // cancel execution } ctx := sess.NewContext() config := oonimkall.CheckInConfig{ Charging: true, OnWiFi: true, Platform: "android", RunType: string(model.RunTypeTimed), SoftwareName: "ooniprobe-android", SoftwareVersion: "2.7.1", WebConnectivity: &oonimkall.CheckInConfigWebConnectivity{}, } config.WebConnectivity.Add("NEWS") config.WebConnectivity.Add("CULTR") result, err := sess.CheckIn(ctx, &config) if !errors.Is(err, context.Canceled) { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("expected nil result here") } } func TestCheckInCheckInFailure(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } sess.TestingCheckInBeforeCheckIn = func(ctx *oonimkall.Context) { ctx.Cancel() // cancel execution } ctx := sess.NewContext() config := oonimkall.CheckInConfig{ Charging: true, OnWiFi: true, Platform: "android", RunType: string(model.RunTypeTimed), SoftwareName: "ooniprobe-android", SoftwareVersion: "2.7.1", WebConnectivity: &oonimkall.CheckInConfigWebConnectivity{}, } config.WebConnectivity.Add("NEWS") config.WebConnectivity.Add("CULTR") result, err := sess.CheckIn(ctx, &config) if !errors.Is(err, context.Canceled) { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("expected nil result here") } } func TestCheckInNoParams(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() config := oonimkall.CheckInConfig{ Charging: true, OnWiFi: true, Platform: "android", RunType: string(model.RunTypeTimed), SoftwareName: "ooniprobe-android", SoftwareVersion: "2.7.1", } result, err := sess.CheckIn(ctx, &config) if err == nil || err.Error() != "oonimkall: missing webconnectivity config" { t.Fatalf("not the error we expected: %+v", err) } if result != nil { t.Fatal("unexpected not nil result here") } } func TestFetchURLListSuccess(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() config := oonimkall.URLListConfig{ Limit: 10, } config.AddCategory("NEWS") config.AddCategory("CULTR") result, err := sess.FetchURLList(ctx, &config) if err != nil { t.Fatalf("unexpected error: %+v", err) } if result == nil || result.Results == nil { t.Fatal("got nil result") } for idx := int64(0); idx < result.Size(); idx++ { entry := result.At(idx) if entry.CategoryCode != "NEWS" && entry.CategoryCode != "CULTR" { t.Fatalf("unexpected category code: %+v", entry) } } if result.At(-1) != nil { t.Fatal("expected nil here") } if result.At(result.Size()) != nil { t.Fatal("expected nil here") } } func TestFetchURLListWithCC(t *testing.T) { if testing.Short() { t.Skip("skip test in short mode") } sess, err := NewSessionForTesting() if err != nil { t.Fatal(err) } ctx := sess.NewContext() config := oonimkall.URLListConfig{ CountryCode: "IT", } config.AddCategory("NEWS") config.AddCategory("CULTR") result, err := sess.FetchURLList(ctx, &config) if err != nil { t.Fatalf("unexpected error: %+v", err) } if result == nil || result.Results == nil { t.Fatal("got nil result") } found := false for _, entry := range result.Results { if entry.CountryCode == "IT" { found = true } } if !found { t.Fatalf("not found url for country code: IT") } } func TestMain(m *testing.M) { // Here we're basically testing whether eventually the finalizers // will run and the number of active sessions and cancels will become // balanced. Especially for the number of active cancels, this is an // indication that we've correctly cleaned them up in the session. if exitcode := m.Run(); exitcode != 0 { os.Exit(exitcode) } for { runtime.GC() m, n := oonimkall.ActiveContexts.Load(), oonimkall.ActiveSessions.Load() fmt.Printf("./oonimkall: ActiveContexts: %d; ActiveSessions: %d\n", m, n) if m == 0 && n == 0 { break } time.Sleep(1 * time.Second) } os.Exit(0) }