567 lines
22 KiB
Go
567 lines
22 KiB
Go
package broadcast
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/tencentim"
|
||
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||
)
|
||
|
||
func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
service := New(Config{NodeID: "node-a", SuperGiftMinValue: 100}, repository, nil, nil)
|
||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_000_000) })
|
||
service.SetSenderProfileSource(fakeSenderProfileSource{profiles: map[int64]SenderProfile{
|
||
42: {UserID: 42, Account: "160042", Nickname: "Sender", Avatar: "https://cdn.example/sender.png", AvatarFrameURL: "https://cdn.example/sender-frame.png"},
|
||
43: {UserID: 43, Account: "160043", Nickname: "Receiver", Avatar: "https://cdn.example/receiver.png", AvatarFrameURL: "https://cdn.example/receiver-frame.png"},
|
||
}})
|
||
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
||
SenderUserId: 42,
|
||
TargetUserId: 43,
|
||
GiftId: "rocket",
|
||
GiftCount: 2,
|
||
GiftValue: 999,
|
||
VisibleRegionId: 1001,
|
||
CommandId: "cmd-send-gift-1",
|
||
GiftName: "Rocket",
|
||
GiftIconUrl: "https://cdn.example/rocket.png",
|
||
GiftEffectTypes: []string{"animation", "global_broadcast"},
|
||
})
|
||
|
||
result, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
|
||
if err != nil {
|
||
t.Fatalf("HandleRoomEvent failed: %v", err)
|
||
}
|
||
if result.Status != broadcastdomain.StatusPending || !result.BroadcastCreated || result.BroadcastEventID != "gift_broadcast:room-1001:cmd-send-gift-1" {
|
||
t.Fatalf("unexpected result: %+v", result)
|
||
}
|
||
record := repository.records[result.BroadcastEventID]
|
||
if record.GroupID != "hy_lalu_bc_r_v2_1001" || record.BroadcastType != broadcastdomain.TypeSuperGift {
|
||
t.Fatalf("unexpected broadcast record: %+v", record)
|
||
}
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
||
t.Fatalf("payload is not json: %v", err)
|
||
}
|
||
if payload["room_id"] != "room-1001" || payload["gift_id"] != "rocket" || payload["scope"] != "region" || payload["gift_icon_url"] != "https://cdn.example/rocket.png" {
|
||
t.Fatalf("payload mismatch: %+v", payload)
|
||
}
|
||
sender, _ := payload["sender"].(map[string]any)
|
||
receiver, _ := payload["receiver"].(map[string]any)
|
||
if sender["nickname"] != "Sender" || sender["avatar_frame_url"] != "https://cdn.example/sender-frame.png" || receiver["nickname"] != "Receiver" {
|
||
t.Fatalf("payload user profile mismatch: sender=%+v receiver=%+v", sender, receiver)
|
||
}
|
||
|
||
again, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
|
||
if err != nil {
|
||
t.Fatalf("second HandleRoomEvent failed: %v", err)
|
||
}
|
||
if again.BroadcastCreated {
|
||
t.Fatalf("second consume must be idempotent: %+v", again)
|
||
}
|
||
}
|
||
|
||
func TestHandleRoomGiftSentCreatesRegionBroadcastForTaggedGiftBelowThreshold(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
service := New(Config{NodeID: "node-a", SuperGiftMinValue: 1000}, repository, nil, nil)
|
||
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
||
SenderUserId: 42,
|
||
TargetUserId: 43,
|
||
GiftId: "tagged-gift",
|
||
GiftCount: 1,
|
||
GiftValue: 10,
|
||
VisibleRegionId: 1001,
|
||
CommandId: "cmd-tagged-gift",
|
||
GiftEffectTypes: []string{"animation", "global_broadcast"},
|
||
})
|
||
|
||
result, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
|
||
if err != nil {
|
||
t.Fatalf("HandleRoomEvent tagged gift failed: %v", err)
|
||
}
|
||
if result.Status != broadcastdomain.StatusPending || !result.BroadcastCreated {
|
||
t.Fatalf("tagged gift should create broadcast: %+v", result)
|
||
}
|
||
record := repository.records[result.BroadcastEventID]
|
||
if record.GroupID != "hy_lalu_bc_r_v2_1001" || record.BroadcastType != broadcastdomain.TypeSuperGift {
|
||
t.Fatalf("unexpected tagged gift broadcast record: %+v", record)
|
||
}
|
||
}
|
||
|
||
func TestHandleRoomPasswordChangedCreatesRegionBroadcast(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
service := New(Config{NodeID: "node-a"}, repository, nil, nil)
|
||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_001_000) })
|
||
envelope := mustPasswordChangedEnvelope(t, &roomeventsv1.RoomPasswordChanged{
|
||
ActorUserId: 42,
|
||
Locked: true,
|
||
VisibleRegionId: 1001,
|
||
})
|
||
|
||
result, err := service.HandleRoomEvent(appcode.WithContext(context.Background(), "lalu"), envelope)
|
||
if err != nil {
|
||
t.Fatalf("HandleRoomEvent password changed failed: %v", err)
|
||
}
|
||
if result.Status != broadcastdomain.StatusPending || !result.BroadcastCreated || result.BroadcastEventID != "room_password_changed:"+envelope.GetEventId() {
|
||
t.Fatalf("unexpected password broadcast result: %+v", result)
|
||
}
|
||
record := repository.records[result.BroadcastEventID]
|
||
if record.GroupID != "hy_lalu_bc_r_v2_1001" || record.Scope != broadcastdomain.ScopeRegion || record.BroadcastType != broadcastdomain.TypeRoomPasswordChanged {
|
||
t.Fatalf("unexpected password broadcast record: %+v", record)
|
||
}
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
||
t.Fatalf("password payload is not json: %v", err)
|
||
}
|
||
if payload["room_id"] != "room-1001" || payload["scope"] != "region" || payload["locked"] != true || payload["actor_user_id"] != float64(42) {
|
||
t.Fatalf("password payload mismatch: %+v", payload)
|
||
}
|
||
action, ok := payload["action"].(map[string]any)
|
||
if !ok || action["type"] != "refresh_room" || action["room_id"] != "room-1001" {
|
||
t.Fatalf("password payload action mismatch: %+v", payload["action"])
|
||
}
|
||
}
|
||
|
||
func TestHandleRoomGiftSentSkipsNonBroadcastCases(t *testing.T) {
|
||
service := New(Config{SuperGiftMinValue: 1000}, newFakeRepository(), nil, nil)
|
||
belowThreshold := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 999, VisibleRegionId: 1001})
|
||
result, err := service.HandleRoomEvent(context.Background(), belowThreshold)
|
||
if err != nil {
|
||
t.Fatalf("below threshold failed: %v", err)
|
||
}
|
||
if result.Status != broadcastdomain.StatusSkipped || result.BroadcastEventID != "" {
|
||
t.Fatalf("below threshold should skip: %+v", result)
|
||
}
|
||
|
||
globalRegion := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{GiftValue: 1000, VisibleRegionId: 0})
|
||
result, err = service.HandleRoomEvent(context.Background(), globalRegion)
|
||
if err != nil {
|
||
t.Fatalf("global region failed: %v", err)
|
||
}
|
||
if result.Status != broadcastdomain.StatusSkipped || result.BroadcastEventID != "" {
|
||
t.Fatalf("global region should skip: %+v", result)
|
||
}
|
||
}
|
||
|
||
func TestProcessPendingBroadcastsPublishesCustomMessage(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
publisher := &fakePublisher{}
|
||
service := New(Config{NodeID: "node-a"}, repository, publisher, nil)
|
||
repository.records["evt-1"] = broadcastdomain.OutboxRecord{
|
||
AppCode: "lalu",
|
||
EventID: "evt-1",
|
||
GroupID: "hy_lalu_bc_g",
|
||
BroadcastType: broadcastdomain.TypeRedPacket,
|
||
PayloadJSON: `{"event_id":"evt-1","broadcast_type":"red_packet"}`,
|
||
Status: broadcastdomain.StatusPending,
|
||
}
|
||
|
||
result, err := service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
|
||
if err != nil {
|
||
t.Fatalf("ProcessPendingBroadcasts failed: %v", err)
|
||
}
|
||
if result.SuccessCount != 1 || len(publisher.messages) != 1 {
|
||
t.Fatalf("publish result mismatch: result=%+v messages=%d", result, len(publisher.messages))
|
||
}
|
||
if repository.records["evt-1"].Status != broadcastdomain.StatusDelivered {
|
||
t.Fatalf("record should be delivered: %+v", repository.records["evt-1"])
|
||
}
|
||
if publisher.messages[0].GroupID != "hy_lalu_bc_g" || publisher.messages[0].Desc != broadcastdomain.TypeRedPacket {
|
||
t.Fatalf("message mismatch: %+v", publisher.messages[0])
|
||
}
|
||
}
|
||
|
||
func TestConsumeRedPacketCreatedBuildsFullIMPayload(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
service := New(Config{NodeID: "node-a"}, repository, nil, nil)
|
||
service.SetSenderProfileSource(fakeSenderProfileSource{profile: SenderProfile{
|
||
UserID: 10001,
|
||
Account: "10001",
|
||
Nickname: "Tom",
|
||
Avatar: "https://example.com/avatar.png",
|
||
RegionCode: "AR",
|
||
}})
|
||
event := broadcastdomain.RedPacketWalletEvent{
|
||
AppCode: "lalu",
|
||
EventID: "wallet-red-packet-created-1",
|
||
EventType: "WalletRedPacketCreated",
|
||
PacketID: "VRRP2026052712340001",
|
||
PacketType: "delayed",
|
||
RoomID: "9001",
|
||
RegionID: 310,
|
||
SenderUserID: 10001,
|
||
TotalAmount: 15000,
|
||
TotalCount: 5,
|
||
RemainingAmount: 15000,
|
||
RemainingCount: 5,
|
||
Status: "waiting_open",
|
||
OpenAtMS: 1_780_000_000_000,
|
||
ExpiresAtMS: 1_780_086_400_000,
|
||
CreatedAtMS: 1_779_999_700_000,
|
||
}
|
||
|
||
if err := service.ConsumeRedPacketWalletEvent(appcode.WithContext(context.Background(), "lalu"), event); err != nil {
|
||
t.Fatalf("ConsumeRedPacketWalletEvent failed: %v", err)
|
||
}
|
||
|
||
record := repository.records[event.EventID]
|
||
data := redPacketPayloadData(t, record.PayloadJSON)
|
||
if data["packetNo"] != event.PacketID || data["packetId"] != event.PacketID || data["scope"] != "ROOM" {
|
||
t.Fatalf("packet identity mismatch: %+v", data)
|
||
}
|
||
if data["senderUserId"] != "10001" || data["actualAccount"] != "10001" || data["userNickname"] != "Tom" || data["userAvatar"] != "https://example.com/avatar.png" {
|
||
t.Fatalf("sender display fields mismatch: %+v", data)
|
||
}
|
||
if data["totalAmount"] != float64(15000) || data["totalCount"] != float64(5) || data["remainAmount"] != float64(15000) || data["remainCount"] != float64(5) {
|
||
t.Fatalf("red packet amount/count fields mismatch: %+v", data)
|
||
}
|
||
if data["packetMode"] != "DELAYED" || data["status"] != "WAITING_OPEN" || data["delaySeconds"] != float64(300) || data["regionCode"] != "AR" {
|
||
t.Fatalf("red packet display state mismatch: %+v", data)
|
||
}
|
||
}
|
||
|
||
func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
||
nowMS := int64(1_779_999_700_000)
|
||
openAtMS := int64(1_780_000_000_000)
|
||
repository := newFakeRepository()
|
||
publisher := &fakePublisher{}
|
||
service := New(Config{NodeID: "node-a"}, repository, publisher, nil)
|
||
service.SetClock(func() time.Time { return time.UnixMilli(nowMS) })
|
||
service.SetSenderProfileSource(fakeSenderProfileSource{profile: SenderProfile{
|
||
UserID: 10001,
|
||
Account: "10001",
|
||
Nickname: "Tom",
|
||
Avatar: "https://example.com/avatar.png",
|
||
RegionCode: "AR",
|
||
}})
|
||
event := broadcastdomain.RedPacketWalletEvent{
|
||
AppCode: "lalu",
|
||
EventID: "wallet-red-packet-created-1",
|
||
EventType: "WalletRedPacketCreated",
|
||
PacketID: "VRRP2026052712340001",
|
||
PacketType: "delayed",
|
||
RoomID: "9001",
|
||
RegionID: 310,
|
||
SenderUserID: 10001,
|
||
TotalAmount: 15000,
|
||
TotalCount: 5,
|
||
RemainingAmount: 15000,
|
||
RemainingCount: 5,
|
||
Status: "waiting_open",
|
||
OpenAtMS: openAtMS,
|
||
ExpiresAtMS: 1_780_086_400_000,
|
||
CreatedAtMS: nowMS,
|
||
}
|
||
|
||
if err := service.ConsumeRedPacketWalletEvent(appcode.WithContext(context.Background(), "lalu"), event); err != nil {
|
||
t.Fatalf("ConsumeRedPacketWalletEvent failed: %v", err)
|
||
}
|
||
|
||
roomCreated := repository.records[event.EventID]
|
||
regionCreated := repository.records[event.EventID+":region_created"]
|
||
openDue := repository.records[event.EventID+":open_due"]
|
||
if roomCreated.GroupID != "9001" || roomCreated.Scope != broadcastdomain.ScopeRoom || roomCreated.NextRetryAtMS != nowMS {
|
||
t.Fatalf("room created outbox mismatch: %+v", roomCreated)
|
||
}
|
||
if regionCreated.GroupID != "hy_lalu_bc_r_v2_310" || regionCreated.Scope != broadcastdomain.ScopeRegion || regionCreated.NextRetryAtMS != nowMS {
|
||
t.Fatalf("region created outbox mismatch: %+v", regionCreated)
|
||
}
|
||
if openDue.GroupID != "9001" || openDue.Scope != broadcastdomain.ScopeRoom || openDue.NextRetryAtMS != openAtMS {
|
||
t.Fatalf("open due outbox mismatch: %+v", openDue)
|
||
}
|
||
regionData := redPacketPayloadData(t, regionCreated.PayloadJSON)
|
||
if regionData["event"] != "CREATED" || regionData["scope"] != "REGION" || regionData["packetMode"] != "DELAYED" {
|
||
t.Fatalf("region created payload mismatch: %+v", regionData)
|
||
}
|
||
openDueData := redPacketPayloadData(t, openDue.PayloadJSON)
|
||
if openDueData["event"] != "OPEN_DUE" || openDueData["scope"] != "ROOM" || openDueData["claimStartTime"] != float64(openAtMS) {
|
||
t.Fatalf("open due payload mismatch: %+v", openDueData)
|
||
}
|
||
|
||
result, err := service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
|
||
if err != nil {
|
||
t.Fatalf("ProcessPendingBroadcasts before open failed: %v", err)
|
||
}
|
||
// v2 区域群投递成功后过渡期会向 v1 旧群补发一条 _legacy 副本,所以消息比 outbox 记录多一条。
|
||
if result.SuccessCount != 2 || len(publisher.messages) != 3 || containsCustomMessageEvent(publisher.messages, event.EventID+":open_due") {
|
||
t.Fatalf("before open only room/region CREATED should be sent: result=%+v messages=%+v", result, publisher.messages)
|
||
}
|
||
legacyCopy := findCustomMessageEvent(publisher.messages, event.EventID+":region_created_legacy")
|
||
if legacyCopy == nil || legacyCopy.GroupID != "hy_lalu_bc_r_310" {
|
||
t.Fatalf("region broadcast should dual-send to legacy v1 group: %+v", publisher.messages)
|
||
}
|
||
|
||
service.SetClock(func() time.Time { return time.UnixMilli(openAtMS) })
|
||
result, err = service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
|
||
if err != nil {
|
||
t.Fatalf("ProcessPendingBroadcasts after open failed: %v", err)
|
||
}
|
||
if result.SuccessCount != 1 || !containsCustomMessageEvent(publisher.messages, event.EventID+":open_due") {
|
||
t.Fatalf("after open should send OPEN_DUE: result=%+v messages=%+v", result, publisher.messages)
|
||
}
|
||
}
|
||
|
||
func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) {
|
||
publisher := &fakePublisher{}
|
||
service := New(Config{NodeID: "node-a"}, newFakeRepository(), publisher, nil)
|
||
|
||
groupID, removed, err := service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 1001)
|
||
if err != nil {
|
||
t.Fatalf("RemoveRegionBroadcastMember failed: %v", err)
|
||
}
|
||
if !removed || groupID != "hy_lalu_bc_r_v2_1001" {
|
||
t.Fatalf("unexpected remove result: group_id=%s removed=%t", groupID, removed)
|
||
}
|
||
// 切区必须同时清 v2 主群和过渡期 v1 旧群的成员关系。
|
||
if len(publisher.deletedGroupIDs) != 2 || publisher.deletedGroupIDs[0] != "hy_lalu_bc_r_v2_1001" || publisher.deletedGroupIDs[1] != "hy_lalu_bc_r_1001" || publisher.deletedUserID != 42 {
|
||
t.Fatalf("unexpected deleted member: group_ids=%v user_id=%d", publisher.deletedGroupIDs, publisher.deletedUserID)
|
||
}
|
||
|
||
groupID, removed, err = service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 0)
|
||
if err != nil {
|
||
t.Fatalf("zero region should be a no-op, got: %v", err)
|
||
}
|
||
if removed || groupID != "" {
|
||
t.Fatalf("zero region should not delete any group member: group_id=%s removed=%t", groupID, removed)
|
||
}
|
||
}
|
||
|
||
func TestRegionBroadcastUsesConfiguredGroupPrefix(t *testing.T) {
|
||
repository := newFakeRepository()
|
||
publisher := &fakePublisher{}
|
||
service := New(Config{NodeID: "node-a", GroupIDPrefix: "test_"}, repository, publisher, nil)
|
||
|
||
result, err := service.PublishRegionBroadcast(appcode.WithContext(context.Background(), "lalu"), PublishInput{
|
||
EventID: "evt-prefixed",
|
||
BroadcastType: broadcastdomain.TypeSuperGift,
|
||
RegionID: 1001,
|
||
PayloadJSON: `{"ok":true}`,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("PublishRegionBroadcast failed: %v", err)
|
||
}
|
||
if result.GroupID != "test_hy_lalu_bc_r_v2_1001" {
|
||
t.Fatalf("prefixed publish group mismatch: %+v", result)
|
||
}
|
||
if record := repository.records["evt-prefixed"]; record.GroupID != "test_hy_lalu_bc_r_v2_1001" {
|
||
t.Fatalf("prefixed outbox group mismatch: %+v", record)
|
||
}
|
||
|
||
groupID, removed, err := service.RemoveRegionBroadcastMember(appcode.WithContext(context.Background(), "lalu"), 42, 1001)
|
||
if err != nil {
|
||
t.Fatalf("RemoveRegionBroadcastMember failed: %v", err)
|
||
}
|
||
if !removed || groupID != "test_hy_lalu_bc_r_v2_1001" || len(publisher.deletedGroupIDs) != 2 || publisher.deletedGroupIDs[0] != "test_hy_lalu_bc_r_v2_1001" || publisher.deletedGroupIDs[1] != "test_hy_lalu_bc_r_1001" {
|
||
t.Fatalf("prefixed remove mismatch: group_id=%s removed=%t deleted=%v", groupID, removed, publisher.deletedGroupIDs)
|
||
}
|
||
}
|
||
|
||
func TestEnsureBroadcastGroupsUsesAVChatRoomAndSurvivesGlobalFailure(t *testing.T) {
|
||
publisher := &fakePublisher{ensureErrors: map[string]error{"hy_lalu_bc_g_v2": errors.New("quota exceeded")}}
|
||
service := New(Config{NodeID: "node-a"}, newFakeRepository(), publisher, fakeRegionSource{regionIDs: []int64{310, 320}})
|
||
|
||
count, err := service.EnsureBroadcastGroups(appcode.WithContext(context.Background(), "lalu"))
|
||
if err == nil {
|
||
t.Fatal("global group failure must be reported")
|
||
}
|
||
// 全局群失败不再阻断区域群补偿:两个区域群仍应建成。
|
||
if count != 2 {
|
||
t.Fatalf("region groups must still be ensured after global failure: count=%d", count)
|
||
}
|
||
if len(publisher.ensuredGroups) != 3 {
|
||
t.Fatalf("expected global+2 region ensure calls: %+v", publisher.ensuredGroups)
|
||
}
|
||
for _, spec := range publisher.ensuredGroups[1:] {
|
||
// 区域群一律生成 v2 并使用 AVChatRoom(无 ApplyJoinOption),不再受 ChatRoom 成员上限约束。
|
||
if spec.Type != tencentim.BroadcastGroupType || spec.ApplyJoinOption != "" {
|
||
t.Fatalf("region group must be AVChatRoom without apply_join_option: %+v", spec)
|
||
}
|
||
}
|
||
if publisher.ensuredGroups[1].GroupID != "hy_lalu_bc_r_v2_310" || publisher.ensuredGroups[2].GroupID != "hy_lalu_bc_r_v2_320" {
|
||
t.Fatalf("region group ids mismatch: %+v", publisher.ensuredGroups)
|
||
}
|
||
}
|
||
|
||
func mustGiftEnvelope(t *testing.T, gift *roomeventsv1.RoomGiftSent) *roomeventsv1.EventEnvelope {
|
||
t.Helper()
|
||
body, err := proto.Marshal(gift)
|
||
if err != nil {
|
||
t.Fatalf("marshal gift failed: %v", err)
|
||
}
|
||
return &roomeventsv1.EventEnvelope{
|
||
EventId: "evt-room-gift-1",
|
||
RoomId: "room-1001",
|
||
EventType: "RoomGiftSent",
|
||
RoomVersion: 7,
|
||
OccurredAtMs: 1_700_000_000_000,
|
||
AppCode: "lalu",
|
||
Body: body,
|
||
}
|
||
}
|
||
|
||
func mustPasswordChangedEnvelope(t *testing.T, event *roomeventsv1.RoomPasswordChanged) *roomeventsv1.EventEnvelope {
|
||
t.Helper()
|
||
body, err := proto.Marshal(event)
|
||
if err != nil {
|
||
t.Fatalf("marshal password changed failed: %v", err)
|
||
}
|
||
return &roomeventsv1.EventEnvelope{
|
||
EventId: "evt-room-password-1",
|
||
RoomId: "room-1001",
|
||
EventType: "RoomPasswordChanged",
|
||
RoomVersion: 8,
|
||
OccurredAtMs: 1_700_000_001_000,
|
||
AppCode: "lalu",
|
||
Body: body,
|
||
}
|
||
}
|
||
|
||
func redPacketPayloadData(t *testing.T, payloadJSON string) map[string]any {
|
||
t.Helper()
|
||
|
||
var payload struct {
|
||
Type string `json:"type"`
|
||
Data map[string]any `json:"data"`
|
||
}
|
||
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
||
t.Fatalf("payload is not json: %v", err)
|
||
}
|
||
if payload.Type != "ROOM_RED_PACKET" || payload.Data == nil {
|
||
t.Fatalf("unexpected red packet payload: %+v", payload)
|
||
}
|
||
return payload.Data
|
||
}
|
||
|
||
func containsCustomMessageEvent(messages []tencentim.CustomGroupMessage, eventID string) bool {
|
||
return findCustomMessageEvent(messages, eventID) != nil
|
||
}
|
||
|
||
func findCustomMessageEvent(messages []tencentim.CustomGroupMessage, eventID string) *tencentim.CustomGroupMessage {
|
||
for index := range messages {
|
||
if messages[index].EventID == eventID {
|
||
return &messages[index]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type fakeRepository struct {
|
||
records map[string]broadcastdomain.OutboxRecord
|
||
}
|
||
|
||
func newFakeRepository() *fakeRepository {
|
||
return &fakeRepository{records: map[string]broadcastdomain.OutboxRecord{}}
|
||
}
|
||
|
||
func (r *fakeRepository) SaveBroadcastOutbox(_ context.Context, record broadcastdomain.OutboxRecord) (bool, broadcastdomain.OutboxRecord, error) {
|
||
if existing, ok := r.records[record.EventID]; ok {
|
||
return false, existing, nil
|
||
}
|
||
r.records[record.EventID] = record
|
||
return true, record, nil
|
||
}
|
||
|
||
func (r *fakeRepository) ClaimPendingBroadcastOutbox(_ context.Context, workerID string, nowMS int64, lockUntilMS int64, limit int, _ int) ([]broadcastdomain.OutboxRecord, error) {
|
||
records := make([]broadcastdomain.OutboxRecord, 0, limit)
|
||
for eventID, record := range r.records {
|
||
if len(records) >= limit {
|
||
break
|
||
}
|
||
if record.Status != broadcastdomain.StatusPending && record.Status != broadcastdomain.StatusRetryable {
|
||
continue
|
||
}
|
||
if record.NextRetryAtMS > nowMS {
|
||
continue
|
||
}
|
||
record.Status = broadcastdomain.StatusDelivering
|
||
record.LockedBy = workerID
|
||
record.LockedUntilMS = lockUntilMS
|
||
r.records[eventID] = record
|
||
records = append(records, record)
|
||
}
|
||
return records, nil
|
||
}
|
||
|
||
func (r *fakeRepository) MarkBroadcastOutboxDelivered(_ context.Context, eventID string, nowMS int64) error {
|
||
record, ok := r.records[eventID]
|
||
if !ok {
|
||
return errors.New("record not found")
|
||
}
|
||
record.Status = broadcastdomain.StatusDelivered
|
||
record.UpdatedAtMS = nowMS
|
||
r.records[eventID] = record
|
||
return nil
|
||
}
|
||
|
||
func (r *fakeRepository) MarkBroadcastOutboxFailed(_ context.Context, eventID string, _ string, nowMS int64, nextRetryAtMS int64, _ int, lastError string) error {
|
||
record, ok := r.records[eventID]
|
||
if !ok {
|
||
return errors.New("record not found")
|
||
}
|
||
record.Status = broadcastdomain.StatusRetryable
|
||
record.AttemptCount++
|
||
record.NextRetryAtMS = nextRetryAtMS
|
||
record.LastError = lastError
|
||
record.UpdatedAtMS = nowMS
|
||
r.records[eventID] = record
|
||
return nil
|
||
}
|
||
|
||
type fakePublisher struct {
|
||
messages []tencentim.CustomGroupMessage
|
||
ensuredGroups []tencentim.GroupSpec
|
||
ensureErrors map[string]error
|
||
deletedGroupIDs []string
|
||
deletedUserID int64
|
||
}
|
||
|
||
func (p *fakePublisher) EnsureGroup(_ context.Context, spec tencentim.GroupSpec) error {
|
||
p.ensuredGroups = append(p.ensuredGroups, spec)
|
||
return p.ensureErrors[spec.GroupID]
|
||
}
|
||
|
||
func (p *fakePublisher) PublishGroupCustomMessage(_ context.Context, message tencentim.CustomGroupMessage) error {
|
||
p.messages = append(p.messages, message)
|
||
return nil
|
||
}
|
||
|
||
func (p *fakePublisher) DeleteGroupMember(_ context.Context, groupID string, userID int64) error {
|
||
p.deletedGroupIDs = append(p.deletedGroupIDs, groupID)
|
||
p.deletedUserID = userID
|
||
return nil
|
||
}
|
||
|
||
type fakeRegionSource struct {
|
||
regionIDs []int64
|
||
}
|
||
|
||
func (s fakeRegionSource) ListActiveRegionIDs(context.Context) ([]int64, error) {
|
||
return s.regionIDs, nil
|
||
}
|
||
|
||
type fakeSenderProfileSource struct {
|
||
profile SenderProfile
|
||
profiles map[int64]SenderProfile
|
||
err error
|
||
}
|
||
|
||
func (s fakeSenderProfileSource) GetSenderProfile(_ context.Context, userID int64) (SenderProfile, error) {
|
||
if s.profiles != nil {
|
||
if profile, ok := s.profiles[userID]; ok {
|
||
return profile, s.err
|
||
}
|
||
}
|
||
profile := s.profile
|
||
if profile.UserID <= 0 {
|
||
profile.UserID = userID
|
||
}
|
||
return profile, s.err
|
||
}
|