@@ -59,9 +59,13 @@ func expectNoResponse(t *testing.T, tx sip.ClientTransaction) {
5959}
6060
6161type 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
6771func (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+
88118func (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