153 lines
6.8 KiB
Go
153 lines
6.8 KiB
Go
package broadcast
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"testing"
|
||
"time"
|
||
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/tencentim"
|
||
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||
"hyapp/services/activity-service/internal/testutil/mysqltest"
|
||
)
|
||
|
||
func TestBroadcastServiceUsesRealMySQLOutbox(t *testing.T) {
|
||
// 这个测试使用生产 initdb 创建隔离 MySQL schema,验证真实 JSON 列、主键幂等、claim 锁和 delivered 更新。
|
||
repository := mysqltest.NewRepository(t)
|
||
publisher := &fakePublisher{}
|
||
service := New(Config{NodeID: "node-real", SuperGiftMinValue: 100}, repository.Repository, publisher, nil)
|
||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_123_456) })
|
||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||
|
||
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
||
SenderUserId: 42,
|
||
TargetUserId: 43,
|
||
GiftId: "super_rocket",
|
||
GiftCount: 3,
|
||
GiftValue: 300000,
|
||
VisibleRegionId: 210,
|
||
CommandId: "cmd-real-send-gift-1",
|
||
})
|
||
|
||
created, err := service.HandleRoomEvent(ctx, envelope)
|
||
if err != nil {
|
||
t.Fatalf("HandleRoomEvent with real mysql failed: %v", err)
|
||
}
|
||
if !created.BroadcastCreated || created.BroadcastEventID != "gift_broadcast:room-1001:cmd-real-send-gift-1" {
|
||
t.Fatalf("unexpected first consume result: %+v", created)
|
||
}
|
||
t.Logf("real mysql enqueue: room_id=%s region_id=%d gift_value=%d broadcast_event_id=%s", envelope.GetRoomId(), int64(210), int64(300000), created.BroadcastEventID)
|
||
|
||
// 同一个 room outbox 事实再次消费必须命中 MySQL 主键幂等,不能产生第二条播报。
|
||
again, err := service.HandleRoomEvent(ctx, envelope)
|
||
if err != nil {
|
||
t.Fatalf("second HandleRoomEvent with real mysql failed: %v", err)
|
||
}
|
||
if again.BroadcastCreated {
|
||
t.Fatalf("duplicate room event should not create another broadcast: %+v", again)
|
||
}
|
||
|
||
record, exists, err := repository.Repository.GetBroadcastOutbox(ctx, created.BroadcastEventID)
|
||
if err != nil {
|
||
t.Fatalf("read real mysql outbox failed: %v", err)
|
||
}
|
||
if !exists || record.Status != broadcastdomain.StatusPending || record.GroupID != "hy_lalu_bc_r_v2_210" {
|
||
t.Fatalf("unexpected pending outbox record: exists=%v record=%+v", exists, record)
|
||
}
|
||
t.Logf("real mysql pending record: event_id=%s group_id=%s status=%s", record.EventID, record.GroupID, record.Status)
|
||
assertJSONField(t, record.PayloadJSON, "room_id", "room-1001")
|
||
assertJSONField(t, record.PayloadJSON, "gift_id", "super_rocket")
|
||
assertJSONField(t, record.PayloadJSON, "scope", broadcastdomain.ScopeRegion)
|
||
|
||
result, err := service.ProcessPendingBroadcasts(ctx, WorkerOptions{WorkerID: "worker-real", BatchSize: 1, PollInterval: time.Second})
|
||
if err != nil {
|
||
t.Fatalf("ProcessPendingBroadcasts with real mysql failed: %v", err)
|
||
}
|
||
if result.ClaimedCount != 1 || result.SuccessCount != 1 || result.FailureCount != 0 {
|
||
t.Fatalf("unexpected process result: %+v", result)
|
||
}
|
||
// v2 区域群投递成功后过渡期会向 v1 旧群补发一条 _legacy 副本。
|
||
if len(publisher.messages) != 2 {
|
||
t.Fatalf("expected v2 message plus legacy copy, got %d", len(publisher.messages))
|
||
}
|
||
assertPublishedMessage(t, publisher.messages[0], "hy_lalu_bc_r_v2_210", created.BroadcastEventID)
|
||
assertPublishedMessage(t, publisher.messages[1], "hy_lalu_bc_r_210", created.BroadcastEventID+"_legacy")
|
||
|
||
delivered, exists, err := repository.Repository.GetBroadcastOutbox(ctx, created.BroadcastEventID)
|
||
if err != nil {
|
||
t.Fatalf("read delivered real mysql outbox failed: %v", err)
|
||
}
|
||
if !exists || delivered.Status != broadcastdomain.StatusDelivered || delivered.LockedBy != "" || delivered.LockedUntilMS != 0 {
|
||
t.Fatalf("unexpected delivered outbox record: exists=%v record=%+v", exists, delivered)
|
||
}
|
||
t.Logf("real mysql delivered record: event_id=%s group_id=%s status=%s", delivered.EventID, delivered.GroupID, delivered.Status)
|
||
}
|
||
|
||
func TestRedPacketCreatedWalletEventUsesRealMySQLOutbox(t *testing.T) {
|
||
repository := mysqltest.NewRepository(t)
|
||
service := New(Config{NodeID: "node-real", SuperGiftMinValue: 100}, repository.Repository, nil, nil)
|
||
service.SetClock(func() time.Time { return time.UnixMilli(1_700_000_456_000) })
|
||
ctx := appcode.WithContext(context.Background(), "lalu")
|
||
|
||
// 只有 delayed 且带 room_id/region_id 的红包事实才会产生房间 CREATED 加区域飘屏两条记录。
|
||
event := broadcastdomain.RedPacketCreatedEvent{
|
||
AppCode: "lalu",
|
||
EventID: "wallet-red-packet-created-real",
|
||
TransactionID: "wallet-tx-red-packet-real",
|
||
CommandID: "cmd-red-packet-real",
|
||
PacketID: "rp-real",
|
||
PacketType: "delayed",
|
||
RoomID: "room-310",
|
||
RegionID: 310,
|
||
PayloadJSON: `{"packet_id":"rp-real","room_id":"room-310","region_id":310}`,
|
||
OpenAtMS: 1_700_000_755_000,
|
||
CreatedAtMS: 1_700_000_455_000,
|
||
}
|
||
if err := service.ConsumeRedPacketCreatedEvent(ctx, event); err != nil {
|
||
t.Fatalf("ConsumeRedPacketCreatedEvent failed: %v", err)
|
||
}
|
||
if err := service.ConsumeRedPacketCreatedEvent(ctx, event); err != nil {
|
||
t.Fatalf("duplicate ConsumeRedPacketCreatedEvent failed: %v", err)
|
||
}
|
||
|
||
roomRecord, exists, err := repository.Repository.GetBroadcastOutbox(ctx, event.EventID)
|
||
if err != nil {
|
||
t.Fatalf("read red packet room broadcast outbox failed: %v", err)
|
||
}
|
||
if !exists || roomRecord.BroadcastType != broadcastdomain.TypeRedPacket || roomRecord.GroupID != "room-310" || roomRecord.Status != broadcastdomain.StatusPending {
|
||
t.Fatalf("unexpected red packet room broadcast record: exists=%v record=%+v", exists, roomRecord)
|
||
}
|
||
|
||
record, exists, err := repository.Repository.GetBroadcastOutbox(ctx, event.EventID+":region_created")
|
||
if err != nil {
|
||
t.Fatalf("read red packet region broadcast outbox failed: %v", err)
|
||
}
|
||
if !exists || record.BroadcastType != broadcastdomain.TypeRedPacket || record.GroupID != "hy_lalu_bc_r_v2_310" || record.Status != broadcastdomain.StatusPending {
|
||
t.Fatalf("unexpected red packet region broadcast record: exists=%v record=%+v", exists, record)
|
||
}
|
||
assertJSONField(t, record.PayloadJSON, "region_id", float64(310))
|
||
}
|
||
|
||
func assertJSONField(t *testing.T, payloadJSON string, key string, want any) {
|
||
t.Helper()
|
||
|
||
var payload map[string]any
|
||
if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
|
||
t.Fatalf("payload is not valid json: %v\npayload=%s", err, payloadJSON)
|
||
}
|
||
if got := payload[key]; got != want {
|
||
t.Fatalf("json field %s mismatch: got=%v want=%v payload=%+v", key, got, want, payload)
|
||
}
|
||
}
|
||
|
||
func assertPublishedMessage(t *testing.T, message tencentim.CustomGroupMessage, groupID string, eventID string) {
|
||
t.Helper()
|
||
|
||
if message.GroupID != groupID || message.EventID != eventID || message.Desc != broadcastdomain.TypeSuperGift || message.Ext != "im_broadcast" {
|
||
t.Fatalf("unexpected Tencent custom message: %+v", message)
|
||
}
|
||
assertJSONField(t, string(message.PayloadJSON), "event_id", eventID)
|
||
}
|