2026-05-17 00:34:56 +08:00

577 lines
19 KiB
Go

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 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 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 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")
}
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 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{},
&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()
if err := db.Create(&teamManagerInfoRow{ID: userID, UserID: userID, Origin: "LIKEI"}).Error; err != nil {
t.Fatalf("seed manager: %v", err)
}
}
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
}