mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
304 lines
10 KiB
Go
304 lines
10 KiB
Go
package tgsig
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"dev.narayana.im/narayana/telegabber/calls/signaling"
|
|
"gotgcalls/ntgcalls"
|
|
|
|
"github.com/zelenin/go-tdlib/client"
|
|
)
|
|
|
|
type fakeTdlib struct {
|
|
createCalls []*client.CreateCallRequest
|
|
acceptCalls []*client.AcceptCallRequest
|
|
discardCalls []*client.DiscardCallRequest
|
|
sentSignal []*client.SendCallSignalingDataRequest
|
|
nextCallID int32
|
|
}
|
|
|
|
func (f *fakeTdlib) CreateCall(req *client.CreateCallRequest) (*client.CallId, error) {
|
|
f.createCalls = append(f.createCalls, req)
|
|
f.nextCallID++
|
|
return &client.CallId{Id: f.nextCallID}, nil
|
|
}
|
|
func (f *fakeTdlib) AcceptCall(req *client.AcceptCallRequest) (*client.Ok, error) {
|
|
f.acceptCalls = append(f.acceptCalls, req)
|
|
return &client.Ok{}, nil
|
|
}
|
|
func (f *fakeTdlib) DiscardCall(req *client.DiscardCallRequest) (*client.Ok, error) {
|
|
f.discardCalls = append(f.discardCalls, req)
|
|
return &client.Ok{}, nil
|
|
}
|
|
func (f *fakeTdlib) SendCallSignalingData(req *client.SendCallSignalingDataRequest) (*client.Ok, error) {
|
|
f.sentSignal = append(f.sentSignal, req)
|
|
return &client.Ok{}, nil
|
|
}
|
|
|
|
type fakeNtg struct {
|
|
onSignal ntgcalls.SignalCallback
|
|
onConnCh ntgcalls.ConnectionChangeCallback
|
|
stopCalls []int64
|
|
}
|
|
|
|
func (f *fakeNtg) OnSignal(cb ntgcalls.SignalCallback) { f.onSignal = cb }
|
|
func (f *fakeNtg) OnConnectionChange(cb ntgcalls.ConnectionChangeCallback) { f.onConnCh = cb }
|
|
func (f *fakeNtg) CreateP2PCall(int64) error { return nil }
|
|
func (f *fakeNtg) SkipExchange(int64, []byte, bool) error { return nil }
|
|
func (f *fakeNtg) ConnectP2P(int64, []ntgcalls.RTCServer, []string, bool) error {
|
|
return nil
|
|
}
|
|
func (f *fakeNtg) SendSignalingData(int64, []byte) error { return nil }
|
|
func (f *fakeNtg) Stop(chatID int64) error {
|
|
f.stopCalls = append(f.stopCalls, chatID)
|
|
return nil
|
|
}
|
|
|
|
// recorders sitting in place of the real per-side handlers, so tests can
|
|
// observe which side the bridge fires Terminate on
|
|
|
|
type recCaller struct {
|
|
started bool
|
|
terminated bool
|
|
terminateReason signaling.TerminationReason
|
|
}
|
|
|
|
func (r *recCaller) Start() { r.started = true }
|
|
func (r *recCaller) Terminate(reason signaling.TerminationReason) {
|
|
r.terminated = true
|
|
r.terminateReason = reason
|
|
}
|
|
|
|
type recCallee struct {
|
|
ringing bool
|
|
accepted bool
|
|
terminated bool
|
|
terminateReason signaling.TerminationReason
|
|
}
|
|
|
|
func (r *recCallee) Ringing() { r.ringing = true }
|
|
func (r *recCallee) Accept() { r.accepted = true }
|
|
func (r *recCallee) Terminate(reason signaling.TerminationReason) {
|
|
r.terminated = true
|
|
r.terminateReason = reason
|
|
}
|
|
|
|
type noopTimers struct{}
|
|
|
|
func (noopTimers) Set(signaling.TimerKind, time.Duration) {}
|
|
func (noopTimers) Cancel(signaling.TimerKind) {}
|
|
|
|
type tgFixture struct {
|
|
mgr *Manager
|
|
adapter *Adapter
|
|
tdlib *fakeTdlib
|
|
ntg *fakeNtg
|
|
caller *recCaller
|
|
callee *recCallee
|
|
bridge *signaling.Bridge
|
|
}
|
|
|
|
func newFixture() *tgFixture {
|
|
tdlib := &fakeTdlib{}
|
|
ntg := &fakeNtg{}
|
|
mgr := NewManager(nil)
|
|
a := New(tdlib, ntg, nil, "jid@gw", mgr)
|
|
return &tgFixture{
|
|
mgr: mgr, adapter: a, tdlib: tdlib, ntg: ntg,
|
|
caller: &recCaller{}, callee: &recCallee{},
|
|
}
|
|
}
|
|
|
|
// TG-originated bridge: tg is the bridge's Callee, recCaller plays the xmpp
|
|
// side. Returns *callBase so the test can drive its state.
|
|
func (f *tgFixture) buildTGOriginated(callID int32, userID int64) *callBase {
|
|
tgSide := f.adapter.NewCallee(callID, userID, false)
|
|
f.bridge = signaling.New(signaling.Config{
|
|
Caller: f.caller, // xmpp-side (recorder)
|
|
Callee: f.callee, // tg-side (recorder; the real tg side is registered with the Manager for endCall lookup)
|
|
Timers: noopTimers{},
|
|
})
|
|
tgSide.callBase.Bind(f.bridge)
|
|
return &tgSide.callBase
|
|
}
|
|
|
|
// buildXmppOriginated wires an XMPP-originated bridge: tg-side is the bridge's
|
|
// Caller. Manually registers the callBase since NewCaller doesn't auto-register
|
|
// (production code registers from Caller.Start after tdlib.CreateCall succeeds).
|
|
func (f *tgFixture) buildXmppOriginated(callID int32, userID int64) *callBase {
|
|
tgSide := f.adapter.NewCaller(userID, false)
|
|
tgSide.callBase.setCallID(callID)
|
|
f.adapter.registerSide(userID, &tgSide.callBase)
|
|
f.mgr.Register(f.adapter.jid, callID, &tgSide.callBase)
|
|
f.bridge = signaling.New(signaling.Config{
|
|
Caller: f.caller, // tg-side here (recorder; real tg side is what the Manager has)
|
|
Callee: f.callee, // xmpp-side
|
|
Timers: noopTimers{},
|
|
})
|
|
tgSide.callBase.Bind(f.bridge)
|
|
return &tgSide.callBase
|
|
}
|
|
|
|
func discardUpdate(callID int32, userID int64, isOutgoing bool, reason client.CallDiscardReason) *client.UpdateCall {
|
|
return &client.UpdateCall{
|
|
Call: &client.Call{
|
|
Id: callID,
|
|
UserId: userID,
|
|
IsOutgoing: isOutgoing,
|
|
IsVideo: false,
|
|
State: &client.CallStateDiscarded{Reason: reason},
|
|
},
|
|
}
|
|
}
|
|
|
|
func hangingUpUpdate(callID int32, userID int64, isOutgoing bool) *client.UpdateCall {
|
|
return &client.UpdateCall{
|
|
Call: &client.Call{
|
|
Id: callID, UserId: userID, IsOutgoing: isOutgoing, IsVideo: false,
|
|
State: &client.CallStateHangingUp{},
|
|
},
|
|
}
|
|
}
|
|
|
|
func pendingUpdate(callID int32, userID int64, isOutgoing, isReceived bool) *client.UpdateCall {
|
|
return &client.UpdateCall{
|
|
Call: &client.Call{
|
|
Id: callID, UserId: userID, IsOutgoing: isOutgoing, IsVideo: false,
|
|
State: &client.CallStatePending{IsCreated: true, IsReceived: isReceived},
|
|
},
|
|
}
|
|
}
|
|
|
|
// every tdlib teardown event -> bridge.Terminate on both halves with the
|
|
// mapped reason; catches new discard reasons leaking through as Unknown
|
|
func TestTGTeardownMatrix_TGOriginated(t *testing.T) {
|
|
errorUpdate := func(callID int32, userID int64) *client.UpdateCall {
|
|
return &client.UpdateCall{Call: &client.Call{
|
|
Id: callID, UserId: userID, IsOutgoing: false,
|
|
State: &client.CallStateError{Error: &client.Error{Code: 500, Message: "boom"}},
|
|
}}
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
build func(callID int32, userID int64) *client.UpdateCall
|
|
want signaling.TerminationReason
|
|
}{
|
|
{"discard/declined", func(c int32, u int64) *client.UpdateCall {
|
|
return discardUpdate(c, u, false, &client.CallDiscardReasonDeclined{})
|
|
}, signaling.ReasonDecline},
|
|
{"discard/missed", func(c int32, u int64) *client.UpdateCall {
|
|
return discardUpdate(c, u, false, &client.CallDiscardReasonMissed{})
|
|
}, signaling.ReasonRingTimeout},
|
|
{"discard/disconnected", func(c int32, u int64) *client.UpdateCall {
|
|
return discardUpdate(c, u, false, &client.CallDiscardReasonDisconnected{})
|
|
}, signaling.ReasonMediaFailed},
|
|
{"discard/hungUp", func(c int32, u int64) *client.UpdateCall {
|
|
return discardUpdate(c, u, false, &client.CallDiscardReasonHungUp{})
|
|
}, signaling.ReasonHangup},
|
|
{"discard/empty", func(c int32, u int64) *client.UpdateCall {
|
|
return discardUpdate(c, u, false, &client.CallDiscardReasonEmpty{})
|
|
}, signaling.ReasonHangup},
|
|
{"hangingUp", func(c int32, u int64) *client.UpdateCall {
|
|
return hangingUpUpdate(c, u, false)
|
|
}, signaling.ReasonHangup},
|
|
{"error", errorUpdate, signaling.ReasonMediaFailed},
|
|
}
|
|
for i, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f := newFixture()
|
|
callID := int32(500 + i)
|
|
userID := int64(8000 + i)
|
|
f.buildTGOriginated(callID, userID)
|
|
f.bridge.Start()
|
|
f.bridge.Ringing()
|
|
|
|
f.mgr.OnUpdateCall("jid@gw", tc.build(callID, userID))
|
|
|
|
if !f.caller.terminated || !f.callee.terminated {
|
|
t.Fatalf("both sides should tear down: caller=%v callee=%v", f.caller.terminated, f.callee.terminated)
|
|
}
|
|
if f.caller.terminateReason != tc.want || f.callee.terminateReason != tc.want {
|
|
t.Errorf("reason: caller=%v callee=%v, want %v", f.caller.terminateReason, f.callee.terminateReason, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// XMPP-originated outbound: transition to Ringing only on Pending{IsReceived:true}.
|
|
// Pending{IsReceived:false} (server-created, peer not notified) must not transition,
|
|
// or <ringing> goes out before the peer is alerting.
|
|
func TestTGPending_XMPPOriginated_DrivesBridgeToRinging(t *testing.T) {
|
|
f := newFixture()
|
|
f.buildXmppOriginated(303, 9303)
|
|
f.bridge.Start()
|
|
|
|
if got := f.bridge.State(); got != signaling.StateOriginating {
|
|
t.Fatalf("pre-Pending state = %s, want originating", got)
|
|
}
|
|
|
|
// first Pending: server-created, must NOT ring
|
|
f.mgr.OnUpdateCall("jid@gw", pendingUpdate(303, 9303, true, false))
|
|
if got := f.bridge.State(); got != signaling.StateOriginating {
|
|
t.Fatalf("post-Pending(IsReceived=false) state = %s, want originating", got)
|
|
}
|
|
if f.callee.ringing {
|
|
t.Error("xmpp-Callee.Ringing fired on Pending(IsReceived=false); want only on IsReceived=true")
|
|
}
|
|
|
|
// second Pending: peer device alerting, ring
|
|
f.mgr.OnUpdateCall("jid@gw", pendingUpdate(303, 9303, true, true))
|
|
if got := f.bridge.State(); got != signaling.StateRinging {
|
|
t.Fatalf("post-Pending(IsReceived=true) state = %s, want ringing", got)
|
|
}
|
|
if !f.callee.ringing {
|
|
t.Error("xmpp-Callee.Ringing did not fire on Pending(IsReceived=true)")
|
|
}
|
|
|
|
// duplicate Pending(IsReceived=true) no-ops via Bridge.Ringing's state guard
|
|
f.callee.ringing = false
|
|
f.mgr.OnUpdateCall("jid@gw", pendingUpdate(303, 9303, true, true))
|
|
if got := f.bridge.State(); got != signaling.StateRinging {
|
|
t.Fatalf("after duplicate Pending state = %s, want ringing", got)
|
|
}
|
|
if f.callee.ringing {
|
|
t.Error("xmpp-Callee.Ringing re-fired on duplicate Pending; want once")
|
|
}
|
|
|
|
// ExchangingKeys drives Ringing -> Accepted. Accept is called without
|
|
// re-emitting <ringing>: the ringback window between the two stanzas
|
|
// is the time the user spent looking at their phone, not 30ms.
|
|
f.mgr.OnUpdateCall("jid@gw", &client.UpdateCall{
|
|
Call: &client.Call{
|
|
Id: 303, UserId: 9303, IsOutgoing: true, IsVideo: false,
|
|
State: &client.CallStateExchangingKeys{},
|
|
},
|
|
})
|
|
if got := f.bridge.State(); got != signaling.StateAccepted {
|
|
t.Fatalf("post-ExchangingKeys state = %s, want accepted", got)
|
|
}
|
|
if !f.callee.accepted {
|
|
t.Error("xmpp-Callee.Accept did not fire on ExchangingKeys")
|
|
}
|
|
}
|
|
|
|
// endCall must drop registrations: subsequent OnUpdateCall for the same
|
|
// (jid, callID) is a no-op (no side gets re-fired).
|
|
func TestTGDiscard_DropsRegistrationFromManager(t *testing.T) {
|
|
f := newFixture()
|
|
f.buildTGOriginated(111, 9011)
|
|
f.bridge.Start()
|
|
|
|
f.mgr.OnUpdateCall("jid@gw", discardUpdate(111, 9011, false, &client.CallDiscardReasonHungUp{}))
|
|
if got := f.mgr.lookup("jid@gw", 111); got != nil {
|
|
t.Errorf("callBase still registered after discard: %v", got)
|
|
}
|
|
// dropSide on the adapter
|
|
if _, ok := f.adapter.lookupSideByUser(9011); ok {
|
|
t.Errorf("adapter still has user 9011 after discard")
|
|
}
|
|
// ntgcalls Stop was called
|
|
if len(f.ntg.stopCalls) != 1 || f.ntg.stopCalls[0] != 9011 {
|
|
t.Errorf("ntg.Stop calls = %v, want [9011]", f.ntg.stopCalls)
|
|
}
|
|
}
|