Add isolated SIP call tool, optional debug capture and split interface schemas
This commit is contained in:
@@ -44,10 +44,21 @@ func (l Loader) ariOptions() (*native.Options, error) {
|
||||
// OpenARI proves that the native server accepts this Agent credential without
|
||||
// creating a channel or dialing. The caller owns and must close the client.
|
||||
func (l Loader) OpenARI() (ari.Client, error) {
|
||||
return l.openARIApplication("go-sip-agent")
|
||||
}
|
||||
|
||||
// OpenLineTestARI owns separate subscriptions and never steals the business
|
||||
// application's WebSocket. It does not originate a channel.
|
||||
func (l Loader) OpenLineTestARI() (ari.Client, error) {
|
||||
return l.openARIApplication("sip-call")
|
||||
}
|
||||
|
||||
func (l Loader) openARIApplication(application string) (ari.Client, error) {
|
||||
options, err := l.ariOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.Application = application
|
||||
client, err := native.Connect(options)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connect native Agent ARI: %T", err)
|
||||
|
||||
@@ -17,8 +17,9 @@ import (
|
||||
"github.com/CyCoreSystems/ari/v5/client/native"
|
||||
)
|
||||
|
||||
// NativeDial contains only the Dispatcher-selected SIP identity and the
|
||||
// Agent's explicitly configured SLIN16 RTP payload profile.
|
||||
// NativeDial contains explicit SIP identity and the business Agent's SLIN16
|
||||
// profile. Business calls use Dispatcher-selected values; isolated line tests
|
||||
// use their declared ENV identity and omit media resources.
|
||||
type NativeDial struct {
|
||||
ExecutionID, TrunkID, DialedCallee, CallerID string
|
||||
AnswerTimeout time.Duration
|
||||
@@ -118,8 +119,13 @@ func (l Loader) Originate(ctx context.Context, request NativeDial) (*NativeCall,
|
||||
return dialWithClient(ctx, client, request)
|
||||
}
|
||||
|
||||
func dialWithClient(ctx context.Context, client ari.Client, request NativeDial) (_ *NativeCall, err error) {
|
||||
call := &NativeCall{client: client}
|
||||
func dialWithClient(ctx context.Context, client ari.Client, request NativeDial) (*NativeCall, error) {
|
||||
return dialWithOptions(ctx, client, request, false)
|
||||
}
|
||||
|
||||
// lineTest omits media/AI resources and uses a distinct ARI application.
|
||||
func dialWithOptions(ctx context.Context, client ari.Client, request NativeDial, lineTest bool) (_ *NativeCall, err error) {
|
||||
call := &NativeCall{client: client, verifyEnd: lineTest}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
cleanupErr := call.Close()
|
||||
@@ -141,9 +147,13 @@ func dialWithClient(ctx context.Context, client ari.Client, request NativeDial)
|
||||
if request.BindSIPCall == nil {
|
||||
return nil, &NativeCallFailure{Phase: "validate", Cause: errors.New("HEP SIP call binding is required before native Dial")}
|
||||
}
|
||||
call.Media, err = media.ListenRTPWithFormat("127.0.0.1:0", request.MediaPayloadType, media.FormatSLIN16, 16000)
|
||||
if err != nil {
|
||||
return nil, &NativeCallFailure{Phase: "rtp_listen", Cause: err}
|
||||
if lineTest {
|
||||
originate.App = "sip-call"
|
||||
} else {
|
||||
call.Media, err = media.ListenRTPWithFormat("127.0.0.1:0", request.MediaPayloadType, media.FormatSLIN16, 16000)
|
||||
if err != nil {
|
||||
return nil, &NativeCallFailure{Phase: "rtp_listen", Cause: err}
|
||||
}
|
||||
}
|
||||
key := ari.NewKey(ari.ChannelKey, request.ExecutionID)
|
||||
call.subscription = client.Bus().Subscribe(key, "StasisStart", "ChannelStateChange", "StasisEnd", "ChannelHangupRequest", "ChannelDestroyed")
|
||||
@@ -179,6 +189,11 @@ func dialWithClient(ctx context.Context, client ari.Client, request NativeDial)
|
||||
call.verifyEnd = true // Created channel has no ExternalMedia or bridge yet.
|
||||
return nil, &NativeCallFailure{Phase: "answer_wait", Cause: err}
|
||||
}
|
||||
if lineTest {
|
||||
call.monitorDone = make(chan struct{})
|
||||
go call.watch(request.ExecutionID)
|
||||
return call, nil
|
||||
}
|
||||
bridgeKey := ari.NewKey(ari.BridgeKey, request.ExecutionID+"-bridge")
|
||||
call.bridge, err = client.Bridge().Create(bridgeKey, "mixing", "go-sip-agent")
|
||||
if err != nil {
|
||||
@@ -254,6 +269,12 @@ func (c *NativeCall) watch(channelID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// EndConfirmed reports an observed ChannelDestroyed or post-Hangup ARI 404.
|
||||
// An accepted Hangup request alone is not evidence of termination.
|
||||
func (c *NativeCall) EndConfirmed() bool {
|
||||
return c != nil && (c.destroyed.Load() || c.confirmedEnd)
|
||||
}
|
||||
|
||||
// Close releases each resource once. Errors remain visible to the caller; a
|
||||
// failed Hangup never silently turns an uncertain call into a success.
|
||||
func (c *NativeCall) Close() error {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package asterisk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/CyCoreSystems/ari/v5"
|
||||
)
|
||||
|
||||
// DialLineTest is the independent sip-call application's single native Dial.
|
||||
// The caller owns host-idle checks, temporary SIP configuration and optional
|
||||
// capture. No ExternalMedia, bridge, AI session or recording is created.
|
||||
func DialLineTest(ctx context.Context, client ari.Client, request NativeDial) (*NativeCall, error) {
|
||||
return dialWithOptions(ctx, client, request, true)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package asterisk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/CyCoreSystems/ari/v5"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLineTestUsesIsolatedApplicationAndNoMediaOrAI(t *testing.T) {
|
||||
events := make(chan ari.Event, 3)
|
||||
channels := &testChannels{events: events, dataErr: nativeHTTPStatusError{code: 404}}
|
||||
client := &testARIClient{channels: channels, bridges: &testBridges{}, bus: &testBus{sub: answerEvents{events: events}}}
|
||||
call, err := dialWithOptions(context.Background(), client, NativeDial{ExecutionID: "siptest-1", TrunkID: "siptest-shuqi", DialedCallee: "708918601010101", CallerID: "BD123", AnswerTimeout: time.Second, BindSIPCall: testBindSIPCall}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if channels.issued != 1 || channels.create.App != "sip-call" || call.Media != nil || len(client.bridges.attached) != 0 || channels.media.App != "" {
|
||||
t.Fatal("line test must send one Dial without ExternalMedia, bridge, or business application")
|
||||
}
|
||||
if err = call.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !call.EndConfirmed() {
|
||||
t.Fatal("teardown must be confirmed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLineTestDoesNotInventConfirmedTermination(t *testing.T) {
|
||||
events := make(chan ari.Event, 3)
|
||||
client := &testARIClient{channels: &testChannels{events: events}, bridges: &testBridges{}, bus: &testBus{sub: answerEvents{events: events}}}
|
||||
call, err := dialWithOptions(context.Background(), client, NativeDial{ExecutionID: "siptest-1", TrunkID: "siptest-shuqi", DialedCallee: "708918601010101", CallerID: "BD123", AnswerTimeout: time.Second, BindSIPCall: testBindSIPCall}, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = call.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if call.EndConfirmed() {
|
||||
t.Fatal("successful Hangup HTTP is not proof that the channel disappeared")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user