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