Restore event and mini hubs, current shop and hunt calendars, and regular seasons; preserve all published windows. Implement story/quiz routes, native shop purchases, avatar sets, currencies, rank-change items and permanent buffs with rollback and reward coverage tests. Refresh client buff stats and update Cash Shop to 0.1.1.
144 lines
5.1 KiB
Go
144 lines
5.1 KiB
Go
package commerce
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
"bytes"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type entitlementGraph struct{ Calls int }
|
|
|
|
func (g *entitlementGraph) ResolveGranted(r []gamedata.BattleReward) ([]gamedata.BattleReward, error) {
|
|
g.Calls++
|
|
return r, nil
|
|
}
|
|
|
|
type entitlementBase struct {
|
|
Calls int
|
|
Rewards []gamedata.Reward
|
|
}
|
|
|
|
func (b *entitlementBase) Apply(_ string, _ []gamedata.Reward, r []gamedata.Reward) ([]byte, error) {
|
|
b.Calls++
|
|
b.Rewards = append(b.Rewards, r...)
|
|
return wire.AppendVarint(nil, 7, 1), nil
|
|
}
|
|
func entitlementFixture(t *testing.T) (*EntitlementEconomy, *player.Inventory, *entitlementBase, *entitlementGraph, *time.Time) {
|
|
t.Helper()
|
|
store := stateio.NewMemory()
|
|
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d := &gamedata.CashEntitlementDesign{AvatarSets: map[uint64]bool{10: true}, TicketTypes: map[uint64]uint64{38: 2, 99: 3}, Attendance: map[uint64][]gamedata.CashAttendanceReward{38: {{ID: 1, Reward: gamedata.BattleReward{Type: 3, Count: 1}}, {ID: 2, Reward: gamedata.BattleReward{Type: 3, Count: 5}}}, 99: {{ID: 1, Reward: gamedata.BattleReward{Type: 3, Count: 1}}, {ID: 2, Reward: gamedata.BattleReward{Type: 3, Count: 7}}}}, AttendanceTypes: map[uint64]uint64{38: 1}}
|
|
base := &entitlementBase{}
|
|
graph := &entitlementGraph{}
|
|
e, err := NewEntitlementEconomy(store, base, graph, items, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
|
|
e.SetClock(func() time.Time { return now }, 0)
|
|
return e, items, base, graph, &now
|
|
}
|
|
func TestEntitlementTypedSkinExpiryRetryAndRenewal(t *testing.T) {
|
|
e, items, base, graph, now := entitlementFixture(t)
|
|
rewards := []gamedata.Reward{{Type: 62, ID: 10, Count: 1}, {Type: 19, ID: 38, Count: 1}, {Type: 45, ID: 999, Count: 1}}
|
|
first, err := e.Apply("buy1", nil, rewards)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if items.ContentTicketExpiry(38) != uint64(now.UnixMilli()+30*86400000) || len(base.Rewards) != 3 || base.Rewards[0].Type != 62 || base.Rewards[1].Type != 45 {
|
|
t.Fatal("expiry or prestige dispatch invalid")
|
|
}
|
|
for _, item := range items.All() {
|
|
if item.Type == 62 {
|
|
t.Fatal("commerce stored synthetic avatar set")
|
|
}
|
|
}
|
|
again, err := e.Apply("buy1", nil, rewards)
|
|
if err != nil || !bytes.Equal(first, again) || graph.Calls != 2 || base.Calls != 1 {
|
|
t.Fatal("retry rerolled or double granted")
|
|
}
|
|
if _, err = e.Apply("buy1", nil, nil); err == nil {
|
|
t.Fatal("identity reuse accepted")
|
|
}
|
|
if _, err = e.Apply("invalid", nil, []gamedata.Reward{{Type: 62, ID: 888, Count: 1}}); err == nil || base.Calls != 1 {
|
|
t.Fatal("unknown skin partially applied")
|
|
}
|
|
if _, err = e.Apply("buy2", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if items.ContentTicketExpiry(38) != uint64(now.UnixMilli()+60*86400000) {
|
|
t.Fatal("renewal lost prior period")
|
|
}
|
|
merged, err := e.MergeAttendance(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok, _ := wire.Bytes(merged, 3); !ok {
|
|
t.Fatal("subscription info missing")
|
|
}
|
|
if _, ok, _ := wire.Bytes(merged, 7); !ok {
|
|
t.Fatal("monthly reward map missing")
|
|
}
|
|
}
|
|
func TestSubscriptionDailyClaimResetExpiryAndPermanentCompletion(t *testing.T) {
|
|
e, _, base, _, now := entitlementFixture(t)
|
|
_, err := e.Apply("buy", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}, {Type: 19, ID: 99, Count: 1}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
calls := base.Calls
|
|
if _, err = e.ClaimSubscriptions("login"); err != nil || base.Calls != calls {
|
|
t.Fatal("purchase day double claim")
|
|
}
|
|
*now = now.Add(24 * time.Hour)
|
|
if _, err = e.ClaimSubscriptions("login2"); err != nil || base.Calls != calls+2 {
|
|
t.Fatalf("daily claims %v calls%d", err, base.Calls)
|
|
}
|
|
calls = base.Calls
|
|
if _, err = e.ClaimSubscriptions("retry"); err != nil || base.Calls != calls {
|
|
t.Fatal("daily retry grants again")
|
|
}
|
|
*now = now.Add(31 * 24 * time.Hour)
|
|
if _, err = e.ClaimSubscriptions("expired"); err != nil || base.Calls != calls {
|
|
t.Fatal("expired or completed claim")
|
|
}
|
|
merged, err := e.MergeAttendance(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok, _ := wire.Bytes(merged, 4); !ok {
|
|
t.Fatal("permanent attendance markers missing")
|
|
}
|
|
}
|
|
func TestSubscriptionFirstRewardAndLostReply(t *testing.T) {
|
|
e, _, base, _, now := entitlementFixture(t)
|
|
_, err := e.Apply("covered", nil, []gamedata.Reward{{Type: 19, ID: 38, Count: 1}, {Type: 3, Count: 1}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(base.Rewards) != 1 || base.Rewards[0].Count != 1 {
|
|
t.Fatalf("first-day reward duplicated: %+v", base.Rewards)
|
|
}
|
|
*now = now.Add(24 * time.Hour)
|
|
first, err := e.ClaimSubscriptions("same-session-request")
|
|
if err != nil || len(first) == 0 {
|
|
t.Fatalf("claim %x %v", first, err)
|
|
}
|
|
calls := base.Calls
|
|
replay, err := e.ClaimSubscriptions("same-session-request")
|
|
if err != nil || !bytes.Equal(first, replay) || base.Calls != calls {
|
|
t.Fatal("lost reply not replayed exactly")
|
|
}
|
|
fresh, err := e.ClaimSubscriptions("new-request-same-day")
|
|
if err != nil || len(fresh) != 0 || base.Calls != calls {
|
|
t.Fatal("new same-day request repeated award")
|
|
}
|
|
}
|