Merge branch 'test' of gitea.haiyihy.com:hy/hyapp-server into codex/admin-refresh-token-test
This commit is contained in:
commit
d4b2bdb789
@ -101,8 +101,8 @@ func RoomGroupIDWithPrefix(prefix string, value string) (string, error) {
|
|||||||
if !roomid.ValidStringID(room) {
|
if !roomid.ValidStringID(room) {
|
||||||
return "", fmt.Errorf("room_id is invalid for group_id")
|
return "", fmt.Errorf("room_id is invalid for group_id")
|
||||||
}
|
}
|
||||||
// 腾讯云自定义 GroupID 不接受 UUID 连字符;内部 room_id 保持不变,只在 IM 边界转义。
|
// 房间群 GroupID 必须和内部 room_id 保持一致;线上已有腾讯 IM 群也是这个 ID,不能在投递边界改写。
|
||||||
groupID := prefix + strings.ReplaceAll(room, "-", "_")
|
groupID := prefix + room
|
||||||
if len(groupID) > roomid.MaxStringIDLength {
|
if len(groupID) > roomid.MaxStringIDLength {
|
||||||
return "", fmt.Errorf("room group_id is too long")
|
return "", fmt.Errorf("room group_id is too long")
|
||||||
}
|
}
|
||||||
@ -164,52 +164,9 @@ func RoomIDFromRoomGroupID(groupID string) (string, bool) {
|
|||||||
if !roomid.ValidStringID(groupID) {
|
if !roomid.ValidStringID(groupID) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if roomID, ok := uuidRoomGroupIDToRoomID(groupID); ok {
|
|
||||||
return roomID, true
|
|
||||||
}
|
|
||||||
return groupID, true
|
return groupID, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func uuidRoomGroupIDToRoomID(groupID string) (string, bool) {
|
|
||||||
parts := strings.Split(groupID, "_")
|
|
||||||
if len(parts) < 6 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
tail := parts[len(parts)-5:]
|
|
||||||
if !isHexLength(tail[0], 8) || !isHexLength(tail[1], 4) || !isHexLength(tail[2], 4) || !isHexLength(tail[3], 4) || !isHexLength(tail[4], 12) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
prefix := strings.Join(parts[:len(parts)-5], "_")
|
|
||||||
if prefix == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
roomID := prefix + "_" + tail[0] + "-" + tail[1] + "-" + tail[2] + "-" + tail[3] + "-" + tail[4]
|
|
||||||
if !roomid.ValidStringID(roomID) {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return roomID, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHexLength(value string, length int) bool {
|
|
||||||
if len(value) != length {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := 0; i < len(value); i++ {
|
|
||||||
char := value[i]
|
|
||||||
if char >= '0' && char <= '9' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if char >= 'a' && char <= 'f' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if char >= 'A' && char <= 'F' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseBroadcastGroupID(groupID string) ParsedGroup {
|
func parseBroadcastGroupID(groupID string) ParsedGroup {
|
||||||
if strings.HasPrefix(groupID, "hy_") && strings.HasSuffix(groupID, globalSuffix) {
|
if strings.HasPrefix(groupID, "hy_") && strings.HasSuffix(groupID, globalSuffix) {
|
||||||
app := strings.TrimSuffix(strings.TrimPrefix(groupID, "hy_"), globalSuffix)
|
app := strings.TrimSuffix(strings.TrimPrefix(groupID, "hy_"), globalSuffix)
|
||||||
|
|||||||
@ -43,13 +43,13 @@ func TestParseRoomAndRejectInvalidBroadcast(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRoomGroupIDEscapesUUIDHyphen(t *testing.T) {
|
func TestRoomGroupIDPreservesRoomID(t *testing.T) {
|
||||||
roomID := "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca"
|
roomID := "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca"
|
||||||
groupID, err := RoomGroupID(roomID)
|
groupID, err := RoomGroupID(roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RoomGroupID failed: %v", err)
|
t.Fatalf("RoomGroupID failed: %v", err)
|
||||||
}
|
}
|
||||||
if groupID != "lalu_2963d975_316e_444d_ad5c_f82fc36c28ca" {
|
if groupID != roomID {
|
||||||
t.Fatalf("room group mismatch: %s", groupID)
|
t.Fatalf("room group mismatch: %s", groupID)
|
||||||
}
|
}
|
||||||
parsed := Parse(groupID)
|
parsed := Parse(groupID)
|
||||||
@ -92,6 +92,9 @@ func TestBroadcastGroupIDWithPrefix(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RoomGroupIDWithPrefix failed: %v", err)
|
t.Fatalf("RoomGroupIDWithPrefix failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if prefixedRoom != "test_lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
|
||||||
|
t.Fatalf("prefixed room group mismatch: %s", prefixedRoom)
|
||||||
|
}
|
||||||
if parsed := ParseWithPrefix(prefixedRoom, "test_"); parsed.Kind != KindRoom || parsed.RoomID != "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
|
if parsed := ParseWithPrefix(prefixedRoom, "test_"); parsed.Kind != KindRoom || parsed.RoomID != "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
|
||||||
t.Fatalf("prefixed room parse mismatch: %+v", parsed)
|
t.Fatalf("prefixed room parse mismatch: %+v", parsed)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -478,6 +479,31 @@ type restResponse struct {
|
|||||||
ErrorCode int `json:"ErrorCode"`
|
ErrorCode int `json:"ErrorCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RESTError 保留腾讯云 IM 业务错误码,让上层能区分缺群、参数错误和限流。
|
||||||
|
type RESTError struct {
|
||||||
|
Code int
|
||||||
|
Info string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 实现 error,沿用既有日志格式,避免线上检索规则失效。
|
||||||
|
func (e RESTError) Error() string {
|
||||||
|
return fmt.Sprintf("tencent im rest failed: code=%d info=%s", e.Code, e.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRESTErrorCode 判断错误链中是否包含指定腾讯云 IM 业务错误码。
|
||||||
|
func IsRESTErrorCode(err error, codes ...int) bool {
|
||||||
|
var restErr RESTError
|
||||||
|
if !errors.As(err, &restErr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, code := range codes {
|
||||||
|
if restErr.Code == code {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (r restResponse) err() error {
|
func (r restResponse) err() error {
|
||||||
if r.ErrorCode == 0 && strings.EqualFold(r.ActionStatus, "OK") {
|
if r.ErrorCode == 0 && strings.EqualFold(r.ActionStatus, "OK") {
|
||||||
return nil
|
return nil
|
||||||
@ -487,7 +513,7 @@ func (r restResponse) err() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("tencent im rest failed: code=%d info=%s", r.ErrorCode, r.ErrorInfo)
|
return RESTError{Code: r.ErrorCode, Info: r.ErrorInfo}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAccountAlreadyImported(response restResponse) bool {
|
func isAccountAlreadyImported(response restResponse) bool {
|
||||||
|
|||||||
@ -2471,7 +2471,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
rooms := data["rooms"].([]any)
|
rooms := data["rooms"].([]any)
|
||||||
first := rooms[0].(map[string]any)
|
first := rooms[0].(map[string]any)
|
||||||
if first["im_group_id"] != "room_1" || first["locked"] != true || first["country_code"] != "US" {
|
if first["im_group_id"] != "room-1" || first["locked"] != true || first["country_code"] != "US" {
|
||||||
t.Fatalf("room list must expose room IM group id: %+v", first)
|
t.Fatalf("room list must expose room IM group id: %+v", first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2836,7 +2836,7 @@ func TestGetMyRoomUsesAuthenticatedOwnerAndReturnsIMGroup(t *testing.T) {
|
|||||||
t.Fatalf("my room must preserve has_room=true: %+v", data)
|
t.Fatalf("my room must preserve has_room=true: %+v", data)
|
||||||
}
|
}
|
||||||
room := data["room"].(map[string]any)
|
room := data["room"].(map[string]any)
|
||||||
if room["room_id"] != "room-owner" || room["im_group_id"] != "room_owner" {
|
if room["room_id"] != "room-owner" || room["im_group_id"] != "room-owner" {
|
||||||
t.Fatalf("my room must expose room id and im group id: %+v", room)
|
t.Fatalf("my room must expose room id and im group id: %+v", room)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,10 +72,10 @@ func TestCreateRoomDataIncludesExplicitIMGroupID(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if data.Room.RoomID != "room-create-im" || data.Room.IMGroupID != "room_create_im" {
|
if data.Room.RoomID != "room-create-im" || data.Room.IMGroupID != "room-create-im" {
|
||||||
t.Fatalf("create response room must expose room_id and im_group_id: %+v", data.Room)
|
t.Fatalf("create response room must expose room_id and im_group_id: %+v", data.Room)
|
||||||
}
|
}
|
||||||
if data.IM.GroupID != "room_create_im" || !data.IM.NeedJoinGroup {
|
if data.IM.GroupID != "room-create-im" || !data.IM.NeedJoinGroup {
|
||||||
t.Fatalf("create response must expose joinable im group: %+v", data.IM)
|
t.Fatalf("create response must expose joinable im group: %+v", data.IM)
|
||||||
}
|
}
|
||||||
if !data.Result.Applied || data.ServerTimeMS != 1700000000000 {
|
if !data.Result.Applied || data.ServerTimeMS != 1700000000000 {
|
||||||
|
|||||||
@ -1088,7 +1088,7 @@ func roomSeatCounts(snapshot *roomv1.RoomSnapshot) (int32, int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func roomIMGroupID(roomID string) string {
|
func roomIMGroupID(roomID string) string {
|
||||||
// 房间业务 ID 保持原始值;腾讯 IM GroupID 在协议边界转义,避免 UUID 连字符被腾讯 REST 拒绝。
|
// 房间 IM 群 ID 必须等于内部 room_id;线上腾讯云已按这个 ID 建群,客户端也按这个字段入群。
|
||||||
groupID, err := imgroup.RoomGroupID(roomID)
|
groupID, err := imgroup.RoomGroupID(roomID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return roomID
|
return roomID
|
||||||
|
|||||||
@ -48,7 +48,17 @@ func (p *TencentIMPublisher) PublishRoomEvent(ctx context.Context, event tencent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
event.GroupID = groupID
|
event.GroupID = groupID
|
||||||
return p.client.PublishRoomEvent(ctx, event)
|
if err := p.client.PublishRoomEvent(ctx, event); err != nil {
|
||||||
|
if !tencentim.IsRESTErrorCode(err, 10010, 10015) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 历史房间可能先产生 outbox 事件、但腾讯云群没有成功创建;保持原 GroupID,补建后只重发一次。
|
||||||
|
if ensureErr := p.client.EnsureRoomGroup(ctx, groupID, 0); ensureErr != nil {
|
||||||
|
return fmt.Errorf("ensure room im group after publish failure: %w", ensureErr)
|
||||||
|
}
|
||||||
|
return p.client.PublishRoomEvent(ctx, event)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublishOutboxEvent 补偿投递 room_outbox 中可转成房间系统消息的事件。
|
// PublishOutboxEvent 补偿投递 room_outbox 中可转成房间系统消息的事件。
|
||||||
@ -76,7 +86,13 @@ func (p *TencentIMPublisher) PublishOutboxEvent(ctx context.Context, envelope *r
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := p.client.DeleteGroupMember(ctx, groupID, event.TargetUserID); err != nil {
|
if err := p.client.DeleteGroupMember(ctx, groupID, event.TargetUserID); err != nil {
|
||||||
return err
|
if !tencentim.IsRESTErrorCode(err, 10010, 10015) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 群缺失时没有成员可删,先补建群,再继续投递 kick 系统消息以推进 outbox。
|
||||||
|
if ensureErr := p.client.EnsureRoomGroup(ctx, groupID, 0); ensureErr != nil {
|
||||||
|
return fmt.Errorf("ensure room im group after member removal failure: %w", ensureErr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -198,8 +198,71 @@ func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
|
|||||||
if len(paths) != 2 || paths[0] != "/v4/group_open_http_svc/delete_group_member" || paths[1] != "/v4/group_open_http_svc/send_group_msg" {
|
if len(paths) != 2 || paths[0] != "/v4/group_open_http_svc/delete_group_member" || paths[1] != "/v4/group_open_http_svc/send_group_msg" {
|
||||||
t.Fatalf("kick outbox should delete member before system message, paths=%+v", paths)
|
t.Fatalf("kick outbox should delete member before system message, paths=%+v", paths)
|
||||||
}
|
}
|
||||||
if bodies[0]["GroupId"] != "room_kick" || bodies[1]["GroupId"] != "room_kick" {
|
if bodies[0]["GroupId"] != "room-kick" || bodies[1]["GroupId"] != "room-kick" {
|
||||||
t.Fatalf("kick outbox must use Tencent-safe room group id, bodies=%+v", bodies)
|
t.Fatalf("kick outbox must use original room group id, bodies=%+v", bodies)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTencentIMPublisherEnsuresRoomGroupAndRetriesWhenGroupMissing(t *testing.T) {
|
||||||
|
paths := make([]string, 0, 3)
|
||||||
|
bodies := make([]map[string]any, 0, 3)
|
||||||
|
client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
|
||||||
|
SDKAppID: 1400000000,
|
||||||
|
SecretKey: "secret",
|
||||||
|
AdminIdentifier: "administrator",
|
||||||
|
AdminUserSigTTL: time.Hour,
|
||||||
|
HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
|
||||||
|
paths = append(paths, request.URL.Path)
|
||||||
|
var body map[string]any
|
||||||
|
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("decode request body failed: %v", err)
|
||||||
|
}
|
||||||
|
bodies = append(bodies, body)
|
||||||
|
if len(paths) == 1 {
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"FAIL","ErrorCode":10015,"ErrorInfo":"bytes_group_id invalid"}`))),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: http.StatusOK,
|
||||||
|
Body: io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`))),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}, nil
|
||||||
|
})},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRESTClient failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publisher := NewTencentIMPublisher(client)
|
||||||
|
err = publisher.PublishRoomEvent(context.Background(), tencentim.RoomEvent{
|
||||||
|
EventID: "evt-retry",
|
||||||
|
RoomID: "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca",
|
||||||
|
EventType: "room_mic_changed",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PublishRoomEvent should retry after ensuring group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantPaths := []string{
|
||||||
|
"/v4/group_open_http_svc/send_group_msg",
|
||||||
|
"/v4/group_open_http_svc/create_group",
|
||||||
|
"/v4/group_open_http_svc/send_group_msg",
|
||||||
|
}
|
||||||
|
if len(paths) != len(wantPaths) {
|
||||||
|
t.Fatalf("path count mismatch: paths=%+v bodies=%+v", paths, bodies)
|
||||||
|
}
|
||||||
|
for index, want := range wantPaths {
|
||||||
|
if paths[index] != want {
|
||||||
|
t.Fatalf("path[%d] mismatch: got %s want %s", index, paths[index], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, body := range bodies {
|
||||||
|
if body["GroupId"] != "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
|
||||||
|
t.Fatalf("request %d must preserve original room group id: %+v", index, body)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user