yumi-golang/internal/service/vip/purchase_test.go
2026-04-29 19:49:18 +08:00

209 lines
6.8 KiB
Go

package vip
import (
"encoding/json"
"errors"
"net/http"
"testing"
"time"
"chatapp3-golang/internal/model"
)
func TestBuildPurchasePlanNewPurchase(t *testing.T) {
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
configs := vipTestConfigs()
plan, err := buildPurchasePlan(now, nil, configs[2], configs)
if err != nil {
t.Fatalf("buildPurchasePlan returned error: %v", err)
}
if plan.OrderType != orderTypePurchase {
t.Fatalf("order type = %s, want %s", plan.OrderType, orderTypePurchase)
}
if plan.CurrentLevel != 0 || plan.TargetLevel != 2 {
t.Fatalf("levels = %d -> %d, want 0 -> 2", plan.CurrentLevel, plan.TargetLevel)
}
if plan.PayableGold != 2000 {
t.Fatalf("payable = %d, want 2000", plan.PayableGold)
}
if !plan.StartAt.Equal(now) || !plan.ExpireAt.Equal(now.AddDate(0, 0, vipDurationDay)) {
t.Fatalf("period = %s -> %s, want %s -> %s", plan.StartAt, plan.ExpireAt, now, now.AddDate(0, 0, vipDurationDay))
}
}
func TestBuildPurchasePlanUpgradePaysDifferenceAndKeepsExpireAt(t *testing.T) {
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
startAt := now.AddDate(0, 0, -5)
expireAt := now.AddDate(0, 0, 25)
configs := vipTestConfigs()
state := &model.UserVipState{
Level: 2,
Status: statusActive,
StartAt: startAt,
ExpireAt: expireAt,
}
plan, err := buildPurchasePlan(now, state, configs[4], configs)
if err != nil {
t.Fatalf("buildPurchasePlan returned error: %v", err)
}
if plan.OrderType != orderTypeUpgrade {
t.Fatalf("order type = %s, want %s", plan.OrderType, orderTypeUpgrade)
}
if plan.PayableGold != 2000 {
t.Fatalf("payable = %d, want 2000", plan.PayableGold)
}
if !plan.StartAt.Equal(startAt) || !plan.ExpireAt.Equal(expireAt) {
t.Fatalf("upgrade period changed to %s -> %s, want %s -> %s", plan.StartAt, plan.ExpireAt, startAt, expireAt)
}
}
func TestBuildPurchasePlanRenewExtendsThirtyDays(t *testing.T) {
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
startAt := now.AddDate(0, 0, -5)
expireAt := now.AddDate(0, 0, 25)
configs := vipTestConfigs()
state := &model.UserVipState{
Level: 3,
Status: statusActive,
StartAt: startAt,
ExpireAt: expireAt,
}
plan, err := buildPurchasePlan(now, state, configs[3], configs)
if err != nil {
t.Fatalf("buildPurchasePlan returned error: %v", err)
}
if plan.OrderType != orderTypeRenew {
t.Fatalf("order type = %s, want %s", plan.OrderType, orderTypeRenew)
}
if plan.PayableGold != 3000 {
t.Fatalf("payable = %d, want 3000", plan.PayableGold)
}
if !plan.ExpireAt.Equal(expireAt.AddDate(0, 0, vipDurationDay)) {
t.Fatalf("renew expireAt = %s, want %s", plan.ExpireAt, expireAt.AddDate(0, 0, vipDurationDay))
}
}
func TestBuildPurchasePlanRejectsDowngrade(t *testing.T) {
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
configs := vipTestConfigs()
state := &model.UserVipState{
Level: 4,
Status: statusActive,
StartAt: now.AddDate(0, 0, -5),
ExpireAt: now.AddDate(0, 0, 25),
}
if _, err := buildPurchasePlan(now, state, configs[2], configs); err == nil {
t.Fatal("buildPurchasePlan returned nil error for downgrade")
}
}
func TestVipResourcePayloadAcceptsStringResourceID(t *testing.T) {
var payload VipResourcePayload
if err := json.Unmarshal([]byte(`{"resourceId":"2045029471274201089","name":"vip","sourceUrl":"https://example.com/a.svga"}`), &payload); err != nil {
t.Fatalf("unmarshal resource payload: %v", err)
}
payload = normalizeResource(payload)
if payload.ResourceID.Int64() != 2045029471274201089 {
t.Fatalf("resource id = %d, want 2045029471274201089", payload.ResourceID.Int64())
}
if payload.URL != "https://example.com/a.svga" || payload.SourceURL != payload.URL || payload.ResourceURL != payload.URL {
t.Fatalf("resource urls = url:%q sourceUrl:%q resourceUrl:%q", payload.URL, payload.SourceURL, payload.ResourceURL)
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal resource payload: %v", err)
}
if got := string(body); got != `{"resourceId":"2045029471274201089","name":"vip","url":"https://example.com/a.svga","sourceUrl":"https://example.com/a.svga","resourceUrl":"https://example.com/a.svga"}` {
t.Fatalf("json = %s", got)
}
}
func TestMapWalletPurchaseErrorInsufficientBalance(t *testing.T) {
err := errors.New(`java api failed: status=406 body={"errorCode":5000,"errorCodeName":"INSUFFICIENT_BALANCE","errorMsg":"balance not made"}`)
appErr := mapWalletPurchaseError(err)
if appErr.Status != http.StatusNotAcceptable {
t.Fatalf("status = %d, want %d", appErr.Status, http.StatusNotAcceptable)
}
if appErr.Code != "vip_insufficient_balance" {
t.Fatalf("code = %s, want vip_insufficient_balance", appErr.Code)
}
if appErr.Message != "insufficient balance" {
t.Fatalf("message = %s, want insufficient balance", appErr.Message)
}
}
func TestMapWalletPurchaseErrorGatewayFailure(t *testing.T) {
err := errors.New("java api failed: status=500 body=boom")
appErr := mapWalletPurchaseError(err)
if appErr.Status != http.StatusBadGateway {
t.Fatalf("status = %d, want %d", appErr.Status, http.StatusBadGateway)
}
if appErr.Code != "vip_wallet_deduct_failed" {
t.Fatalf("code = %s, want vip_wallet_deduct_failed", appErr.Code)
}
}
func TestVIPGrantResourcesFromConfigMapsBackpackTypes(t *testing.T) {
config := model.VipLevelConfig{
BadgeResourceID: 10,
AvatarFrameResourceID: 11,
EntryEffectResourceID: 12,
ChatBubbleResourceID: 13,
FloatPictureResourceID: 14,
}
resources := vipGrantResourcesFromConfig(config)
if len(resources) != 5 {
t.Fatalf("resources len = %d, want 5", len(resources))
}
assertGrant := func(index int, resourceID int64, propsType string, badge bool) {
t.Helper()
got := resources[index]
if got.ResourceID != resourceID || got.PropsType != propsType || got.Badge != badge {
t.Fatalf("resource[%d] = %+v, want id=%d type=%s badge=%v", index, got, resourceID, propsType, badge)
}
}
assertGrant(0, 10, "", true)
assertGrant(1, 11, vipPropsTypeAvatarFrame, false)
assertGrant(2, 12, vipPropsTypeRide, false)
assertGrant(3, 13, vipPropsTypeChatBubble, false)
assertGrant(4, 14, vipPropsTypeFloatPicture, false)
}
func TestVIPGrantDaysUntilCeilsPartialDay(t *testing.T) {
now := time.Date(2026, 4, 29, 10, 0, 0, 0, time.UTC)
expireAt := now.Add((24 * time.Hour) + time.Second)
if got := vipGrantDaysUntil(now, expireAt); got != 2 {
t.Fatalf("days = %d, want 2", got)
}
if got := vipGrantDaysUntil(now, now); got != 1 {
t.Fatalf("expired days = %d, want 1", got)
}
}
func vipTestConfigs() map[int]model.VipLevelConfig {
configs := make(map[int]model.VipLevelConfig, levelCount)
for level := 1; level <= levelCount; level++ {
configs[level] = model.VipLevelConfig{
Level: level,
LevelCode: levelCode(level),
DisplayName: levelDisplayName(level),
Enabled: true,
DurationDays: vipDurationDay,
PriceGold: int64(level * 1000),
}
}
return configs
}