mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
217 lines
6 KiB
Go
217 lines
6 KiB
Go
package xmppsig
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"dev.narayana.im/narayana/telegabber/calls/signaling"
|
|
"dev.narayana.im/narayana/telegabber/xmpp/jingle"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"gosrc.io/xmpp/stanza"
|
|
)
|
|
|
|
// goroutine-safe; Session.Send fires from background goroutines after
|
|
// JMI <proceed>
|
|
type recSender struct {
|
|
mu sync.Mutex
|
|
sent []stanza.Packet
|
|
iqSent []*stanza.IQ
|
|
}
|
|
|
|
func (r *recSender) Send(p stanza.Packet) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.sent = append(r.sent, p)
|
|
if iq, ok := p.(*stanza.IQ); ok {
|
|
r.iqSent = append(r.iqSent, iq)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *recSender) SendIQ(_ context.Context, iq *stanza.IQ) (chan stanza.IQ, error) {
|
|
ch := make(chan stanza.IQ, 1)
|
|
return ch, r.Send(iq)
|
|
}
|
|
|
|
func (r *recSender) SendRaw(string) error { return nil }
|
|
|
|
func (r *recSender) snapshot() []stanza.Packet {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
out := make([]stanza.Packet, len(r.sent))
|
|
copy(out, r.sent)
|
|
return out
|
|
}
|
|
|
|
func (r *recSender) firstMessage() *stanza.Message {
|
|
for _, p := range r.snapshot() {
|
|
switch m := p.(type) {
|
|
case *stanza.Message:
|
|
return m
|
|
case stanza.Message:
|
|
return &m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// records bridge-side Terminate firings; serves as the other-side stand-in
|
|
// for tests of either xmppsig.Caller or xmppsig.Callee
|
|
type recBridgeSide struct {
|
|
mu sync.Mutex
|
|
startedOrAccept bool
|
|
terminated bool
|
|
terminateReason signaling.TerminationReason
|
|
}
|
|
|
|
func (r *recBridgeSide) Start() { r.mu.Lock(); r.startedOrAccept = true; r.mu.Unlock() }
|
|
func (r *recBridgeSide) Ringing() {}
|
|
func (r *recBridgeSide) Accept() { r.mu.Lock(); r.startedOrAccept = true; r.mu.Unlock() }
|
|
func (r *recBridgeSide) Terminate(reason signaling.TerminationReason) {
|
|
r.mu.Lock()
|
|
r.terminated = true
|
|
r.terminateReason = reason
|
|
r.mu.Unlock()
|
|
}
|
|
func (r *recBridgeSide) isTerminated() bool {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.terminated
|
|
}
|
|
func (r *recBridgeSide) reason() signaling.TerminationReason {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.terminateReason
|
|
}
|
|
|
|
type noopTimers struct{}
|
|
|
|
func (noopTimers) Set(signaling.TimerKind, time.Duration) {}
|
|
func (noopTimers) Cancel(signaling.TimerKind) {}
|
|
|
|
// real pion PC; faking webrtc is more invasive than running on loopback
|
|
func pcFactory(t *testing.T) func() (*webrtc.PeerConnection, error) {
|
|
t.Helper()
|
|
return func() (*webrtc.PeerConnection, error) {
|
|
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.Cleanup(func() { _ = pc.Close() })
|
|
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{
|
|
Direction: webrtc.RTPTransceiverDirectionSendrecv,
|
|
}); err != nil {
|
|
_ = pc.Close()
|
|
return nil, err
|
|
}
|
|
return pc, nil
|
|
}
|
|
}
|
|
|
|
// OnTerminated fires bridge.Terminate on a goroutine (deferred re-entry),
|
|
// so callers poll
|
|
func waitUntil(t *testing.T, desc string, d time.Duration, cond func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(d)
|
|
for time.Now().Before(deadline) {
|
|
if cond() {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("waitUntil(%s): timed out after %v", desc, d)
|
|
}
|
|
|
|
// JMI <propose> must use a full JID (userID@gw/telegabber). XEP-0353
|
|
// requires it so the recipient can route <proceed>/<reject> back.
|
|
|
|
func TestCallerStart_SendsProposeWithFullLocalJID(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),
|
|
})
|
|
caller, err := a.NewCaller("alice@xmpp.example", 12345)
|
|
if err != nil {
|
|
t.Fatalf("NewCaller: %v", err)
|
|
}
|
|
br := signaling.New(signaling.Config{
|
|
Caller: caller, Callee: &recBridgeSide{}, Timers: noopTimers{},
|
|
})
|
|
caller.Bind(br)
|
|
|
|
br.Start() // triggers caller.Start synchronously
|
|
|
|
msg := send.firstMessage()
|
|
if msg == nil {
|
|
t.Fatal("no message sent - Caller.Start did not produce a JMI <propose>")
|
|
}
|
|
if msg.From != "12345@gw.example/telegabber" {
|
|
t.Errorf("propose from = %q, want %q (full JID with resource per XEP-0353)", msg.From, "12345@gw.example/telegabber")
|
|
}
|
|
if msg.To != "alice@xmpp.example" {
|
|
t.Errorf("propose to = %q, want alice@xmpp.example", msg.To)
|
|
}
|
|
if string(msg.Type) != "chat" {
|
|
t.Errorf("propose type = %q, want chat", msg.Type)
|
|
}
|
|
|
|
// payload: one <propose> in JMI ns with a <description media="audio">
|
|
xmlBytes, err := xml.Marshal(msg)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
s := string(xmlBytes)
|
|
if !strings.Contains(s, `<propose xmlns="urn:xmpp:jingle-message:0"`) {
|
|
t.Errorf("propose missing JMI namespace: %s", s)
|
|
}
|
|
if !strings.Contains(s, `<description xmlns="urn:xmpp:jingle:apps:rtp:1" media="audio"`) {
|
|
t.Errorf("propose missing rtp description with media=audio: %s", s)
|
|
}
|
|
}
|
|
|
|
// cross-side teardown for TG-originated calls:
|
|
// inbound <reject>/<retract>/session-terminate -> observer -> bridge.Terminate
|
|
|
|
func TestCallerOnTerminated_RejectedReason_TearsDownTGCalleeWithDecline(t *testing.T) {
|
|
caller, tgSide := newStartedCaller(t)
|
|
caller.OnTerminated("rejected")
|
|
|
|
waitUntil(t, "tg-Callee.Terminate", time.Second, tgSide.isTerminated)
|
|
if tgSide.reason() != signaling.ReasonDecline {
|
|
t.Errorf("tg-side reason = %v, want ReasonDecline", tgSide.reason())
|
|
}
|
|
}
|
|
|
|
// Caller bound to a TG-originated bridge, started so the session is in
|
|
// StateProposed; returns the Caller and the tg-side stand-in
|
|
func newStartedCaller(t *testing.T) (*Caller, *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),
|
|
})
|
|
caller, err := a.NewCaller("alice@xmpp.example", 12345)
|
|
if err != nil {
|
|
t.Fatalf("NewCaller: %v", err)
|
|
}
|
|
tgSide := &recBridgeSide{}
|
|
br := signaling.New(signaling.Config{
|
|
Caller: caller, Callee: tgSide, Timers: noopTimers{},
|
|
})
|
|
caller.Bind(br)
|
|
br.Start()
|
|
waitUntil(t, "session proposed", time.Second, func() bool {
|
|
s := caller.sessionRef()
|
|
return s != nil && s.State() == jingle.StateProposed
|
|
})
|
|
return caller, tgSide
|
|
}
|