//nolint:all // Test file keeps scenario setup inline. package mobile import ( "context" "errors" "log" "strings" "sync" "testing" "time" "github.com/openlibrecommunity/olcrtc/internal/logger" "github.com/openlibrecommunity/olcrtc/internal/protect" ) 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 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") SetLink("direct") SetDNS("9.9.9.9:53") SetVP8Options(-1, 999) mu.Lock() got := defaults mu.Unlock() if got.transport != dataTransport || got.link != defaultLink || got.dnsServer != "9.9.9.9:53" || got.vp8FPS != 1 || got.vp8BatchSize != 64 { 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("jazz") != "jazz" { t.Fatal("normalizeCarrier() returned unexpected value") } if got := buildRoomURL("telemost", "abc"); got != "https://telemost.yandex.ru/j/abc" { t.Fatalf("telemost room URL = %q", got) } if got := buildRoomURL("jazz", ""); got != "any" { t.Fatalf("jazz empty room URL = %q", got) } if got := buildRoomURL(carrierWBStream, "room"); got != "room" { 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, "room", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errCarrierRequired) { t.Fatalf("startWithConfig(missing carrier) = %v", err) } if err := startWithConfig("telemost", dataTransport, "", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errRoomIDRequired) { t.Fatalf("startWithConfig(missing room) = %v", err) } if err := startWithConfig("jazz", dataTransport, "", "", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errClientIDRequired) { t.Fatalf("startWithConfig(missing client) = %v", err) } if err := startWithConfig("jazz", dataTransport, "", "client", "", 1080, "", "", mobileConfig{}); !errors.Is(err, errKeyHexRequired) { t.Fatalf("startWithConfig(missing key) = %v", err) } mu.Lock() cancel = func() {} mu.Unlock() if err := startWithConfig("jazz", dataTransport, "", "client", "key", 1080, "", "", mobileConfig{}); !errors.Is(err, errAlreadyRunning) { t.Fatalf("startWithConfig(running) = %v", err) } resetMobileGlobals(t) } func TestStartWithInjectedRunnerLifecycle(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) runClientWithReady = func( ctx context.Context, linkName, transportName, carrierName, roomURL, keyHex, clientID string, localAddr string, dnsServer, socksUser, socksPass string, onReady func(), videoWidth int, videoHeight int, videoFPS int, videoBitrate string, videoHW string, videoQRSize int, videoQRRecovery string, videoCodec string, videoTileModule int, videoTileRS int, vp8FPS int, vp8BatchSize int, seiFPS int, seiBatchSize int, seiFragmentSize int, seiAckTimeoutMS int, ) error { if linkName != defaultLink || transportName != dataTransport || carrierName != carrierJazz || roomURL != "any" || clientID != "client" || localAddr != "127.0.0.1:1080" || dnsServer != defaultDNSServer || vp8FPS != 60 || vp8BatchSize != 8 { t.Fatalf("RunWithReady args mismatch: link=%q transport=%q carrier=%q room=%q client=%q local=%q dns=%q vp8=%d/%d", linkName, transportName, carrierName, roomURL, clientID, localAddr, dnsServer, vp8FPS, vp8BatchSize) } onReady() <-ctx.Done() return ctx.Err() } if err := StartWithTransport(carrierJazz, "dc", "", "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") } } func TestStartUsesDefaultsAndCheckWithInjectedRunner(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) runClientWithReady = func( ctx context.Context, linkName, transportName, carrierName, roomURL, keyHex, clientID string, localAddr string, dnsServer, socksUser, socksPass string, onReady func(), videoWidth int, videoHeight int, videoFPS int, videoBitrate string, videoHW string, videoQRSize int, videoQRRecovery string, videoCodec string, videoTileModule int, videoTileRS int, vp8FPS int, vp8BatchSize int, seiFPS int, seiBatchSize int, seiFragmentSize int, seiAckTimeoutMS int, ) error { if transportName != defaultTransport || roomURL != "https://telemost.yandex.ru/j/room" || localAddr != "127.0.0.1:1081" || socksUser != "u" || socksPass != "p" { t.Fatalf("Start args mismatch: transport=%q room=%q local=%q user/pass=%q/%q", transportName, roomURL, localAddr, socksUser, socksPass) } onReady() <-ctx.Done() return ctx.Err() } if err := Start("telemost", "room", "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() runClientWithReady = func( ctx context.Context, linkName, transportName, carrierName, roomURL, keyHex, clientID string, localAddr string, dnsServer, socksUser, socksPass string, onReady func(), videoWidth int, videoHeight int, videoFPS int, videoBitrate string, videoHW string, videoQRSize int, videoQRRecovery string, videoCodec string, videoTileModule int, videoTileRS int, vp8FPS int, vp8BatchSize int, seiFPS int, seiBatchSize int, seiFragmentSize int, seiAckTimeoutMS int, ) error { if transportName != dataTransport || vp8FPS != 1 || vp8BatchSize != 64 { t.Fatalf("Check args mismatch: transport=%q vp8=%d/%d", transportName, vp8FPS, vp8BatchSize) } onReady() <-ctx.Done() return nil } elapsed, err := Check("jazz", "dc", "", "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 TestCheckTimeoutAndRunError(t *testing.T) { resetMobileGlobals(t) t.Cleanup(func() { resetMobileGlobals(t) }) runClientWithReady = func( ctx context.Context, linkName, transportName, carrierName, roomURL, keyHex, clientID string, localAddr string, dnsServer, socksUser, socksPass string, onReady func(), videoWidth int, videoHeight int, videoFPS int, videoBitrate string, videoHW string, videoQRSize int, videoQRRecovery string, videoCodec string, videoTileModule int, videoTileRS int, vp8FPS int, vp8BatchSize int, seiFPS int, seiBatchSize int, seiFragmentSize int, seiAckTimeoutMS int, ) error { <-ctx.Done() return nil } if _, err := Check("telemost", defaultTransport, "room", "client", "key", 1083, 1, 30, 1); !errors.Is(err, errStartTimedOut) { t.Fatalf("Check(timeout) error = %v, want %v", err, errStartTimedOut) } want := errors.New("check failed") runClientWithReady = func( context.Context, string, string, string, string, string, string, string, string, string, string, func(), int, int, int, string, string, int, string, string, int, int, int, int, int, int, int, int, ) error { return want } if _, err := Check("telemost", defaultTransport, "room", "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 = errors.New("run failed") 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) } }