228 lines
8.0 KiB
Go
228 lines
8.0 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) })
|
|
envelope := mustGiftEnvelope(t, &roomeventsv1.RoomGiftSent{
|
|
SenderUserId: 42,
|
|
TargetUserId: 43,
|
|
GiftId: "rocket",
|
|
GiftCount: 2,
|
|
GiftValue: 999,
|
|
VisibleRegionId: 1001,
|
|
CommandId: "cmd-send-gift-1",
|
|
})
|
|
|
|
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_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" {
|
|
t.Fatalf("payload mismatch: %+v", payload)
|
|
}
|
|
|
|
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 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 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_1001" {
|
|
t.Fatalf("unexpected remove result: group_id=%s removed=%t", groupID, removed)
|
|
}
|
|
if publisher.deletedGroupID != "hy_lalu_bc_r_1001" || publisher.deletedUserID != 42 {
|
|
t.Fatalf("unexpected deleted member: group_id=%s user_id=%d", publisher.deletedGroupID, 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 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,
|
|
}
|
|
}
|
|
|
|
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, _ 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
|
|
}
|
|
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
|
|
deletedGroupID string
|
|
deletedUserID int64
|
|
}
|
|
|
|
func (p *fakePublisher) EnsureGroup(context.Context, tencentim.GroupSpec) error {
|
|
return nil
|
|
}
|
|
|
|
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.deletedGroupID = groupID
|
|
p.deletedUserID = userID
|
|
return nil
|
|
}
|