Files
bd2/go/internal/server/commerce/entitlements_test.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

139 lines
5.0 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) != 2 || base.Rewards[0].Type != 45 {
t.Fatal("expiry or prestige dispatch invalid")
}
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")
}
}