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.
161 lines
5.4 KiB
Go
161 lines
5.4 KiB
Go
package commerce
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"bd2server/internal/server/accountstate"
|
|
"bd2server/internal/server/events"
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestEventShopPublishesAllSchedulesAndNativePurchaseReplay(t *testing.T) {
|
|
s, eco, store := serviceFixture(t, 0)
|
|
key := gamedata.CashProductKey{GroupID: 700, ProductID: 3}
|
|
d := gamedata.CashProductDesign{Key: key, PriceType: 8, PriceID: 99, PriceCount: 5, RandomBoxID: 20, PurchaseLimitType: 4, PurchaseLimitCount: 2, BulkOrderAvailability: 1}
|
|
s.catalog.designs[key] = d
|
|
s.shopWindows = map[gamedata.CashProductKey][2]uint64{}
|
|
design := &gamedata.CashCatalog{EventShops: []gamedata.EventShopDesign{{ID: 7, ProductGroupID: 700}}}
|
|
rows := []events.Schedule{{UID: 9, Type: 15, ID: 7, Start: 100, End: 200}}
|
|
if err := s.AttachEventShopSchedules(design, rows); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, now := range []int64{50, 150, 250} {
|
|
s.SetClock(func() time.Time { return time.UnixMilli(now) }, 0)
|
|
_, out, _, err := s.HandleSession("/CashShopInfo", wire.AppendVarint(nil, 1, 1), "s")
|
|
raw, found, _ := wire.Bytes(out, 1)
|
|
group, _, _ := wire.Varint(raw, 1)
|
|
uid, _, _ := wire.Varint(raw, 8)
|
|
if err != nil || !found || group != 700 || uid != 9 {
|
|
t.Fatalf("calendar filtered at %d: %v", now, err)
|
|
}
|
|
if s.IsAvailable(key) != (now == 150) {
|
|
t.Fatal("window authorization differs")
|
|
}
|
|
}
|
|
req := wire.AppendVarint(nil, 1, 1)
|
|
req = wire.AppendVarint(req, 3, 700)
|
|
line := wire.AppendVarint(nil, 1, 3)
|
|
line = wire.AppendVarint(line, 3, 2)
|
|
req = wire.AppendBytes(req, 4, line)
|
|
if _, _, handled, err := s.HandleSession("/CashShopBuy", req, "s"); !handled || err == nil || eco.calls != 0 {
|
|
t.Fatal("expired purchase authorized")
|
|
}
|
|
s.SetClock(func() time.Time { return time.UnixMilli(150) }, 0)
|
|
_, out, handled, err := s.HandleSession("/CashShopBuy", req, "s")
|
|
if err != nil || !handled || eco.calls != 1 {
|
|
t.Fatal(err, handled, eco.calls)
|
|
}
|
|
if len(eco.costs) != 1 || eco.costs[0] != (gamedata.Reward{Type: 8, ID: 99, Count: 10}) || eco.rewards[0].Count != 2 {
|
|
t.Fatal(eco)
|
|
}
|
|
// Reload service state, then replay the same request without another grant.
|
|
s2, err := NewService(s.catalog, store, eco)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s2.shopWindows = map[gamedata.CashProductKey][2]uint64{}
|
|
if err = s2.AttachEventShopSchedules(design, rows); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s2.SetClock(func() time.Time { return time.UnixMilli(250) }, 0)
|
|
_, replay, _, err := s2.HandleSession("/CashShopBuy", req, "s")
|
|
if err != nil || !bytes.Equal(out, replay) || eco.calls != 1 {
|
|
t.Fatal("replay failed", err)
|
|
}
|
|
req[1] = 2
|
|
if _, _, _, err = s.HandleSession("/CashShopBuy", req, "s"); err == nil || eco.calls != 1 {
|
|
t.Fatal("purchase limit ignored")
|
|
}
|
|
}
|
|
|
|
func TestNativeEventShopSQLiteRollsBackDebitGrantAndPurchaseCount(t *testing.T) {
|
|
fixture, _, _ := serviceFixture(t, 0)
|
|
key := gamedata.CashProductKey{GroupID: 700, ProductID: 3}
|
|
fixture.catalog.designs[key] = gamedata.CashProductDesign{Key: key, PriceType: 3, PriceCount: 5, RandomBoxID: 20, PurchaseLimitType: 4, PurchaseLimitCount: 1}
|
|
design := &gamedata.CashCatalog{EventShops: []gamedata.EventShopDesign{{ID: 7, ProductGroupID: 700}}}
|
|
rows := []events.Schedule{{UID: 9, Type: 15, ID: 7, Start: 100, End: 200}}
|
|
path := filepath.Join(t.TempDir(), "state.db")
|
|
repo, err := accountstate.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(repo, player.Currency{FreeJewelry: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = wallet.EnsurePersisted(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
open := func() *Service {
|
|
t.Helper()
|
|
s, err := NewService(fixture.catalog, repo, walletPurchaseEconomy{wallet, repo})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.shopWindows = map[gamedata.CashProductKey][2]uint64{}
|
|
if err = s.AttachEventShopSchedules(design, rows); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.SetClock(func() time.Time { return time.UnixMilli(150) }, 0)
|
|
return s
|
|
}
|
|
s := open()
|
|
s.AttachPurchaseHook(func(string, gamedata.CashProductDesign, uint64) error { return fmt.Errorf("grant hook failed") })
|
|
req := wire.AppendVarint(nil, 1, 1)
|
|
req = wire.AppendVarint(req, 3, 700)
|
|
line := wire.AppendVarint(nil, 1, 3)
|
|
line = wire.AppendVarint(line, 3, 1)
|
|
req = wire.AppendBytes(req, 4, line)
|
|
op, err := repo.BeginOperation()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, handled, err := s.HandleSession("/CashShopBuy", req, "s"); !handled || err == nil {
|
|
t.Fatal("hook failure swallowed")
|
|
}
|
|
if err = op.Rollback(); err != nil && !errors.Is(err, stateio.ErrStateRecoveryRequired) {
|
|
t.Fatal(err)
|
|
}
|
|
repo.Close()
|
|
repo, err = accountstate.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer repo.Close()
|
|
wallet, err = player.OpenWallet(repo, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().FreeJewelry != 10 {
|
|
t.Fatal("debit survived rollback")
|
|
}
|
|
for _, name := range []string{"commerce", "commerce_test_rewards"} {
|
|
raw, err := repo.Load(name)
|
|
if err != nil || raw != nil {
|
|
t.Fatal("grant or receipt survived rollback", name, err)
|
|
}
|
|
}
|
|
s = open()
|
|
op, err = repo.BeginOperation()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, err = s.HandleSession("/CashShopBuy", req, "s"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = op.Commit(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().FreeJewelry != 5 || !s.HasPurchased(key) {
|
|
t.Fatal("retry did not persist")
|
|
}
|
|
}
|