Files
tiennm99bot/internal/modules/stats/startup_test.go
T
tiennm99 d84bc3d1fb feat(noitu): make /noitu the group game and add /noitutop
/noitu now posts the group play card that /noitupvp used to, and the solo
game against the bot moves to /noitubot. In a private chat /noitu points
the user to a group, or to /noitubot.

/noitutop ranks a group's players across all of its cards by wins, best
game and games played.

A one-time startup migration moves /stats history to the new names in
order: noitu to noitubot first, then noitupvp to noitu.
2026-10-09 16:22:54 +07:00

149 lines
5.4 KiB
Go

package stats
import (
"context"
"errors"
"testing"
"github.com/tiennm99/tiennm99bot/internal/storage"
"github.com/tiennm99/tiennm99bot/internal/systemstate"
)
func TestInitStore_SwapsNoituCommandStats(t *testing.T) {
assertNoituCommandSwap(t, storage.NewMemoryProvider())
}
// assertNoituCommandSwap seeds the solo game's history under "noitu" and the
// group game's under "noitupvp", runs the startup migration twice, and checks
// that the solo history landed on "noitubot" and the group history on
// "noitu" without the two mixing.
func assertNoituCommandSwap(t *testing.T, provider storage.Provider) {
t.Helper()
ctx := context.Background()
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
docs := storage.Typed[usageEntry](statsColl)
seeds := map[string]usageEntry{
usageKey("noitu", 0): {Cmd: "noitu", N: 5},
usageKey("noitu", 7): {Cmd: "noitu", UserID: 7, Username: "alice", N: 2},
usageKey("noitupvp", 0): {Cmd: "noitupvp", N: 3},
usageKey("noitupvp", 7): {Cmd: "noitupvp", UserID: 7, Username: "alice", N: 4},
usageKey("noitupvp", 8): {Cmd: "noitupvp", UserID: 8, Username: "bob", N: 6},
// A command that only shares the prefix stays where it is.
usageKey("noitutop", 7): {Cmd: "noitutop", UserID: 7, Username: "alice", N: 1},
}
for key, entry := range seeds {
if err := docs.Put(ctx, key, entry); err != nil {
t.Fatalf("seed %s: %v", key, err)
}
}
for run := range 2 {
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore run %d: %v", run+1, err)
}
}
want := map[string]usageEntry{
usageKey("noitubot", 0): {Cmd: "noitubot", N: 5},
usageKey("noitubot", 7): {Cmd: "noitubot", UserID: 7, Username: "alice", N: 2},
usageKey("noitu", 0): {Cmd: "noitu", N: 3},
usageKey("noitu", 7): {Cmd: "noitu", UserID: 7, Username: "alice", N: 4},
usageKey("noitu", 8): {Cmd: "noitu", UserID: 8, Username: "bob", N: 6},
usageKey("noitutop", 7): {Cmd: "noitutop", UserID: 7, Username: "alice", N: 1},
}
for key, w := range want {
got, _, err := docs.Get(ctx, key)
if err != nil {
t.Fatalf("get %s: %v", key, err)
}
if got != w {
t.Errorf("%s = %+v, want %+v", key, got, w)
}
}
for _, key := range []string{usageKey("noitupvp", 0), usageKey("noitupvp", 7), usageKey("noitupvp", 8)} {
if _, _, err := docs.Get(ctx, key); !errors.Is(err, storage.ErrNotFound) {
t.Errorf("old key %s err = %v, want ErrNotFound", key, err)
}
}
state := systemstate.New(systemColl)
for key, count := range map[string]int64{
"stats:command-rename:noitu-to-noitubot": 7,
"stats:command-rename:noitupvp-to-noitu": 13,
} {
rec, ok, err := state.Get(ctx, key)
if err != nil || !ok || rec.Status != "complete" || rec.Count != count {
t.Errorf("marker %s = %+v ok=%v err=%v, want complete count %d", key, rec, ok, err, count)
}
}
}
// A boot that stopped after the first rename resumes with the second only:
// the rows already under the new "noitu" name are group history and must not
// move to noitubot again.
func TestInitStore_NoituSwapResumesAfterFirstStep(t *testing.T) {
ctx := context.Background()
provider := storage.NewMemoryProvider()
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
docs := storage.Typed[usageEntry](statsColl)
if err := migrateCommandRename(ctx, statsColl, systemColl, commandRenames[0]); err != nil {
t.Fatalf("first step: %v", err)
}
for key, entry := range map[string]usageEntry{
usageKey("noitubot", 7): {Cmd: "noitubot", UserID: 7, Username: "alice", N: 2},
// Moved by an interrupted second step before the restart.
usageKey("noitu", 7): {Cmd: "noitu", UserID: 7, Username: "alice", N: 4},
usageKey("noitupvp", 8): {Cmd: "noitupvp", UserID: 8, Username: "bob", N: 6},
} {
if err := docs.Put(ctx, key, entry); err != nil {
t.Fatalf("seed %s: %v", key, err)
}
}
if err := InitStore(ctx, statsColl, systemColl); err != nil {
t.Fatalf("InitStore: %v", err)
}
for key, n := range map[string]int64{
usageKey("noitubot", 7): 2,
usageKey("noitu", 7): 4,
usageKey("noitu", 8): 6,
} {
got, _, err := docs.Get(ctx, key)
if err != nil || got.N != n {
t.Errorf("%s = %+v err=%v, want n %d", key, got, err, n)
}
}
}
// A rename merges into rows already under the new name and keeps a row that
// was only ever deleted history marked deleted.
func TestMigrateCommandRename_MergesAndKeepsDeletedFlag(t *testing.T) {
ctx := context.Background()
provider := storage.NewMemoryProvider()
statsColl := provider.Collection("stats")
systemColl := provider.Collection(systemstate.CollectionName)
docs := storage.Typed[usageEntry](statsColl)
for key, entry := range map[string]usageEntry{
usageKey("old", 0): {Cmd: "old", N: 2, Deleted: true},
usageKey("old", 7): {Cmd: "old", UserID: 7, Username: "alice", N: 3},
usageKey("new", 7): {Cmd: "new", UserID: 7, Username: "al", N: 5},
} {
if err := docs.Put(ctx, key, entry); err != nil {
t.Fatalf("seed %s: %v", key, err)
}
}
if err := migrateCommandRename(ctx, statsColl, systemColl, commandRename{oldCmd: "old", newCmd: "new", markerKey: "test-rename"}); err != nil {
t.Fatalf("migrate: %v", err)
}
if got, _, _ := docs.Get(ctx, usageKey("new", 0)); got != (usageEntry{Cmd: "new", N: 2, Deleted: true}) {
t.Errorf("anonymous = %+v", got)
}
if got, _, _ := docs.Get(ctx, usageKey("new", 7)); got != (usageEntry{Cmd: "new", UserID: 7, Username: "alice", N: 8}) {
t.Errorf("user = %+v", got)
}
}