367 lines
11 KiB
Go
367 lines
11 KiB
Go
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 := db.AutoMigrate(&model.User{}, &model.GameAccount{}, &model.RentalListing{}); 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 := db.AutoMigrate(&model.GameAccount{}, &model.RentalListing{}); 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 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
|
|
}
|