mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
224 lines
6.7 KiB
Go
224 lines
6.7 KiB
Go
package xmppsig
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"dev.narayana.im/narayana/telegabber/calls/signaling"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/jingle"
|
|
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
// cross-side teardown for XMPP-originated calls:
|
|
// inbound terminate -> Session observer -> bridge.Terminate -> both halves Terminate
|
|
|
|
func TestCalleeOnTerminated_SessionTerminate_TearsDownTGCaller(t *testing.T) {
|
|
callee, _, tgSide := newBoundCallee(t)
|
|
callee.OnTerminated(jingle.ReasonSuccess)
|
|
waitUntil(t, "tg-Caller.Terminate", time.Second, tgSide.isTerminated)
|
|
if tgSide.reason() != signaling.ReasonHangup {
|
|
t.Errorf("tg-side reason = %v, want ReasonHangup", tgSide.reason())
|
|
}
|
|
}
|
|
|
|
// inbound JMI <retract> in StateRinging drives the same teardown
|
|
func TestCalleeInboundRetract_TearsDownTGCaller(t *testing.T) {
|
|
callee, sess, tgSide := newBoundCallee(t)
|
|
// simulate the post-propose StateRinging by hand
|
|
sess.HandleJMI("alice@xmpp.example/desktop", &jingle.JMIPropose{
|
|
ID: sess.SID(),
|
|
Descriptions: []jingle.JMIDescription{{Media: "audio"}},
|
|
})
|
|
waitUntil(t, "session in ringing", time.Second, func() bool {
|
|
return sess.State() == jingle.StateRinging
|
|
})
|
|
|
|
mgr := callee.a.cfg.Manager
|
|
ret := stanza.NewMessage(stanza.Attrs{
|
|
Type: stanza.MessageTypeChat,
|
|
From: "alice@xmpp.example/desktop",
|
|
To: "gw.example",
|
|
Id: "ret-1",
|
|
})
|
|
ret.Extensions = []stanza.MsgExtension{&jingle.JMIRetract{ID: sess.SID()}}
|
|
mgr.HandleMessage(&ret)
|
|
|
|
waitUntil(t, "tg-Caller.Terminate", time.Second, tgSide.isTerminated)
|
|
if tgSide.reason() != signaling.ReasonPeerGone {
|
|
t.Errorf("tg-side reason = %v, want ReasonPeerGone", tgSide.reason())
|
|
}
|
|
}
|
|
|
|
// outgoing JMI <ringing>/<proceed> `from` must match propose's `to`,
|
|
// otherwise libdino's from.equals_bare(peer_state.jid) silently drops
|
|
// the proceed and no session-initiate follows
|
|
func TestCallee_RingingProceedFromMatchesProposeTo(t *testing.T) {
|
|
send := &recSender{}
|
|
mgr := &jingle.Manager{LocalJID: "gw.example", Sender: send}
|
|
a := NewAdapter(AdapterConfig{
|
|
Sender: send, LocalJID: "gw.example", Manager: mgr,
|
|
PCFactory: pcFactory(t),
|
|
})
|
|
|
|
// realistic XMPP-originated propose
|
|
prop := jingle.IncomingProposal{
|
|
SID: "incoming-sid-1",
|
|
From: "alice@xmpp.example/desktop",
|
|
To: "12345@gw.example",
|
|
Media: []string{"audio"},
|
|
}
|
|
_, sess, err := a.NewCallee(prop)
|
|
if err != nil {
|
|
t.Fatalf("NewCallee: %v", err)
|
|
}
|
|
mgr.Register(sess)
|
|
|
|
// Bring the session into StateRinging via the same path production
|
|
// uses, so Ringing/AcceptProposal pass their state guards.
|
|
sess.HandleJMI(prop.From, &jingle.JMIPropose{
|
|
ID: prop.SID,
|
|
Descriptions: []jingle.JMIDescription{{Media: "audio"}},
|
|
})
|
|
waitUntil(t, "session in ringing", time.Second, func() bool {
|
|
return sess.State() == jingle.StateRinging
|
|
})
|
|
|
|
if err := sess.Ringing(context.Background()); err != nil {
|
|
t.Fatalf("Ringing: %v", err)
|
|
}
|
|
if err := sess.AcceptProposal(context.Background()); err != nil {
|
|
t.Fatalf("AcceptProposal: %v", err)
|
|
}
|
|
|
|
// every JMI <message>: from's bare must equal propose.To and from must
|
|
// be a full JID (Conversations crashes on bare-JID `with`)
|
|
wantBare := prop.To
|
|
wantFrom := prop.To + "/" + prop.SID
|
|
dontWantFrom := "gw.example"
|
|
var sawRinging, sawProceed bool
|
|
for _, pkt := range send.snapshot() {
|
|
var msg *stanza.Message
|
|
switch m := pkt.(type) {
|
|
case *stanza.Message:
|
|
msg = m
|
|
case stanza.Message:
|
|
msg = &m
|
|
default:
|
|
continue
|
|
}
|
|
if msg.From == dontWantFrom {
|
|
t.Errorf("outbound JMI message from=%q (the bare component domain); want bare=%q with a resource", msg.From, wantBare)
|
|
}
|
|
if msg.From != wantFrom {
|
|
t.Errorf("outbound JMI message from=%q; want %q", msg.From, wantFrom)
|
|
}
|
|
if !strings.Contains(msg.From, "/") {
|
|
t.Errorf("outbound JMI message from=%q is bare; Conversations routes bare-JID updates through a crash-prone proposal-state lambda", msg.From)
|
|
}
|
|
for _, ext := range msg.Extensions {
|
|
switch ext.(type) {
|
|
case *jingle.JMIRinging:
|
|
sawRinging = true
|
|
case *jingle.JMIProceed:
|
|
sawProceed = true
|
|
}
|
|
}
|
|
}
|
|
if !sawRinging {
|
|
t.Error("never observed an outbound <ringing>")
|
|
}
|
|
if !sawProceed {
|
|
t.Error("never observed an outbound <proceed>")
|
|
}
|
|
}
|
|
|
|
// <ringing> must come from Callee.Ringing (driven by tdlib Pending), not
|
|
// from Callee.Accept; otherwise back-to-back ringing+proceed produces a
|
|
// millisecond ringback window in Dino/anotherim and "discovering devices"
|
|
// in Conversations
|
|
func TestCalleeAccept_DoesNotEmitRinging(t *testing.T) {
|
|
send := &recSender{}
|
|
mgr := &jingle.Manager{LocalJID: "gw.example", Sender: send}
|
|
a := NewAdapter(AdapterConfig{
|
|
Sender: send, LocalJID: "gw.example", Manager: mgr,
|
|
PCFactory: pcFactory(t),
|
|
})
|
|
prop := jingle.IncomingProposal{
|
|
SID: "sid-accept-no-ring",
|
|
From: "alice@xmpp.example/desktop",
|
|
To: "12345@gw.example",
|
|
Media: []string{"audio"},
|
|
}
|
|
callee, sess, err := a.NewCallee(prop)
|
|
if err != nil {
|
|
t.Fatalf("NewCallee: %v", err)
|
|
}
|
|
mgr.Register(sess)
|
|
|
|
sess.HandleJMI(prop.From, &jingle.JMIPropose{
|
|
ID: prop.SID,
|
|
Descriptions: []jingle.JMIDescription{{Media: "audio"}},
|
|
})
|
|
waitUntil(t, "session in ringing", time.Second, func() bool {
|
|
return sess.State() == jingle.StateRinging
|
|
})
|
|
|
|
// Accept without Ringing: <proceed> must appear, <ringing> must not
|
|
callee.Accept()
|
|
waitUntil(t, "proceed emitted", time.Second, func() bool {
|
|
for _, pkt := range send.snapshot() {
|
|
msg, ok := pkt.(*stanza.Message)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, ext := range msg.Extensions {
|
|
if _, isProceed := ext.(*jingle.JMIProceed); isProceed {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
for _, pkt := range send.snapshot() {
|
|
msg, ok := pkt.(*stanza.Message)
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, ext := range msg.Extensions {
|
|
if _, isRinging := ext.(*jingle.JMIRinging); isRinging {
|
|
t.Fatalf("Accept emitted <ringing>; <ringing> must come exclusively from Callee.Ringing (split was deliberate, see callee.go)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func newBoundCallee(t *testing.T) (*Callee, *jingle.Session, *recBridgeSide) {
|
|
t.Helper()
|
|
send := &recSender{}
|
|
mgr := &jingle.Manager{LocalJID: "gw.example", Sender: send}
|
|
a := NewAdapter(AdapterConfig{
|
|
Sender: send, LocalJID: "gw.example", Manager: mgr,
|
|
PCFactory: pcFactory(t),
|
|
})
|
|
callee, sess, err := a.NewCallee(jingle.IncomingProposal{
|
|
SID: "incoming-sid-1", From: "alice@xmpp.example/desktop", To: "gw.example", Media: []string{"audio"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewCallee: %v", err)
|
|
}
|
|
// register so inbound stanzas in integration tests find the session
|
|
mgr.Register(sess)
|
|
|
|
tgSide := &recBridgeSide{}
|
|
br := signaling.New(signaling.Config{
|
|
Caller: tgSide, Callee: callee, Timers: noopTimers{},
|
|
})
|
|
callee.Bind(br)
|
|
// bridge.Start moves out of StateIdle so terminate fires for real
|
|
br.Start()
|
|
return callee, sess, tgSide
|
|
}
|