Files
go-sip/internal/asterisk/call_test.go
T

218 lines
8.8 KiB
Go

package asterisk
import (
"context"
"errors"
"net"
"strings"
"testing"
"time"
"github.com/CyCoreSystems/ari/v5"
)
type testARIClient struct {
ari.Client
channels *testChannels
bridges *testBridges
bus *testBus
closed int
}
func (c *testARIClient) Channel() ari.Channel { return c.channels }
func (c *testARIClient) Bridge() ari.Bridge { return c.bridges }
func (c *testARIClient) Bus() ari.Bus { return c.bus }
func (c *testARIClient) Close() { c.closed++ }
type testBus struct {
ari.Bus
sub ari.Subscription
}
func (b *testBus) Subscribe(_ *ari.Key, _ ...string) ari.Subscription { return b.sub }
type testChannels struct {
ari.Channel
events chan ari.Event
originate ari.OriginateRequest
media ari.ExternalMediaOptions
issued int
originateErr error
externalErr error
hungup []string
mediaPeerIP string
}
func (c *testChannels) Originate(_ *ari.Key, request ari.OriginateRequest) (*ari.ChannelHandle, error) {
c.issued++
c.originate = request
if c.originateErr != nil {
return nil, c.originateErr
}
if c.events != nil {
c.events <- &ari.StasisStart{EventData: ari.EventData{Type: "StasisStart"}, Channel: ari.ChannelData{ID: request.ChannelID}}
}
return ari.NewChannelHandle(ari.NewKey(ari.ChannelKey, request.ChannelID), c, nil), nil
}
func (c *testChannels) Get(key *ari.Key) *ari.ChannelHandle { return ari.NewChannelHandle(key, c, nil) }
func (c *testChannels) ExternalMedia(key *ari.Key, options ari.ExternalMediaOptions) (*ari.ChannelHandle, error) {
c.media = options
if c.externalErr != nil {
return nil, c.externalErr
}
return ari.NewChannelHandle(key, c, nil), nil
}
func (c *testChannels) GetVariable(_ *ari.Key, name string) (string, error) {
switch name {
case "UNICASTRTP_LOCAL_ADDRESS":
return c.mediaPeerIP, nil
case "UNICASTRTP_LOCAL_PORT":
return "17000", nil
}
return "", errors.New("unexpected ARI variable")
}
func (c *testChannels) Hangup(key *ari.Key, _ string) error {
c.hungup = append(c.hungup, key.ID)
return nil
}
type testBridges struct {
ari.Bridge
attached []string
deleted int
createErr error
}
func (b *testBridges) Create(key *ari.Key, _, _ string) (*ari.BridgeHandle, error) {
if b.createErr != nil {
return nil, b.createErr
}
return ari.NewBridgeHandle(key, b, nil), nil
}
func (b *testBridges) Get(key *ari.Key) *ari.BridgeHandle { return ari.NewBridgeHandle(key, b, nil) }
func (b *testBridges) AddChannel(_ *ari.Key, channelID string) error {
b.attached = append(b.attached, channelID)
return nil
}
func (b *testBridges) Delete(_ *ari.Key) error { b.deleted++; return nil }
func TestNativeCallUsesOneARIOriginateAndActualRTPBridge(t *testing.T) {
events := make(chan ari.Event, 2)
client := &testARIClient{
channels: &testChannels{events: events, mediaPeerIP: "127.0.0.1"},
bridges: &testBridges{},
bus: &testBus{sub: answerEvents{events: events}},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
call, err := dialWithClient(ctx, client, NativeDial{
ExecutionID: "exec-1", TrunkID: "shuqi", DialedCallee: "708915000000001", CallerID: "BD1234",
AnswerTimeout: time.Second, MediaPayloadType: 118,
})
if err != nil {
t.Fatal(err)
}
if call.Media == nil || client.channels.issued != 1 || client.channels.originate.Endpoint != "PJSIP/708915000000001@shuqi" ||
client.channels.originate.CallerID != "BD1234" || client.channels.originate.ChannelID != "exec-1" ||
client.channels.media.Format != "slin16" || client.channels.media.App != "go-sip-agent" ||
len(client.bridges.attached) != 2 || client.bridges.attached[0] != "exec-1" {
t.Fatal("native channel/ExternalMedia was not attached exactly once to the approved bridge")
}
if host, _, err := net.SplitHostPort(client.channels.media.ExternalHost); err != nil || host != "127.0.0.1" {
t.Fatal("native RTP capture must bind the local Cell only")
}
if err := call.Close(); err != nil || len(client.channels.hungup) != 2 || client.bridges.deleted != 1 || client.closed != 1 {
t.Fatalf("native call resources must be closed exactly once: err=%v", err)
}
}
func TestNativeCallStopsMediaOnMatchingCarrierHangup(t *testing.T) {
events := make(chan ari.Event, 3)
client := &testARIClient{channels: &testChannels{events: events, mediaPeerIP: "127.0.0.1"}, bridges: &testBridges{}, bus: &testBus{sub: answerEvents{events: events}}}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
call, err := dialWithClient(ctx, client, NativeDial{ExecutionID: "exec-1", TrunkID: "shuqi", DialedCallee: "708915000000001", CallerID: "BD1234", AnswerTimeout: time.Second, MediaPayloadType: 118})
if err != nil {
t.Fatal(err)
}
defer call.Close()
events <- &ari.ChannelDestroyed{EventData: ari.EventData{Type: "ChannelDestroyed"}, Channel: ari.ChannelData{ID: "someone-else"}, Cause: 16}
select {
case <-call.Context.Done():
t.Fatal("another caller cannot cancel this approved call")
case <-time.After(10 * time.Millisecond):
}
events <- &ari.ChannelDestroyed{EventData: ari.EventData{Type: "ChannelDestroyed"}, Channel: ari.ChannelData{ID: "exec-1"}, Cause: 16}
select {
case <-call.Context.Done():
if cause := context.Cause(call.Context); cause == nil || !strings.Contains(cause.Error(), "channel ended") {
t.Fatalf("real call must expose carrier hangup cause, got %v", cause)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("carrier hangup left real AI/media running")
}
if err := call.Close(); err != nil || len(client.channels.hungup) != 1 || client.channels.hungup[0] != "exec-1-media" {
t.Fatalf("destroyed carrier channel must not be hung up again: hungup=%v err=%v", client.channels.hungup, err)
}
}
func TestNativeCallUnknownOriginationMustAttemptHangupWithoutRetry(t *testing.T) {
client := &testARIClient{
channels: &testChannels{originateErr: errors.New("connection lost during originate")},
bridges: &testBridges{},
bus: &testBus{sub: answerEvents{events: make(chan ari.Event)}},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if _, err := dialWithClient(ctx, client, NativeDial{ExecutionID: "exec-1", TrunkID: "shuqi", DialedCallee: "708915000000001", CallerID: "BD1234", AnswerTimeout: time.Second, MediaPayloadType: 118}); err == nil || !strings.Contains(err.Error(), "outcome unknown") {
t.Fatalf("unknown originate outcome must never be reported as a completed call: %v", err)
}
if client.channels.issued != 1 || len(client.channels.hungup) != 1 || client.channels.hungup[0] != "exec-1" || client.closed != 1 {
t.Fatal("unknown originate outcome must not dial twice and must attempt to stop the issued channel")
}
}
func TestNativeCallUnknownBridgeOrMediaCreationCleansKnownIdentities(t *testing.T) {
for _, step := range []string{"bridge", "external-media"} {
t.Run(step, func(t *testing.T) {
events := make(chan ari.Event, 1)
channels := &testChannels{events: events, mediaPeerIP: "127.0.0.1"}
bridges := &testBridges{}
if step == "bridge" {
bridges.createErr = errors.New("lost bridge response")
} else {
channels.externalErr = errors.New("lost external-media response")
}
client := &testARIClient{channels: channels, bridges: bridges, bus: &testBus{sub: answerEvents{events: events}}}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if _, err := dialWithClient(ctx, client, NativeDial{ExecutionID: "exec-1", TrunkID: "shuqi", DialedCallee: "708915000000001", CallerID: "BD1234", AnswerTimeout: time.Second, MediaPayloadType: 118}); err == nil {
t.Fatal("unknown bridge/media outcome cannot be treated as a live audio session")
}
wantHungup := 1
if step == "external-media" {
wantHungup = 2
}
if channels.issued != 1 || len(channels.hungup) != wantHungup || bridges.deleted != 1 || client.closed != 1 {
t.Fatalf("known ARI identities must be cleaned without a second dial: hungup=%v deleted=%d closed=%d", channels.hungup, bridges.deleted, client.closed)
}
})
}
}
func TestNativeCallAnswerTimeoutNeverCreatesFakeMedia(t *testing.T) {
client := &testARIClient{
channels: &testChannels{mediaPeerIP: "127.0.0.1"},
bridges: &testBridges{},
bus: &testBus{sub: answerEvents{events: make(chan ari.Event)}},
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
if _, err := dialWithClient(ctx, client, NativeDial{ExecutionID: "exec-1", TrunkID: "shuqi", DialedCallee: "708915000000001", CallerID: "BD1234", AnswerTimeout: time.Second, MediaPayloadType: 118}); err == nil || !strings.Contains(err.Error(), "deadline") {
t.Fatalf("unanswered channel must fail rather than synthesize media: %v", err)
}
if client.channels.issued != 1 || len(client.channels.hungup) == 0 || client.closed != 1 {
t.Fatal("timed-out real channel cannot leak or be originated a second time")
}
}