package mobile import ( "context" "errors" "log" "strings" "sync" "testing" "time" "github.com/openlibrecommunity/olcrtc/internal/client" "github.com/openlibrecommunity/olcrtc/internal/control" "github.com/openlibrecommunity/olcrtc/internal/logger" "github.com/openlibrecommunity/olcrtc/internal/protect" "github.com/openlibrecommunity/olcrtc/internal/transport/vp8channel" ) type testProtector struct { called int } func (p *testProtector) Protect(fd int) bool { p.called = fd return true } type testLogWriter struct { got string } func (w *testLogWriter) WriteLog(msg string) { w.got += msg } func resetMobileGlobals(t *testing.T) { t.Helper() mu.Lock() if cancel != nil { cancel() } cancel = nil done = nil ready = nil errRun = nil runClientWithReady = clientRunWithReady defaults = mobileConfig{} defaultsSet = sync.Once{} mu.Unlock() protect.Protector = nil logger.SetVerbose(false) } var clientRunWithReady = runClientWithReady //nolint:gochecknoglobals // package-level state intentional const testRoomID = "room" var ( errMobileCheckFailed = errors.New("check failed") errMobileRunFailed = errors.New("run failed") ) func TestProtectorAndLogging(t *testing.T) { resetMobileGlobals(t) p := &testProtector{} SetProtector(p) if protect.Protector == nil || !protect.Protector(123) || p.called != 123 { t.Fatal("SetProtector() did not install adapter") } SetProtector(nil) if protect.Protector != nil { t.Fatal("SetProtector(nil) did not clear protector") } w := &testLogWriter{} SetLogWriter(w) log.Print("hello") if !strings.Contains(w.got, "hello") { t.Fatalf("log writer got %q, want hello", w.got) } } func TestDefaultsAndSetters(t *testing.T) { resetMobileGlobals(t) SetTransport("dc") SetDNS("9.9.9.9:53") SetVP8Options(-1, 999) SetLivenessOptions(2500, 750, -1) mu.Lock() got := defaults mu.Unlock() if got.transport != dataTransport || got.dnsServer != "9.9.9.9:53" || got.vp8FPS != 1 || got.vp8BatchSize != 64 || got.livenessInterval != 2500*time.Millisecond || got.livenessTimeout != 750*time.Millisecond || got.livenessFailures != control.DefaultFailures { t.Fatalf("defaults = %+v", got) } SetDebug(true) if !logger.IsVerbose() { t.Fatal("SetDebug(true) did not enable verbose") } SetDebug(false) if logger.IsVerbose() { t.Fatal("SetDebug(false) did not disable verbose") } } func TestNormalizeBuildRoomAndClamp(t *testing.T) { tests := map[string]string{ "datachannel": dataTransport, "data": dataTransport, "dc": dataTransport, "vp8channel": defaultTransport, "vp8": defaultTransport, "bad": defaultTransport, } for in, want := range tests { if got := normalizeTransport(in); got != want { t.Fatalf("normalizeTransport(%q) = %q, want %q", in, got, want) } } if normalizeCarrier(carrierWBStream) != carrierWBStream || normalizeCarrier("jitsi") != "jitsi" { t.Fatal("normalizeCarrier() returned unexpected value") } if got := buildRoomURL("telemost", "abc"); got != "abc" { t.Fatalf("telemost room URL = %q", got) } if got := buildRoomURL(carrierWBStream, testRoomID); got != testRoomID { t.Fatalf("wbstream room URL = %q", got) } if clampAtLeastOne(0, 10) != 1 || clampAtLeastOne(11, 10) != 10 || clampAtLeastOne(5, 10) != 5 { t.Fatal("clampAtLeastOne() returned unexpected value") } } func TestStartValidation(t *testing.T) { resetMobileGlobals(t) if err := startWithConfig("", dataTransport, testRoomID, "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errCarrierRequired) { //nolint:lll // long test description t.Fatalf("startWithConfig(missing carrier) = %v", err) } if err := startWithConfig("telemost", dataTransport, "", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errRoomIDRequired) { //nolint:lll // long test description t.Fatalf("startWithConfig(missing room) = %v", err) } if err := startWithConfig("jitsi", dataTransport, testRoomID, "", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errClientIDRequired) { //nolint:lll // long test description t.Fatalf("startWithConfig(missing client) = %v", err) } if err := startWithConfig("jitsi", dataTransport, testRoomID, "client", "", 1080, "", "", mobileConfig{}); !errors.Is(err, errKeyHexRequired) { //nolint:lll // long test description t.Fatalf("startWithConfig(missing key) = %v", err) } mu.Lock() cancel = func() {} mu.Unlock() if err := startWithConfig("jitsi", dataTransport, testRoomID, "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errAlreadyRunning) { //nolint:lll // long test description t.Fatalf("startWithConfig(running) = %v", err) } resetMobileGlobals(t) } //nolint:cyclop // table-driven test naturally has many branches func TestStartWithInjectedRunnerLifecycle(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) SetLivenessOptions(2500, 750, 4) SetSocksListenHost("0.0.0.0") runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error { opts, _ := cfg.TransportOptions.(vp8channel.Options) if cfg.Transport != dataTransport || cfg.Carrier != "jitsi" || cfg.RoomURL != testRoomID || cfg.DeviceID != "client" || cfg.LocalAddr != "0.0.0.0:1080" || cfg.DNSServer != defaultDNSServer || opts.FPS != 60 || opts.BatchSize != 8 || cfg.Liveness.Interval != 2500*time.Millisecond || cfg.Liveness.Timeout != 750*time.Millisecond || cfg.Liveness.Failures != 4 { t.Fatalf( "RunWithReady args mismatch: transport=%q carrier=%q room=%q client=%q "+ "local=%q dns=%q vp8=%d/%d liveness=%+v", cfg.Transport, cfg.Carrier, cfg.RoomURL, cfg.DeviceID, cfg.LocalAddr, cfg.DNSServer, opts.FPS, opts.BatchSize, cfg.Liveness, ) } onReady() <-ctx.Done() return ctx.Err() } if err := StartWithTransport("jitsi", "dc", testRoomID, "client", "key", 1080, "", ""); err != nil { t.Fatalf("StartWithTransport() error = %v", err) } if !IsRunning() { t.Fatal("IsRunning() = false, want true") } if err := WaitReady(100); err != nil { t.Fatalf("WaitReady() error = %v", err) } Stop() if IsRunning() { t.Fatal("IsRunning() = true after Stop") } } //nolint:cyclop // table-driven test naturally has many branches func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error { if cfg.Transport != defaultTransport || cfg.RoomURL != testRoomID || cfg.LocalAddr != "127.0.0.1:1081" || cfg.SOCKSUser != "u" || cfg.SOCKSPass != "p" || cfg.Liveness.Interval != control.DefaultInterval || cfg.Liveness.Timeout != control.DefaultTimeout || cfg.Liveness.Failures != control.DefaultFailures { t.Fatalf("Start args mismatch: transport=%q room=%q local=%q user/pass=%q/%q liveness=%+v", cfg.Transport, cfg.RoomURL, cfg.LocalAddr, cfg.SOCKSUser, cfg.SOCKSPass, cfg.Liveness) } onReady() <-ctx.Done() return ctx.Err() } if err := Start("telemost", testRoomID, "client", "key", 1081, "u", "p"); err != nil { t.Fatalf("Start() error = %v", err) } if err := WaitReady(100); err != nil { t.Fatalf("WaitReady() error = %v", err) } Stop() SetLivenessOptions(3000, 1000, 5) runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error { opts, _ := cfg.TransportOptions.(vp8channel.Options) if cfg.Transport != dataTransport || opts.FPS != 1 || opts.BatchSize != 64 || cfg.Liveness.Interval != 3000*time.Millisecond || cfg.Liveness.Timeout != time.Second || cfg.Liveness.Failures != 5 { t.Fatalf("Check args mismatch: transport=%q vp8=%d/%d liveness=%+v", cfg.Transport, opts.FPS, opts.BatchSize, cfg.Liveness) } onReady() <-ctx.Done() return nil } elapsed, err := Check("jitsi", "dc", testRoomID, "client", "key", 1082, 100, -1, 999) if err != nil { t.Fatalf("Check() error = %v", err) } if elapsed < 0 { t.Fatalf("Check() elapsed = %d", elapsed) } } func TestPingPassesLiveness(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) SetLivenessOptions(4000, 1500, 6) seen := make(chan control.Config, 1) runClientWithReady = func(ctx context.Context, cfg client.Config, onReady func()) error { seen <- cfg.Liveness onReady() <-ctx.Done() return nil } _, _ = Ping("jitsi", "dc", testRoomID, "client", "key", 1085, 100, "http://127.0.0.1/", 30, 1) select { case got := <-seen: if got.Interval != 4000*time.Millisecond || got.Timeout != 1500*time.Millisecond || got.Failures != 6 { t.Fatalf("Ping liveness = %+v", got) } default: t.Fatal("Ping did not start client") } } func TestCheckTimeoutAndRunError(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) runClientWithReady = func(ctx context.Context, _ client.Config, _ func()) error { <-ctx.Done() return nil } if _, err := Check("telemost", defaultTransport, testRoomID, "client", "key", 1083, 1, 30, 1); !errors.Is(err, errStartTimedOut) { //nolint:lll // long test description t.Fatalf("Check(timeout) error = %v, want %v", err, errStartTimedOut) } want := errMobileCheckFailed runClientWithReady = func(context.Context, client.Config, func()) error { return want } if _, err := Check( "telemost", defaultTransport, testRoomID, "client", "key", 1084, 100, 30, 1, ); !errors.Is(err, want) { t.Fatalf("Check(run error) = %v, want %v", err, want) } } func TestWaitReadyStatesAndStop(t *testing.T) { resetMobileGlobals(t) if err := WaitReady(1); !errors.Is(err, errNotRunning) { t.Fatalf("WaitReady(not running) = %v", err) } mu.Lock() errRun = errMobileRunFailed mu.Unlock() if err := WaitReady(1); err == nil || err.Error() != "run failed" { t.Fatalf("WaitReady(run err) = %v", err) } mu.Lock() errRun = nil ready = make(chan struct{}) done = make(chan struct{}) cancel = func() {} mu.Unlock() if err := WaitReady(1); !errors.Is(err, errStartTimedOut) { t.Fatalf("WaitReady(timeout) = %v", err) } mu.Lock() close(ready) mu.Unlock() if err := WaitReady(1); err != nil { t.Fatalf("WaitReady(ready) error = %v", err) } mu.Lock() cancel = func() {} done = make(chan struct{}) doneCh := done mu.Unlock() go func() { time.Sleep(time.Millisecond) close(doneCh) }() Stop() mu.Lock() cancel = nil mu.Unlock() } func TestLogBridge(t *testing.T) { w := &testLogWriter{} n, err := (&logBridge{w: w}).Write([]byte("abc")) if err != nil || n != 3 || w.got != "abc" { t.Fatalf("logBridge.Write() = (%d, %v), got %q", n, err, w.got) } }