Files
free-turn-proxy/mobile/api_test.go
T

209 lines
5.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mobile
import (
"encoding/json"
"strings"
"sync"
"testing"
"github.com/samosvalishe/free-turn-proxy/internal/session"
)
const testConfig = `{"peer":"1.2.3.4:5000","clientId":"deadbeef","vk":{"links":["https://vk.ru/call/join/CODE"]}}`
// recorder - EventSink для тестов.
type recorder struct {
mu sync.Mutex
states []string
logs []string
captcha []string
}
func (r *recorder) OnState(state string, _, _ int, _ string) {
r.mu.Lock()
defer r.mu.Unlock()
r.states = append(r.states, state)
}
func (r *recorder) OnLog(level, msg string, _ int64) {
r.mu.Lock()
defer r.mu.Unlock()
r.logs = append(r.logs, level+" "+msg)
}
func (r *recorder) OnCaptcha(url string) {
r.mu.Lock()
defer r.mu.Unlock()
r.captcha = append(r.captcha, url)
}
func TestStateConstantsMatchSessionPhases(t *testing.T) {
pairs := map[string]session.Phase{
StateIdle: session.PhaseIdle,
StateConnecting: session.PhaseConnecting,
StateConnected: session.PhaseConnected,
StateCaptcha: session.PhaseCaptcha,
StateError: session.PhaseError,
}
if len(pairs) != 5 {
t.Fatalf("State constants collide: %v", pairs)
}
for state, phase := range pairs {
if state != string(phase) {
t.Fatalf("state %q != phase %q", state, phase)
}
}
}
func TestGetStateIdleWithoutSession(t *testing.T) {
Stop()
if got := GetState(); got.State != StateIdle {
t.Fatalf("GetState().State = %q, want %q", got.State, StateIdle)
}
}
func TestDefaultConfigJSONParses(t *testing.T) {
var dto map[string]any
if err := json.Unmarshal([]byte(DefaultConfigJSON()), &dto); err != nil {
t.Fatalf("DefaultConfigJSON() is not valid JSON: %v", err)
}
// Дефолты не содержат peer и ссылок - валидацию проходить не обязаны.
if msg := ValidateConfig(DefaultConfigJSON()); msg == "" {
t.Error("ValidateConfig(defaults) = \"\", want error about peer")
}
}
func TestValidateConfig(t *testing.T) {
if msg := ValidateConfig(testConfig); msg != "" {
t.Errorf("ValidateConfig(valid) = %q, want empty", msg)
}
if msg := ValidateConfig(`{"peer":"1.2.3.4:5000","bogus":1}`); msg == "" {
t.Error("ValidateConfig(unknown field) = \"\", want error")
}
if msg := ValidateConfig("{"); msg == "" {
t.Error("ValidateConfig(malformed) = \"\", want error")
}
}
func TestConfigToArgs(t *testing.T) {
got, err := ConfigToArgs(testConfig)
if err != nil {
t.Fatalf("ConfigToArgs() error = %v", err)
}
for _, want := range []string{"-peer 1.2.3.4:5000", "-links CODE", "-client-id deadbeef"} {
if !strings.Contains(got, want) {
t.Errorf("ConfigToArgs() = %q, missing %q", got, want)
}
}
if _, err := ConfigToArgs("{"); err == nil {
t.Error("ConfigToArgs(malformed) error = nil")
}
}
func TestStartRequiresClientID(t *testing.T) {
t.Cleanup(Stop)
err := Start(`{"peer":"1.2.3.4:5000","vk":{"links":["CODE"]}}`)
if err == nil || !strings.Contains(err.Error(), "clientId") {
t.Fatalf("Start() error = %v, want clientId requirement", err)
}
}
func TestStartManualCaptchaRequiresSink(t *testing.T) {
SetEventSink(nil)
t.Cleanup(Stop)
in := `{"peer":"1.2.3.4:5000","clientId":"deadbeef","vk":{"links":["CODE"],"manualCaptcha":true}}`
err := Start(in)
if err == nil || !strings.Contains(err.Error(), "event sink") {
t.Fatalf("Start() error = %v, want event sink requirement", err)
}
}
func TestStartRejectsBadConfig(t *testing.T) {
t.Cleanup(Stop)
if err := Start("{"); err == nil {
t.Fatal("Start(malformed) error = nil")
}
if GetState().State != StateIdle {
t.Errorf("state after failed Start = %q", GetState().State)
}
}
func TestSinkReceivesStateAndLogs(t *testing.T) {
rec := &recorder{}
SetEventSink(rec)
t.Cleanup(func() {
Stop()
SetEventSink(nil)
})
if err := Start(testConfig); err != nil {
t.Fatalf("Start() error = %v", err)
}
Stop()
rec.mu.Lock()
defer rec.mu.Unlock()
if len(rec.states) == 0 || rec.states[0] != StateConnecting {
t.Fatalf("states = %v, want first %q", rec.states, StateConnecting)
}
if len(rec.logs) == 0 {
t.Error("sink received no log lines")
}
}
func TestDoubleStartRejected(t *testing.T) {
t.Cleanup(Stop)
if err := Start(testConfig); err != nil {
t.Fatalf("Start() error = %v", err)
}
if err := Start(testConfig); err == nil || !strings.Contains(err.Error(), "already running") {
t.Fatalf("second Start() error = %v, want already running", err)
}
}
func TestStopIsIdempotent(t *testing.T) {
if err := Start(testConfig); err != nil {
t.Fatalf("Start() error = %v", err)
}
Stop()
Stop()
if got := GetState().State; got != StateIdle {
t.Fatalf("GetState().State = %q, want %q", got, StateIdle)
}
}
func TestRestartReplacesSession(t *testing.T) {
t.Cleanup(Stop)
if err := Start(testConfig); err != nil {
t.Fatalf("Start() error = %v", err)
}
if err := Restart(testConfig, 0); err != nil {
t.Fatalf("Restart() error = %v", err)
}
if got := GetState().State; got == StateIdle {
t.Errorf("GetState().State = %q, want running session", got)
}
}
// tcp-режим не требует tun: приложение зовёт Start, а не StartTunnel.
func TestTCPModeConfig(t *testing.T) {
const tcpConfig = `{"peer":"1.2.3.4:5000","clientId":"deadbeef","proxy":{"mode":"tcp"},"vk":{"links":["https://vk.ru/call/join/CODE"]}}`
if msg := ValidateConfig(tcpConfig); msg != "" {
t.Errorf("ValidateConfig(tcp) = %q, want empty", msg)
}
args, err := ConfigToArgs(tcpConfig)
if err != nil {
t.Fatalf("ConfigToArgs() error = %v", err)
}
if !strings.Contains(args, "-mode tcp") {
t.Errorf("ConfigToArgs() = %q, missing -mode tcp", args)
}
tcpWithTunnel := `{"peer":"1.2.3.4:5000","clientId":"deadbeef","proxy":{"mode":"tcp"},` +
`"tunnel":{"mode":"wg","config":"[Interface]","mtu":1280},"vk":{"links":["https://vk.ru/call/join/CODE"]}}`
if msg := ValidateConfig(tcpWithTunnel); msg == "" {
t.Error("ValidateConfig(tcp+wg) = \"\", want error")
}
}