package managercenter import ( "context" "errors" "strings" "testing" "time" "chatapp3-golang/internal/integration" "chatapp3-golang/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type fakeManagerGateway struct { recharges map[int64][]integration.UserTotalRecharge rechargeErr error abilities map[int64]integration.PropsNobleVIPAbility maxAbility integration.PropsNobleVIPAbility giveRequests []integration.GivePropsBackpackRequest punishRequests []integration.PunishAccountRequest unblockRequests []integration.UnblockAccountRequest switchIDs []int64 cacheCleared []int64 badgeActivations []fakeBadgeActivation } type fakeBadgeActivation struct { userID int64 badgeID int64 days int } func (f *fakeManagerGateway) GetUserProfile(context.Context, int64) (integration.UserProfile, error) { return integration.UserProfile{}, nil } func (f *fakeManagerGateway) GetUserProfileByAccountOne(context.Context, string) (integration.UserProfile, error) { return integration.UserProfile{}, nil } func (f *fakeManagerGateway) MapUserTotalRecharge(_ context.Context, userIDs []int64) (map[int64][]integration.UserTotalRecharge, error) { if f.rechargeErr != nil { return nil, f.rechargeErr } result := make(map[int64][]integration.UserTotalRecharge, len(userIDs)) for _, userID := range userIDs { result[userID] = f.recharges[userID] } return result, nil } func (f *fakeManagerGateway) GivePropsBackpack(_ context.Context, req integration.GivePropsBackpackRequest) error { f.giveRequests = append(f.giveRequests, req) return nil } func (f *fakeManagerGateway) GetNobleVIPAbility(_ context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error) { return f.abilities[sourceID], nil } func (f *fakeManagerGateway) GetUserMaxNobleVIPAbility(context.Context, int64) (integration.PropsNobleVIPAbility, error) { return f.maxAbility, nil } func (f *fakeManagerGateway) SwitchUseProps(_ context.Context, _ int64, propsID int64) error { f.switchIDs = append(f.switchIDs, propsID) return nil } func (f *fakeManagerGateway) ActivateTemporaryBadge(_ context.Context, userID int64, badgeID int64, days int) error { f.badgeActivations = append(f.badgeActivations, fakeBadgeActivation{userID: userID, badgeID: badgeID, days: days}) return nil } func (f *fakeManagerGateway) RemoveUserProfileCacheAll(_ context.Context, userID int64) error { f.cacheCleared = append(f.cacheCleared, userID) return nil } func (f *fakeManagerGateway) PunishAccount(_ context.Context, req integration.PunishAccountRequest) error { f.punishRequests = append(f.punishRequests, req) return nil } func (f *fakeManagerGateway) UnblockAccount(_ context.Context, req integration.UnblockAccountRequest) error { f.unblockRequests = append(f.unblockRequests, req) return nil } func TestListPropsRequiresAdminFreeAndFiltersVIPLevels(t *testing.T) { service, _ := newManagerCenterTestService(t) seedManager(t, service.db.(*gorm.DB), 1) seedProps(t, service.db.(*gorm.DB), propsSourceRecordRow{ID: 101, Type: propsTypeRide, Name: "Giftable ride", AdminFree: true}, propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false}, propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true}, propsSourceRecordRow{ID: 204, Type: propsTypeNobleVIP, Name: "VIP 4", AdminFree: true}, ) seedAbilities(t, service.db.(*gorm.DB), propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3}, propsNobleVIPAbilityRow{ID: 204, VIPLevel: 4}, ) resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1}, "") if err != nil { t.Fatalf("ListProps() error = %v", err) } got := map[int64]PropsView{} for _, item := range resp.Records { got[item.ID.Int64()] = item } if _, ok := got[101]; !ok { t.Fatalf("records = %+v, want giftable ride", resp.Records) } if _, ok := got[102]; ok { t.Fatalf("records = %+v, non-admin-free props must be hidden", resp.Records) } if item, ok := got[201]; !ok || item.Days != defaultVIPGiftDays || item.VIPLevel != 3 { t.Fatalf("vip 201 = %+v, want level 3 with 3 days", item) } if _, ok := got[204]; ok { t.Fatalf("records = %+v, VIP level 4 must be hidden", resp.Records) } } func TestListPropsReturnsVIPLevelConfigs(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedVIPConfigs(t, db, model.VipLevelConfig{ID: 301, SysOrigin: "LIKEI", Level: 1, LevelCode: "VIP1", DisplayName: "VIP 1", Enabled: true, DurationDays: 30}, model.VipLevelConfig{ID: 302, SysOrigin: "LIKEI", Level: 2, LevelCode: "VIP2", DisplayName: "VIP 2", Enabled: false, DurationDays: 30}, model.VipLevelConfig{ID: 303, SysOrigin: "ATYOU", Level: 1, LevelCode: "VIP1", DisplayName: "ATYOU VIP 1", Enabled: true, DurationDays: 30}, model.VipLevelConfig{ID: 304, SysOrigin: "LIKEI", Level: 4, LevelCode: "VIP4", DisplayName: "VIP 4", Enabled: true, DurationDays: 30}, ) resp, err := service.ListProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, propsTypeNobleVIP) if err != nil { t.Fatalf("ListProps() error = %v", err) } if len(resp.Records) != 1 { t.Fatalf("records = %+v, want only one enabled LIKEI vip config", resp.Records) } got := resp.Records[0] if got.ID.Int64() != 301 || got.Type != propsTypeNobleVIP || got.VIPLevel != 1 || got.Days != 30 { t.Fatalf("vip props = %+v, want config 301 level 1", got) } } func TestGetProfileToleratesRechargeQueryFailure(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1"}) fake.rechargeErr = errors.New("order service unavailable") resp, err := service.GetProfile(context.Background(), AuthUser{UserID: 1}) if err != nil { t.Fatalf("GetProfile() error = %v", err) } if resp.Manager.HasRecharge || resp.Manager.CanBan { t.Fatalf("manager = %+v, want unknown recharge state and ban disabled", resp.Manager) } } func TestGetProfileReturnsManagerFeatures(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManagerWithFeatures(t, db, 1, ManagerFeatures{ AddBDLeader: true, GiftProps: false, BanUser: true, UnbanUser: false, }) seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1"}) resp, err := service.GetProfile(context.Background(), AuthUser{UserID: 1}) if err != nil { t.Fatalf("GetProfile() error = %v", err) } if !resp.Features.AddBDLeader || resp.Features.GiftProps || !resp.Features.BanUser || resp.Features.UnbanUser { t.Fatalf("features = %+v, want row switches returned", resp.Features) } } func TestAddBDLeaderCreatesLeaderAndListEntry(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2002", UserNickname: "leader", OriginSys: "LIKEI"}, ) resp, err := service.AddBDLeader(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, AddBDLeaderRequest{ UserID: ID(2), Contact: "whatsapp", }) if err != nil { t.Fatalf("AddBDLeader() error = %v", err) } if !resp.Success || resp.Leader.UserID.Int64() != 2 || resp.Leader.RegionID != "SA" { t.Fatalf("resp = %+v, want created leader for user 2 in manager region", resp) } var bdInfo businessDevelopmentBaseInfoRow if err := db.Where("user_id = ?", int64(2)).First(&bdInfo).Error; err != nil { t.Fatalf("load bd info: %v", err) } if bdInfo.BDLeadUserID == nil || *bdInfo.BDLeadUserID != 2 || bdInfo.Region != "SA" { t.Fatalf("bd info = %+v, want self-linked BD leader info", bdInfo) } var identity userHistoryIdentityRow if err := db.Where("user_id = ?", int64(2)).First(&identity).Error; err != nil { t.Fatalf("load identity: %v", err) } if !identity.IsBD { t.Fatalf("identity = %+v, want is_bd=true", identity) } list, err := service.ListBDLeaders(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}) if err != nil { t.Fatalf("ListBDLeaders() error = %v", err) } if list.Count != 1 || list.Records[0].UserID.Int64() != 2 || list.Records[0].Account != "2002" { t.Fatalf("list = %+v, want one created leader", list) } } func TestAddBDLeaderRejectsExistingLeader(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2002", OriginSys: "LIKEI"}) if err := db.Create(&bdLeaderRow{ID: 10, UserID: 2, Origin: "LIKEI", RegionID: "SA", CreateUser: 1}).Error; err != nil { t.Fatalf("seed bd leader: %v", err) } _, err := service.AddBDLeader(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, AddBDLeaderRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "user_already_bd_leader") } func TestAddBDLeaderRejectsTargetFromOtherOrigin(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2002", OriginSys: "ATYOU"}) _, err := service.AddBDLeader(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, AddBDLeaderRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "bd_leader_origin_mismatch") } func TestAddBDLeaderRejectsDisabledFeature(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManagerWithFeatures(t, db, 1, ManagerFeatures{GiftProps: true, BanUser: true, UnbanUser: true}) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2002", OriginSys: "LIKEI"}) _, err := service.AddBDLeader(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, AddBDLeaderRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "manager_feature_disabled") var count int64 if err := db.Model(&bdLeaderRow{}).Where("user_id = ?", int64(2)).Count(&count).Error; err != nil { t.Fatalf("count bd leaders: %v", err) } if count != 0 { t.Fatalf("bd leader count = %d, want 0", count) } } func TestSendPropsRejectsNonAdminFree(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) seedProps(t, db, propsSourceRecordRow{ID: 102, Type: propsTypeRide, Name: "Paid ride", AdminFree: false}) _, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(102), }) assertAppErrorCode(t, err, "prop_not_giftable") if len(fake.giveRequests) != 0 { t.Fatalf("give requests = %+v, want none", fake.giveRequests) } } func TestSendNobleVIPGivesAccessoriesAndSwitchesMax(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) seedProps(t, db, propsSourceRecordRow{ID: 201, Type: propsTypeNobleVIP, Name: "VIP 3", AdminFree: true}) seedAbilities(t, db, propsNobleVIPAbilityRow{ID: 201, VIPLevel: 3}) fake.abilities[201] = integration.PropsNobleVIPAbility{ ID: integration.Int64Value(201), AvatarFrameID: integration.Int64Value(302), CarID: integration.Int64Value(301), ChatBubbleID: integration.Int64Value(303), DataCardID: integration.Int64Value(304), } fake.maxAbility = fake.abilities[201] resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(201), }) if err != nil { t.Fatalf("SendProps() error = %v", err) } if !resp.Success || resp.Days != defaultVIPGiftDays { t.Fatalf("resp = %+v, want success and VIP days", resp) } if len(fake.giveRequests) != 5 { t.Fatalf("give requests = %+v, want main VIP plus 4 accessories", fake.giveRequests) } for _, req := range fake.giveRequests { if req.AcceptUserID != 2 || req.Origin != adminFreeOrigin || req.OpUser != 1 || !req.UseProps || req.AllowGive { t.Fatalf("give request = %+v, want admin-free grant to target", req) } } wantSwitch := []int64{201, 302, 304, 301, 303} if !equalInt64Slice(fake.switchIDs, wantSwitch) { t.Fatalf("switch ids = %+v, want %+v", fake.switchIDs, wantSwitch) } if !equalInt64Slice(fake.cacheCleared, []int64{2}) { t.Fatalf("cacheCleared = %+v, want [2]", fake.cacheCleared) } } func TestSendPropsRejectsDisabledFeature(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, BanUser: true, UnbanUser: true}) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) seedProps(t, db, propsSourceRecordRow{ID: 101, Type: propsTypeRide, Name: "Giftable ride", AdminFree: true}) _, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(101), }) assertAppErrorCode(t, err, "manager_feature_disabled") if len(fake.giveRequests) != 0 { t.Fatalf("give requests = %+v, want none", fake.giveRequests) } } func TestSendBadgeActivatesBadgeBackpack(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) seedProps(t, db, propsSourceRecordRow{ID: 501, Type: propsTypeBadge, Name: "Activity badge", AdminFree: true}) resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(501), }) if err != nil { t.Fatalf("SendProps() error = %v", err) } if !resp.Success || resp.Type != propsTypeBadge || resp.Days != defaultGiftDays { t.Fatalf("resp = %+v, want badge success with default days", resp) } if len(fake.badgeActivations) != 1 { t.Fatalf("badge activations = %+v, want one", fake.badgeActivations) } if got := fake.badgeActivations[0]; got.userID != 2 || got.badgeID != 501 || got.days != defaultGiftDays { t.Fatalf("badge activation = %+v, want user 2 badge 501 days %d", got, defaultGiftDays) } if len(fake.giveRequests) != 0 { t.Fatalf("give requests = %+v, want no props backpack grant", fake.giveRequests) } } func TestSendVIPUsesVIPLevelConfig(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1", UserNickname: "manager", OriginSys: "LIKEI"}, userBaseInfoRow{ID: 2, Account: "2", OriginSys: "LIKEI"}, ) seedVIPConfigs(t, db, model.VipLevelConfig{ ID: 301, SysOrigin: "LIKEI", Level: 3, LevelCode: "VIP3", DisplayName: "VIP 3", Enabled: true, DurationDays: 30, PriceGold: 3000, }) resp, err := service.SendProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(301), }) if err != nil { t.Fatalf("SendProps() error = %v", err) } if !resp.Success || resp.Type != propsTypeNobleVIP || resp.VIPLevel != 3 || resp.Days != 30 { t.Fatalf("resp = %+v, want vip gift success", resp) } var state model.UserVipState if err := db.Where("sys_origin = ? AND user_id = ?", "LIKEI", int64(2)).First(&state).Error; err != nil { t.Fatalf("load state: %v", err) } if state.Level != 3 || state.Status != vipStatusActive { t.Fatalf("state = %+v, want vip3 active", state) } var orders int64 if err := db.Model(&model.VipOrderRecord{}).Where("user_id = ? AND order_type = ?", int64(2), vipOrderTypeGMGift).Count(&orders).Error; err != nil { t.Fatalf("count orders: %v", err) } if orders != 1 { t.Fatalf("orders = %d, want 1", orders) } var records int64 if err := db.Model(&gmVIPGiftRecordRow{}).Where("user_id = ? AND vip_level = ?", int64(2), 3).Count(&records).Error; err != nil { t.Fatalf("count gift records: %v", err) } if records != 1 { t.Fatalf("gift records = %d, want 1", records) } } func TestVIPGiftCleanupExpiresOldVIPResourcesOnly(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) if err := db.AutoMigrate(&managerVIPCleanupPropsRow{}, &managerVIPCleanupBadgeRow{}); err != nil { t.Fatalf("auto migrate cleanup tables: %v", err) } now := time.Date(2026, 5, 30, 10, 0, 0, 0, time.UTC) future := now.AddDate(0, 0, 20) userID := int64(2) seedManagerVIPCleanupProps(t, db, managerVIPCleanupPropsRow{ID: 1, UserID: userID, PropsID: 101, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, managerVIPCleanupPropsRow{ID: 2, UserID: userID, PropsID: 201, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, managerVIPCleanupPropsRow{ID: 3, UserID: userID, PropsID: 999, Type: propsTypeAvatarFrame, ExpireTime: future, UseProps: true, AllowGive: true}, managerVIPCleanupPropsRow{ID: 4, UserID: userID, PropsID: 103, Type: propsTypeDataCard, ExpireTime: future, UseProps: true, AllowGive: true}, ) seedManagerVIPCleanupBadges(t, db, managerVIPCleanupBadgeRow{ID: 1, UserID: userID, BadgeID: 100, ExpireTime: future, UseProps: true}, managerVIPCleanupBadgeRow{ID: 2, UserID: userID, BadgeID: 200, ExpireTime: future, UseProps: true}, ) target := vipGiftResourcesFromConfig(model.VipLevelConfig{ BadgeResourceID: 200, AvatarFrameResourceID: 201, }) all := append(vipGiftResourcesFromConfig(model.VipLevelConfig{ BadgeResourceID: 100, AvatarFrameResourceID: 101, BackgroundCardResourceID: 103, }), target...) if err := service.cleanupSupersededVIPGiftResources(context.Background(), userID, target, all, now); err != nil { t.Fatalf("cleanupSupersededVIPGiftResources() error = %v", err) } assertManagerVIPCleanupProps(t, db, 101, now, false, false) assertManagerVIPCleanupProps(t, db, 103, now, false, false) assertManagerVIPCleanupProps(t, db, 201, future, true, true) assertManagerVIPCleanupProps(t, db, 999, future, true, true) assertManagerVIPCleanupBadge(t, db, 100, now, false) assertManagerVIPCleanupBadge(t, db, 200, future, true) } func TestSendVIPRejectsLevelAboveGiftLimit(t *testing.T) { service, _ := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 1, Account: "1", UserNickname: "manager", OriginSys: "LIKEI"}, userBaseInfoRow{ID: 2, Account: "2", OriginSys: "LIKEI"}, ) seedVIPConfigs(t, db, model.VipLevelConfig{ ID: 304, SysOrigin: "LIKEI", Level: 4, LevelCode: "VIP4", DisplayName: "VIP 4", Enabled: true, DurationDays: 30, }) _, err := service.SendProps(context.Background(), AuthUser{UserID: 1, SysOrigin: "LIKEI"}, SendPropsRequest{ AcceptUserID: ID(2), PropsID: ID(304), }) assertAppErrorCode(t, err, "vip_not_giftable") } type managerVIPCleanupPropsRow struct { ID int64 `gorm:"column:id;primaryKey"` UserID int64 `gorm:"column:user_id"` PropsID int64 `gorm:"column:props_id"` Type string `gorm:"column:type"` ExpireTime time.Time `gorm:"column:expire_time"` UseProps bool `gorm:"column:is_use_props"` AllowGive bool `gorm:"column:allow_give"` UpdateTime time.Time `gorm:"column:update_time"` } func (managerVIPCleanupPropsRow) TableName() string { return "user_props_backpack" } type managerVIPCleanupBadgeRow struct { ID int64 `gorm:"column:id;primaryKey"` UserID int64 `gorm:"column:user_id"` BadgeID int64 `gorm:"column:badge_id"` ExpireType string `gorm:"column:expire_type"` ExpireTime time.Time `gorm:"column:expire_time"` UseProps bool `gorm:"column:is_use_props"` UpdateTime time.Time `gorm:"column:update_time"` } func (managerVIPCleanupBadgeRow) TableName() string { return "user_badge_backpack" } func seedManagerVIPCleanupProps(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupPropsRow) { t.Helper() for _, row := range rows { if err := db.Create(&row).Error; err != nil { t.Fatalf("seed manager cleanup props: %v", err) } } } func seedManagerVIPCleanupBadges(t *testing.T, db *gorm.DB, rows ...managerVIPCleanupBadgeRow) { t.Helper() for _, row := range rows { if err := db.Create(&row).Error; err != nil { t.Fatalf("seed manager cleanup badge: %v", err) } } } func assertManagerVIPCleanupProps(t *testing.T, db *gorm.DB, propsID int64, expireTime time.Time, useProps bool, allowGive bool) { t.Helper() var row managerVIPCleanupPropsRow if err := db.Where("props_id = ?", propsID).First(&row).Error; err != nil { t.Fatalf("load manager cleanup props %d: %v", propsID, err) } if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps || row.AllowGive != allowGive { t.Fatalf("props %d = expire:%s use:%v allow:%v, want expire:%s use:%v allow:%v", propsID, row.ExpireTime, row.UseProps, row.AllowGive, expireTime, useProps, allowGive) } } func assertManagerVIPCleanupBadge(t *testing.T, db *gorm.DB, badgeID int64, expireTime time.Time, useProps bool) { t.Helper() var row managerVIPCleanupBadgeRow if err := db.Where("badge_id = ?", badgeID).First(&row).Error; err != nil { t.Fatalf("load manager cleanup badge %d: %v", badgeID, err) } if !row.ExpireTime.Equal(expireTime) || row.UseProps != useProps { t.Fatalf("badge %d = expire:%s use:%v, want expire:%s use:%v", badgeID, row.ExpireTime, row.UseProps, expireTime, useProps) } } func TestBanUserRejectsDisabledFeature(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, GiftProps: true, UnbanUser: true}) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) _, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "manager_feature_disabled") if len(fake.punishRequests) != 0 { t.Fatalf("punish requests = %+v, want none", fake.punishRequests) } } func TestBanUserRejectsRechargedUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) fake.recharges[2] = []integration.UserTotalRecharge{{Amount: integration.DecimalString("1.00")}} _, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "user_recharged") if len(fake.punishRequests) != 0 { t.Fatalf("punish requests = %+v, want none", fake.punishRequests) } } func TestBanUserFailsWhenRechargeQueryUnavailable(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) fake.rechargeErr = errors.New("order service unavailable") _, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "recharge_query_failed") if len(fake.punishRequests) != 0 { t.Fatalf("punish requests = %+v, want none", fake.punishRequests) } } func TestBanUserRejectsArchivedUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusArchive}) _, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "user_already_banned") if len(fake.punishRequests) != 0 { t.Fatalf("punish requests = %+v, want none", fake.punishRequests) } } func TestBanUserRejectsFrozenUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusFreeze}) _, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "user_already_banned") if len(fake.punishRequests) != 0 { t.Fatalf("punish requests = %+v, want none", fake.punishRequests) } } func TestBanUserWithoutRechargeCallsPunishment(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2"}) resp, err := service.BanUser(context.Background(), AuthUser{UserID: 1}, BanUserRequest{ UserID: ID(2), Reason: "spam", }) if err != nil { t.Fatalf("BanUser() error = %v", err) } if !resp.Success || resp.AccountStatus != accountStatusArchive { t.Fatalf("resp = %+v, want archived success", resp) } if len(fake.punishRequests) != 1 { t.Fatalf("punish requests = %+v, want one", fake.punishRequests) } got := fake.punishRequests[0] if got.BeOptUserID != 2 || got.OptUserID != 1 || got.Status != accountStatusArchive { t.Fatalf("punish request = %+v, want target 2 manager 1 archive", got) } } func TestUnbanUserRejectsDisabledFeature(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManagerWithFeatures(t, db, 1, ManagerFeatures{AddBDLeader: true, GiftProps: true, BanUser: true}) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusArchive}) _, err := service.UnbanUser(context.Background(), AuthUser{UserID: 1}, UnbanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "manager_feature_disabled") if len(fake.unblockRequests) != 0 { t.Fatalf("unblock requests = %+v, want none", fake.unblockRequests) } } func TestUnbanUserRejectsNormalUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusNormal}) _, err := service.UnbanUser(context.Background(), AuthUser{UserID: 1}, UnbanUserRequest{UserID: ID(2)}) assertAppErrorCode(t, err, "user_not_banned") if len(fake.unblockRequests) != 0 { t.Fatalf("unblock requests = %+v, want none", fake.unblockRequests) } } func TestUnbanUserCallsUnblockAccount(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusArchive}) resp, err := service.UnbanUser(context.Background(), AuthUser{UserID: 1}, UnbanUserRequest{ UserID: ID(2), Reason: "appeal passed", }) if err != nil { t.Fatalf("UnbanUser() error = %v", err) } if !resp.Success || resp.AccountStatus != accountStatusNormal { t.Fatalf("resp = %+v, want normal success", resp) } if len(fake.unblockRequests) != 1 { t.Fatalf("unblock requests = %+v, want one", fake.unblockRequests) } got := fake.unblockRequests[0] if got.BeOptUserID != 2 || got.OptUserID != 1 { t.Fatalf("unblock request = %+v, want target 2 manager 1", got) } if got.Description == "" || !strings.Contains(got.Description, "appeal passed") { t.Fatalf("description = %q, want reason included", got.Description) } } func TestUnbanUserAllowsFrozenUser(t *testing.T) { service, fake := newManagerCenterTestService(t) db := service.db.(*gorm.DB) seedManager(t, db, 1) seedUsers(t, db, userBaseInfoRow{ID: 2, Account: "2", AccountStatus: accountStatusFreeze}) resp, err := service.UnbanUser(context.Background(), AuthUser{UserID: 1}, UnbanUserRequest{UserID: ID(2)}) if err != nil { t.Fatalf("UnbanUser() error = %v", err) } if !resp.Success || resp.AccountStatus != accountStatusNormal { t.Fatalf("resp = %+v, want normal success", resp) } if len(fake.unblockRequests) != 1 { t.Fatalf("unblock requests = %+v, want one", fake.unblockRequests) } } func newManagerCenterTestService(t *testing.T) (*Service, *fakeManagerGateway) { 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( &teamManagerInfoRow{}, &userBaseInfoRow{}, &bdLeaderRow{}, &businessDevelopmentBaseInfoRow{}, &userHistoryIdentityRow{}, &administratorRow{}, &propsSourceRecordRow{}, &propsNobleVIPAbilityRow{}, &model.VipLevelConfig{}, &model.UserVipState{}, &model.VipOrderRecord{}, &gmVIPGiftRecordRow{}, ); err != nil { t.Fatalf("auto migrate: %v", err) } fake := &fakeManagerGateway{ recharges: map[int64][]integration.UserTotalRecharge{}, abilities: map[int64]integration.PropsNobleVIPAbility{}, } return NewService(db, fake), fake } func seedManager(t *testing.T, db *gorm.DB, userID int64) { t.Helper() seedManagerWithFeatures(t, db, userID, ManagerFeatures{ AddBDLeader: true, GiftProps: true, BanUser: true, UnbanUser: true, }) } func seedManagerWithFeatures(t *testing.T, db *gorm.DB, userID int64, features ManagerFeatures) { t.Helper() if err := db.Create(&teamManagerInfoRow{ ID: userID, UserID: userID, Origin: "LIKEI", RegionID: "SA", CanAddBDLeader: boolPtr(features.AddBDLeader), CanGiftProps: boolPtr(features.GiftProps), CanBanUser: boolPtr(features.BanUser), CanUnbanUser: boolPtr(features.UnbanUser), }).Error; err != nil { t.Fatalf("seed manager: %v", err) } } func boolPtr(value bool) *bool { return &value } func seedUsers(t *testing.T, db *gorm.DB, rows ...userBaseInfoRow) { t.Helper() for _, row := range rows { if row.AccountStatus == "" { row.AccountStatus = "ACTIVE" } if err := db.Create(&row).Error; err != nil { t.Fatalf("seed user: %v", err) } } } func seedProps(t *testing.T, db *gorm.DB, rows ...propsSourceRecordRow) { t.Helper() for _, row := range rows { if err := db.Create(&row).Error; err != nil { t.Fatalf("seed props: %v", err) } } } func seedAbilities(t *testing.T, db *gorm.DB, rows ...propsNobleVIPAbilityRow) { t.Helper() for _, row := range rows { if err := db.Create(&row).Error; err != nil { t.Fatalf("seed ability: %v", err) } } } func seedVIPConfigs(t *testing.T, db *gorm.DB, rows ...model.VipLevelConfig) { t.Helper() now := time.Now() for _, row := range rows { if row.CreateTime.IsZero() { row.CreateTime = now } if row.UpdateTime.IsZero() { row.UpdateTime = now } if err := db.Create(&row).Error; err != nil { t.Fatalf("seed vip config: %v", err) } } } func assertAppErrorCode(t *testing.T, err error, code string) { t.Helper() if err == nil { t.Fatalf("error is nil, want code %s", code) } var appErr *AppError if !errors.As(err, &appErr) { t.Fatalf("error = %T %v, want AppError", err, err) } if appErr.Code != code { t.Fatalf("error code = %s, want %s", appErr.Code, code) } } func equalInt64Slice(a, b []int64) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }