feat: add agora rtc token join endpoint
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
rtctokenbuilder "github.com/AgoraIO/Tools/DynamicKey/AgoraDynamicKey/go/src/rtctokenbuilder2"
|
||||
"github.com/tinode/chat/server/store"
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
const (
|
||||
imReasonAgoraDisabled = "AGORA_DISABLED"
|
||||
imReasonAgoraConfigMissing = "AGORA_CONFIG_MISSING"
|
||||
imReasonCallNotFound = "CALL_NOT_FOUND"
|
||||
imReasonCallNotAllowed = "CALL_NOT_ALLOWED"
|
||||
imReasonCallStateInvalid = "CALL_STATE_INVALID"
|
||||
)
|
||||
|
||||
type imCallRTCJoinRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
CallID string `json:"call_id"`
|
||||
Seq int64 `json:"seq"`
|
||||
CallType string `json:"call_type"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type imAgoraConfig struct {
|
||||
Enabled bool
|
||||
AuthMode string
|
||||
AppID string
|
||||
AppCertificate string
|
||||
TokenTTLSeconds int
|
||||
ChannelPrefix string
|
||||
UIDMode string
|
||||
}
|
||||
|
||||
func serveIMCallRTCJoin(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, ok := authenticateIMHTTPBearer(wrt, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var body imCallRTCJoinRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(wrt, req.Body, 1<<20)).Decode(&body); err != nil {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
body.Topic = strings.TrimSpace(body.Topic)
|
||||
body.CallID = strings.TrimSpace(body.CallID)
|
||||
body.CallType = strings.ToLower(strings.TrimSpace(body.CallType))
|
||||
body.Role = strings.ToLower(strings.TrimSpace(body.Role))
|
||||
if body.Topic == "" || body.CallID == "" || body.Seq <= 0 ||
|
||||
(body.CallType != constCallTypeAudio && body.CallType != constCallTypeVideo) ||
|
||||
(body.Role != "caller" && body.Role != "callee") {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
|
||||
return
|
||||
}
|
||||
if types.GetTopicCat(body.Topic) != types.TopicCatP2P {
|
||||
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
|
||||
return
|
||||
}
|
||||
config, err := loadIMAgoraConfig()
|
||||
if err != nil {
|
||||
if errors.Is(err, types.ErrNotFound) || errors.Is(err, types.ErrMalformed) {
|
||||
writeIMError(wrt, req, http.StatusServiceUnavailable, 30060, imReasonAgoraConfigMissing, "agora config missing", nil)
|
||||
return
|
||||
}
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
if !config.Enabled {
|
||||
writeIMError(wrt, req, http.StatusServiceUnavailable, 30061, imReasonAgoraDisabled, "agora disabled", nil)
|
||||
return
|
||||
}
|
||||
sub, err := store.Subs.Get(principal.TenantID, body.Topic, principal.Uid, false)
|
||||
if err != nil {
|
||||
writeIMRelationError(wrt, req, err)
|
||||
return
|
||||
}
|
||||
if sub == nil || !(sub.ModeGiven & sub.ModeWant).IsReader() {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30062, imReasonCallNotAllowed, "call not allowed", nil)
|
||||
return
|
||||
}
|
||||
record, err := findCallRecordForRTCJoin(principal.TenantID, principal.Uid, body)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, types.ErrNotFound):
|
||||
writeIMError(wrt, req, http.StatusNotFound, 30063, imReasonCallNotFound, "call not found", nil)
|
||||
case errors.Is(err, types.ErrPermissionDenied):
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30062, imReasonCallNotAllowed, "call not allowed", nil)
|
||||
default:
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !callStateAllowsRTCJoin(record.Status) {
|
||||
writeIMError(wrt, req, http.StatusConflict, 30064, imReasonCallStateInvalid, "call state invalid", map[string]any{"status": record.Status})
|
||||
return
|
||||
}
|
||||
channelName := config.ChannelPrefix + record.CallID
|
||||
uid, err := agoraUIDForUser(config, principal.Uid)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusServiceUnavailable, 30060, imReasonAgoraConfigMissing, "agora config missing", nil)
|
||||
return
|
||||
}
|
||||
token := ""
|
||||
var expiresAt any
|
||||
if config.AuthMode == "token" {
|
||||
token, expiresAt, err = buildAgoraRTCToken(config, channelName, principal.Uid)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusServiceUnavailable, 30060, imReasonAgoraConfigMissing, "agora rtc token generation failed", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{
|
||||
"provider": "agora",
|
||||
"auth_mode": config.AuthMode,
|
||||
"app_id": config.AppID,
|
||||
"channel_name": channelName,
|
||||
"uid": uid,
|
||||
"token": token,
|
||||
"token_expires_at": expiresAt,
|
||||
"call_id": record.CallID,
|
||||
"topic": record.Topic,
|
||||
"seq": record.SeqID,
|
||||
"call_type": record.CallType,
|
||||
"role": body.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func loadIMAgoraConfig() (imAgoraConfig, error) {
|
||||
values, err := store.ThirdPartyConfig.Get("im_shengwang",
|
||||
"agora_enabled",
|
||||
"agora_auth_mode",
|
||||
"agora_app_id",
|
||||
"agora_app_certificate",
|
||||
"agora_token_ttl_seconds",
|
||||
"agora_channel_prefix",
|
||||
"agora_uid_mode",
|
||||
)
|
||||
if err != nil {
|
||||
return imAgoraConfig{}, err
|
||||
}
|
||||
config := imAgoraConfig{
|
||||
Enabled: imTruthy(values["agora_enabled"]),
|
||||
AuthMode: strings.ToLower(strings.TrimSpace(values["agora_auth_mode"])),
|
||||
AppID: strings.TrimSpace(values["agora_app_id"]),
|
||||
AppCertificate: strings.TrimSpace(values["agora_app_certificate"]),
|
||||
TokenTTLSeconds: 3600,
|
||||
ChannelPrefix: strings.TrimSpace(values["agora_channel_prefix"]),
|
||||
UIDMode: strings.ToLower(strings.TrimSpace(values["agora_uid_mode"])),
|
||||
}
|
||||
if config.AuthMode == "" {
|
||||
config.AuthMode = "app_id"
|
||||
}
|
||||
if config.ChannelPrefix == "" {
|
||||
config.ChannelPrefix = "call_"
|
||||
}
|
||||
if config.UIDMode == "" {
|
||||
config.UIDMode = "string"
|
||||
}
|
||||
if ttl := strings.TrimSpace(values["agora_token_ttl_seconds"]); ttl != "" {
|
||||
parsed, err := strconv.Atoi(ttl)
|
||||
if err != nil || parsed <= 0 {
|
||||
return imAgoraConfig{}, types.ErrMalformed
|
||||
}
|
||||
config.TokenTTLSeconds = parsed
|
||||
}
|
||||
if !config.Enabled {
|
||||
return config, nil
|
||||
}
|
||||
if config.AppID == "" || (config.AuthMode != "app_id" && config.AuthMode != "token") {
|
||||
return imAgoraConfig{}, types.ErrMalformed
|
||||
}
|
||||
if config.AuthMode == "token" && config.AppCertificate == "" {
|
||||
return imAgoraConfig{}, types.ErrMalformed
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func findCallRecordForRTCJoin(tenantID types.TenantID, uid types.Uid, body imCallRTCJoinRequest) (*types.CallRecord, error) {
|
||||
if store.Calls == nil {
|
||||
return nil, types.ErrUnsupported
|
||||
}
|
||||
records, _, err := store.Calls.List(tenantID, uid, body.Topic, "", 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range records {
|
||||
record := &records[i]
|
||||
if record.CallID != body.CallID || record.Topic != body.Topic || record.SeqID != body.Seq || record.CallType != body.CallType {
|
||||
continue
|
||||
}
|
||||
if record.CallerID != uid && record.CalleeID != uid {
|
||||
return nil, types.ErrPermissionDenied
|
||||
}
|
||||
if body.Role == "caller" && record.CallerID != uid {
|
||||
return nil, types.ErrPermissionDenied
|
||||
}
|
||||
if body.Role == "callee" && record.CalleeID != uid {
|
||||
return nil, types.ErrPermissionDenied
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
return nil, types.ErrNotFound
|
||||
}
|
||||
|
||||
func callStateAllowsRTCJoin(status string) bool {
|
||||
switch status {
|
||||
case types.CallStatusStarted, types.CallStatusAccepted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func agoraUIDForUser(config imAgoraConfig, uid types.Uid) (any, error) {
|
||||
switch config.UIDMode {
|
||||
case "int":
|
||||
decoded := store.DecodeUid(uid)
|
||||
if decoded <= 0 || decoded > math.MaxUint32 {
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
return decoded, nil
|
||||
case "string":
|
||||
return uid.UserId(), nil
|
||||
default:
|
||||
return nil, types.ErrMalformed
|
||||
}
|
||||
}
|
||||
|
||||
func buildAgoraRTCToken(config imAgoraConfig, channelName string, uid types.Uid) (string, string, error) {
|
||||
ttl := uint32(config.TokenTTLSeconds)
|
||||
if config.UIDMode == "int" {
|
||||
decoded := store.DecodeUid(uid)
|
||||
if decoded <= 0 || decoded > math.MaxUint32 {
|
||||
return "", "", types.ErrMalformed
|
||||
}
|
||||
token, err := rtctokenbuilder.BuildTokenWithUid(config.AppID, config.AppCertificate, channelName, uint32(decoded), rtctokenbuilder.RolePublisher, ttl, ttl)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return token, agoraTokenExpiresAt(config.TokenTTLSeconds), nil
|
||||
}
|
||||
account := uid.UserId()
|
||||
if account == "" {
|
||||
return "", "", types.ErrMalformed
|
||||
}
|
||||
token, err := rtctokenbuilder.BuildTokenWithUserAccount(config.AppID, config.AppCertificate, channelName, account, rtctokenbuilder.RolePublisher, ttl, ttl)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return token, agoraTokenExpiresAt(config.TokenTTLSeconds), nil
|
||||
}
|
||||
|
||||
func imTruthy(value string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func agoraTokenExpiresAt(ttlSeconds int) string {
|
||||
return time.Now().UTC().Add(time.Duration(ttlSeconds) * time.Second).Format(time.RFC3339Nano)
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -860,6 +860,7 @@ func main() {
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/system-notifications/", serveIMSystemNotificationRead)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/favorites", serveIMFavorites)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/favorites/", serveIMFavoriteResource)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/calls/rtc-join", serveIMCallRTCJoin)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/calls", serveIMCalls)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/topics/", serveIMTopicMessageAction)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/conversations/", serveIMConversationContext)
|
||||
|
||||
Reference in New Issue
Block a user