Skip to content

Commit cbbf048

Browse files
committed
adding test
1 parent 9e46651 commit cbbf048

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed

pkg/sip/service_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,3 +782,147 @@ func TestCANCELSendsBothResponses(t *testing.T) {
782782
// Verify we received the critical 487 response
783783
require.True(t, invite487Received, "Should have received 487 Request Terminated response to INVITE when CANCEL is sent")
784784
}
785+
786+
// TestSameCallIDForAuthFlow verifies that the same LiveKit call ID is assigned to both
787+
// the initial INVITE (without auth) and the subsequent INVITE (with auth)
788+
func TestSameCallIDForAuthFlow(t *testing.T) {
789+
const (
790+
fromUser = "test@example.com"
791+
toUser = "agent@example.com"
792+
username = "testuser"
793+
password = "testpass"
794+
callID = "same-call-id@test.com"
795+
)
796+
797+
var capturedCallIDs []string
798+
var mu sync.Mutex
799+
800+
h := &TestHandler{
801+
GetAuthCredentialsFunc: func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
802+
// Capture the LiveKit call ID from the first request
803+
mu.Lock()
804+
capturedCallIDs = append(capturedCallIDs, call.LkCallId)
805+
mu.Unlock()
806+
807+
return AuthInfo{
808+
Result: AuthPassword,
809+
Username: username,
810+
Password: password,
811+
}, nil
812+
},
813+
DispatchCallFunc: func(ctx context.Context, info *CallInfo) CallDispatch {
814+
return CallDispatch{
815+
Result: DispatchNoRuleReject,
816+
// No room config needed for reject
817+
}
818+
},
819+
OnSessionEndFunc: func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
820+
// No-op for tests to avoid async logging issues
821+
},
822+
}
823+
824+
// Create service with authentication enabled
825+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
826+
localIP, err := config.GetLocalIP()
827+
require.NoError(t, err)
828+
829+
sipServerAddress := fmt.Sprintf("%s:%d", localIP, sipPort)
830+
831+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
832+
require.NoError(t, err)
833+
834+
// Use a no-op logger to avoid panics from async logging after test completion
835+
log := logger.LogRLogger(logr.Discard())
836+
s, err := NewService("", &config.Config{
837+
HideInboundPort: false, // Enable authentication
838+
SIPPort: sipPort,
839+
SIPPortListen: sipPort,
840+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
841+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
842+
require.NoError(t, err)
843+
require.NotNil(t, s)
844+
t.Cleanup(s.Stop)
845+
846+
s.SetHandler(h)
847+
require.NoError(t, s.Start())
848+
849+
sipUserAgent, err := sipgo.NewUA(
850+
sipgo.WithUserAgent(fromUser),
851+
sipgo.WithUserAgentLogger(slog.New(logger.ToSlogHandler(s.log))),
852+
)
853+
require.NoError(t, err)
854+
855+
sipClient, err := sipgo.NewClient(sipUserAgent)
856+
require.NoError(t, err)
857+
858+
offer, err := sdp.NewOffer(localIP, 0xB0B, sdp.EncryptionNone)
859+
require.NoError(t, err)
860+
offerData, err := offer.SDP.Marshal()
861+
require.NoError(t, err)
862+
863+
// Create first INVITE request (without auth)
864+
inviteRecipient := sip.Uri{User: toUser, Host: sipServerAddress}
865+
inviteRequest1 := sip.NewRequest(sip.INVITE, inviteRecipient)
866+
inviteRequest1.SetDestination(sipServerAddress)
867+
inviteRequest1.SetBody(offerData)
868+
inviteRequest1.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
869+
inviteRequest1.AppendHeader(sip.NewHeader("Call-ID", callID))
870+
871+
tx1, err := sipClient.TransactionRequest(inviteRequest1)
872+
require.NoError(t, err)
873+
t.Cleanup(tx1.Terminate)
874+
875+
// Should receive 100 Trying first, then 407 Unauthorized
876+
res1 := getResponseOrFail(t, tx1)
877+
require.Equal(t, sip.StatusCode(100), res1.StatusCode, "First request should receive 100 Trying")
878+
res1 = getResponseOrFail(t, tx1)
879+
require.Equal(t, sip.StatusCode(407), res1.StatusCode, "First request should receive 407 Unauthorized")
880+
881+
// Get the challenge from first response
882+
authHeader1 := res1.GetHeader("Proxy-Authenticate")
883+
require.NotNil(t, authHeader1, "First response should have Proxy-Authenticate header")
884+
challenge1 := authHeader1.Value()
885+
886+
// Parse the challenge to extract nonce and realm
887+
challenge, err := digest.ParseChallenge(challenge1)
888+
require.NoError(t, err, "Should be able to parse challenge")
889+
890+
// Compute the digest response using the challenge and credentials
891+
cred, err := digest.Digest(challenge, digest.Options{
892+
Method: "INVITE",
893+
URI: inviteRecipient.String(),
894+
Username: username,
895+
Password: password,
896+
})
897+
require.NoError(t, err, "Should be able to compute digest response")
898+
899+
// Create second INVITE request (with auth) using the SAME Call-ID
900+
inviteRequest2 := sip.NewRequest(sip.INVITE, inviteRecipient)
901+
inviteRequest2.SetDestination(sipServerAddress)
902+
inviteRequest2.SetBody(offerData)
903+
inviteRequest2.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
904+
inviteRequest2.AppendHeader(sip.NewHeader("Call-ID", callID))
905+
inviteRequest2.AppendHeader(sip.NewHeader("Proxy-Authorization", cred.String()))
906+
907+
tx2, err := sipClient.TransactionRequest(inviteRequest2)
908+
require.NoError(t, err)
909+
t.Cleanup(tx2.Terminate)
910+
911+
// Should receive 100 Trying first, then proceed with authentication
912+
res2 := getResponseOrFail(t, tx2)
913+
require.Equal(t, sip.StatusCode(100), res2.StatusCode, "Second request should receive 100 Trying")
914+
915+
// Wait a bit for the handler to be called
916+
time.Sleep(100 * time.Millisecond)
917+
918+
// Verify we captured exactly 2 call IDs
919+
mu.Lock()
920+
require.Len(t, capturedCallIDs, 2, "Should have captured 2 call IDs")
921+
require.Equal(t, capturedCallIDs[0], capturedCallIDs[1], "Both requests should have the same LiveKit call ID")
922+
require.NotEmpty(t, capturedCallIDs[0], "Call ID should not be empty")
923+
require.Contains(t, capturedCallIDs[0], "SCL_", "Call ID should have SCL_ prefix")
924+
mu.Unlock()
925+
926+
t.Logf("First call ID: %s", capturedCallIDs[0])
927+
t.Logf("Second call ID: %s", capturedCallIDs[1])
928+
}

0 commit comments

Comments
 (0)