306 lines
9.8 KiB
Go
306 lines
9.8 KiB
Go
package session
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"bd2server/internal/server/accountstate"
|
|
"bd2server/internal/server/cryptox"
|
|
"bd2server/internal/server/events"
|
|
"bd2server/internal/server/eventtasks"
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/mail"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/protocol"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
// This adapter writes the real normalized wallet and inventory grant ledgers.
|
|
type attendanceTransactionEconomy struct {
|
|
wallet *player.Wallet
|
|
inventory *player.Inventory
|
|
calls int
|
|
}
|
|
|
|
func (e *attendanceTransactionEconomy) Apply(identity string, costs, rewards []gamedata.Reward) ([]byte, error) {
|
|
if len(costs) != 0 {
|
|
return nil, fmt.Errorf("unexpected attendance costs")
|
|
}
|
|
e.calls++
|
|
if _, err := e.wallet.GrantQuestOnce(identity, rewards); err != nil {
|
|
return nil, err
|
|
}
|
|
var items []gamedata.BattleReward
|
|
for _, r := range rewards {
|
|
if r.Type == 9 || r.Type == 8 {
|
|
items = append(items, gamedata.BattleReward{Type: r.Type, ID: r.ID, Count: r.Count})
|
|
}
|
|
}
|
|
if _, err := e.inventory.GrantOnce(identity, items); err != nil {
|
|
return nil, err
|
|
}
|
|
return wire.AppendBytes(nil, 1, nil), nil
|
|
}
|
|
|
|
func TestAttendanceBatchRollsBackAndSuccessfulRetryGrantsOnce(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "state.db")
|
|
now := time.Now().UTC()
|
|
registry := events.NewRegistry()
|
|
if err := registry.Replace([]events.Schedule{
|
|
{UID: 1, Type: 0, ID: 1, Start: now.Add(-24 * time.Hour).UnixMilli(), End: now.Add(48 * time.Hour).UnixMilli()},
|
|
{UID: 2, Type: 1, ID: 2, Start: now.Truncate(24 * time.Hour).UnixMilli(), End: now.Add(48 * time.Hour).UnixMilli()},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
design := &gamedata.EventTasksDesign{
|
|
Attendance: map[uint64]gamedata.EventAttendance{1: {ID: 1, Group: 1}},
|
|
AttendanceRewards: map[uint64][]gamedata.EventAttendanceReward{1: {{ID: 1, Group: 1, Day: 1, Basic: gamedata.Reward{Type: 4, Count: 100}}}},
|
|
LimitRewards: map[[2]uint64]uint64{{2, 1}: 987},
|
|
}
|
|
open := func() (*accountstate.Repository, *player.Wallet, *player.Inventory, *eventtasks.Service, *mail.Service, *attendanceTransactionEconomy) {
|
|
t.Helper()
|
|
repo, err := accountstate.Open(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = repo.Close() })
|
|
wallet, err := player.OpenWallet(repo, player.Currency{Gold: 12})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inv, err := player.OpenInventory(repo, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
eco := &attendanceTransactionEconomy{wallet: wallet, inventory: inv}
|
|
svc, err := eventtasks.Open(repo, design, registry, eco)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mailbox, err := mail.OpenService(repo, &mail.Starter{Version: "2.35.10", MailCount: 1}, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mailbox.AttachAttendanceRewardEconomy(eco); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc.AttachAttendanceMail(mailbox)
|
|
return repo, wallet, inv, svc, mailbox, eco
|
|
}
|
|
makeServer := func(repo *accountstate.Repository, svc *eventtasks.Service, mailbox *mail.Service) *Server {
|
|
t.Helper()
|
|
server, err := NewServer(fakeLogin{}, svc, mailbox, &mutatingDomain{store: repo})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := server.AttachStateStore(repo); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return server
|
|
}
|
|
batch := func(server *Server, cookie, endpoint string, seq uint64, fail bool) error {
|
|
t.Helper()
|
|
request := wire.AppendVarint(nil, 1, seq)
|
|
if endpoint == "/MailOpen" {
|
|
request = wire.AppendVarint(request, 2, 1)
|
|
}
|
|
requests := []protocol.BatchRequest{{Path: endpoint, RequestData: base64.StdEncoding.EncodeToString(request)}}
|
|
if fail {
|
|
requests = append(requests, protocol.BatchRequest{Path: "/InjectedFailure", RequestData: base64.StdEncoding.EncodeToString(wire.AppendVarint(nil, 1, seq+1))})
|
|
}
|
|
plain, err := json.Marshal(requests)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, err := cryptox.EncryptBase64(plain, server.KeyForTest())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = server.DispatchRaw("/BatchRequest", []byte(body), "s="+cookie)
|
|
return err
|
|
}
|
|
mailState := func(repo *accountstate.Repository) map[string]any {
|
|
t.Helper()
|
|
result := map[string]any{}
|
|
core, err := repo.Load("mail")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result["core"] = string(core)
|
|
for _, bucket := range []string{"dynamic", "issued", "history"} {
|
|
entries, err := repo.ListEntries("mail", bucket)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result[bucket] = entries
|
|
}
|
|
return result
|
|
}
|
|
assertUnclaimed := func(wallet *player.Wallet, inv *player.Inventory) {
|
|
t.Helper()
|
|
if wallet.Snapshot().Gold != 12 || len(inv.All()) != 0 {
|
|
t.Fatalf("unclaimed mail credited rewards: wallet=%+v items=%+v", wallet.Snapshot(), inv.All())
|
|
}
|
|
}
|
|
repo, wallet, inv, svc, mailbox, eco := open()
|
|
if err := wallet.EnsurePersisted(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.EnsurePersisted(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := svc.SetNewbieStep(0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := repo.Load("eventtasks")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
beforeMail := mailState(repo)
|
|
server := makeServer(repo, svc, mailbox)
|
|
session := login(t, server)
|
|
if err := batch(server, session.Cookie, "/Attendance", 2, true); err == nil {
|
|
t.Fatal("failed attendance batch accepted")
|
|
}
|
|
if eco.calls != 0 {
|
|
t.Fatalf("attendance credited economy instead of mailing rewards: calls=%d", eco.calls)
|
|
}
|
|
if err := repo.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Rollback fences the old domain objects; reopen every repository/domain.
|
|
repo, wallet, inv, svc, mailbox, eco = open()
|
|
after, err := repo.Load("eventtasks")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != string(before) {
|
|
t.Fatalf("attendance progress, claim markers or receipts survived rollback: %s", after)
|
|
}
|
|
if !reflect.DeepEqual(mailState(repo), beforeMail) {
|
|
t.Fatal("attendance mail or issued ledger survived rollback")
|
|
}
|
|
assertUnclaimed(wallet, inv)
|
|
server = makeServer(repo, svc, mailbox)
|
|
session = login(t, server)
|
|
if err := batch(server, session.Cookie, "/Attendance", 2, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := batch(server, session.Cookie, "/Attendance", 3, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if eco.calls != 0 {
|
|
t.Fatalf("attendance mail prematurely credited economy: calls=%d", eco.calls)
|
|
}
|
|
assertUnclaimed(wallet, inv)
|
|
issuedMail := mailState(repo)
|
|
if len(issuedMail["dynamic"].(map[string][]byte)) != 1 || len(issuedMail["issued"].(map[string][]byte)) != 1 {
|
|
t.Fatal("attendance did not issue exactly one mail")
|
|
}
|
|
if err := repo.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo, wallet, inv, svc, mailbox, eco = open()
|
|
server = makeServer(repo, svc, mailbox)
|
|
session = login(t, server)
|
|
if err := batch(server, session.Cookie, "/Attendance", 2, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(mailState(repo), issuedMail) {
|
|
t.Fatal("relogin duplicated attendance mail")
|
|
}
|
|
assertUnclaimed(wallet, inv)
|
|
if err := batch(server, session.Cookie, "/MailOpen", 3, true); err == nil {
|
|
t.Fatal("failed mailbox batch accepted")
|
|
}
|
|
if eco.calls != 1 {
|
|
t.Fatalf("mail claim did not reach real economy before rollback: calls=%d", eco.calls)
|
|
}
|
|
if err := repo.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo, wallet, inv, svc, mailbox, eco = open()
|
|
if !reflect.DeepEqual(mailState(repo), issuedMail) {
|
|
t.Fatal("mail claim/history persisted after failed batch")
|
|
}
|
|
assertUnclaimed(wallet, inv)
|
|
server = makeServer(repo, svc, mailbox)
|
|
session = login(t, server)
|
|
if err := batch(server, session.Cookie, "/MailOpen", 2, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := batch(server, session.Cookie, "/MailOpen", 2, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := batch(server, session.Cookie, "/MailOpen", 3, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().Gold != 112 || len(inv.All()) != 1 || inv.All()[0].Count != 1 {
|
|
t.Fatal("mail replay duplicated persisted rewards")
|
|
}
|
|
if err := repo.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
repo, wallet, inv, svc, mailbox, eco = open()
|
|
defer repo.Close()
|
|
items := inv.All()
|
|
if wallet.Snapshot().Gold != 112 || len(items) != 1 || items[0].Type != 9 || items[0].ID != 987 || items[0].Count != 1 {
|
|
t.Fatalf("successful attendance not persisted exactly once: wallet=%+v items=%+v", wallet.Snapshot(), items)
|
|
}
|
|
for _, domain := range []string{"wallet", "items"} {
|
|
grants, err := repo.ListEntries(domain, "granted")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(grants) != 1 {
|
|
t.Fatalf("%s durable grant ledger duplicated or missing: %v", domain, grants)
|
|
}
|
|
}
|
|
claimedMail := mailState(repo)
|
|
var claimed struct {
|
|
Opened []uint64 `json:"opened"`
|
|
}
|
|
if err := json.Unmarshal([]byte(claimedMail["core"].(string)), &claimed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(claimed.Opened, []uint64{1}) || len(claimedMail["history"].(map[string][]byte)) != 1 {
|
|
t.Fatal("mail claim/history was not persisted")
|
|
}
|
|
server = makeServer(repo, svc, mailbox)
|
|
session = login(t, server)
|
|
if err := batch(server, session.Cookie, "/MailOpen", 2, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().Gold != 112 || len(inv.All()) != 1 || inv.All()[0].Count != 1 {
|
|
t.Fatal("reopened mail claim duplicated rewards")
|
|
}
|
|
var state struct {
|
|
Attendance map[string]struct {
|
|
Count uint64
|
|
LastDay string
|
|
Obtained, History map[string]bool
|
|
}
|
|
LoginDays map[string]int64
|
|
}
|
|
saved, err := repo.Load("eventtasks")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(saved, &state); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(state.Attendance) != 2 || len(state.LoginDays) != 1 {
|
|
t.Fatalf("successful attendance progress missing: %s", saved)
|
|
}
|
|
for uid, a := range state.Attendance {
|
|
if a.Count != 1 || a.LastDay == "" || len(a.Obtained) != 1 || len(a.History) != 1 {
|
|
t.Fatalf("claim markers missing for %s: %+v", uid, a)
|
|
}
|
|
}
|
|
}
|