Skip to content

Commit c76eef7

Browse files
committed
Add tests for Hold/Unhold service
1 parent 29fe1b7 commit c76eef7

File tree

1 file changed

+379
-3
lines changed

1 file changed

+379
-3
lines changed

pkg/sip/service_test.go

Lines changed: 379 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ func expectNoResponse(t *testing.T, tx sip.ClientTransaction) {
5959
}
6060

6161
type TestHandler struct {
62-
GetAuthCredentialsFunc func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error)
63-
DispatchCallFunc func(ctx context.Context, info *CallInfo) CallDispatch
64-
OnSessionEndFunc func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string)
62+
GetAuthCredentialsFunc func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error)
63+
DispatchCallFunc func(ctx context.Context, info *CallInfo) CallDispatch
64+
OnSessionEndFunc func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string)
65+
RegisterHoldSIPParticipantTopicFunc func(sipCallId string) error
66+
DeregisterHoldSIPParticipantTopicFunc func(sipCallId string)
67+
RegisterUnholdSIPParticipantTopicFunc func(sipCallId string) error
68+
DeregisterUnholdSIPParticipantTopicFunc func(sipCallId string)
6569
}
6670

6771
func (h TestHandler) GetAuthCredentials(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
@@ -85,6 +89,32 @@ func (h TestHandler) DeregisterTransferSIPParticipantTopic(sipCallId string) {
8589
// no-op
8690
}
8791

92+
func (h TestHandler) RegisterHoldSIPParticipantTopic(sipCallId string) error {
93+
if h.RegisterHoldSIPParticipantTopicFunc != nil {
94+
return h.RegisterHoldSIPParticipantTopicFunc(sipCallId)
95+
}
96+
return nil
97+
}
98+
99+
func (h TestHandler) DeregisterHoldSIPParticipantTopic(sipCallId string) {
100+
if h.DeregisterHoldSIPParticipantTopicFunc != nil {
101+
h.DeregisterHoldSIPParticipantTopicFunc(sipCallId)
102+
}
103+
}
104+
105+
func (h TestHandler) RegisterUnholdSIPParticipantTopic(sipCallId string) error {
106+
if h.RegisterUnholdSIPParticipantTopicFunc != nil {
107+
return h.RegisterUnholdSIPParticipantTopicFunc(sipCallId)
108+
}
109+
return nil
110+
}
111+
112+
func (h TestHandler) DeregisterUnholdSIPParticipantTopic(sipCallId string) {
113+
if h.DeregisterUnholdSIPParticipantTopicFunc != nil {
114+
h.DeregisterUnholdSIPParticipantTopicFunc(sipCallId)
115+
}
116+
}
117+
88118
func (h TestHandler) OnSessionEnd(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
89119
if h.OnSessionEndFunc != nil {
90120
h.OnSessionEndFunc(ctx, callIdentifier, callInfo, reason)
@@ -623,3 +653,349 @@ func TestDigestAuthStandardFlow(t *testing.T) {
623653
t.Logf("Second request got status: %d", res2.StatusCode)
624654
}
625655
}
656+
657+
func TestService_HoldSIPParticipant_Success(t *testing.T) {
658+
const (
659+
expectedFromUser = "hold-test-user"
660+
expectedToUser = "hold-test-target"
661+
callID = "hold-test-call-id"
662+
)
663+
664+
holdRegistered := make(chan string, 1)
665+
unholdRegistered := make(chan string, 1)
666+
667+
h := &TestHandler{
668+
GetAuthCredentialsFunc: func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
669+
return AuthInfo{Result: AuthAccept}, nil
670+
},
671+
DispatchCallFunc: func(ctx context.Context, info *CallInfo) CallDispatch {
672+
return CallDispatch{
673+
Result: DispatchAccept,
674+
Room: RoomConfig{
675+
RoomName: "test-room",
676+
Participant: ParticipantConfig{
677+
Identity: "test-identity",
678+
Name: "test-participant",
679+
Metadata: "test-metadata",
680+
},
681+
},
682+
ProjectID: "test-project",
683+
}
684+
},
685+
RegisterHoldSIPParticipantTopicFunc: func(sipCallId string) error {
686+
holdRegistered <- sipCallId
687+
return nil
688+
},
689+
RegisterUnholdSIPParticipantTopicFunc: func(sipCallId string) error {
690+
unholdRegistered <- sipCallId
691+
return nil
692+
},
693+
}
694+
695+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
696+
localIP, err := config.GetLocalIP()
697+
require.NoError(t, err)
698+
699+
sipServerAddress := fmt.Sprintf("%s:%d", localIP, sipPort)
700+
701+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
702+
require.NoError(t, err)
703+
704+
log := logger.LogRLogger(logr.Discard())
705+
s, err := NewService("", &config.Config{
706+
HideInboundPort: false,
707+
SIPPort: sipPort,
708+
SIPPortListen: sipPort,
709+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
710+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
711+
require.NoError(t, err)
712+
require.NotNil(t, s)
713+
t.Cleanup(s.Stop)
714+
715+
s.SetHandler(h)
716+
require.NoError(t, s.Start())
717+
718+
// Establish a call first
719+
sipUserAgent, err := sipgo.NewUA(
720+
sipgo.WithUserAgent(expectedFromUser),
721+
sipgo.WithUserAgentLogger(slog.New(logger.ToSlogHandler(s.log))),
722+
)
723+
require.NoError(t, err)
724+
725+
sipClient, err := sipgo.NewClient(sipUserAgent)
726+
require.NoError(t, err)
727+
728+
offer, err := sdp.NewOffer(localIP, 0xB0B, sdp.EncryptionNone)
729+
require.NoError(t, err)
730+
offerData, err := offer.SDP.Marshal()
731+
require.NoError(t, err)
732+
733+
inviteRecipient := sip.Uri{User: expectedToUser, Host: sipServerAddress}
734+
inviteRequest := sip.NewRequest(sip.INVITE, inviteRecipient)
735+
inviteRequest.SetDestination(sipServerAddress)
736+
inviteRequest.SetBody(offerData)
737+
inviteRequest.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
738+
inviteRequest.AppendHeader(sip.NewHeader("Call-ID", callID))
739+
740+
tx, err := sipClient.TransactionRequest(inviteRequest)
741+
require.NoError(t, err)
742+
t.Cleanup(tx.Terminate)
743+
744+
// Wait for call establishment
745+
res := getResponseOrFail(t, tx)
746+
require.Equal(t, sip.StatusCode(100), res.StatusCode)
747+
748+
res = getResponseOrFail(t, tx)
749+
require.Equal(t, sip.StatusCode(200), res.StatusCode)
750+
751+
// Wait for hold/unhold registration
752+
select {
753+
case sipCallId := <-holdRegistered:
754+
require.NotEmpty(t, sipCallId)
755+
case <-time.After(2 * time.Second):
756+
t.Fatal("Timeout waiting for hold registration")
757+
}
758+
759+
select {
760+
case sipCallId := <-unholdRegistered:
761+
require.NotEmpty(t, sipCallId)
762+
case <-time.After(2 * time.Second):
763+
t.Fatal("Timeout waiting for unhold registration")
764+
}
765+
766+
// Test hold functionality
767+
holdReq := &rpc.InternalHoldSIPParticipantRequest{
768+
SipCallId: callID,
769+
}
770+
771+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
772+
defer cancel()
773+
774+
holdResp, err := s.HoldSIPParticipant(ctx, holdReq)
775+
require.NoError(t, err)
776+
require.NotNil(t, holdResp)
777+
778+
// Test unhold functionality
779+
unholdReq := &rpc.InternalUnholdSIPParticipantRequest{
780+
SipCallId: callID,
781+
}
782+
783+
unholdResp, err := s.UnholdSIPParticipant(ctx, unholdReq)
784+
require.NoError(t, err)
785+
require.NotNil(t, unholdResp)
786+
}
787+
788+
func TestService_HoldSIPParticipant_NotFound(t *testing.T) {
789+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
790+
791+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
792+
require.NoError(t, err)
793+
794+
log := logger.LogRLogger(logr.Discard())
795+
s, err := NewService("", &config.Config{
796+
HideInboundPort: false,
797+
SIPPort: sipPort,
798+
SIPPortListen: sipPort,
799+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
800+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
801+
require.NoError(t, err)
802+
require.NotNil(t, s)
803+
t.Cleanup(s.Stop)
804+
805+
// Test hold with non-existent call
806+
holdReq := &rpc.InternalHoldSIPParticipantRequest{
807+
SipCallId: "non-existent-call-id",
808+
}
809+
810+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
811+
defer cancel()
812+
813+
holdResp, err := s.HoldSIPParticipant(ctx, holdReq)
814+
require.Error(t, err)
815+
require.Nil(t, holdResp)
816+
817+
// Verify it's a NotFound error
818+
require.Contains(t, err.Error(), "unknown call")
819+
}
820+
821+
func TestService_UnholdSIPParticipant_NotFound(t *testing.T) {
822+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
823+
824+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
825+
require.NoError(t, err)
826+
827+
log := logger.LogRLogger(logr.Discard())
828+
s, err := NewService("", &config.Config{
829+
HideInboundPort: false,
830+
SIPPort: sipPort,
831+
SIPPortListen: sipPort,
832+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
833+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
834+
require.NoError(t, err)
835+
require.NotNil(t, s)
836+
t.Cleanup(s.Stop)
837+
838+
// Test unhold with non-existent call
839+
unholdReq := &rpc.InternalUnholdSIPParticipantRequest{
840+
SipCallId: "non-existent-call-id",
841+
}
842+
843+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
844+
defer cancel()
845+
846+
unholdResp, err := s.UnholdSIPParticipant(ctx, unholdReq)
847+
require.Error(t, err)
848+
require.Nil(t, unholdResp)
849+
850+
// Verify it's a NotFound error
851+
require.Contains(t, err.Error(), "unknown call")
852+
}
853+
854+
func TestService_HoldUnhold_ContextCancellation(t *testing.T) {
855+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
856+
857+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
858+
require.NoError(t, err)
859+
860+
log := logger.LogRLogger(logr.Discard())
861+
s, err := NewService("", &config.Config{
862+
HideInboundPort: false,
863+
SIPPort: sipPort,
864+
SIPPortListen: sipPort,
865+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
866+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
867+
require.NoError(t, err)
868+
require.NotNil(t, s)
869+
t.Cleanup(s.Stop)
870+
871+
// Test with cancelled context
872+
ctx, cancel := context.WithCancel(context.Background())
873+
cancel() // Cancel immediately
874+
875+
holdReq := &rpc.InternalHoldSIPParticipantRequest{
876+
SipCallId: "test-call-id",
877+
}
878+
879+
holdResp, err := s.HoldSIPParticipant(ctx, holdReq)
880+
require.Error(t, err)
881+
require.Nil(t, holdResp)
882+
883+
unholdReq := &rpc.InternalUnholdSIPParticipantRequest{
884+
SipCallId: "test-call-id",
885+
}
886+
887+
unholdResp, err := s.UnholdSIPParticipant(ctx, unholdReq)
888+
require.Error(t, err)
889+
require.Nil(t, unholdResp)
890+
}
891+
892+
func TestService_HoldUnhold_RegistrationErrors(t *testing.T) {
893+
const (
894+
expectedFromUser = "hold-reg-error-user"
895+
expectedToUser = "hold-reg-error-target"
896+
callID = "hold-reg-error-call-id"
897+
)
898+
899+
h := &TestHandler{
900+
GetAuthCredentialsFunc: func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
901+
return AuthInfo{Result: AuthAccept}, nil
902+
},
903+
DispatchCallFunc: func(ctx context.Context, info *CallInfo) CallDispatch {
904+
return CallDispatch{
905+
Result: DispatchAccept,
906+
Room: RoomConfig{
907+
RoomName: "test-room",
908+
Participant: ParticipantConfig{
909+
Identity: "test-identity",
910+
Name: "test-participant",
911+
Metadata: "test-metadata",
912+
},
913+
},
914+
ProjectID: "test-project",
915+
}
916+
},
917+
RegisterHoldSIPParticipantTopicFunc: func(sipCallId string) error {
918+
return fmt.Errorf("hold registration failed")
919+
},
920+
RegisterUnholdSIPParticipantTopicFunc: func(sipCallId string) error {
921+
return fmt.Errorf("unhold registration failed")
922+
},
923+
}
924+
925+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
926+
localIP, err := config.GetLocalIP()
927+
require.NoError(t, err)
928+
929+
sipServerAddress := fmt.Sprintf("%s:%d", localIP, sipPort)
930+
931+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
932+
require.NoError(t, err)
933+
934+
log := logger.LogRLogger(logr.Discard())
935+
s, err := NewService("", &config.Config{
936+
HideInboundPort: false,
937+
SIPPort: sipPort,
938+
SIPPortListen: sipPort,
939+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
940+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
941+
require.NoError(t, err)
942+
require.NotNil(t, s)
943+
t.Cleanup(s.Stop)
944+
945+
s.SetHandler(h)
946+
require.NoError(t, s.Start())
947+
948+
// Establish a call first
949+
sipUserAgent, err := sipgo.NewUA(
950+
sipgo.WithUserAgent(expectedFromUser),
951+
sipgo.WithUserAgentLogger(slog.New(logger.ToSlogHandler(s.log))),
952+
)
953+
require.NoError(t, err)
954+
955+
sipClient, err := sipgo.NewClient(sipUserAgent)
956+
require.NoError(t, err)
957+
958+
offer, err := sdp.NewOffer(localIP, 0xB0B, sdp.EncryptionNone)
959+
require.NoError(t, err)
960+
offerData, err := offer.SDP.Marshal()
961+
require.NoError(t, err)
962+
963+
inviteRecipient := sip.Uri{User: expectedToUser, Host: sipServerAddress}
964+
inviteRequest := sip.NewRequest(sip.INVITE, inviteRecipient)
965+
inviteRequest.SetDestination(sipServerAddress)
966+
inviteRequest.SetBody(offerData)
967+
inviteRequest.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
968+
inviteRequest.AppendHeader(sip.NewHeader("Call-ID", callID))
969+
970+
tx, err := sipClient.TransactionRequest(inviteRequest)
971+
require.NoError(t, err)
972+
t.Cleanup(tx.Terminate)
973+
974+
// Wait for call establishment
975+
res := getResponseOrFail(t, tx)
976+
require.Equal(t, sip.StatusCode(100), res.StatusCode)
977+
978+
res = getResponseOrFail(t, tx)
979+
require.Equal(t, sip.StatusCode(200), res.StatusCode)
980+
981+
// Test hold functionality - should still work even if registration fails
982+
holdReq := &rpc.InternalHoldSIPParticipantRequest{
983+
SipCallId: callID,
984+
}
985+
986+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
987+
defer cancel()
988+
989+
holdResp, err := s.HoldSIPParticipant(ctx, holdReq)
990+
require.NoError(t, err)
991+
require.NotNil(t, holdResp)
992+
993+
// Test unhold functionality - should still work even if registration fails
994+
unholdReq := &rpc.InternalUnholdSIPParticipantRequest{
995+
SipCallId: callID,
996+
}
997+
998+
unholdResp, err := s.UnholdSIPParticipant(ctx, unholdReq)
999+
require.NoError(t, err)
1000+
require.NotNil(t, unholdResp)
1001+
}

0 commit comments

Comments
 (0)