1package engine
2
3import (
4	"context"
5	"errors"
6	"io/ioutil"
7	"net/http"
8	"net/http/httptest"
9	"net/url"
10	"os"
11	"syscall"
12	"testing"
13	"time"
14
15	"github.com/apex/log"
16	"github.com/google/go-cmp/cmp"
17	"github.com/ooni/probe-engine/geolocate"
18	"github.com/ooni/probe-engine/model"
19	"github.com/ooni/probe-engine/netx"
20	"github.com/ooni/probe-engine/probeservices"
21	"github.com/ooni/probe-engine/version"
22)
23
24func TestNewSessionBuilderChecks(t *testing.T) {
25	if testing.Short() {
26		t.Skip("skip test in short mode")
27	}
28	t.Run("with no settings", func(t *testing.T) {
29		newSessionMustFail(t, SessionConfig{})
30	})
31	t.Run("with only assets dir", func(t *testing.T) {
32		newSessionMustFail(t, SessionConfig{
33			AssetsDir: "testdata",
34		})
35	})
36	t.Run("with also logger", func(t *testing.T) {
37		newSessionMustFail(t, SessionConfig{
38			AssetsDir: "testdata",
39			Logger:    model.DiscardLogger,
40		})
41	})
42	t.Run("with also software name", func(t *testing.T) {
43		newSessionMustFail(t, SessionConfig{
44			AssetsDir:    "testdata",
45			Logger:       model.DiscardLogger,
46			SoftwareName: "ooniprobe-engine",
47		})
48	})
49	t.Run("with software version and wrong tempdir", func(t *testing.T) {
50		newSessionMustFail(t, SessionConfig{
51			AssetsDir:       "testdata",
52			Logger:          model.DiscardLogger,
53			SoftwareName:    "ooniprobe-engine",
54			SoftwareVersion: "0.0.1",
55			TempDir:         "./nonexistent",
56		})
57	})
58}
59
60func TestNewSessionBuilderGood(t *testing.T) {
61	if testing.Short() {
62		t.Skip("skip test in short mode")
63	}
64	newSessionForTesting(t)
65}
66
67func newSessionMustFail(t *testing.T, config SessionConfig) {
68	sess, err := NewSession(config)
69	if err == nil {
70		t.Fatal("expected an error here")
71	}
72	if sess != nil {
73		t.Fatal("expected nil session here")
74	}
75}
76
77func TestSessionTorArgsTorBinary(t *testing.T) {
78	if testing.Short() {
79		t.Skip("skip test in short mode")
80	}
81	sess, err := NewSession(SessionConfig{
82		AssetsDir: "testdata",
83		AvailableProbeServices: []model.Service{{
84			Address: "https://ams-pg-test.ooni.org",
85			Type:    "https",
86		}},
87		Logger:          model.DiscardLogger,
88		SoftwareName:    "ooniprobe-engine",
89		SoftwareVersion: "0.0.1",
90		TorArgs:         []string{"antani1", "antani2", "antani3"},
91		TorBinary:       "mascetti",
92	})
93	if err != nil {
94		t.Fatal(err)
95	}
96	if sess.TorBinary() != "mascetti" {
97		t.Fatal("not the TorBinary we expected")
98	}
99	if len(sess.TorArgs()) != 3 {
100		t.Fatal("not the TorArgs length we expected")
101	}
102	if sess.TorArgs()[0] != "antani1" {
103		t.Fatal("not the TorArgs[0] we expected")
104	}
105	if sess.TorArgs()[1] != "antani2" {
106		t.Fatal("not the TorArgs[1] we expected")
107	}
108	if sess.TorArgs()[2] != "antani3" {
109		t.Fatal("not the TorArgs[2] we expected")
110	}
111}
112
113func newSessionForTestingNoLookupsWithProxyURL(t *testing.T, URL *url.URL) *Session {
114	sess, err := NewSession(SessionConfig{
115		AssetsDir: "testdata",
116		AvailableProbeServices: []model.Service{{
117			Address: "https://ams-pg-test.ooni.org",
118			Type:    "https",
119		}},
120		Logger:          model.DiscardLogger,
121		ProxyURL:        URL,
122		SoftwareName:    "ooniprobe-engine",
123		SoftwareVersion: "0.0.1",
124	})
125	if err != nil {
126		t.Fatal(err)
127	}
128	return sess
129}
130
131func newSessionForTestingNoLookups(t *testing.T) *Session {
132	return newSessionForTestingNoLookupsWithProxyURL(t, nil)
133}
134
135func newSessionForTestingNoBackendsLookup(t *testing.T) *Session {
136	sess := newSessionForTestingNoLookups(t)
137	if err := sess.MaybeLookupLocation(); err != nil {
138		t.Fatal(err)
139	}
140	log.Infof("Platform: %s", sess.Platform())
141	log.Infof("ProbeASN: %d", sess.ProbeASN())
142	log.Infof("ProbeASNString: %s", sess.ProbeASNString())
143	log.Infof("ProbeCC: %s", sess.ProbeCC())
144	log.Infof("ProbeIP: %s", sess.ProbeIP())
145	log.Infof("ProbeNetworkName: %s", sess.ProbeNetworkName())
146	log.Infof("ResolverASN: %d", sess.ResolverASN())
147	log.Infof("ResolverASNString: %s", sess.ResolverASNString())
148	log.Infof("ResolverIP: %s", sess.ResolverIP())
149	log.Infof("ResolverNetworkName: %s", sess.ResolverNetworkName())
150	return sess
151}
152
153func newSessionForTesting(t *testing.T) *Session {
154	sess := newSessionForTestingNoBackendsLookup(t)
155	if err := sess.MaybeLookupBackends(); err != nil {
156		t.Fatal(err)
157	}
158	return sess
159}
160
161func TestNewOrchestraClient(t *testing.T) {
162	if testing.Short() {
163		t.Skip("skip test in short mode")
164	}
165	sess := newSessionForTestingNoLookups(t)
166	defer sess.Close()
167	clnt, err := sess.NewOrchestraClient(context.Background())
168	if err != nil {
169		t.Fatal(err)
170	}
171	if clnt == nil {
172		t.Fatal("expected non nil client here")
173	}
174}
175
176func TestInitOrchestraClientMaybeRegisterError(t *testing.T) {
177	if testing.Short() {
178		t.Skip("skip test in short mode")
179	}
180	ctx, cancel := context.WithCancel(context.Background())
181	cancel() // so we fail immediately
182	sess := newSessionForTestingNoLookups(t)
183	defer sess.Close()
184	clnt, err := probeservices.NewClient(sess, model.Service{
185		Address: "https://ams-pg-test.ooni.org/",
186		Type:    "https",
187	})
188	if err != nil {
189		t.Fatal(err)
190	}
191	outclnt, err := sess.initOrchestraClient(
192		ctx, clnt, clnt.MaybeLogin,
193	)
194	if !errors.Is(err, context.Canceled) {
195		t.Fatal("not the error we expected")
196	}
197	if outclnt != nil {
198		t.Fatal("expected a nil client here")
199	}
200}
201
202func TestInitOrchestraClientMaybeLoginError(t *testing.T) {
203	if testing.Short() {
204		t.Skip("skip test in short mode")
205	}
206	ctx := context.Background()
207	sess := newSessionForTestingNoLookups(t)
208	defer sess.Close()
209	clnt, err := probeservices.NewClient(sess, model.Service{
210		Address: "https://ams-pg-test.ooni.org/",
211		Type:    "https",
212	})
213	if err != nil {
214		t.Fatal(err)
215	}
216	expected := errors.New("mocked error")
217	outclnt, err := sess.initOrchestraClient(
218		ctx, clnt, func(context.Context) error {
219			return expected
220		},
221	)
222	if !errors.Is(err, expected) {
223		t.Fatal("not the error we expected")
224	}
225	if outclnt != nil {
226		t.Fatal("expected a nil client here")
227	}
228}
229
230func TestBouncerError(t *testing.T) {
231	if testing.Short() {
232		t.Skip("skip test in short mode")
233	}
234	// Combine proxy testing with a broken proxy with errors
235	// in reaching out to the bouncer.
236	server := httptest.NewServer(http.HandlerFunc(
237		func(w http.ResponseWriter, r *http.Request) {
238			w.WriteHeader(500)
239		},
240	))
241	defer server.Close()
242	URL, err := url.Parse(server.URL)
243	if err != nil {
244		t.Fatal(err)
245	}
246	sess := newSessionForTestingNoLookupsWithProxyURL(t, URL)
247	defer sess.Close()
248	if sess.ProxyURL() == nil {
249		t.Fatal("expected to see explicit proxy here")
250	}
251	if err := sess.MaybeLookupBackends(); err == nil {
252		t.Fatal("expected an error here")
253	}
254}
255
256func TestMaybeLookupBackendsNewClientError(t *testing.T) {
257	if testing.Short() {
258		t.Skip("skip test in short mode")
259	}
260	sess := newSessionForTestingNoLookups(t)
261	sess.availableProbeServices = []model.Service{{
262		Type:    "onion",
263		Address: "httpo://jehhrikjjqrlpufu.onion",
264	}}
265	defer sess.Close()
266	err := sess.MaybeLookupBackends()
267	if !errors.Is(err, ErrAllProbeServicesFailed) {
268		t.Fatal("not the error we expected")
269	}
270}
271
272func TestSessionLocationLookup(t *testing.T) {
273	if testing.Short() {
274		t.Skip("skip test in short mode")
275	}
276	sess := newSessionForTestingNoLookups(t)
277	defer sess.Close()
278	if err := sess.MaybeLookupLocation(); err != nil {
279		t.Fatal(err)
280	}
281	if sess.ProbeASNString() == geolocate.DefaultProbeASNString {
282		t.Fatal("unexpected ProbeASNString")
283	}
284	if sess.ProbeASN() == geolocate.DefaultProbeASN {
285		t.Fatal("unexpected ProbeASN")
286	}
287	if sess.ProbeCC() == geolocate.DefaultProbeCC {
288		t.Fatal("unexpected ProbeCC")
289	}
290	if sess.ProbeIP() == geolocate.DefaultProbeIP {
291		t.Fatal("unexpected ProbeIP")
292	}
293	if sess.ProbeNetworkName() == geolocate.DefaultProbeNetworkName {
294		t.Fatal("unexpected ProbeNetworkName")
295	}
296	if sess.ResolverASN() == geolocate.DefaultResolverASN {
297		t.Fatal("unexpected ResolverASN")
298	}
299	if sess.ResolverASNString() == geolocate.DefaultResolverASNString {
300		t.Fatal("unexpected ResolverASNString")
301	}
302	if sess.ResolverIP() == geolocate.DefaultResolverIP {
303		t.Fatal("unexpected ResolverIP")
304	}
305	if sess.ResolverNetworkName() == geolocate.DefaultResolverNetworkName {
306		t.Fatal("unexpected ResolverNetworkName")
307	}
308	if sess.KibiBytesSent() <= 0 {
309		t.Fatal("unexpected KibiBytesSent")
310	}
311	if sess.KibiBytesReceived() <= 0 {
312		t.Fatal("unexpected KibiBytesReceived")
313	}
314}
315
316func TestSessionCloseCancelsTempDir(t *testing.T) {
317	if testing.Short() {
318		t.Skip("skip test in short mode")
319	}
320	sess := newSessionForTestingNoLookups(t)
321	tempDir := sess.TempDir()
322	if _, err := os.Stat(tempDir); err != nil {
323		t.Fatal(err)
324	}
325	if err := sess.Close(); err != nil {
326		t.Fatal(err)
327	}
328	if _, err := os.Stat(tempDir); !errors.Is(err, syscall.ENOENT) {
329		t.Fatal("not the error we expected")
330	}
331}
332
333func TestSessionDownloadResources(t *testing.T) {
334	if testing.Short() {
335		t.Skip("skip test in short mode")
336	}
337	tmpdir, err := ioutil.TempDir("", "test-download-resources-idempotent")
338	if err != nil {
339		t.Fatal(err)
340	}
341	ctx := context.Background()
342	sess := newSessionForTestingNoLookups(t)
343	defer sess.Close()
344	sess.SetAssetsDir(tmpdir)
345	err = sess.MaybeUpdateResources(ctx)
346	if err != nil {
347		t.Fatal(err)
348	}
349	readfile := func(path string) (err error) {
350		_, err = ioutil.ReadFile(path)
351		return
352	}
353	if err := readfile(sess.ASNDatabasePath()); err != nil {
354		t.Fatal(err)
355	}
356	if err := readfile(sess.CountryDatabasePath()); err != nil {
357		t.Fatal(err)
358	}
359}
360
361func TestGetAvailableProbeServices(t *testing.T) {
362	if testing.Short() {
363		t.Skip("skip test in short mode")
364	}
365	sess, err := NewSession(SessionConfig{
366		AssetsDir:       "testdata",
367		Logger:          model.DiscardLogger,
368		SoftwareName:    "ooniprobe-engine",
369		SoftwareVersion: "0.0.1",
370	})
371	if err != nil {
372		t.Fatal(err)
373	}
374	defer sess.Close()
375	all := sess.GetAvailableProbeServices()
376	diff := cmp.Diff(all, probeservices.Default())
377	if diff != "" {
378		t.Fatal(diff)
379	}
380}
381
382func TestMaybeLookupBackendsFailure(t *testing.T) {
383	if testing.Short() {
384		t.Skip("skip test in short mode")
385	}
386	sess, err := NewSession(SessionConfig{
387		AssetsDir:       "testdata",
388		Logger:          model.DiscardLogger,
389		SoftwareName:    "ooniprobe-engine",
390		SoftwareVersion: "0.0.1",
391	})
392	if err != nil {
393		t.Fatal(err)
394	}
395	defer sess.Close()
396	ctx, cancel := context.WithCancel(context.Background())
397	cancel() // so we fail immediately
398	err = sess.MaybeLookupBackendsContext(ctx)
399	if !errors.Is(err, ErrAllProbeServicesFailed) {
400		t.Fatal("unexpected error")
401	}
402}
403
404func TestMaybeLookupTestHelpersIdempotent(t *testing.T) {
405	if testing.Short() {
406		t.Skip("skip test in short mode")
407	}
408	sess, err := NewSession(SessionConfig{
409		AssetsDir:       "testdata",
410		Logger:          model.DiscardLogger,
411		SoftwareName:    "ooniprobe-engine",
412		SoftwareVersion: "0.0.1",
413	})
414	if err != nil {
415		t.Fatal(err)
416	}
417	defer sess.Close()
418	ctx := context.Background()
419	if err = sess.MaybeLookupBackendsContext(ctx); err != nil {
420		t.Fatal(err)
421	}
422	if err = sess.MaybeLookupBackendsContext(ctx); err != nil {
423		t.Fatal(err)
424	}
425	if sess.QueryProbeServicesCount() != 1 {
426		t.Fatal("unexpected number of queries sent to the bouncer")
427	}
428}
429
430func TestAllProbeServicesUnsupported(t *testing.T) {
431	if testing.Short() {
432		t.Skip("skip test in short mode")
433	}
434	sess, err := NewSession(SessionConfig{
435		AssetsDir:       "testdata",
436		Logger:          model.DiscardLogger,
437		SoftwareName:    "ooniprobe-engine",
438		SoftwareVersion: "0.0.1",
439	})
440	if err != nil {
441		t.Fatal(err)
442	}
443	defer sess.Close()
444	sess.AppendAvailableProbeService(model.Service{
445		Address: "mascetti",
446		Type:    "antani",
447	})
448	err = sess.MaybeLookupBackends()
449	if !errors.Is(err, ErrAllProbeServicesFailed) {
450		t.Fatal("unexpected error")
451	}
452}
453
454func TestStartTunnelGood(t *testing.T) {
455	if testing.Short() {
456		t.Skip("skip test in short mode")
457	}
458	sess := newSessionForTestingNoLookups(t)
459	defer sess.Close()
460	ctx := context.Background()
461	if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil {
462		t.Fatal(err)
463	}
464	if err := sess.MaybeStartTunnel(ctx, "psiphon"); err != nil {
465		t.Fatal(err) // check twice, must be idempotent
466	}
467	if sess.ProxyURL() == nil {
468		t.Fatal("expected non-nil ProxyURL")
469	}
470}
471
472func TestStartTunnelNonexistent(t *testing.T) {
473	if testing.Short() {
474		t.Skip("skip test in short mode")
475	}
476	sess := newSessionForTestingNoLookups(t)
477	defer sess.Close()
478	ctx := context.Background()
479	if err := sess.MaybeStartTunnel(ctx, "antani"); err.Error() != "unsupported tunnel" {
480		t.Fatal("not the error we expected")
481	}
482	if sess.ProxyURL() != nil {
483		t.Fatal("expected nil ProxyURL")
484	}
485}
486
487func TestStartTunnelEmptyString(t *testing.T) {
488	if testing.Short() {
489		t.Skip("skip test in short mode")
490	}
491	sess := newSessionForTestingNoLookups(t)
492	defer sess.Close()
493	ctx := context.Background()
494	if sess.MaybeStartTunnel(ctx, "") != nil {
495		t.Fatal("expected no error here")
496	}
497	if sess.ProxyURL() != nil {
498		t.Fatal("expected nil ProxyURL")
499	}
500}
501
502func TestStartTunnelEmptyStringWithProxy(t *testing.T) {
503	if testing.Short() {
504		t.Skip("skip test in short mode")
505	}
506	proxyURL := &url.URL{Scheme: "socks5", Host: "127.0.0.1:9050"}
507	sess := newSessionForTestingNoLookups(t)
508	sess.proxyURL = proxyURL
509	defer sess.Close()
510	ctx := context.Background()
511	if sess.MaybeStartTunnel(ctx, "") != nil {
512		t.Fatal("expected no error here")
513	}
514	diff := cmp.Diff(proxyURL, sess.ProxyURL())
515	if diff != "" {
516		t.Fatal(diff)
517	}
518}
519
520func TestStartTunnelWithAlreadyExistingTunnel(t *testing.T) {
521	if testing.Short() {
522		t.Skip("skip test in short mode")
523	}
524	sess := newSessionForTestingNoLookups(t)
525	defer sess.Close()
526	ctx := context.Background()
527	if sess.MaybeStartTunnel(ctx, "psiphon") != nil {
528		t.Fatal("expected no error here")
529	}
530	prev := sess.ProxyURL()
531	err := sess.MaybeStartTunnel(ctx, "tor")
532	if !errors.Is(err, ErrAlreadyUsingProxy) {
533		t.Fatal("expected another error here")
534	}
535	cur := sess.ProxyURL()
536	diff := cmp.Diff(prev, cur)
537	if diff != "" {
538		t.Fatal(diff)
539	}
540}
541
542func TestStartTunnelWithAlreadyExistingProxy(t *testing.T) {
543	if testing.Short() {
544		t.Skip("skip test in short mode")
545	}
546	sess := newSessionForTestingNoLookups(t)
547	defer sess.Close()
548	ctx := context.Background()
549	orig := &url.URL{Scheme: "socks5", Host: "[::1]:9050"}
550	sess.proxyURL = orig
551	err := sess.MaybeStartTunnel(ctx, "psiphon")
552	if !errors.Is(err, ErrAlreadyUsingProxy) {
553		t.Fatal("expected another error here")
554	}
555	cur := sess.ProxyURL()
556	diff := cmp.Diff(orig, cur)
557	if diff != "" {
558		t.Fatal(diff)
559	}
560}
561
562func TestStartTunnelCanceledContext(t *testing.T) {
563	if testing.Short() {
564		t.Skip("skip test in short mode")
565	}
566	sess := newSessionForTestingNoLookups(t)
567	defer sess.Close()
568	ctx, cancel := context.WithCancel(context.Background())
569	cancel() // immediately cancel
570	err := sess.MaybeStartTunnel(ctx, "psiphon")
571	if !errors.Is(err, context.Canceled) {
572		t.Fatal("not the error we expected")
573	}
574}
575
576func TestUserAgentNoProxy(t *testing.T) {
577	if testing.Short() {
578		t.Skip("skip test in short mode")
579	}
580	expect := "ooniprobe-engine/0.0.1 ooniprobe-engine/" + version.Version
581	sess := newSessionForTestingNoLookups(t)
582	ua := sess.UserAgent()
583	diff := cmp.Diff(expect, ua)
584	if diff != "" {
585		t.Fatal(diff)
586	}
587}
588
589func TestNewOrchestraClientMaybeLookupBackendsFailure(t *testing.T) {
590	if testing.Short() {
591		t.Skip("skip test in short mode")
592	}
593	sess := newSessionForTestingNoLookups(t)
594	ctx, cancel := context.WithCancel(context.Background())
595	cancel() // fail immediately
596	client, err := sess.NewOrchestraClient(ctx)
597	if !errors.Is(err, ErrAllProbeServicesFailed) {
598		t.Fatal("not the error we expected")
599	}
600	if client != nil {
601		t.Fatal("expected nil client here")
602	}
603}
604
605type httpTransportThatSleeps struct {
606	txp netx.HTTPRoundTripper
607	st  time.Duration
608}
609
610func (txp httpTransportThatSleeps) RoundTrip(req *http.Request) (*http.Response, error) {
611	resp, err := txp.txp.RoundTrip(req)
612	time.Sleep(txp.st)
613	return resp, err
614}
615
616func (txp httpTransportThatSleeps) CloseIdleConnections() {
617	txp.txp.CloseIdleConnections()
618}
619
620func TestNewOrchestraClientMaybeLookupLocationFailure(t *testing.T) {
621	if testing.Short() {
622		t.Skip("skip test in short mode")
623	}
624	sess := newSessionForTestingNoLookups(t)
625	sess.httpDefaultTransport = httpTransportThatSleeps{
626		txp: sess.httpDefaultTransport,
627		st:  5 * time.Second,
628	}
629	// The transport sleeps for five seconds, so the context should be expired by
630	// the time in which we attempt at looking up the location. Because the
631	// implementation performs the round-trip and _then_ sleeps, it means we'll
632	// see the context expired error when performing the location lookup.
633	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
634	defer cancel()
635	client, err := sess.NewOrchestraClient(ctx)
636	if !errors.Is(err, geolocate.ErrAllIPLookuppersFailed) {
637		t.Fatalf("not the error we expected: %+v", err)
638	}
639	if client != nil {
640		t.Fatal("expected nil client here")
641	}
642}
643
644func TestNewOrchestraClientProbeServicesNewClientFailure(t *testing.T) {
645	if testing.Short() {
646		t.Skip("skip test in short mode")
647	}
648	sess := newSessionForTestingNoLookups(t)
649	sess.selectedProbeServiceHook = func(svc *model.Service) {
650		svc.Type = "antani" // should really not be supported for a long time
651	}
652	client, err := sess.NewOrchestraClient(context.Background())
653	if !errors.Is(err, probeservices.ErrUnsupportedEndpoint) {
654		t.Fatal("not the error we expected")
655	}
656	if client != nil {
657		t.Fatal("expected nil client here")
658	}
659}
660