ooni-probe-cli/pkg/oonimkall/session_integration_test.go
Simone Basso d3c5196474
fix(ooniprobe): use ooniprobe-cli-unattended for unattended runs (#714)
This diff changes the software name used by unattended runs for which
we did not override the default software name (`ooniprobe-cli`).

It will become `ooniprobe-cli-unattended`. This software name is in line
with the one we use for Android, iOS, and desktop unattended runs.

While working in this diff, I introduced string constants for the run
types and a string constant for the default software name.

See https://github.com/ooni/probe/issues/2081.
2022-04-29 13:41:09 +02:00

491 lines
13 KiB
Go

package oonimkall_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"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) {
if testing.Short() {
t.Skip("skip test in short mode")
}
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 TestMaybeUpdateResourcesWithCancelledContext(t *testing.T) {
// Note that MaybeUpdateResources is now a deprecated stub that
// does nothing. We will remove it when we bump major.
dir, err := ioutil.TempDir("", "xx")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
sess, err := NewSessionForTestingWithAssetsDir(dir)
if err != nil {
t.Fatal(err)
}
ctx := sess.NewContext()
ctx.Cancel() // cause immediate failure
err = sess.MaybeUpdateResources(ctx)
// Explanation: we embed resources. We should change the API
// and remove the context. Until we do that, let us just assert
// that we have embedding and the context does not matter.
if err != nil {
t.Fatal(err)
}
}
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) {
sess, err := NewSessionForTesting()
if err != nil {
t.Fatal(err)
}
ctx := sess.NewContext()
config := oonimkall.CheckInConfig{
Charging: true,
OnWiFi: true,
Platform: "android",
RunType: 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) {
sess, err := NewSessionForTesting()
if err != nil {
t.Fatal(err)
}
ctx := sess.NewContext()
config := oonimkall.CheckInConfig{
Charging: true,
OnWiFi: true,
Platform: "android",
RunType: 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) {
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: 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) {
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: 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) {
sess, err := NewSessionForTesting()
if err != nil {
t.Fatal(err)
}
ctx := sess.NewContext()
config := oonimkall.CheckInConfig{
Charging: true,
OnWiFi: true,
Platform: "android",
RunType: 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) {
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)
}