305 lines
10 KiB
Go
305 lines
10 KiB
Go
package tencentim
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestRealTencentIMCreateSendAndDestroyGroup(t *testing.T) {
|
||
if os.Getenv("TENCENT_IM_REAL_TEST") != "1" {
|
||
t.Skip("set TENCENT_IM_REAL_TEST=1 and Tencent IM env vars to run the real REST smoke test")
|
||
}
|
||
|
||
cfg := realRESTConfigFromEnv(t)
|
||
client, err := NewRESTClient(cfg)
|
||
if err != nil {
|
||
t.Fatalf("NewRESTClient failed: %v", err)
|
||
}
|
||
|
||
now := time.Now().UTC()
|
||
nowMS := now.UnixMilli()
|
||
groupID := fmt.Sprintf("hy_codex_bc_r_%d", nowMS)
|
||
eventID := fmt.Sprintf("codex_real_im_%d", nowMS)
|
||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||
defer cancel()
|
||
|
||
// 真实 IM 测试必须使用临时 GroupID,避免污染业务全局/区域播报群。
|
||
if err := client.EnsureGroup(ctx, GroupSpec{
|
||
GroupID: groupID,
|
||
Name: "codex real im smoke",
|
||
Type: DefaultGroupType,
|
||
ApplyJoinOption: "FreeAccess",
|
||
}); err != nil {
|
||
t.Fatalf("real Tencent IM create group failed: %v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||
defer cleanupCancel()
|
||
if err := client.DestroyGroup(cleanupCtx, groupID); err != nil {
|
||
t.Errorf("real Tencent IM destroy group cleanup failed: %v", err)
|
||
}
|
||
})
|
||
memberUserID := nowMS % 9_000_000_000
|
||
if err := realImportAccount(ctx, client, memberUserID); err != nil {
|
||
t.Fatalf("real Tencent IM account import failed: %v", err)
|
||
}
|
||
if err := realAddGroupMember(ctx, client, groupID, memberUserID); err != nil {
|
||
t.Fatalf("real Tencent IM add group member failed: %v", err)
|
||
}
|
||
|
||
const actorUserID = "323411852102995968"
|
||
const targetUserID = "323411852102995969"
|
||
c2cEventID := fmt.Sprintf("codex_real_im_c2c_%d", nowMS)
|
||
c2cPayload, err := json.Marshal(map[string]any{
|
||
"event_id": c2cEventID,
|
||
"event_type": "room_user_kicked",
|
||
"actor_user_id": actorUserID,
|
||
"target_user_id": targetUserID,
|
||
"sender_user_id": actorUserID,
|
||
"receiver_user_id": targetUserID,
|
||
"sent_at_ms": nowMS,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("marshal c2c smoke payload failed: %v", err)
|
||
}
|
||
if err := client.PublishUserCustomMessage(ctx, CustomUserMessage{
|
||
ToAccount: strconv.FormatInt(memberUserID, 10),
|
||
EventID: c2cEventID,
|
||
Desc: "codex_real_im_c2c_probe",
|
||
Ext: "im_c2c_smoke",
|
||
PayloadJSON: c2cPayload,
|
||
}); err != nil {
|
||
t.Fatalf("real Tencent IM send C2C custom message failed: %v", err)
|
||
}
|
||
// SyncOtherMachine=2 不同步管理员发送端,因此漫游消息必须从实际接收用户视角回读。
|
||
c2cData, c2cCloudData := realWaitForC2CPayload(t, ctx, client, strconv.FormatInt(memberUserID, 10), cfg.AdminIdentifier, c2cEventID, now.Unix())
|
||
realAssertStringUserIDs(t, c2cData, map[string]string{
|
||
"actor_user_id": actorUserID,
|
||
"target_user_id": targetUserID,
|
||
"sender_user_id": actorUserID,
|
||
"receiver_user_id": targetUserID,
|
||
})
|
||
realAssertStringUserIDs(t, c2cCloudData, map[string]string{
|
||
"actor_user_id": actorUserID,
|
||
"target_user_id": targetUserID,
|
||
"sender_user_id": actorUserID,
|
||
"receiver_user_id": targetUserID,
|
||
})
|
||
|
||
roomEvent := RoomEvent{
|
||
GroupID: groupID,
|
||
EventID: eventID,
|
||
RoomID: "codex-room-smoke",
|
||
EventType: "room_mic_change",
|
||
ActorUserID: actorUserID,
|
||
TargetUserID: targetUserID,
|
||
SeatNo: 3,
|
||
RoomVersion: 22,
|
||
Attributes: map[string]string{
|
||
"action": "change",
|
||
"from_seat": "1",
|
||
"to_seat": "3",
|
||
"mic_session_id": "codex-real-im-mic-session",
|
||
"target_user_ids": `["323411852102995968","323411852102995969"]`,
|
||
},
|
||
}
|
||
|
||
// 使用生产 RoomEvent 序列化和发送入口,避免真实测试绕开本次修复的代码路径。
|
||
if err := client.PublishRoomEvent(ctx, roomEvent); err != nil {
|
||
t.Fatalf("real Tencent IM send group custom message failed: %v", err)
|
||
}
|
||
groupData, groupCloudData := realWaitForGroupPayload(t, ctx, client, groupID, eventID)
|
||
for source, payload := range map[string]string{"Data": groupData, "CloudCustomData": groupCloudData} {
|
||
realAssertStringUserIDs(t, payload, map[string]string{
|
||
"actor_user_id": actorUserID,
|
||
"target_user_id": targetUserID,
|
||
})
|
||
var decoded map[string]any
|
||
if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
|
||
t.Fatalf("decode group %s payload failed: %v", source, err)
|
||
}
|
||
attributes, ok := decoded["attributes"].(map[string]any)
|
||
if !ok {
|
||
t.Fatalf("group %s payload attributes type = %T, want object", source, decoded["attributes"])
|
||
}
|
||
var targetUserIDs []any
|
||
if err := json.Unmarshal([]byte(fmt.Sprint(attributes["target_user_ids"])), &targetUserIDs); err != nil {
|
||
t.Fatalf("decode group %s target_user_ids failed: %v", source, err)
|
||
}
|
||
if len(targetUserIDs) != 2 || targetUserIDs[0] != actorUserID || targetUserIDs[1] != targetUserID {
|
||
t.Fatalf("group %s target_user_ids = %#v, want exact string IDs", source, targetUserIDs)
|
||
}
|
||
}
|
||
if err := client.DeleteGroupMember(ctx, groupID, memberUserID); err != nil {
|
||
t.Fatalf("real Tencent IM delete group member failed: %v", err)
|
||
}
|
||
|
||
t.Logf("real Tencent IM transport and history readback passed: sdk_app_id=%d group_id=%s group_event_id=%s c2c_event_id=%s exact_actor_user_id=%s removed_user_id=%d", cfg.SDKAppID, groupID, eventID, c2cEventID, actorUserID, memberUserID)
|
||
}
|
||
|
||
type realHistoryMessage struct {
|
||
MsgBody []messageElement `json:"MsgBody"`
|
||
CloudCustomData string `json:"CloudCustomData"`
|
||
}
|
||
|
||
type realGroupHistoryResponse struct {
|
||
restResponse
|
||
RspMsgList []realHistoryMessage `json:"RspMsgList"`
|
||
}
|
||
|
||
type realC2CHistoryResponse struct {
|
||
restResponse
|
||
MsgList []realHistoryMessage `json:"MsgList"`
|
||
}
|
||
|
||
func realWaitForGroupPayload(t *testing.T, ctx context.Context, client *RESTClient, groupID, eventID string) (string, string) {
|
||
t.Helper()
|
||
for attempt := 0; attempt < 10; attempt++ {
|
||
var response realGroupHistoryResponse
|
||
if err := client.post(ctx, "v4/group_open_http_svc/group_msg_get_simple", map[string]any{
|
||
"GroupId": groupID,
|
||
"ReqMsgNumber": 20,
|
||
}, &response); err != nil {
|
||
t.Fatalf("real Tencent IM group history request failed: %v", err)
|
||
}
|
||
if err := response.err(); err != nil {
|
||
t.Fatalf("real Tencent IM group history failed: %v", err)
|
||
}
|
||
if data, cloudData, ok := realFindHistoryPayload(response.RspMsgList, eventID); ok {
|
||
return data, cloudData
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
t.Fatalf("real Tencent IM group history readback timed out: %v", ctx.Err())
|
||
case <-time.After(500 * time.Millisecond):
|
||
}
|
||
}
|
||
t.Fatalf("real Tencent IM group history did not contain event_id=%s", eventID)
|
||
return "", ""
|
||
}
|
||
|
||
func realWaitForC2CPayload(t *testing.T, ctx context.Context, client *RESTClient, operatorAccount, peerAccount, eventID string, sentAtSeconds int64) (string, string) {
|
||
t.Helper()
|
||
for attempt := 0; attempt < 10; attempt++ {
|
||
var response realC2CHistoryResponse
|
||
if err := client.post(ctx, "v4/openim/admin_getroammsg", map[string]any{
|
||
"Operator_Account": operatorAccount,
|
||
"Peer_Account": peerAccount,
|
||
"MaxCnt": 100,
|
||
"MinTime": sentAtSeconds - 60,
|
||
"MaxTime": time.Now().UTC().Unix() + 60,
|
||
}, &response); err != nil {
|
||
t.Fatalf("real Tencent IM C2C history request failed: %v", err)
|
||
}
|
||
if err := response.err(); err != nil {
|
||
t.Fatalf("real Tencent IM C2C history failed: %v", err)
|
||
}
|
||
if data, cloudData, ok := realFindHistoryPayload(response.MsgList, eventID); ok {
|
||
return data, cloudData
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
t.Fatalf("real Tencent IM C2C history readback timed out: %v", ctx.Err())
|
||
case <-time.After(500 * time.Millisecond):
|
||
}
|
||
}
|
||
t.Fatalf("real Tencent IM C2C history did not contain event_id=%s", eventID)
|
||
return "", ""
|
||
}
|
||
|
||
func realFindHistoryPayload(messages []realHistoryMessage, eventID string) (string, string, bool) {
|
||
for _, message := range messages {
|
||
for _, element := range message.MsgBody {
|
||
if element.MsgType != "TIMCustomElem" || !strings.Contains(element.MsgContent.Data, eventID) {
|
||
continue
|
||
}
|
||
return element.MsgContent.Data, message.CloudCustomData, true
|
||
}
|
||
}
|
||
return "", "", false
|
||
}
|
||
|
||
func realAssertStringUserIDs(t *testing.T, payload string, expected map[string]string) {
|
||
t.Helper()
|
||
var decoded map[string]any
|
||
if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
|
||
t.Fatalf("decode Tencent IM payload failed: %v", err)
|
||
}
|
||
for key, want := range expected {
|
||
got, ok := decoded[key].(string)
|
||
if !ok {
|
||
t.Fatalf("Tencent IM payload %s type = %T, want string", key, decoded[key])
|
||
}
|
||
if got != want {
|
||
t.Fatalf("Tencent IM payload %s = %q, want %q", key, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func realImportAccount(ctx context.Context, client *RESTClient, userID int64) error {
|
||
return client.ImportAccount(ctx, AccountProfile{
|
||
UserID: userID,
|
||
Nick: "codex-im-smoke",
|
||
})
|
||
}
|
||
|
||
func realAddGroupMember(ctx context.Context, client *RESTClient, groupID string, userID int64) error {
|
||
var response restResponse
|
||
if err := client.post(ctx, "v4/group_open_http_svc/add_group_member", map[string]any{
|
||
"GroupId": groupID,
|
||
"Silence": 1,
|
||
"MemberList": []map[string]any{
|
||
{"Member_Account": strconv.FormatInt(userID, 10)},
|
||
},
|
||
}, &response); err != nil {
|
||
return err
|
||
}
|
||
return response.err()
|
||
}
|
||
|
||
func realRESTConfigFromEnv(t *testing.T) RESTConfig {
|
||
t.Helper()
|
||
|
||
sdkAppID, err := strconv.ParseInt(requiredEnv(t, "TENCENT_IM_SDK_APP_ID"), 10, 64)
|
||
if err != nil || sdkAppID <= 0 {
|
||
t.Fatalf("TENCENT_IM_SDK_APP_ID is invalid")
|
||
}
|
||
endpoint := strings.TrimSpace(os.Getenv("TENCENT_IM_ENDPOINT"))
|
||
if endpoint == "" {
|
||
endpoint = DefaultEndpoint
|
||
}
|
||
adminUserSigTTL := time.Hour
|
||
if rawTTL := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_USERSIG_TTL")); rawTTL != "" {
|
||
parsed, err := time.ParseDuration(rawTTL)
|
||
if err != nil || parsed <= 0 {
|
||
t.Fatalf("TENCENT_IM_ADMIN_USERSIG_TTL is invalid")
|
||
}
|
||
adminUserSigTTL = parsed
|
||
}
|
||
|
||
return RESTConfig{
|
||
SDKAppID: sdkAppID,
|
||
SecretKey: requiredEnv(t, "TENCENT_IM_SECRET_KEY"),
|
||
AdminIdentifier: requiredEnv(t, "TENCENT_IM_ADMIN_IDENTIFIER"),
|
||
AdminUserSigTTL: adminUserSigTTL,
|
||
Endpoint: endpoint,
|
||
GroupType: DefaultGroupType,
|
||
}
|
||
}
|
||
|
||
func requiredEnv(t *testing.T, key string) string {
|
||
t.Helper()
|
||
|
||
value := strings.TrimSpace(os.Getenv(key))
|
||
if value == "" {
|
||
t.Fatalf("%s is required for real Tencent IM smoke test", key)
|
||
}
|
||
return value
|
||
}
|