2026-07-04 01:53:10 +08:00

567 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}