377 lines
13 KiB
Go
377 lines
13 KiB
Go
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestStarterAnswersMailInfoWithoutCapture(t *testing.T) {
|
|
seed, err := Load(filepath.Join("..", "..", "..", "seed", "v2_35_10", "mail.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, proto, ok, err := seed.Handle("/MailInfo", wire.AppendVarint(nil, 1, 249))
|
|
if err != nil || !ok || code != packetCode || len(proto) == 0 {
|
|
t.Fatalf("MailInfo: code=%d bytes=%d ok=%t err=%v", code, len(proto), ok, err)
|
|
}
|
|
if _, _, ok, err := seed.Handle("/MailOpen", wire.AppendVarint(nil, 1, 1)); ok || err != nil {
|
|
t.Fatal("seed claimed an endpoint it does not own")
|
|
}
|
|
if _, _, ok, err := seed.Handle("/MailInfo", nil); !ok || err == nil {
|
|
t.Fatal("missing sequence was accepted")
|
|
}
|
|
}
|
|
|
|
func TestDynamicCompensationMailPersistsAndIsIdempotent(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", MailCount: 1, MaxMailID: 100}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Date(2026, 9, 24, 0, 0, 0, 0, time.UTC)
|
|
rewards := []gamedata.Reward{{Type: 4, Count: 123}}
|
|
if err := service.EnqueueCompensation("daily:2026-09-23:1", "日常任务到期补发", "任务已完成但未领取。", rewards, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.EnqueueCompensation("daily:2026-09-23:1", "日常任务到期补发", "任务已完成但未领取。", rewards, now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reopened, err := OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(reopened.dynamic) != 1 || reopened.issued["daily:2026-09-23:1"] != 101 {
|
|
t.Fatalf("dynamic=%+v issued=%+v", reopened.dynamic, reopened.issued)
|
|
}
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
request = wire.AppendVarint(request, 2, 101)
|
|
if _, _, _, err := reopened.Handle("/MailOpen", request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().Gold != 123 {
|
|
t.Fatalf("gold=%d", wallet.Snapshot().Gold)
|
|
}
|
|
}
|
|
|
|
func TestMailOpenGrantsItemsAndCurrencyAndPersists(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", MailCount: 3, MaxMailID: 12, Mails: []MailDBInfo{
|
|
{MailID: 11, MailType: 2, ExpiresAt: 100, SentAt: 10, RewardTypes: []uint64{3, 4}, RewardIDs: []uint64{0, 0}, RewardCounts: []uint64{70, 123456789}},
|
|
{MailID: 12, MailType: 2, ExpiresAt: 100, SentAt: 10, RewardTypes: []uint64{8}, RewardIDs: []uint64{9}, RewardCounts: []uint64{10}},
|
|
}}
|
|
starter := &player.Starter{Version: "2.35.10"}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, starter)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{Gold: 321})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
request = wire.AppendBytes(request, 2, packed([]uint64{11, 12}))
|
|
code, response, ok, err := service.Handle("/MailOpen", request)
|
|
if err != nil || !ok || code != 132 {
|
|
t.Fatalf("code=%d ok=%v err=%v", code, ok, err)
|
|
}
|
|
if wallet.Snapshot().FreeJewelry != 70 {
|
|
t.Fatalf("wallet=%+v", wallet.Snapshot())
|
|
}
|
|
if wallet.Snapshot().Gold != 123457110 {
|
|
t.Fatalf("gold did not stack into wallet: %+v", wallet.Snapshot())
|
|
}
|
|
if len(inv.All()) != 1 || inv.All()[0].ID != 9 || inv.All()[0].Count != 10 {
|
|
t.Fatalf("items=%+v", inv.All())
|
|
}
|
|
bundle, found, _ := wire.Bytes(response, 1)
|
|
if !found || len(bundle) == 0 {
|
|
t.Fatal("reward bundle missing")
|
|
}
|
|
infoReq := wire.AppendVarint(nil, 1, 2)
|
|
_, info, _, err := service.Handle("/MailInfo", infoReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, found, _ := wire.Bytes(info, 1); found {
|
|
t.Fatal("opened mail remained visible")
|
|
}
|
|
count, found, _ := wire.Varint(info, 2)
|
|
if !found || count != 1 {
|
|
t.Fatalf("mail count=%d found=%v", count, found)
|
|
}
|
|
service, err = OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, info, _, _ = service.Handle("/MailInfo", infoReq)
|
|
if _, found, _ := wire.Bytes(info, 1); found {
|
|
t.Fatal("opened mail was not persisted")
|
|
}
|
|
}
|
|
|
|
func TestMailOpenGrantsNonResourceItemDBInfoType(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 13, Mails: []MailDBInfo{{
|
|
MailID: 13, MailType: 2, ExpiresAt: 100, SentAt: 10,
|
|
RewardTypes: []uint64{14}, RewardIDs: []uint64{1}, RewardCounts: []uint64{2},
|
|
}}}
|
|
starter := &player.Starter{Version: "2.35.10"}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, starter)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
request = wire.AppendBytes(request, 2, packed([]uint64{13}))
|
|
if _, _, _, err := service.Handle("/MailOpen", request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
items := inv.All()
|
|
if len(items) != 1 || items[0].ID != 1 || items[0].Type != 14 || items[0].Count != 2 {
|
|
t.Fatalf("items=%+v", items)
|
|
}
|
|
}
|
|
|
|
func TestMailOpenGrantsCatalystCurrencyAndPersists(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 14, Mails: []MailDBInfo{{
|
|
MailID: 14, MailType: 2, ExpiresAt: 100, SentAt: 10,
|
|
RewardTypes: []uint64{12}, RewardIDs: []uint64{0}, RewardCounts: []uint64{250},
|
|
}}}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{Catalyst: 10})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, seed, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
request = wire.AppendBytes(request, 2, packed([]uint64{14}))
|
|
_, response, _, err := service.Handle("/MailOpen", request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().Catalyst != 260 || len(inv.All()) != 0 {
|
|
t.Fatalf("wallet=%+v items=%+v", wallet.Snapshot(), inv.All())
|
|
}
|
|
bundle, found, _ := wire.Bytes(response, 1)
|
|
if !found {
|
|
t.Fatal("reward bundle missing")
|
|
}
|
|
item, found, _ := wire.Bytes(bundle, 1)
|
|
if !found {
|
|
t.Fatal("currency ItemDBInfo missing")
|
|
}
|
|
if typ, _, _ := wire.Varint(item, 3); typ != 12 {
|
|
t.Fatalf("currency type=%d", typ)
|
|
}
|
|
if count, _, _ := wire.Varint(item, 4); count != 250 {
|
|
t.Fatalf("currency count=%d", count)
|
|
}
|
|
if _, _, _, err := service.Handle("/MailOpen", request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wallet.Snapshot().Catalyst != 260 {
|
|
t.Fatalf("replay catalyst=%d", wallet.Snapshot().Catalyst)
|
|
}
|
|
reopened, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reopened.Snapshot().Catalyst != 260 {
|
|
t.Fatalf("persisted catalyst=%d", reopened.Snapshot().Catalyst)
|
|
}
|
|
}
|
|
|
|
func TestWatchedSeedReloadsOnlyValidAtomicReplacement(t *testing.T) {
|
|
dir := t.TempDir()
|
|
seedPath := filepath.Join(dir, "seed.json")
|
|
first := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 11, Mails: []MailDBInfo{{MailID: 11, MailType: 2, ExpiresAt: 100, SentAt: 10}}}
|
|
if err := first.Write(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
starter, err := Load(seedPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, starter, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.AttachSeedPath(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := wire.AppendVarint(nil, 1, 1)
|
|
if _, response, _, err := service.Handle("/MailInfo", request); err != nil {
|
|
t.Fatal(err)
|
|
} else if id, _, _ := wire.Varint(mustFirstMail(t, response), 1); id != 11 {
|
|
t.Fatalf("initial mail ID=%d", id)
|
|
}
|
|
second := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 12, Mails: []MailDBInfo{{MailID: 12, MailType: 2, ExpiresAt: 100, SentAt: 10}}}
|
|
if err := second.Write(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, response, _, err := service.Handle("/MailInfo", request); err != nil {
|
|
t.Fatal(err)
|
|
} else if id, _, _ := wire.Varint(mustFirstMail(t, response), 1); id != 12 {
|
|
t.Fatalf("reloaded mail ID=%d", id)
|
|
}
|
|
if err := os.WriteFile(seedPath, []byte("not json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, err := service.Handle("/MailInfo", request); err == nil {
|
|
t.Fatal("accepted malformed watched seed")
|
|
}
|
|
// The malformed file did not replace the last known-good mailbox.
|
|
if service.Starter.Mails[0].MailID != 12 {
|
|
t.Fatalf("last known-good seed lost: %+v", service.Starter.Mails)
|
|
}
|
|
}
|
|
|
|
func TestExpandedWatchedSeedAdvancesDynamicMailAllocator(t *testing.T) {
|
|
dir := t.TempDir()
|
|
seedPath := filepath.Join(dir, "seed.json")
|
|
first := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 11, Mails: []MailDBInfo{{MailID: 11, MailType: 2, ExpiresAt: 100, SentAt: 10}}}
|
|
if err := first.Write(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storage := stateio.NewMemory()
|
|
inv, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := OpenService(storage, first, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.EnsurePersisted(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.AttachSeedPath(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := &Starter{Version: "2.35.10", MailCount: 3, MaxMailID: 101, Mails: []MailDBInfo{
|
|
{MailID: 11, MailType: 2, ExpiresAt: 100, SentAt: 10},
|
|
{MailID: 101, MailType: 2, ExpiresAt: 100, SentAt: 10},
|
|
}}
|
|
if err := second.Write(seedPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, _, err := service.Handle("/MailInfo", wire.AppendVarint(nil, 1, 1)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if service.state.NextDynamicMailID != 102 {
|
|
t.Fatalf("next dynamic mail ID=%d", service.state.NextDynamicMailID)
|
|
}
|
|
reopened, err := OpenService(storage, second, inv, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reopened.state.NextDynamicMailID != 102 {
|
|
t.Fatalf("reopened next dynamic mail ID=%d", reopened.state.NextDynamicMailID)
|
|
}
|
|
}
|
|
|
|
func mustFirstMail(t *testing.T, response []byte) []byte {
|
|
t.Helper()
|
|
entry, found, err := wire.Bytes(response, 1)
|
|
if err != nil || !found {
|
|
t.Fatalf("missing first mail: found=%v err=%v", found, err)
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func TestMailOpenAcceptsOfficialPackedRequest(t *testing.T) {
|
|
request := []byte{0x08, 0xef, 0x01, 0x12, 0x0f, 0x85, 0xc1, 0xe1, 0xce, 0x30, 0x88, 0xc1, 0xe1, 0xce, 0x30, 0x8a, 0xc1, 0xe1, 0xce, 0x30}
|
|
ids, err := requestMailIDs(request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []uint64{13050077317, 13050077320, 13050077322}
|
|
if len(ids) != len(want) {
|
|
t.Fatalf("ids=%v", ids)
|
|
}
|
|
for i := range want {
|
|
if ids[i] != want[i] {
|
|
t.Fatalf("ids=%v", ids)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStarterMailProtocolEncodingAndRoundTrip(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", MailCount: 2, MaxMailID: 7, Mails: []MailDBInfo{{
|
|
MailID: 7, MailType: 2, Title: "Welcome", Body: "Rewards", ExpiresAt: 1000, SentAt: 10,
|
|
RewardTypes: []uint64{3, 8}, RewardIDs: []uint64{0, 17}, RewardCounts: []uint64{5, 300},
|
|
}}}
|
|
path := filepath.Join(t.TempDir(), "mail.json")
|
|
if err := seed.Write(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := Load(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// MailInfoResponse contains MailDBInfo field 1 plus count and maximum ID.
|
|
// Reward arrays are packed protobuf varints; default ID 0 remains a slot.
|
|
wantMail := []byte{0x08, 7, 0x10, 2, 0x2a, 7, 'W', 'e', 'l', 'c', 'o', 'm', 'e', 0x32, 7, 'R', 'e', 'w', 'a', 'r', 'd', 's', 0x38, 0xe8, 7, 0x42, 2, 3, 8, 0x4a, 2, 0, 17, 0x52, 3, 5, 0xac, 2, 0x68, 10}
|
|
want := append([]byte{0x0a, byte(len(wantMail))}, wantMail...)
|
|
want = append(want, 0x10, 2, 0x18, 7)
|
|
code, actual, handled, err := loaded.Handle("/MailInfo", wire.AppendVarint(nil, 1, 249))
|
|
if err != nil || !handled || code != 131 || !bytes.Equal(actual, want) {
|
|
t.Fatalf("code=%d proto=%x handled=%v err=%v", code, actual, handled, err)
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsRewardLengthMismatch(t *testing.T) {
|
|
seed := &Starter{Version: "2.35.10", Mails: []MailDBInfo{{MailID: 1, MailType: 2, ExpiresAt: 1, SentAt: 1, RewardTypes: []uint64{8}, RewardIDs: []uint64{1}}}, MailCount: 2, MaxMailID: 1}
|
|
if seed.Validate() == nil {
|
|
t.Fatal("accepted bad rewards")
|
|
}
|
|
}
|