mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
277 lines
7.9 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|