feat: add agora rtc token join endpoint

This commit is contained in:
richjohnrambo
2026-09-02 01:05:15 +08:00
parent 318ce04f7d
commit dc5e09f2ae
9 changed files with 624 additions and 11 deletions
+231
View File
@@ -221,6 +221,16 @@ type testCallsStore struct {
next string
}
type testThirdPartyConfigStore map[string]string
func (s testThirdPartyConfigStore) Get(_ string, keys ...string) (map[string]string, error) {
result := make(map[string]string, len(keys))
for _, key := range keys {
result[key] = s[key]
}
return result, nil
}
type testConversationsStore struct {
topic string
settings *types.ConversationSettings
@@ -4741,6 +4751,227 @@ func TestServeIMTopicCallsRejectsUnauthorizedWithoutListing(t *testing.T) {
}
}
func TestServeIMCallRTCJoinReturnsAgoraAppIDMode(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-caller", 7, 42)
oldSubs := store.Subs
oldCalls := store.Calls
oldThirdParty := store.ThirdPartyConfig
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
calls := &testCallsStore{items: []types.CallRecord{{
ID: 9,
TenantID: 7,
CallID: "call-1",
Topic: "p2pTopic",
SeqID: 12,
CallerID: 42,
CalleeID: 43,
CallType: "video",
Status: types.CallStatusStarted,
StartedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
CreatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
}}}
store.Subs = subs
store.Calls = calls
store.ThirdPartyConfig = testThirdPartyConfigStore{
"agora_enabled": "1",
"agora_auth_mode": "app_id",
"agora_app_id": "app123",
"agora_channel_prefix": "call_",
"agora_uid_mode": "string",
"agora_token_ttl_seconds": "3600",
}
defer func() {
store.Subs = oldSubs
store.Calls = oldCalls
store.ThirdPartyConfig = oldThirdParty
}()
subs.EXPECT().Get(types.TenantID(7), "p2pTopic", types.Uid(42), false).Return(&types.Subscription{
TenantID: 7, User: types.Uid(42).String(), Topic: "p2pTopic", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"p2pTopic","call_id":"call-1","seq":12,"call_type":"video","role":"caller"}`))
req.Header.Set("Authorization", "Bearer token-caller")
rec := httptest.NewRecorder()
serveIMCallRTCJoin(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("rtc join status: got %d body=%s", rec.Code, rec.Body.String())
}
if !calls.listed || calls.tenant != 7 || calls.user != 42 || calls.topic != "p2pTopic" || calls.limit != 100 {
t.Fatalf("calls lookup mismatch: %#v", calls)
}
resp := decodeIMHTTPResponse(t, rec)
data := resp.Data.(map[string]any)
if data["provider"] != "agora" || data["auth_mode"] != "app_id" || data["app_id"] != "app123" ||
data["channel_name"] != "call_call-1" || data["uid"] != types.Uid(42).UserId() || data["token"] != "" {
t.Fatalf("unexpected rtc join payload: %#v", data)
}
}
func TestServeIMCallRTCJoinReturnsSameAgoraChannelForCallee(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
oldStore := store.Store
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth).AnyTimes()
tokenAuth.EXPECT().IsInitialized().Return(true).AnyTimes()
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-caller")).
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil).AnyTimes()
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-callee")).
Return(&auth.Rec{TenantID: 7, Uid: 43, AuthLevel: auth.LevelAuth}, nil, nil).AnyTimes()
store.Store = mockStore
oldSubs := store.Subs
oldCalls := store.Calls
oldThirdParty := store.ThirdPartyConfig
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
calls := &testCallsStore{items: []types.CallRecord{{
ID: 9,
TenantID: 7,
CallID: "call-1",
Topic: "p2pTopic",
SeqID: 12,
CallerID: 42,
CalleeID: 43,
CallType: "audio",
Status: types.CallStatusStarted,
StartedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
CreatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
}}}
store.Subs = subs
store.Calls = calls
store.ThirdPartyConfig = testThirdPartyConfigStore{
"agora_enabled": "1",
"agora_auth_mode": "app_id",
"agora_app_id": "app123",
"agora_channel_prefix": "call_",
"agora_uid_mode": "string",
"agora_token_ttl_seconds": "3600",
}
defer func() {
store.Store = oldStore
store.Subs = oldSubs
store.Calls = oldCalls
store.ThirdPartyConfig = oldThirdParty
}()
subs.EXPECT().Get(types.TenantID(7), "p2pTopic", types.Uid(42), false).Return(&types.Subscription{
TenantID: 7, User: types.Uid(42).String(), Topic: "p2pTopic", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
}, nil)
subs.EXPECT().Get(types.TenantID(7), "p2pTopic", types.Uid(43), false).Return(&types.Subscription{
TenantID: 7, User: types.Uid(43).String(), Topic: "p2pTopic", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
}, nil)
callerReq := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"p2pTopic","call_id":"call-1","seq":12,"call_type":"audio","role":"caller"}`))
callerReq.Header.Set("Authorization", "Bearer token-caller")
callerRec := httptest.NewRecorder()
serveIMCallRTCJoin(callerRec, callerReq)
if callerRec.Code != http.StatusOK {
t.Fatalf("caller rtc join status: got %d body=%s", callerRec.Code, callerRec.Body.String())
}
calleeReq := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"p2pTopic","call_id":"call-1","seq":12,"call_type":"audio","role":"callee"}`))
calleeReq.Header.Set("Authorization", "Bearer token-callee")
calleeRec := httptest.NewRecorder()
serveIMCallRTCJoin(calleeRec, calleeReq)
if calleeRec.Code != http.StatusOK {
t.Fatalf("callee rtc join status: got %d body=%s", calleeRec.Code, calleeRec.Body.String())
}
callerData := decodeIMHTTPResponse(t, callerRec).Data.(map[string]any)
calleeData := decodeIMHTTPResponse(t, calleeRec).Data.(map[string]any)
if callerData["channel_name"] != calleeData["channel_name"] {
t.Fatalf("channel mismatch: caller=%#v callee=%#v", callerData, calleeData)
}
if callerData["uid"] == calleeData["uid"] || calleeData["uid"] != types.Uid(43).UserId() {
t.Fatalf("unexpected rtc uid: caller=%#v callee=%#v", callerData, calleeData)
}
if callerData["token"] != "" || calleeData["token"] != "" {
t.Fatalf("app_id mode token must be empty: caller=%#v callee=%#v", callerData, calleeData)
}
}
func TestServeIMCallRTCJoinReturnsAgoraTokenMode(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldSubs := store.Subs
oldCalls := store.Calls
oldThirdParty := store.ThirdPartyConfig
subs := mock_store.NewMockSubsPersistenceInterface(ctrl)
calls := &testCallsStore{items: []types.CallRecord{{
ID: 9,
TenantID: 7,
CallID: "call-1",
Topic: "p2pTopic",
SeqID: 12,
CallerID: 42,
CalleeID: 43,
CallType: "video",
Status: types.CallStatusStarted,
StartedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
CreatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2026, 9, 2, 9, 0, 0, 0, time.UTC),
}}}
store.Subs = subs
store.Calls = calls
store.ThirdPartyConfig = testThirdPartyConfigStore{
"agora_enabled": "1",
"agora_auth_mode": "token",
"agora_app_id": "00000000000000000000000000000000",
"agora_app_certificate": "11111111111111111111111111111111",
"agora_channel_prefix": "call_",
"agora_uid_mode": "string",
"agora_token_ttl_seconds": "3600",
}
defer func() {
store.Subs = oldSubs
store.Calls = oldCalls
store.ThirdPartyConfig = oldThirdParty
}()
subs.EXPECT().Get(types.TenantID(7), "p2pTopic", types.Uid(42), false).Return(&types.Subscription{
TenantID: 7, User: types.Uid(42).String(), Topic: "p2pTopic", ModeWant: types.ModeCPublic, ModeGiven: types.ModeCPublic,
}, nil)
req := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"p2pTopic","call_id":"call-1","seq":12,"call_type":"video","role":"caller"}`))
req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder()
serveIMCallRTCJoin(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("rtc join status: got %d body=%s", rec.Code, rec.Body.String())
}
data := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if data["auth_mode"] != "token" || data["token"] == "" || data["token_expires_at"] == nil {
t.Fatalf("unexpected token payload: %#v", data)
}
if _, found := data["app_certificate"]; found {
t.Fatalf("app certificate must not be returned: %#v", data)
}
}
func TestServeIMCallRTCJoinRejectsDisabledAgora(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldThirdParty := store.ThirdPartyConfig
store.ThirdPartyConfig = testThirdPartyConfigStore{"agora_enabled": "0"}
defer func() { store.ThirdPartyConfig = oldThirdParty }()
req := httptest.NewRequest(http.MethodPost, "/v1/im/calls/rtc-join", strings.NewReader(`{"topic":"p2pTopic","call_id":"call-1","seq":12,"call_type":"video","role":"caller"}`))
req.Header.Set("Authorization", "Bearer token-1")
rec := httptest.NewRecorder()
serveIMCallRTCJoin(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("rtc join status: got %d body=%s", rec.Code, rec.Body.String())
}
resp := decodeIMHTTPResponse(t, rec)
if resp.Reason != imReasonAgoraDisabled {
t.Fatalf("reason: got %q", resp.Reason)
}
}
func TestServeIMMeQRCodesAndResolve(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()