package smashegg import ( "context" "testing" "time" "chatapp3-golang/internal/config" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type fakeSmashEggGateway struct { giftCalls []fakeGiftBackpackCall propsCalls []integration.GivePropsBackpackRequest switches int changes []integration.GoldReceiptCommand } type fakeGiftBackpackCall struct { userID int64 giftID int64 quantity int } func (g *fakeSmashEggGateway) ChangeGoldBalance(_ context.Context, cmd integration.GoldReceiptCommand) error { g.changes = append(g.changes, cmd) return nil } func (g *fakeSmashEggGateway) GivePropsBackpack(_ context.Context, req integration.GivePropsBackpackRequest) error { g.propsCalls = append(g.propsCalls, req) return nil } func (g *fakeSmashEggGateway) IncrGiftBackpack(_ context.Context, userID int64, giftID int64, quantity int) error { g.giftCalls = append(g.giftCalls, fakeGiftBackpackCall{ userID: userID, giftID: giftID, quantity: quantity, }) return nil } func (g *fakeSmashEggGateway) SwitchUseProps(context.Context, int64, int64) error { g.switches++ return nil } func (g *fakeSmashEggGateway) ActivateTemporaryBadge(context.Context, int64, int64, int) error { return nil } func (g *fakeSmashEggGateway) RemoveUserProfileCacheAll(context.Context, int64) error { return nil } func (g *fakeSmashEggGateway) MapGoldBalance(context.Context, []int64) (map[int64]int64, error) { return map[int64]int64{}, nil } func (g *fakeSmashEggGateway) MapUserTotalRecharge(context.Context, []int64) (map[int64][]integration.UserTotalRecharge, error) { return map[int64][]integration.UserTotalRecharge{}, nil } func TestGrantResourceRewardRoutesGiftToGiftBackpack(t *testing.T) { gateway := &fakeSmashEggGateway{} service := &Service{java: gateway} giftID := int64(2044086730826510337) if err := service.grantResourceReward(context.Background(), &model.SmashEggDrawRecord{ UserID: 2042274349343506434, RewardGroupID: &giftID, ResourceType: resourceTypeGift, DurationDays: 7, }); err != nil { t.Fatalf("grantResourceReward() error = %v", err) } if len(gateway.giftCalls) != 1 { t.Fatalf("gift backpack calls = %d, want 1", len(gateway.giftCalls)) } if got := gateway.giftCalls[0]; got.userID != 2042274349343506434 || got.giftID != giftID || got.quantity != 1 { t.Fatalf("gift backpack call = %+v", got) } if len(gateway.propsCalls) != 0 { t.Fatalf("props backpack calls = %d, want 0", len(gateway.propsCalls)) } if gateway.switches != 0 { t.Fatalf("switch use props calls = %d, want 0", gateway.switches) } } func TestDrawIdempotencyReturnsOriginalResultWithoutSecondCharge(t *testing.T) { service, gateway, db := newSmashEggDrawTestService(t) seedSmashEggDrawConfig(t, db) req := DrawRequest{Times: 1, IdempotencyKey: "egg-h5-key-1"} first, err := service.Draw(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, req) if err != nil { t.Fatalf("Draw() first error = %v", err) } second, err := service.Draw(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, req) if err != nil { t.Fatalf("Draw() second error = %v", err) } if second.DrawNo != first.DrawNo { t.Fatalf("second drawNo = %s, want %s", second.DrawNo, first.DrawNo) } if len(gateway.changes) != 2 { t.Fatalf("wallet changes = %d, want one pay+reward", len(gateway.changes)) } var recordCount int64 if err := db.Model(&model.SmashEggDrawRecord{}).Where("draw_no = ?", first.DrawNo).Count(&recordCount).Error; err != nil { t.Fatalf("count records: %v", err) } if recordCount != 1 { t.Fatalf("record count = %d, want 1", recordCount) } } func TestDrawIdempotencyRejectsDifferentRequest(t *testing.T) { service, gateway, db := newSmashEggDrawTestService(t) seedSmashEggDrawConfig(t, db) _, err := service.Draw(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, DrawRequest{Times: 1, IdempotencyKey: "egg-h5-key-2"}) if err != nil { t.Fatalf("Draw() first error = %v", err) } _, err = service.Draw(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, DrawRequest{Times: 10, IdempotencyKey: "egg-h5-key-2"}) if err == nil { t.Fatal("Draw() second expected idempotency conflict") } if len(gateway.changes) != 2 { t.Fatalf("wallet changes = %d, want only first pay+reward", len(gateway.changes)) } } func newSmashEggDrawTestService(t *testing.T) (*Service, *fakeSmashEggGateway, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate( &model.SmashEggConfig{}, &model.SmashEggDrawOptionConfig{}, &model.SmashEggRewardConfig{}, &model.SmashEggDrawRecord{}, &model.ActivityIdempotency{}, ); err != nil { t.Fatalf("migrate sqlite: %v", err) } gateway := &fakeSmashEggGateway{} return NewService(config.Config{}, db, gateway), gateway, db } func seedSmashEggDrawConfig(t *testing.T, db *gorm.DB) { t.Helper() now := time.Now() configRow := model.SmashEggConfig{ ID: 1, SysOrigin: "LIKEI", Enabled: true, Timezone: defaultTimezone, RTPBasisPoints: defaultRTPBasisPoints, CreateTime: now, UpdateTime: now, } options := []model.SmashEggDrawOptionConfig{ {ID: 2, ConfigID: 1, SysOrigin: "LIKEI", OptionKey: optionKeyOne, Label: "Smash 1", Times: 1, PriceGold: 100, Sort: 1, Enabled: true, CreateTime: now, UpdateTime: now}, {ID: 3, ConfigID: 1, SysOrigin: "LIKEI", OptionKey: optionKeyTen, Label: "Smash 10", Times: 10, PriceGold: 1000, Sort: 2, Enabled: true, CreateTime: now, UpdateTime: now}, } rewards := []model.SmashEggRewardConfig{ {ID: 4, ConfigID: 1, SysOrigin: "LIKEI", PoolType: poolTypeGeneral, RewardType: rewardTypeGold, RewardGroupName: "50 Gold", GoldAmount: 50, RewardValueGold: 50, Sort: 1, Enabled: true, CreateTime: now, UpdateTime: now}, {ID: 5, ConfigID: 1, SysOrigin: "LIKEI", PoolType: poolTypeGeneral, RewardType: rewardTypeGold, RewardGroupName: "100 Gold", GoldAmount: 100, RewardValueGold: 100, Sort: 2, Enabled: true, CreateTime: now, UpdateTime: now}, } if err := db.Create(&configRow).Error; err != nil { t.Fatalf("seed config: %v", err) } if err := db.Create(&options).Error; err != nil { t.Fatalf("seed options: %v", err) } if err := db.Create(&rewards).Error; err != nil { t.Fatalf("seed rewards: %v", err) } }