Skip to content

Commit 907c826

Browse files
committed
adding test
1 parent f54f156 commit 907c826

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
@@ -623,3 +623,147 @@ func TestDigestAuthStandardFlow(t *testing.T) {
623623
t.Logf("Second request got status: %d", res2.StatusCode)
624624
}
625625
}
626+
627+
// TestSameCallIDForAuthFlow verifies that the same LiveKit call ID is assigned to both
628+
// the initial INVITE (without auth) and the subsequent INVITE (with auth)
629+
func TestSameCallIDForAuthFlow(t *testing.T) {
630+
const (
631+
fromUser = "test@example.com"
632+
toUser = "agent@example.com"
633+
username = "testuser"
634+
password = "testpass"
635+
callID = "same-call-id@test.com"
636+
)
637+
638+
var capturedCallIDs []string
639+
var mu sync.Mutex
640+
641+
h := &TestHandler{
642+
GetAuthCredentialsFunc: func(ctx context.Context, call *rpc.SIPCall) (AuthInfo, error) {
643+
// Capture the LiveKit call ID from the first request
644+
mu.Lock()
645+
capturedCallIDs = append(capturedCallIDs, call.LkCallId)
646+
mu.Unlock()
647+
648+
return AuthInfo{
649+
Result: AuthPassword,
650+
Username: username,
651+
Password: password,
652+
}, nil
653+
},
654+
DispatchCallFunc: func(ctx context.Context, info *CallInfo) CallDispatch {
655+
return CallDispatch{
656+
Result: DispatchNoRuleReject,
657+
// No room config needed for reject
658+
}
659+
},
660+
OnSessionEndFunc: func(ctx context.Context, callIdentifier *CallIdentifier, callInfo *livekit.SIPCallInfo, reason string) {
661+
// No-op for tests to avoid async logging issues
662+
},
663+
}
664+
665+
// Create service with authentication enabled
666+
sipPort := rand.Intn(testPortSIPMax-testPortSIPMin) + testPortSIPMin
667+
localIP, err := config.GetLocalIP()
668+
require.NoError(t, err)
669+
670+
sipServerAddress := fmt.Sprintf("%s:%d", localIP, sipPort)
671+
672+
mon, err := stats.NewMonitor(&config.Config{MaxCpuUtilization: 0.9})
673+
require.NoError(t, err)
674+
675+
// Use a no-op logger to avoid panics from async logging after test completion
676+
log := logger.LogRLogger(logr.Discard())
677+
s, err := NewService("", &config.Config{
678+
HideInboundPort: false, // Enable authentication
679+
SIPPort: sipPort,
680+
SIPPortListen: sipPort,
681+
RTPPort: rtcconfig.PortRange{Start: testPortRTPMin, End: testPortRTPMax},
682+
}, mon, log, func(projectID string) rpc.IOInfoClient { return nil })
683+
require.NoError(t, err)
684+
require.NotNil(t, s)
685+
t.Cleanup(s.Stop)
686+
687+
s.SetHandler(h)
688+
require.NoError(t, s.Start())
689+
690+
sipUserAgent, err := sipgo.NewUA(
691+
sipgo.WithUserAgent(fromUser),
692+
sipgo.WithUserAgentLogger(slog.New(logger.ToSlogHandler(s.log))),
693+
)
694+
require.NoError(t, err)
695+
696+
sipClient, err := sipgo.NewClient(sipUserAgent)
697+
require.NoError(t, err)
698+
699+
offer, err := sdp.NewOffer(localIP, 0xB0B, sdp.EncryptionNone)
700+
require.NoError(t, err)
701+
offerData, err := offer.SDP.Marshal()
702+
require.NoError(t, err)
703+
704+
// Create first INVITE request (without auth)
705+
inviteRecipient := sip.Uri{User: toUser, Host: sipServerAddress}
706+
inviteRequest1 := sip.NewRequest(sip.INVITE, inviteRecipient)
707+
inviteRequest1.SetDestination(sipServerAddress)
708+
inviteRequest1.SetBody(offerData)
709+
inviteRequest1.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
710+
inviteRequest1.AppendHeader(sip.NewHeader("Call-ID", callID))
711+
712+
tx1, err := sipClient.TransactionRequest(inviteRequest1)
713+
require.NoError(t, err)
714+
t.Cleanup(tx1.Terminate)
715+
716+
// Should receive 100 Trying first, then 407 Unauthorized
717+
res1 := getResponseOrFail(t, tx1)
718+
require.Equal(t, sip.StatusCode(100), res1.StatusCode, "First request should receive 100 Trying")
719+
res1 = getResponseOrFail(t, tx1)
720+
require.Equal(t, sip.StatusCode(407), res1.StatusCode, "First request should receive 407 Unauthorized")
721+
722+
// Get the challenge from first response
723+
authHeader1 := res1.GetHeader("Proxy-Authenticate")
724+
require.NotNil(t, authHeader1, "First response should have Proxy-Authenticate header")
725+
challenge1 := authHeader1.Value()
726+
727+
// Parse the challenge to extract nonce and realm
728+
challenge, err := digest.ParseChallenge(challenge1)
729+
require.NoError(t, err, "Should be able to parse challenge")
730+
731+
// Compute the digest response using the challenge and credentials
732+
cred, err := digest.Digest(challenge, digest.Options{
733+
Method: "INVITE",
734+
URI: inviteRecipient.String(),
735+
Username: username,
736+
Password: password,
737+
})
738+
require.NoError(t, err, "Should be able to compute digest response")
739+
740+
// Create second INVITE request (with auth) using the SAME Call-ID
741+
inviteRequest2 := sip.NewRequest(sip.INVITE, inviteRecipient)
742+
inviteRequest2.SetDestination(sipServerAddress)
743+
inviteRequest2.SetBody(offerData)
744+
inviteRequest2.AppendHeader(sip.NewHeader("Content-Type", "application/sdp"))
745+
inviteRequest2.AppendHeader(sip.NewHeader("Call-ID", callID))
746+
inviteRequest2.AppendHeader(sip.NewHeader("Proxy-Authorization", cred.String()))
747+
748+
tx2, err := sipClient.TransactionRequest(inviteRequest2)
749+
require.NoError(t, err)
750+
t.Cleanup(tx2.Terminate)
751+
752+
// Should receive 100 Trying first, then proceed with authentication
753+
res2 := getResponseOrFail(t, tx2)
754+
require.Equal(t, sip.StatusCode(100), res2.StatusCode, "Second request should receive 100 Trying")
755+
756+
// Wait a bit for the handler to be called
757+
time.Sleep(100 * time.Millisecond)
758+
759+
// Verify we captured exactly 2 call IDs
760+
mu.Lock()
761+
require.Len(t, capturedCallIDs, 2, "Should have captured 2 call IDs")
762+
require.Equal(t, capturedCallIDs[0], capturedCallIDs[1], "Both requests should have the same LiveKit call ID")
763+
require.NotEmpty(t, capturedCallIDs[0], "Call ID should not be empty")
764+
require.Contains(t, capturedCallIDs[0], "SCL_", "Call ID should have SCL_ prefix")
765+
mu.Unlock()
766+
767+
t.Logf("First call ID: %s", capturedCallIDs[0])
768+
t.Logf("Second call ID: %s", capturedCallIDs[1])
769+
}

0 commit comments

Comments
 (0)