package tencentim import ( "bytes" "context" "encoding/json" "io" "net/http" "strings" "testing" "time" ) // roundTripFunc 允许测试拦截腾讯云 REST 请求而不访问外网。 type roundTripFunc func(*http.Request) (*http.Response, error) // RoundTrip 实现 http.RoundTripper。 func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) } // TestRESTClientEnsureRoomGroupBuildsTencentRequest 锁定建群请求的 REST 命令和核心字段。 func TestRESTClientEnsureRoomGroupBuildsTencentRequest(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) if err := client.EnsureRoomGroup(context.Background(), "room-1001", 10001); err != nil { t.Fatalf("EnsureRoomGroup failed: %v", err) } if capturedPath != "/"+createGroupCommand { t.Fatalf("path mismatch: got %q", capturedPath) } if capturedBody["GroupId"] != "room-1001" || capturedBody["Type"] != DefaultGroupType { t.Fatalf("unexpected create group body: %+v", capturedBody) } } // TestRESTClientPublishRoomEventBuildsCustomMessage 锁定房间系统事件的自定义消息载荷。 func TestRESTClientPublishRoomEventBuildsCustomMessage(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) err := client.PublishRoomEvent(context.Background(), RoomEvent{ GroupID: "room_1001", EventID: "evt-1", RoomID: "room-1001", EventType: "room_gift_sent", ActorUserID: 10001, TargetUserID: 10002, GiftValue: 20, RoomVersion: 7, }) if err != nil { t.Fatalf("PublishRoomEvent failed: %v", err) } if capturedPath != "/"+sendGroupMsgCommand { t.Fatalf("path mismatch: got %q", capturedPath) } if capturedBody["GroupId"] != "room_1001" { t.Fatalf("group id mismatch: %+v", capturedBody) } body, ok := capturedBody["MsgBody"].([]any) if !ok || len(body) != 1 { t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"]) } element, ok := body[0].(map[string]any) if !ok || element["MsgType"] != "TIMCustomElem" { t.Fatalf("unexpected message element: %+v", body[0]) } content, ok := element["MsgContent"].(map[string]any) if !ok || content["Ext"] != "room_system_message" || content["Desc"] != "room_gift_sent" { t.Fatalf("unexpected custom content: %+v", element["MsgContent"]) } if !strings.Contains(content["Data"].(string), `"event_id":"evt-1"`) { t.Fatalf("event payload missing event_id: %s", content["Data"]) } if !strings.Contains(content["Data"].(string), `"room_id":"room-1001"`) { t.Fatalf("event payload must keep internal room_id: %s", content["Data"]) } } func TestRESTClientPublishRoomEventFlattensAttributesForClientPayload(t *testing.T) { var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) err := client.PublishRoomEvent(context.Background(), RoomEvent{ GroupID: "room_1001", EventID: "evt-robot-lucky-1", RoomID: "room-1001", EventType: "lucky_gift_drawn", ActorUserID: 10001, TargetUserID: 10002, GiftValue: 500, Attributes: map[string]string{ "event_type": "should_not_override", "draw_id": "robot_lucky_draw_1", "gift_id": "gift_lucky_1", "gift_count": "1", "pool_id": "robot_lucky_display", "multiplier_ppm": "5000000", "effective_reward_coins": "500", "reward_status": "granted", }, }) if err != nil { t.Fatalf("PublishRoomEvent failed: %v", err) } body, ok := capturedBody["MsgBody"].([]any) if !ok || len(body) != 1 { t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"]) } element := body[0].(map[string]any) content := element["MsgContent"].(map[string]any) var payload map[string]any if err := json.Unmarshal([]byte(content["Data"].(string)), &payload); err != nil { t.Fatalf("decode custom data failed: %v", err) } if payload["event_type"] != "lucky_gift_drawn" { t.Fatalf("attributes must not override top-level event_type: %+v", payload) } if payload["draw_id"] != "robot_lucky_draw_1" || payload["effective_reward_coins"] != "500" || payload["reward_status"] != "granted" { t.Fatalf("lucky gift attributes must be flattened for existing clients: %+v", payload) } attributes, ok := payload["attributes"].(map[string]any) if !ok || attributes["effective_reward_coins"] != "500" { t.Fatalf("attributes must still be kept for diagnostics and fallback clients: %+v", payload["attributes"]) } } func TestRESTClientPublishGroupCustomMessageBuildsBroadcastPayload(t *testing.T) { var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) err := client.PublishGroupCustomMessage(context.Background(), CustomGroupMessage{ GroupID: "hy_lalu_bc_r_1001", EventID: "gift_broadcast:room-1:cmd-1", Desc: "super_gift", Ext: "im_broadcast", FromAccount: "administrator", PayloadJSON: json.RawMessage(`{"event_id":"gift_broadcast:room-1:cmd-1","broadcast_type":"super_gift"}`), }) if err != nil { t.Fatalf("PublishGroupCustomMessage failed: %v", err) } if capturedBody["GroupId"] != "hy_lalu_bc_r_1001" || capturedBody["From_Account"] != "administrator" { t.Fatalf("broadcast target mismatch: %+v", capturedBody) } if capturedBody["CloudCustomData"] != `{"event_id":"gift_broadcast:room-1:cmd-1","broadcast_type":"super_gift"}` { t.Fatalf("cloud custom data mismatch: %+v", capturedBody) } } func TestRESTClientPublishUserCustomMessageBuildsC2CPayload(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body: %v", err) } return okRESTResponse(), nil }) err := client.PublishUserCustomMessage(context.Background(), CustomUserMessage{ ToAccount: "10001", EventID: "evt-wallet-1", Desc: "WalletBalanceChanged", Ext: "wallet_notice", PayloadJSON: json.RawMessage(`{"event_id":"evt-wallet-1"}`), }) if err != nil { t.Fatalf("PublishUserCustomMessage failed: %v", err) } if capturedPath != "/"+sendC2CMsgCommand { t.Fatalf("unexpected command path: %s", capturedPath) } if capturedBody["To_Account"] != "10001" || capturedBody["SyncOtherMachine"].(float64) != 2 { t.Fatalf("unexpected c2c target: %+v", capturedBody) } if capturedBody["From_Account"] != "administrator" { t.Fatalf("unexpected c2c sender: %+v", capturedBody) } body, ok := capturedBody["MsgBody"].([]any) if !ok || len(body) != 1 { t.Fatalf("unexpected msg body: %+v", capturedBody["MsgBody"]) } element := body[0].(map[string]any) if element["MsgType"] != "TIMCustomElem" { t.Fatalf("unexpected msg type: %+v", element) } content := element["MsgContent"].(map[string]any) if content["Desc"] != "WalletBalanceChanged" || content["Ext"] != "wallet_notice" || content["Data"] != `{"event_id":"evt-wallet-1"}` { t.Fatalf("unexpected custom content: %+v", content) } } func TestRESTClientImportAccountBuildsTencentRequest(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) if err := client.ImportAccount(context.Background(), AccountProfile{ UserID: 10002, Nick: "Lingxian", FaceURL: "https://cdn.example/avatar.png", }); err != nil { t.Fatalf("ImportAccount failed: %v", err) } if capturedPath != "/"+accountImportCommand { t.Fatalf("path mismatch: got %q", capturedPath) } if capturedBody["UserID"] != "10002" || capturedBody["Nick"] != "Lingxian" || capturedBody["FaceUrl"] != "https://cdn.example/avatar.png" { t.Fatalf("unexpected account import body: %+v", capturedBody) } } func TestRESTClientImportAccountTreatsExistingAccountAsSuccess(t *testing.T) { client := newTestRESTClient(t, func(_ *http.Request) (*http.Response, error) { payload := []byte(`{"ActionStatus":"FAIL","ErrorCode":70107,"ErrorInfo":"account already exists"}`) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), Header: make(http.Header), }, nil }) if err := client.ImportAccount(context.Background(), AccountProfile{UserID: 10002}); err != nil { t.Fatalf("existing account must be idempotent success: %v", err) } } // TestRESTClientDestroyGroupBuildsTencentRequest 锁定临时群清理的 REST 请求。 func TestRESTClientDestroyGroupBuildsTencentRequest(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) if err := client.DestroyGroup(context.Background(), "hy_codex_bc_r_210"); err != nil { t.Fatalf("DestroyGroup failed: %v", err) } if capturedPath != "/"+destroyGroupCommand { t.Fatalf("path mismatch: got %q", capturedPath) } if capturedBody["GroupId"] != "hy_codex_bc_r_210" { t.Fatalf("unexpected destroy group body: %+v", capturedBody) } } // TestRESTClientDeleteRoomGroupMemberBuildsTencentRequest 锁定踢出 IM 群成员的 REST 请求。 func TestRESTClientDeleteRoomGroupMemberBuildsTencentRequest(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) if err := client.DeleteRoomGroupMember(context.Background(), "room-1001", 10002); err != nil { t.Fatalf("DeleteRoomGroupMember failed: %v", err) } if capturedPath != "/"+deleteGroupMemberCommand { t.Fatalf("path mismatch: got %q", capturedPath) } members, ok := capturedBody["MemberToDel_Account"].([]any) if !ok || len(members) != 1 || members[0] != "10002" { t.Fatalf("unexpected member removal body: %+v", capturedBody) } if capturedBody["GroupId"] != "room-1001" || capturedBody["Silence"].(float64) != 1 { t.Fatalf("unexpected group removal body: %+v", capturedBody) } } // TestRESTClientKickUserBuildsTencentRequest 锁定封禁链路使用腾讯云 IM 失效账号登录态接口。 func TestRESTClientKickUserBuildsTencentRequest(t *testing.T) { var capturedPath string var capturedBody map[string]any client := newTestRESTClient(t, func(request *http.Request) (*http.Response, error) { capturedPath = request.URL.Path if err := json.NewDecoder(request.Body).Decode(&capturedBody); err != nil { t.Fatalf("decode request body failed: %v", err) } return okRESTResponse(), nil }) if err := client.KickUser(context.Background(), 10002); err != nil { t.Fatalf("KickUser failed: %v", err) } if capturedPath != "/"+kickUserCommand { t.Fatalf("path mismatch: got %q", capturedPath) } if capturedBody["UserID"] != "10002" { t.Fatalf("unexpected kick body: %+v", capturedBody) } } func newTestRESTClient(t *testing.T, roundTrip roundTripFunc) *RESTClient { t.Helper() client, err := NewRESTClient(RESTConfig{ SDKAppID: 1400000000, SecretKey: "secret", AdminIdentifier: "administrator", AdminUserSigTTL: time.Hour, HTTPClient: &http.Client{ Transport: roundTrip, }, }) if err != nil { t.Fatalf("NewRESTClient failed: %v", err) } return client } func okRESTResponse() *http.Response { payload := []byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`) return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader(payload)), Header: make(http.Header), } }