telegabber/calls/signaling/bridge_test.go
2026-05-27 08:37:47 -07:00

277 lines
7.9 KiB
Go

package signaling
import (
"testing"
"time"
)
type fakeCaller struct {
started bool
terminated bool
terminateReason TerminationReason
}
func (f *fakeCaller) Start() { f.started = true }
func (f *fakeCaller) Terminate(r TerminationReason) {
f.terminated = true
f.terminateReason = r
}
type fakeCallee struct {
ringing bool
accepted bool
terminated bool
terminateReason TerminationReason
}
func (f *fakeCallee) Ringing() { f.ringing = true }
func (f *fakeCallee) Accept() { f.accepted = true }
func (f *fakeCallee) Terminate(r TerminationReason) {
f.terminated = true
f.terminateReason = r
}
type fakeTimers struct {
set map[TimerKind]time.Duration
cancel map[TimerKind]int
}
func newFakeTimers() *fakeTimers {
return &fakeTimers{set: map[TimerKind]time.Duration{}, cancel: map[TimerKind]int{}}
}
func (f *fakeTimers) Set(k TimerKind, d time.Duration) { f.set[k] = d }
func (f *fakeTimers) Cancel(k TimerKind) { f.cancel[k]++ }
type fakes struct {
caller *fakeCaller
callee *fakeCallee
timers *fakeTimers
}
func newBridge() (*Bridge, *fakes) {
f := &fakes{caller: &fakeCaller{}, callee: &fakeCallee{}, timers: newFakeTimers()}
b := New(Config{Caller: f.caller, Callee: f.callee, Timers: f.timers})
return b, f
}
func driveTo(t *testing.T, target State) (*Bridge, *fakes) {
t.Helper()
b, f := newBridge()
if target == StateIdle {
return b, f
}
b.Start()
if target == StateOriginating {
return b, f
}
b.Ringing()
if target == StateRinging {
return b, f
}
b.CalleeAccepted()
if target == StateAccepted {
return b, f
}
b.MediaConnected(CallerSide)
b.MediaConnected(CalleeSide)
if target == StateEstablished {
return b, f
}
t.Fatalf("driveTo: unknown target %s", target)
return nil, nil
}
func TestTerminalStateIgnoresEvents(t *testing.T) {
b, _ := driveTo(t, StateRinging)
b.Timeout(TimerRing)
if b.State() != StateTerminated {
t.Fatalf("setup: state = %s", b.State())
}
b.Start()
b.Ringing()
b.CalleeAccepted()
b.MediaConnected(CallerSide)
b.MediaConnected(CalleeSide)
b.Terminate(ReasonHangup)
b.Timeout(TimerExchange)
if b.State() != StateTerminated {
t.Errorf("state drifted to %s", b.State())
}
}
// teardown matrix: every call-ending event tears down both sides via
// Terminate, leaves the bridge in StateTerminated, and cancels every timer
func checkTerminated(t *testing.T, b *Bridge, f *fakes, wantReason TerminationReason) {
t.Helper()
if b.State() != StateTerminated {
t.Fatalf("bridge state = %s, want terminated", b.State())
}
if !f.caller.terminated {
t.Errorf("caller.terminated = false, want true")
}
if !f.callee.terminated {
t.Errorf("callee.terminated = false, want true")
}
if f.caller.terminateReason != wantReason {
t.Errorf("caller.terminateReason = %v, want %v", f.caller.terminateReason, wantReason)
}
if f.callee.terminateReason != wantReason {
t.Errorf("callee.terminateReason = %v, want %v", f.callee.terminateReason, wantReason)
}
for _, k := range []TimerKind{TimerRing, TimerExchange, TimerConnect} {
if f.timers.cancel[k] == 0 {
t.Errorf("timer %v not cancelled on terminate", k)
}
}
}
func TestBridgeTeardown_TearsDownBoth(t *testing.T) {
cases := []struct {
name string
states []State
trigger func(*Bridge)
want TerminationReason
}{
{"Terminate",
[]State{StateOriginating, StateRinging, StateAccepted, StateEstablished},
func(b *Bridge) { b.Terminate(ReasonHangup) }, ReasonHangup},
{"MediaFailed",
[]State{StateAccepted, StateEstablished},
func(b *Bridge) { b.MediaFailed(ReasonMediaFailed) }, ReasonMediaFailed},
{"TimerRing",
[]State{StateOriginating, StateRinging},
func(b *Bridge) { b.Timeout(TimerRing) }, ReasonRingTimeout},
{"TimerExchange",
[]State{StateAccepted},
func(b *Bridge) { b.Timeout(TimerExchange) }, ReasonExchangeTimeout},
{"TimerConnect",
[]State{StateEstablished},
func(b *Bridge) { b.Timeout(TimerConnect) }, ReasonConnectTimeout},
}
for _, tc := range cases {
for _, st := range tc.states {
t.Run(tc.name+"/"+st.String(), func(t *testing.T) {
b, f := driveTo(t, st)
tc.trigger(b)
checkTerminated(t, b, f, tc.want)
})
}
}
}
// further teardowns after the first must not re-fire Terminate
func TestBridgeTeardown_Idempotent(t *testing.T) {
b, f := driveTo(t, StateEstablished)
b.Terminate(ReasonHangup)
if !f.caller.terminated || !f.callee.terminated {
t.Fatalf("setup: both sides should be torn down")
}
// subsequent Terminates must no-op; recorded reasons must not change
f.caller.terminateReason = ReasonUnknown // sentinel
f.callee.terminateReason = ReasonUnknown // sentinel
b.Terminate(ReasonMediaFailed)
b.Terminate(ReasonDecline)
b.MediaFailed(ReasonMediaFailed)
b.Timeout(TimerRing)
if f.caller.terminateReason != ReasonUnknown {
t.Errorf("caller.Terminate re-fired (reason now %v)", f.caller.terminateReason)
}
if f.callee.terminateReason != ReasonUnknown {
t.Errorf("callee.Terminate re-fired (reason now %v)", f.callee.terminateReason)
}
}
// 0 before Established, positive after; needed by tgsig.DiscardCall
func TestBridge_Duration(t *testing.T) {
b, _ := newBridge()
if d := b.Duration(); d != 0 {
t.Errorf("Duration before Start = %v, want 0", d)
}
b.Start()
b.Ringing()
b.CalleeAccepted()
b.MediaConnected(CallerSide)
if d := b.Duration(); d != 0 {
t.Errorf("Duration after one-sided MediaConnected = %v, want 0 (not yet Established)", d)
}
b.MediaConnected(CalleeSide)
time.Sleep(20 * time.Millisecond)
if d := b.Duration(); d < 10*time.Millisecond {
t.Errorf("Duration after Established = %v, want >= 10ms", d)
}
}
// only one side up: cancel TimerExchange, arm TimerConnect, don't fire OnEstablished
func TestBridge_MediaConnectedOneSide_DoesNotEstablish(t *testing.T) {
f := &fakes{caller: &fakeCaller{}, callee: &fakeCallee{}, timers: newFakeTimers()}
var established int
b := New(Config{
Caller: f.caller, Callee: f.callee, Timers: f.timers,
OnEstablished: func() { established++ },
})
b.Start()
b.Ringing()
b.CalleeAccepted()
b.MediaConnected(CallerSide)
if b.State() != StateAccepted {
t.Errorf("state after one-sided MediaConnected = %s, want accepted", b.State())
}
if established != 0 {
t.Errorf("OnEstablished fired prematurely (%d times) with only one side up", established)
}
if _, ok := f.timers.set[TimerConnect]; !ok {
t.Errorf("TimerConnect not armed after first MediaConnected")
}
if f.timers.cancel[TimerExchange] == 0 {
t.Errorf("TimerExchange not cancelled after first MediaConnected")
}
}
// OnEstablished / OnTerminated fire exactly once
func TestBridge_LifecycleHooks(t *testing.T) {
f := &fakes{caller: &fakeCaller{}, callee: &fakeCallee{}, timers: newFakeTimers()}
var established, terminated int
b := New(Config{
Caller: f.caller, Callee: f.callee, Timers: f.timers,
OnEstablished: func() { established++ },
OnTerminated: func() { terminated++ },
})
b.Start()
b.Ringing()
b.CalleeAccepted()
b.MediaConnected(CallerSide)
b.MediaConnected(CalleeSide)
b.MediaConnected(CallerSide) // duplicate post-Established; must be no-op
b.Terminate(ReasonHangup)
b.Terminate(ReasonHangup) // duplicate; must not re-fire OnTerminated
if established != 1 {
t.Errorf("OnEstablished fired %d times, want 1", established)
}
if terminated != 1 {
t.Errorf("OnTerminated fired %d times, want 1", terminated)
}
}
// CalleeAccepted outside Originating/Ringing must be a no-op
func TestBridge_CalleeAcceptedStateGuard(t *testing.T) {
for _, st := range []State{StateIdle, StateAccepted, StateEstablished, StateTerminated} {
t.Run(st.String(), func(t *testing.T) {
var b *Bridge
var f *fakes
if st == StateTerminated {
b, f = driveTo(t, StateRinging)
b.Terminate(ReasonHangup)
} else {
b, f = driveTo(t, st)
}
// driveTo invokes CalleeAccepted along the way; reset so we
// observe only the upcoming call
f.callee.accepted = false
b.CalleeAccepted()
if f.callee.accepted {
t.Errorf("Callee.Accept fired from state %s", st)
}
})
}
}