1package engine 2 3import ( 4 "context" 5 "errors" 6 "net/url" 7 "sync" 8 "testing" 9 10 "github.com/apex/log" 11 "github.com/google/go-cmp/cmp" 12 "github.com/ooni/probe-cli/v3/internal/engine/geolocate" 13 "github.com/ooni/probe-cli/v3/internal/engine/model" 14) 15 16func (s *Session) GetAvailableProbeServices() []model.Service { 17 return s.getAvailableProbeServicesUnlocked() 18} 19 20func (s *Session) AppendAvailableProbeService(svc model.Service) { 21 s.availableProbeServices = append(s.availableProbeServices, svc) 22} 23 24func (s *Session) QueryProbeServicesCount() int64 { 25 return s.queryProbeServicesCount.Load() 26} 27 28// mockableProbeServicesClientForCheckIn allows us to mock the 29// probeservices.Client used by Session.CheckIn. 30type mockableProbeServicesClientForCheckIn struct { 31 // Config is the config passed to the call. 32 Config *model.CheckInConfig 33 34 // Results contains the results of the call. This field MUST be 35 // non-nil if and only if Error is nil. 36 Results *model.CheckInInfo 37 38 // Error indicates whether the call failed. This field MUST be 39 // non-nil if and only if Error is nil. 40 Error error 41 42 // mu provides mutual exclusion. 43 mu sync.Mutex 44} 45 46// CheckIn implements sessionProbeServicesClientForCheckIn.CheckIn. 47func (c *mockableProbeServicesClientForCheckIn) CheckIn( 48 ctx context.Context, config model.CheckInConfig) (*model.CheckInInfo, error) { 49 defer c.mu.Unlock() 50 c.mu.Lock() 51 if c.Config != nil { 52 return nil, errors.New("called more than once") 53 } 54 c.Config = &config 55 if c.Results == nil && c.Error == nil { 56 return nil, errors.New("misconfigured mockableProbeServicesClientForCheckIn") 57 } 58 return c.Results, c.Error 59} 60 61func TestSessionCheckInSuccessful(t *testing.T) { 62 results := &model.CheckInInfo{ 63 WebConnectivity: &model.CheckInInfoWebConnectivity{ 64 ReportID: "xxx-x-xx", 65 URLs: []model.URLInfo{{ 66 CategoryCode: "NEWS", 67 CountryCode: "IT", 68 URL: "https://www.repubblica.it/", 69 }, { 70 CategoryCode: "NEWS", 71 CountryCode: "IT", 72 URL: "https://www.unita.it/", 73 }}, 74 }, 75 } 76 mockedClnt := &mockableProbeServicesClientForCheckIn{ 77 Results: results, 78 } 79 s := &Session{ 80 location: &geolocate.Results{ 81 ASN: 137, 82 CountryCode: "IT", 83 }, 84 softwareName: "miniooni", 85 softwareVersion: "0.1.0-dev", 86 testMaybeLookupLocationContext: func(ctx context.Context) error { 87 return nil 88 }, 89 testNewProbeServicesClientForCheckIn: func( 90 ctx context.Context) (sessionProbeServicesClientForCheckIn, error) { 91 return mockedClnt, nil 92 }, 93 } 94 out, err := s.CheckIn(context.Background(), &model.CheckInConfig{}) 95 if err != nil { 96 t.Fatal(err) 97 } 98 if diff := cmp.Diff(results, out); diff != "" { 99 t.Fatal(diff) 100 } 101 if mockedClnt.Config.Platform != s.Platform() { 102 t.Fatal("invalid Config.Platform") 103 } 104 if mockedClnt.Config.ProbeASN != "AS137" { 105 t.Fatal("invalid Config.ProbeASN") 106 } 107 if mockedClnt.Config.ProbeCC != "IT" { 108 t.Fatal("invalid Config.ProbeCC") 109 } 110 if mockedClnt.Config.RunType != "timed" { 111 t.Fatal("invalid Config.RunType") 112 } 113 if mockedClnt.Config.SoftwareName != "miniooni" { 114 t.Fatal("invalid Config.SoftwareName") 115 } 116 if mockedClnt.Config.SoftwareVersion != "0.1.0-dev" { 117 t.Fatal("invalid Config.SoftwareVersion") 118 } 119 if mockedClnt.Config.WebConnectivity.CategoryCodes == nil { 120 t.Fatal("invalid ...CategoryCodes") 121 } 122} 123 124func TestSessionCheckInCannotLookupLocation(t *testing.T) { 125 errMocked := errors.New("mocked error") 126 s := &Session{ 127 testMaybeLookupLocationContext: func(ctx context.Context) error { 128 return errMocked 129 }, 130 } 131 out, err := s.CheckIn(context.Background(), &model.CheckInConfig{}) 132 if !errors.Is(err, errMocked) { 133 t.Fatal("no the error we expected", err) 134 } 135 if out != nil { 136 t.Fatal("expected nil result here") 137 } 138} 139 140func TestSessionCheckInCannotCreateProbeServicesClient(t *testing.T) { 141 errMocked := errors.New("mocked error") 142 s := &Session{ 143 location: &geolocate.Results{ 144 ASN: 137, 145 CountryCode: "IT", 146 }, 147 softwareName: "miniooni", 148 softwareVersion: "0.1.0-dev", 149 testMaybeLookupLocationContext: func(ctx context.Context) error { 150 return nil 151 }, 152 testNewProbeServicesClientForCheckIn: func( 153 ctx context.Context) (sessionProbeServicesClientForCheckIn, error) { 154 return nil, errMocked 155 }, 156 } 157 out, err := s.CheckIn(context.Background(), &model.CheckInConfig{}) 158 if !errors.Is(err, errMocked) { 159 t.Fatal("no the error we expected", err) 160 } 161 if out != nil { 162 t.Fatal("expected nil result here") 163 } 164} 165 166func TestLowercaseMaybeLookupLocationContextWithCancelledContext(t *testing.T) { 167 s := &Session{} 168 ctx, cancel := context.WithCancel(context.Background()) 169 cancel() // immediately kill the context 170 err := s.maybeLookupLocationContext(ctx) 171 if !errors.Is(err, context.Canceled) { 172 t.Fatal("not the error we expected", err) 173 } 174} 175 176func TestNewProbeServicesClientForCheckIn(t *testing.T) { 177 s := &Session{} 178 ctx, cancel := context.WithCancel(context.Background()) 179 cancel() // immediately kill the context 180 clnt, err := s.newProbeServicesClientForCheckIn(ctx) 181 if !errors.Is(err, context.Canceled) { 182 t.Fatal("not the error we expected", err) 183 } 184 if clnt != nil { 185 t.Fatal("expected nil client here") 186 } 187} 188 189func TestSessionNewSubmitterWithCancelledContext(t *testing.T) { 190 sess := newSessionForTesting(t) 191 ctx, cancel := context.WithCancel(context.Background()) 192 cancel() // fail immediately 193 subm, err := sess.NewSubmitter(ctx) 194 if !errors.Is(err, context.Canceled) { 195 t.Fatal("not the error we expected", err) 196 } 197 if subm != nil { 198 t.Fatal("expected nil submitter here") 199 } 200} 201 202func TestSessionMaybeLookupLocationContextLookupLocationContextFailure(t *testing.T) { 203 errMocked := errors.New("mocked error") 204 sess := newSessionForTestingNoLookups(t) 205 sess.testLookupLocationContext = func(ctx context.Context) (*geolocate.Results, error) { 206 return nil, errMocked 207 } 208 err := sess.MaybeLookupLocationContext(context.Background()) 209 if !errors.Is(err, errMocked) { 210 t.Fatal("not the error we expected", err) 211 } 212} 213 214func TestSessionFetchURLListWithCancelledContext(t *testing.T) { 215 sess := &Session{} 216 ctx, cancel := context.WithCancel(context.Background()) 217 cancel() // cause failure 218 resp, err := sess.FetchURLList(ctx, model.URLListConfig{}) 219 if !errors.Is(err, context.Canceled) { 220 t.Fatal("not the error we expected", err) 221 } 222 if resp != nil { 223 t.Fatal("expected nil response here") 224 } 225} 226 227func TestSessionFetchTorTargetsWithCancelledContext(t *testing.T) { 228 sess := &Session{} 229 ctx, cancel := context.WithCancel(context.Background()) 230 cancel() // cause failure 231 resp, err := sess.FetchTorTargets(ctx, "IT") 232 if !errors.Is(err, context.Canceled) { 233 t.Fatal("not the error we expected", err) 234 } 235 if resp != nil { 236 t.Fatal("expected nil response here") 237 } 238} 239 240func TestSessionFetchPsiphonConfigWithCancelledContext(t *testing.T) { 241 sess := &Session{} 242 ctx, cancel := context.WithCancel(context.Background()) 243 cancel() // cause failure 244 resp, err := sess.FetchPsiphonConfig(ctx) 245 if !errors.Is(err, context.Canceled) { 246 t.Fatal("not the error we expected", err) 247 } 248 if resp != nil { 249 t.Fatal("expected nil response here") 250 } 251} 252 253func TestNewSessionWithFakeTunnel(t *testing.T) { 254 ctx := context.Background() 255 sess, err := NewSession(ctx, SessionConfig{ 256 Logger: log.Log, 257 ProxyURL: &url.URL{Scheme: "fake"}, 258 SoftwareName: "miniooni", 259 SoftwareVersion: "0.1.0-dev", 260 TunnelDir: "testdata", 261 }) 262 if err != nil { 263 t.Fatal(err) 264 } 265 if sess == nil { 266 t.Fatal("expected non-nil session here") 267 } 268 if sess.ProxyURL() == nil { 269 t.Fatal("expected non-nil proxyURL here") 270 } 271 if sess.tunnel == nil { 272 t.Fatal("expected non-nil tunnel here") 273 } 274 sess.Close() // ensure we don't crash 275} 276 277func TestNewSessionWithFakeTunnelAndCancelledContext(t *testing.T) { 278 ctx, cancel := context.WithCancel(context.Background()) 279 cancel() // fail immediately 280 sess, err := NewSession(ctx, SessionConfig{ 281 Logger: log.Log, 282 ProxyURL: &url.URL{Scheme: "fake"}, 283 SoftwareName: "miniooni", 284 SoftwareVersion: "0.1.0-dev", 285 TunnelDir: "testdata", 286 }) 287 if !errors.Is(err, context.Canceled) { 288 t.Fatal("not the error we expected", err) 289 } 290 if sess != nil { 291 t.Fatal("expected nil session here") 292 } 293} 294