package service import ( "os" "strconv" "testing" "github.com/topfans/backend/pkg/database" "github.com/topfans/backend/pkg/models" "github.com/topfans/backend/services/assetService/repository" "gorm.io/gorm" ) // TestCalculateBuff 纯函数,无需 DB,保留原有行为。 func TestCalculateBuff(t *testing.T) { tests := []struct { likeCount int expected int }{ {0, 0}, {4, 0}, {5, 10}, {9, 10}, {10, 20}, {29, 20}, {30, 30}, {100, 30}, } for _, tt := range tests { result := CalculateBuff(tt.likeCount) if result != tt.expected { t.Errorf("CalculateBuff(%d) = %d, want %d", tt.likeCount, result, tt.expected) } } } // assetLevelTestDB 资产级幂等测试专用 DB 连接。 // // 自包含: // - 通过 TEST_DB_HOST/PORT/USER/PASSWORD/NAME 覆盖,默认 localhost:15432 / postgres / 123456 / top-fans。 // - 连不上 t.Skip(不污染全局 TestMain,避免影响其它测试)。 // - AutoMigrate asset_level_records / asset_exhibition_hours_log / season / asset_level(幂等所需)。 func assetLevelTestDB(t *testing.T) *gorm.DB { t.Helper() if os.Getenv("SKIP_DB_TESTS") != "" { t.Skip("SKIP_DB_TESTS set") } host := getEnvOrDefault("TEST_DB_HOST", "localhost") portStr := getEnvOrDefault("TEST_DB_PORT", "15432") port, _ := strconv.Atoi(portStr) if port == 0 { port = 15432 } user := getEnvOrDefault("TEST_DB_USER", "postgres") password := getEnvOrDefault("TEST_DB_PASSWORD", "123456") dbname := getEnvOrDefault("TEST_DB_NAME", "top-fans") if err := database.Init(database.Config{ Host: host, Port: port, User: user, Password: password, DBName: dbname, SSLMode: "disable", TimeZone: "Asia/Shanghai", }); err != nil { t.Skipf("Skipping: failed to connect to test database %s:%d/%s as %s: %v", host, port, dbname, user, err) } db := database.GetDB() // AutoMigrate 幂等所需最小表集(若已存在则跳过)。 if err := db.AutoMigrate( &models.Season{}, &models.AssetLevel{}, &models.AssetLevelRecord{}, &models.AssetLevelChangeLog{}, &models.AssetExhibitionHoursLog{}, ); err != nil { t.Logf("Warning: AutoMigrate asset_level tables (may already exist): %v", err) } // 确保至少存在 N 等级(供 CheckUpgrade 不会因为没有 level 而 panic)。 var n int64 db.Model(&models.AssetLevel{}).Where("level = ?", models.LevelN).Count(&n) if n == 0 { db.Create(&models.AssetLevel{ Level: models.LevelN, LevelOrder: 1, RequireHours: 99999999, RequireLikes: 99999999, }) } return db } func getEnvOrDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } // TestAddExhibitionHours_AssetIdempotent 资产级幂等测试。 // // 验收: // - 同一 sourceID 二次调用,不重复累加 SeasonExhibitionHours / LifetimeExhibitionHours。 // - 二次调用返回 upgraded=false。 // - asset_exhibition_hours_log 中恰好 1 条记录。 // // 数据隔离: // - 用 sentinel assetID + sourceID,只删自己 sentinel 行,不调用任何全局 cleanup helper。 func TestAddExhibitionHours_AssetIdempotent(t *testing.T) { db := assetLevelTestDB(t) levelRepo := repository.NewAssetLevelRepository(db) seasonRepo := repository.NewSeasonRepository(db) decayRepo := repository.NewSeasonDecayConfigRepository(db) svc := NewAssetLevelService(levelRepo, seasonRepo, decayRepo) assetID := int64(99900091) srcID := "test_asset_exh_log_91_001" // 先清 sentinel 行(包含上一次失败或并发残留),defer 再清一次确保退出干净。 db.Exec("DELETE FROM asset_exhibition_hours_log WHERE source_id = ?", srcID) db.Exec("DELETE FROM asset_level_records WHERE asset_id = ?", assetID) defer func() { db.Exec("DELETE FROM asset_exhibition_hours_log WHERE source_id = ?", srcID) db.Exec("DELETE FROM asset_level_records WHERE asset_id = ?", assetID) }() // 1) 首次:应该累加,新等级(sourceID 不同每次应该都能跑完 add 路径)。 lvl1, up1, err := svc.AddExhibitionHours(assetID, 5, srcID) if err != nil { t.Fatalf("first AddExhibitionHours err: %v", err) } _ = lvl1 _ = up1 // 2) 二次:同 sourceID,应该跳过累加、不触发升级。 lvl2, up2, err := svc.AddExhibitionHours(assetID, 5, srcID) if err != nil { t.Fatalf("second AddExhibitionHours err: %v", err) } if up2 { t.Errorf("want upgraded=false on duplicate source_id, got true (level=%s)", lvl2) } if lvl2 == "" { t.Errorf("want non-empty level on duplicate, got empty string") } // 3) 校验累加表:SeasonExhibitionHours 应仅为 5(一次生效)。 var rec models.AssetLevelRecord if err := db.Where("asset_id = ?", assetID).First(&rec).Error; err != nil { t.Fatalf("read asset level record: %v", err) } if rec.SeasonExhibitionHours != 5 { t.Errorf("want SeasonExhibitionHours=5 (one apply), got %d", rec.SeasonExhibitionHours) } if rec.LifetimeExhibitionHours != 5 { t.Errorf("want LifetimeExhibitionHours=5 (one apply), got %d", rec.LifetimeExhibitionHours) } // 4) 校验幂等 log 表:恰好 1 条。 var logCount int64 if err := db.Model(&models.AssetExhibitionHoursLog{}). Where("source_id = ?", srcID).Count(&logCount).Error; err != nil { t.Fatalf("count asset exhibition log: %v", err) } if logCount != 1 { t.Errorf("want exactly 1 asset_exhibition_hours_log row, got %d", logCount) } }