package listing import ( "context" "encoding/json" "errors" "testing" "time" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" ) type stubConfigReader map[string]string func (s stubConfigReader) FindValue(_ context.Context, key string) (string, error) { return s[key], nil } func TestConsumableValueOnlyCountsChargedResources(t *testing.T) { value := consumableValue(map[string]any{ "resources": []any{ map[string]any{"mode": "收费", "quantity": float64(5), "price": "0.1元/个"}, map[string]any{"mode": "赠送", "quantity": float64(3), "price": "0.6元/个"}, map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/2个"}, }, }) if value != 1.5 { t.Fatalf("expected 1.5, got %.2f", value) } } func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) { base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerRatio: 25}, 1000, 20) if base != 40 || total != 60 || ratio != 25 { t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio) } } func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) { base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPriceCent: 7000}, 1000, 20) if base != 50 || total != 70 || ratio != 20 { t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio) } } func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) { req := CreateRequest{ Title: "测试账号", ServerRegion: "烽火地带", PriceCent: 10000, DepositAmountCent: 200, HafCoinAmount: 1000000, ScreenshotURLS: []string{"https://example.com/a.png"}, AssetSummary: map[string]any{ "resources": []any{ map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"}, }, }, } if err := validateRequest(req, publishRules{}); err != ErrDepositTooLow { t.Fatalf("expected ErrDepositTooLow, got %v", err) } req.DepositAmountCent = 300 if err := validateRequest(req, publishRules{}); err != nil { t.Fatalf("expected valid request, got %v", err) } } func TestValidateRequiredOnlineTime(t *testing.T) { req := CreateRequest{ AssetSummary: map[string]any{ "online_time": map[string]any{ "start": "09:00", "end": "23:00", }, }, } if err := validateRequiredOnlineTime(req); err != nil { t.Fatalf("expected valid online time, got %v", err) } } func TestValidateRequiredOnlineTimeRejectsMissing(t *testing.T) { req := CreateRequest{AssetSummary: map[string]any{}} if err := validateRequiredOnlineTime(req); err != ErrMissingOnlineTime { t.Fatalf("expected ErrMissingOnlineTime, got %v", err) } } func TestValidateRequiredOnlineTimeRejectsInvalidRange(t *testing.T) { req := CreateRequest{ AssetSummary: map[string]any{ "online_time": map[string]any{ "start": "23:00", "end": "09:00", }, }, } if err := validateRequiredOnlineTime(req); err != ErrInvalidOnlineTime { t.Fatalf("expected ErrInvalidOnlineTime, got %v", err) } } func TestCreateRequiresPublishAgreements(t *testing.T) { service := NewService(&Repository{}, nil) _, err := service.Create(t.Context(), 1, CreateRequest{}) if err != ErrAgreementRequired { t.Fatalf("expected ErrAgreementRequired, got %v", err) } } func TestPublishCooldownUsesConfiguredMinutes(t *testing.T) { service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "2"}) cooldown, err := service.publishCooldown(t.Context()) if err != nil { t.Fatalf("expected no error, got %v", err) } if cooldown != 2*time.Minute { t.Fatalf("expected 2m cooldown, got %s", cooldown) } } func TestPublishCooldownCanBeDisabled(t *testing.T) { service := NewService(nil, stubConfigReader{publishCooldownMinutesKey: "0"}) cooldown, err := service.publishCooldown(t.Context()) if err != nil { t.Fatalf("expected no error, got %v", err) } if cooldown != 0 { t.Fatalf("expected disabled cooldown, got %s", cooldown) } } func TestRepositoryCreateRejectsRecentOwnerPublish(t *testing.T) { db := database.NewTestDB() if err := database.MigrateListingLifecycleTestSchema(db); err != nil { t.Fatalf("failed to migrate test db: %v", err) } user := model.User{Phone: "18800000000", RealnameStatus: "verified", Status: "active"} if err := db.Create(&user).Error; err != nil { t.Fatalf("failed to create user: %v", err) } latestCreatedAt := time.Now().Add(-2 * time.Minute) if err := db.Create(&model.RentalListing{ ListingNo: "202606270001", AccountID: 1, OwnerID: user.ID, Status: "published", CreatedAt: latestCreatedAt, ReviewStatus: "approved", }).Error; err != nil { t.Fatalf("failed to create latest listing: %v", err) } repo := NewRepository(db, nil) _, err := repo.Create(t.Context(), user.ID, validCreateRequest(), false, 5*time.Minute) var cooldownErr PublishCooldownError if !errors.As(err, &cooldownErr) { t.Fatalf("expected PublishCooldownError, got %v", err) } if cooldownErr.Remaining <= 0 { t.Fatalf("expected positive remaining cooldown, got %s", cooldownErr.Remaining) } } func TestRepositoryUpdateAllowsOfflineListingResubmit(t *testing.T) { db := database.NewTestDB() if err := database.MigrateListingLifecycleTestSchema(db); err != nil { t.Fatalf("failed to migrate test db: %v", err) } repo := NewRepository(db, nil) req := CreateRequest{ Title: "测试账号", ServerRegion: "QQ", LoginPlatform: "QQ账号密码", RankLevel: "黑鹰", HafCoinAmount: 100000000, PriceCent: 10000, DepositAmountCent: 50000, ScreenshotURLS: []string{"https://example.com/a.png"}, AssetSummary: map[string]any{ "online_time": map[string]any{"start": "09:00", "end": "23:00"}, }, } created, err := repo.Create(t.Context(), 1, req, false, 0) if err != nil { t.Fatalf("failed to create listing: %v", err) } if _, err := repo.Offline(t.Context(), 1, created.ID); err != nil { t.Fatalf("failed to offline listing: %v", err) } req.Title = "二次编辑账号" updated, err := repo.Update(t.Context(), 1, created.ID, req, true) if err != nil { t.Fatalf("expected offline listing can be updated, got %v", err) } if updated.Title != "二次编辑账号" { t.Fatalf("expected title updated, got %q", updated.Title) } if updated.Status != "draft" || updated.ReviewStatus != "pending" { t.Fatalf("expected draft/pending after resubmit, got %s/%s", updated.Status, updated.ReviewStatus) } } func TestRepositoryRejectsCompletedListingOwnerMutations(t *testing.T) { db := database.NewTestDB() if err := database.MigrateListingLifecycleTestSchema(db); err != nil { t.Fatalf("failed to migrate test db: %v", err) } repo := NewRepository(db, nil) account := model.GameAccount{ OwnerID: 1, GameName: "delta_force", ServerRegion: "QQ", LoginPlatform: "QQ账号密码", Title: "已完成账号", Status: "offline", } if err := db.Create(&account).Error; err != nil { t.Fatalf("failed to create account: %v", err) } listing := model.RentalListing{ ListingNo: "202606270099", AccountID: account.ID, OwnerID: 1, Status: "completed", ReviewStatus: "approved", PriceCent: 10000, DepositAmountCent: 50000, } if err := db.Create(&listing).Error; err != nil { t.Fatalf("failed to create listing: %v", err) } if _, err := repo.Update(t.Context(), 1, listing.ID, validCreateRequest(), false); err != ErrListingLocked { t.Fatalf("Update expected ErrListingLocked, got %v", err) } if _, err := repo.SubmitReview(t.Context(), 1, listing.ID, false); err != ErrListingLocked { t.Fatalf("SubmitReview expected ErrListingLocked, got %v", err) } if _, err := repo.Offline(t.Context(), 1, listing.ID); err != ErrListingLocked { t.Fatalf("Offline expected ErrListingLocked, got %v", err) } } func validCreateRequest() CreateRequest { return CreateRequest{ Title: "测试账号", ServerRegion: "QQ", LoginPlatform: "QQ账号密码", RankLevel: "黑鹰", HafCoinAmount: 100000000, PriceCent: 10000, DepositAmountCent: 50000, ScreenshotURLS: []string{"https://example.com/a.png"}, AssetSummary: map[string]any{ "online_time": map[string]any{"start": "09:00", "end": "23:00"}, }, } } func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) { item := &ListingDTO{ PriceCent: 23800, AssetSummary: map[string]any{ "price_breakdown": map[string]any{ "seller_total_price": 200, }, }, } applySellerListingPrice(item) if item.PriceCent != 20000 { t.Fatalf("expected seller price 20000, got %d", item.PriceCent) } } func TestApplySellerListingPriceKeepsFallbackPrice(t *testing.T) { item := &ListingDTO{ PriceCent: 23800, AssetSummary: map[string]any{ "price_breakdown": map[string]any{ "seller_total_price": 0, }, }, } applySellerListingPrice(item) if item.PriceCent != 23800 { t.Fatalf("expected fallback price 23800, got %d", item.PriceCent) } } func TestParseExternalUploadItemsAcceptsSingleObject(t *testing.T) { items, err := parseExternalUploadItems(json.RawMessage(validExternalUploadDataJSON())) if err != nil { t.Fatalf("expected single upload parsed, got %v", err) } if len(items) != 1 || items[0].LoginMethod != "QQ账号密码" { t.Fatalf("unexpected items: %#v", items) } } func TestParseExternalUploadItemsReportsMissingFields(t *testing.T) { _, err := parseExternalUploadItems(json.RawMessage(`{"loginMethod":"QQ账号密码","currency":{"hafuCoin":197.1}}`)) if err == nil { t.Fatal("expected validation error") } validationErr, ok := err.(UploadValidationError) if !ok { t.Fatalf("expected UploadValidationError, got %T", err) } expected := []string{"data.rank", "data.level", "data.currency.recycleRatio", "data.dailyConsumption"} for _, field := range expected { if !containsString(validationErr.Missing, field) { t.Fatalf("expected missing %s in %#v", field, validationErr.Missing) } } } func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) { req := externalAccountToCreateRequest("客服1", 1772526103000, ExternalAccountData{ LoginMethod: "QQ账号密码", Rank: "黑鹰", Level: 60, SafeSlots: 9, SecretKD: 1.6, DailyLossM: 7, Deposit: 400, BanRecord: "无", CommonRegion: "郑州", Currency: ExternalUploadCurrency{ HafuCoin: 197.1, RecycleRatio: 43, RecycleRent: 458, }, DailyConsumption: ExternalDailyConsumption{Stamina: 7, Weight: 7}, Inventory: ExternalUploadInventory{ AWMBullets: 45, Level6Helmets: 14, Level6Armor: 17, Skins: []string{"信条", "电锯惊魂"}, }, }) if req.ServerRegion != "QQ" { t.Fatalf("expected QQ server region, got %q", req.ServerRegion) } if req.HafCoinAmount != 197100000 { t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount) } if req.PriceCent != 45800 || req.DepositAmountCent != 40000 { t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent) } if req.AssetSummary["season_insurance"] != "3*3" { t.Fatalf("expected 3*3 insurance, got %#v", req.AssetSummary["season_insurance"]) } if req.AssetSummary["daily_loss_m"] != float64(7) { t.Fatalf("expected daily loss 7, got %#v", req.AssetSummary["daily_loss_m"]) } if req.AssetSummary["stamina_level"] != "7级" || req.AssetSummary["load_level"] != "7级" { t.Fatalf("unexpected stamina/load: %#v", req.AssetSummary) } groups := req.AssetSummary["skin_groups"].(map[string][]string) if len(groups["melee"]) != 2 { t.Fatalf("expected melee skins mapped, got %#v", groups) } if len(req.ScreenshotURLS) != 1 || req.ScreenshotURLS[0] != defaultUploadScreenshot { t.Fatalf("expected default screenshot, got %#v", req.ScreenshotURLS) } } func validExternalUploadDataJSON() string { return `{ "loginMethod":"QQ账号密码", "rank":"黑鹰", "level":60, "safeSlots":9, "secretKD":1.6, "dailyLossM":7, "deposit":400, "currency":{"hafuCoin":197.1,"recycleRatio":43,"recycleRent":458}, "dailyConsumption":{"stamina":7,"weight":7} }` } func containsString(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false }