package provider import ( "context" "os" "strconv" "testing" "time" "dubbo.apache.org/dubbo-go/v3/common/constant" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "github.com/topfans/backend/pkg/database" "github.com/topfans/backend/pkg/models" pb "github.com/topfans/backend/pkg/proto/asset" "github.com/topfans/backend/services/assetService/repository" "github.com/topfans/backend/services/assetService/service" "gorm.io/gorm" ) // ---- GetAssetsByType (DB-backed, 自包含) ---- // // 这些用例验证 starbook 分类查询的过滤语义: // - type 过滤: 只返回请求类型的分组 // - grade 过滤: regular 类型下按等级筛选 // - category 过滤: 分类维度筛选 // - 未认证: ctx 无可信身份时拒绝 // // 自包含约定(遵循 asset_level_service_test.go 的范式): // - TEST_DB_* 覆盖,默认 localhost:15432 / postgres / 123456 / top-fans // - 连不上则 t.Skip,不污染其它测试 // - 只清理本用例写入的 sentinel owner_uid 行,不做大范围 cleanup const ( // sentinel 身份: 高位取值避免与既有测试/业务数据碰撞 sbTestOwnerUID = int64(990201) sbTestStarID = int64(990202) ) func sbEnvOrDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } // starbookTestDB 建立 GetAssetsByType 用例专用的 DB 连接。 func starbookTestDB(t *testing.T) *gorm.DB { t.Helper() if os.Getenv("SKIP_DB_TESTS") != "" { t.Skip("SKIP_DB_TESTS set") } portStr := sbEnvOrDefault("TEST_DB_PORT", "15432") port, _ := strconv.Atoi(portStr) if port == 0 { port = 15432 } if err := database.Init(database.Config{ Host: sbEnvOrDefault("TEST_DB_HOST", "localhost"), Port: port, User: sbEnvOrDefault("TEST_DB_USER", "postgres"), Password: sbEnvOrDefault("TEST_DB_PASSWORD", "123456"), DBName: sbEnvOrDefault("TEST_DB_NAME", "top-fans"), SSLMode: "disable", TimeZone: "Asia/Shanghai", }); err != nil { t.Skipf("Skipping: cannot connect to test database: %v", err) } db := database.GetDB() if err := db.AutoMigrate(&models.Asset{}, &models.AssetRegistry{}); err != nil { t.Skipf("Skipping: AutoMigrate failed: %v", err) } return db } // sbCleanup 只删本用例的 sentinel 行(assets/registry + FK 依赖的 user/star)。 func sbCleanup(t *testing.T, db *gorm.DB) { t.Helper() db.Where("owner_uid = ? AND star_id = ?", sbTestOwnerUID, sbTestStarID).Delete(&models.AssetRegistry{}) db.Where("owner_uid = ? AND star_id = ?", sbTestOwnerUID, sbTestStarID).Delete(&models.Asset{}) db.Exec("DELETE FROM stars WHERE star_id = ?", sbTestStarID) db.Exec("DELETE FROM users WHERE id = ?", sbTestOwnerUID) } // sbSeed 写入 sentinel 藏品 + registry: // - regular grade 1 // - regular grade 3 // - collection (category=手办) func sbSeed(t *testing.T, db *gorm.DB) { t.Helper() sbCleanup(t, db) now := time.Now().UnixMilli() // assets FK: owner_uid -> users(id), star_id -> stars(star_id)。 // 显式 id 插入后按 CLAUDE.md 规范同步序列,避免后续 GORM 自增撞主键。 require.NoError(t, db.Exec( `INSERT INTO users (id, mobile, password_hash, created_at, updated_at) VALUES (?, ?, 'x', ?, ?) ON CONFLICT (id) DO NOTHING`, sbTestOwnerUID, "99902010001", now, now).Error) require.NoError(t, db.Exec(`SELECT setval('users_id_seq', (SELECT MAX(id) FROM users))`).Error) require.NoError(t, db.Exec( `INSERT INTO stars (star_id, name, identity_id, created_at, updated_at) VALUES (?, 'sb_test_star', 'sb_test_identity', ?, ?) ON CONFLICT (star_id) DO NOTHING`, sbTestStarID, now, now).Error) require.NoError(t, db.Exec(`SELECT setval('stars_star_id_seq', (SELECT MAX(star_id) FROM stars))`).Error) newAsset := func(name string) *models.Asset { a := &models.Asset{ OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Name: name, CoverURL: "https://cdn/" + name + ".png", Status: 1, IsActive: true, CreatedAt: now, UpdatedAt: now, } require.NoError(t, db.Create(a).Error) return a } grade1 := int32(1) grade3 := int32(3) cat := "手办" regA := newAsset("regular_g1") regB := newAsset("regular_g3") colC := newAsset("collection_x") regs := []*models.AssetRegistry{ {AssetID: regA.ID, AssetType: models.AssetTypeRegular, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Grade: &grade1, DisplayStatus: 1}, {AssetID: regB.ID, AssetType: models.AssetTypeRegular, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, Grade: &grade3, DisplayStatus: 1}, {AssetID: colC.ID, AssetType: models.AssetTypeCollection, OwnerUID: sbTestOwnerUID, StarID: sbTestStarID, CollectionCategory: &cat, DisplayStatus: 1}, } for _, r := range regs { require.NoError(t, db.Create(r).Error) } t.Cleanup(func() { sbCleanup(t, db) }) } // sbProvider 组装真实 service(真实 repo + test DB),无关依赖传 nil。 func sbProvider(db *gorm.DB) *AssetProvider { assetRepo := repository.NewAssetRepository(db) registryRepo := repository.NewAssetRegistryRepository(db) svc := service.NewAssetService(assetRepo, nil, nil, nil, db, registryRepo, nil) return &AssetProvider{assetService: svc} } // sbCtx 构造携带可信身份的 Dubbo attachments ctx。 func sbCtx(userID, starID int64) context.Context { return context.WithValue(context.Background(), constant.AttachmentKey, map[string]interface{}{ "user_id": userID, "star_id": starID, }) } func TestGetAssetsByType_FilterByTypeRegular(t *testing.T) { db := starbookTestDB(t) sbSeed(t, db) p := sbProvider(db) resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{ Type: models.AssetTypeRegular, }) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.Len(t, resp.Data.Groups, 1, "type=regular 只应返回 regular 分组") assert.Equal(t, models.AssetTypeRegular, resp.Data.Groups[0].Type) // regular 分组含 grade 1 + grade 3 两个等级 var total int32 for _, g := range resp.Data.Groups[0].Grades { total += g.TotalCount } assert.Equal(t, int32(2), total, "regular 应含两条(grade1+grade3)") } func TestGetAssetsByType_FilterByGrade(t *testing.T) { db := starbookTestDB(t) sbSeed(t, db) p := sbProvider(db) resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{ Type: models.AssetTypeRegular, Grade: 3, }) require.NoError(t, err) require.NotNil(t, resp.Data) require.Len(t, resp.Data.Groups, 1) grades := resp.Data.Groups[0].Grades require.Len(t, grades, 1, "grade=3 过滤后只剩一个等级段") assert.Equal(t, int32(3), grades[0].Grade) assert.Equal(t, int32(1), grades[0].TotalCount, "grade=3 只有一条") } func TestGetAssetsByType_FilterByCategory(t *testing.T) { db := starbookTestDB(t) sbSeed(t, db) p := sbProvider(db) resp, err := p.GetAssetsByType(sbCtx(sbTestOwnerUID, sbTestStarID), &pb.GetAssetsByTypeRequest{ Type: models.AssetTypeCollection, Category: "手办", }) require.NoError(t, err) require.NotNil(t, resp.Data) require.Len(t, resp.Data.Groups, 1, "type=collection + category=手办 只返回该分组") assert.Equal(t, models.AssetTypeCollection, resp.Data.Groups[0].Type) assert.Equal(t, int32(1), resp.Data.Groups[0].TotalCount) } func TestGetAssetsByType_NoIdentity(t *testing.T) { // 无需 DB: 缺身份必须在触达 service 前拒绝。 p := &AssetProvider{} resp, err := p.GetAssetsByType(context.Background(), &pb.GetAssetsByTypeRequest{ Type: models.AssetTypeRegular, }) require.Error(t, err) if resp != nil && resp.Base != nil { assert.Equal(t, uint32(codes.Unauthenticated), resp.Base.Code) } }