Files
bd2/go/internal/server/world/overwhelm_test.go
T

122 lines
4.0 KiB
Go

package world
import (
"bd2server/internal/server/accountstate"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"bytes"
"errors"
"fmt"
"path/filepath"
"testing"
"time"
)
type overwhelmFailedState struct{ stateio.Store }
func (s overwhelmFailedState) Save(name string, b []byte) error {
if name == "field_monster_runtime" {
return fmt.Errorf("failed receipt")
}
return s.Store.Save(name, b)
}
type overwhelmPersistedEconomy struct{ stateio.Store }
func (s overwhelmPersistedEconomy) Apply(_ string, _ []gamedata.Reward, rewards []gamedata.Reward) ([]byte, error) {
if e := s.Save("overwhelm_test_reward", []byte{1}); e != nil {
return nil, e
}
return nil, nil
}
func TestOverwhelmRewardAndReceiptRollbackTogether(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.db")
repo, e := accountstate.Open(path)
if e != nil {
t.Fatal(e)
}
s := testService()
s.AttachFieldMonsterState(overwhelmFailedState{repo})
s.BeginSession("login")
s.monsterLoader = func(int) ([]gamedata.FieldMonsterDesign, error) {
return []gamedata.FieldMonsterDesign{{ID: 9, GroupID: 7, BattleDeck: 33, UseBattleSkip: 1, RegenSeconds: 10, Reward: gamedata.Reward{Type: 5, ID: 400, Count: 2}}}, nil
}
s.researchEconomy = overwhelmPersistedEconomy{repo}
s.AttachOverwhelmAuthorization(func(string, uint64) error { return repo.Save("overwhelm_test_authorization", []byte{1}) })
op, e := repo.BeginOperation()
if e != nil {
t.Fatal(e)
}
if _, _, _, e = s.Handle("/Overwhelm", overwhelmRequest(1, 9)); e == nil {
t.Fatal("expected failed final receipt")
}
if e = op.Rollback(); e != nil && !errors.Is(e, stateio.ErrStateRecoveryRequired) {
t.Fatal(e)
}
repo.Close()
repo, e = accountstate.Open(path)
if e != nil {
t.Fatal(e)
}
defer repo.Close()
for _, name := range []string{"overwhelm_test_authorization", "overwhelm_test_reward", "field_monster_runtime"} {
b, e := repo.Load(name)
if e != nil || b != nil {
t.Fatal("partial batch survived rollback", name, e)
}
}
}
func overwhelmRequest(seq uint64, ids ...uint64) []byte {
b := wire.AppendVarint(nil, 1, seq)
for _, id := range ids {
m := wire.AppendVarint(nil, 1, 7)
m = wire.AppendVarint(m, 2, id)
m = wire.AppendVarint(m, 3, 33)
m = wire.AppendVarint(m, 4, 2)
b = wire.AppendBytes(b, 2, m)
}
return b
}
func TestOverwhelmBatchPrevalidationAuthorizationAndGenerationReplay(t *testing.T) {
s := testService()
store := stateio.NewMemory()
s.AttachFieldMonsterState(store)
s.BeginSession("login")
now := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
s.monsterNow = func() time.Time { return now }
s.monsterLoader = func(int) ([]gamedata.FieldMonsterDesign, error) {
return []gamedata.FieldMonsterDesign{{ID: 9, GroupID: 7, BattleDeck: 33, UseBattleSkip: 1, RegenSeconds: 10, Reward: gamedata.Reward{Type: 5, ID: 400, Count: 2}}, {ID: 10, GroupID: 7, BattleDeck: 33, UseBattleSkip: 1, RegenSeconds: 10, Reward: gamedata.Reward{Type: 5, ID: 400, Count: 2}}}, nil
}
eco := &researchEconomySpy{}
s.researchEconomy = eco
auth := 0
s.AttachOverwhelmAuthorization(func(identity string, n uint64) error {
if n != 2 {
return fmt.Errorf("wrong batch size")
}
auth++
return nil
})
if _, _, _, e := s.Handle("/Overwhelm", overwhelmRequest(1, 9, 999)); e == nil || eco.calls != 0 || auth != 0 {
t.Fatal("invalid tail partially settled batch", e)
}
req := overwhelmRequest(2, 9, 10)
code, first, _, e := s.Handle("/Overwhelm", req)
if e != nil || code != 275 || eco.calls != 2 || auth != 1 {
t.Fatal("valid batch not settled", code, eco.calls, auth, e)
}
now = now.Add(11 * time.Second)
_, retry, _, e := s.Handle("/Overwhelm", req)
if e != nil || !bytes.Equal(first, retry) || eco.calls != 2 || auth != 1 {
t.Fatal("retry awarded or authorized twice", e)
}
if _, _, _, e = s.Handle("/Overwhelm", overwhelmRequest(2, 10, 9)); e == nil {
t.Fatal("sequence reuse accepted")
}
if _, _, _, e = s.Handle("/Overwhelm", overwhelmRequest(3, 9, 10)); e != nil || eco.calls != 4 {
t.Fatal("new generation did not award", eco.calls, e)
}
}