package repository import ( "context" "errors" "testing" "time" "github.com/topfans/backend/pkg/models" "gorm.io/gorm" ) // setupPeripheralTestData helper: 准备测试用 asset + 可选 peripheral_info // ★ 使用包内共享 helper(createTestStar / createTestUser / createTestAsset), // // 资源会自动被 cleanupTestDB 通过 mobile LIKE '199%' / identity_id LIKE 'test_peripheral_%' 清理 // (star identity_id "test_peripheral_asset" 匹配 cleanupTestDB 的 test_asset_% 规则) func setupPeripheralTestData(t *testing.T, db *gorm.DB, withPeripheralInfo bool) int64 { t.Helper() star := createTestStar(t, db, "test_peripheral_asset") user := createTestUser(t, db, "19900009001") asset := createTestAsset(t, db, user.ID, star.StarID, "test_peripheral_asset") if withPeripheralInfo { // peripheral_info 用 raw SQL INSERT(避开任何潜在的 GORM 钩子) if err := db.Exec(`INSERT INTO peripheral_info (asset_id, star_id, user_id, code, image, brand, company, hash, verifier, first_verified_at, created_at, updated_at) VALUES (?, 87, 0, 'PERI-2026-001', '', 'TopFans', '上海文化', '0xabc', '官方', 1715600000000, 1, 1)`, asset.ID).Error; err != nil { t.Fatalf("Failed to create test peripheral_info: %v", err) } } return asset.ID } func TestPeripheralRepo_GetAssetForVerification_Exists(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, false) got, err := repo.GetAssetForVerification(context.Background(), assetID) if err != nil { t.Fatalf("unexpected error: %v", err) } if got == nil { t.Fatal("expected asset, got nil") } if got.ID != assetID { t.Errorf("expected id=%d, got %d", assetID, got.ID) } } func TestPeripheralRepo_GetAssetForVerification_NotFound(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) got, err := repo.GetAssetForVerification(context.Background(), 999999999999) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != nil { t.Errorf("expected nil, got %+v", got) } } func TestPeripheralRepo_GetPeripheralInfo_Exists(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, true) got, err := repo.GetPeripheralInfo(context.Background(), assetID) if err != nil { t.Fatalf("unexpected error: %v", err) } if got == nil { t.Fatal("expected PeripheralInfo, got nil") } if got.Brand != "TopFans" { t.Errorf("expected brand=TopFans, got %s", got.Brand) } } func TestPeripheralRepo_GetPeripheralInfo_NotFound(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) got, err := repo.GetPeripheralInfo(context.Background(), 999999999999) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != nil { t.Errorf("expected nil, got %+v", got) } } // TestPeripheralRepo_GetPeripheralInfoByID_Exists 验证按 peripheral_info 主键 id 查到记录 // ★ 回归测试:2026-07-17 by-hash 接口 50003,根因是 GetPeripheralInfoByHash 错把 peripheral_info.id // // 传给 GetPeripheralInfo(asset_id 查),查不到。新增按 id 查的方法。 func TestPeripheralRepo_GetPeripheralInfoByID_Exists(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) setupPeripheralTestData(t, db, true) // 创建 peripheral_info // 直接查 peripheral_info.id(setupPeripheralTestData 没返回这个值) var peripheralID int64 if err := db.Raw(`SELECT id FROM peripheral_info WHERE code = 'PERI-2026-001' LIMIT 1`).Scan(&peripheralID).Error; err != nil { t.Fatalf("query peripheral_info.id failed: %v", err) } if peripheralID == 0 { t.Fatal("expected peripheral_info.id > 0, got 0") } got, err := repo.GetPeripheralInfoByID(context.Background(), peripheralID) if err != nil { t.Fatalf("unexpected error: %v", err) } if got == nil { t.Fatal("expected PeripheralInfo, got nil") } if got.ID != peripheralID { t.Errorf("expected id=%d, got %d", peripheralID, got.ID) } if got.Brand != "TopFans" { t.Errorf("expected brand=TopFans, got %s", got.Brand) } } func TestPeripheralRepo_GetPeripheralInfoByID_NotFound(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) got, err := repo.GetPeripheralInfoByID(context.Background(), 999999999999) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != nil { t.Errorf("expected nil, got %+v", got) } } func TestPeripheralRepo_GetPeripheralInfoByID_InvalidID(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) _, err := repo.GetPeripheralInfoByID(context.Background(), 0) if err == nil { t.Fatal("expected error for id=0, got nil") } } // TestPeripheralRepo_ExistsRegistry_True 验证 ExistsRegistry 命中已有记录 func TestPeripheralRepo_ExistsRegistry_True(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, false) // 写入 1 条 asset_registry 记录(owner_uid=100) db.Exec(`INSERT INTO asset_registry (owner_uid, asset_id, star_id, asset_type, status, created_at, updated_at) VALUES (?, ?, 1, 'peripheral', 1, 1, 1)`, 100, assetID) defer db.Exec("DELETE FROM asset_registry WHERE owner_uid = ? AND asset_id = ?", 100, assetID) exists, err := repo.ExistsRegistry(context.Background(), 100, assetID, "peripheral") if err != nil { t.Fatalf("unexpected error: %v", err) } if !exists { t.Error("expected exists=true, got false") } } // TestPeripheralRepo_ExistsRegistry_False 验证 ExistsRegistry 未命中时返 false func TestPeripheralRepo_ExistsRegistry_False(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, false) // 不写入任何 registry 记录,查询应返 false exists, err := repo.ExistsRegistry(context.Background(), 100, assetID, "peripheral") if err != nil { t.Fatalf("unexpected error: %v", err) } if exists { t.Error("expected exists=false, got true") } } // TestPeripheralRepo_CountRecentMint 验证 CountRecentMint 仅统计 since 时间内的记录 func TestPeripheralRepo_CountRecentMint(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) star := createTestStar(t, db, "test_peripheral_count") user := createTestUser(t, db, "19900009002") // ★ 4 个 asset_id(3 条最近 + 1 条 25h 前),因 uk_registry_asset_type_id UNIQUE(asset_type, asset_id) recentAsset1 := createTestAsset(t, db, user.ID, star.StarID, "test_peripheral_recent1") recentAsset2 := createTestAsset(t, db, user.ID, star.StarID, "test_peripheral_recent2") recentAsset3 := createTestAsset(t, db, user.ID, star.StarID, "test_peripheral_recent3") oldAsset := createTestAsset(t, db, user.ID, star.StarID, "test_peripheral_old") ownerUID := int64(101) // 清理 owner_uid=101 的旧记录(避免别的测试残留影响) db.Exec("DELETE FROM asset_registry WHERE owner_uid = ?", ownerUID) // 3 条最近,1 条 25h 前 now := time.Now().UnixMilli() db.Exec(`INSERT INTO asset_registry (owner_uid, asset_id, star_id, asset_type, status, created_at, updated_at) VALUES (?, ?, ?, 'peripheral', 1, ?, ?), (?, ?, ?, 'peripheral', 1, ?, ?), (?, ?, ?, 'peripheral', 1, ?, ?)`, ownerUID, recentAsset1.ID, star.StarID, now, now, ownerUID, recentAsset2.ID, star.StarID, now, now, ownerUID, recentAsset3.ID, star.StarID, now, now) db.Exec(`INSERT INTO asset_registry (owner_uid, asset_id, star_id, asset_type, status, created_at, updated_at) VALUES (?, ?, ?, 'peripheral', 1, ?, ?)`, ownerUID, oldAsset.ID, star.StarID, now-25*3600*1000, now-25*3600*1000) defer db.Exec("DELETE FROM asset_registry WHERE owner_uid = ?", ownerUID) count, err := repo.CountRecentMint(context.Background(), ownerUID, "peripheral", 24*time.Hour) if err != nil { t.Fatalf("unexpected error: %v", err) } if count != 3 { t.Errorf("expected count=3, got %d", count) } } // TestPeripheralRepo_InsertPeripheralRegistry verifies a peripheral registry row is inserted and returns generated fields. func TestPeripheralRepo_InsertPeripheralRegistry(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, false) defer db.Where("asset_id = ?", assetID).Delete(&models.AssetRegistry{}) var asset models.Asset if err := db.Select("id", "star_id").First(&asset, assetID).Error; err != nil { t.Fatalf("query test asset failed: %v", err) } owner := createTestUser(t, db, "19900009003") reg := &models.AssetRegistry{ OwnerUID: owner.ID, AssetID: assetID, StarID: asset.StarID, AssetType: "peripheral", Status: models.AssetRegistryStatusActive, } newID, createdAtMs, err := repo.InsertPeripheralRegistry(context.Background(), reg) if err != nil { t.Fatalf("unexpected error: %v", err) } if newID <= 0 { t.Errorf("expected newID > 0, got %d", newID) } if createdAtMs <= 0 { t.Errorf("expected createdAtMs > 0, got %d", createdAtMs) } var got models.AssetRegistry if err := db.First(&got, newID).Error; err != nil { t.Fatalf("verify insert failed: %v", err) } if got.AssetType != "peripheral" { t.Errorf("expected asset_type=peripheral, got %s", got.AssetType) } if got.AssetID != assetID { t.Errorf("expected asset_id=%d, got %d", assetID, got.AssetID) } if got.OwnerUID != owner.ID { t.Errorf("expected owner_uid=%d, got %d", owner.ID, got.OwnerUID) } } // TestPeripheralRepo_RefreshVerifyCount verifies verify_count is refreshed from peripheral registry rows only. func TestPeripheralRepo_RefreshVerifyCount(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) star := createTestStar(t, db, "test_asset_peripheral_refresh") assetOwner := createTestUser(t, db, "19900009004") asset := createTestAsset(t, db, assetOwner.ID, star.StarID, "test_asset_peripheral_refresh") otherAsset := createTestAsset(t, db, assetOwner.ID, star.StarID, "test_asset_peripheral_refresh_other") defer db.Where("asset_id IN ?", []int64{asset.ID, otherAsset.ID}).Delete(&models.AssetRegistry{}) registryOwner := createTestUser(t, db, "19900009005") createRegistry := func(assetID int64, assetType string) { t.Helper() reg := &models.AssetRegistry{ OwnerUID: registryOwner.ID, AssetID: assetID, StarID: star.StarID, AssetType: assetType, Status: models.AssetRegistryStatusActive, } if err := db.Create(reg).Error; err != nil { t.Fatalf("create test registry failed: %v", err) } } createRegistry(asset.ID, "peripheral") createRegistry(asset.ID, models.AssetTypeRegular) createRegistry(otherAsset.ID, "peripheral") if err := db.Table("assets").Where("id = ?", asset.ID).Update("verify_count", 99).Error; err != nil { t.Fatalf("set stale verify_count failed: %v", err) } if err := repo.RefreshVerifyCount(context.Background(), asset.ID); err != nil { t.Fatalf("unexpected error: %v", err) } var verifyCount int64 if err := db.Table("assets").Select("verify_count").Where("id = ?", asset.ID).Scan(&verifyCount).Error; err != nil { t.Fatalf("query verify_count failed: %v", err) } if verifyCount != 1 { t.Errorf("expected verify_count=1, got %d", verifyCount) } } // TestPeripheralRepo_InsertPeripheralRegistry_Duplicate 验证并发插入触发 PG 唯一约束冲突时 // // 返 sentinel ErrDuplicateRegistry(而非通用 PG 错误), // 让 service 层能 errors.Is 识别并转 BizCodeAlreadyAdded (50004)。 func TestPeripheralRepo_InsertPeripheralRegistry_Duplicate(t *testing.T) { db := setupTestDB(t) defer cleanupTestDB(t, db) repo := NewPeripheralRepository(db) assetID := setupPeripheralTestData(t, db, false) defer db.Where("asset_id = ?", assetID).Delete(&models.AssetRegistry{}) var asset models.Asset if err := db.Select("id", "star_id").First(&asset, assetID).Error; err != nil { t.Fatalf("query test asset failed: %v", err) } owner := createTestUser(t, db, "19900009006") mkReg := func() *models.AssetRegistry { return &models.AssetRegistry{ OwnerUID: owner.ID, AssetID: assetID, StarID: asset.StarID, AssetType: "peripheral", Status: models.AssetRegistryStatusActive, } } // 第一次 INSERT 应成功 if _, _, err := repo.InsertPeripheralRegistry(context.Background(), mkReg()); err != nil { t.Fatalf("first insert should succeed, got: %v", err) } // 第二次 INSERT 同 (owner_uid, star_id, asset_type, asset_id) 应触发 PG 23505 _, _, err := repo.InsertPeripheralRegistry(context.Background(), mkReg()) if err == nil { t.Fatal("expected error on duplicate insert, got nil") } if !errors.Is(err, ErrDuplicateRegistry) { t.Errorf("expected ErrDuplicateRegistry, got %v", err) } }