FILE / ScuroNeko/Laniakea
bot_test.go
Исходный файл и его история в репозитории.
Align docs, examples, and tests with AppData and NoData Update handler, scene, runner, and plugin generics to the new naming Ignore .codex and record the cleanup in the changelog
442 lines
13 KiB
Go
442 lines
13 KiB
Go
package laniakea
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
|
"git.scuroneko.dev/scuroneko/slog"
|
|
)
|
|
|
|
func TestGetUpdateTypesReturnsCopy(t *testing.T) {
|
|
bot := &Bot[NoData]{updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage}}
|
|
|
|
got := bot.GetUpdateTypes()
|
|
got[0] = tgapi.UpdateTypeCallbackQuery
|
|
|
|
if want := []tgapi.UpdateType{tgapi.UpdateTypeMessage}; !reflect.DeepEqual(bot.updateTypes, want) {
|
|
t.Fatalf("GetUpdateTypes exposed internal slice: got %v want %v", bot.updateTypes, want)
|
|
}
|
|
}
|
|
|
|
func TestAddPluginsSnapshotsConfiguration(t *testing.T) {
|
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
|
plugin := NewPlugin[NoData]("demo")
|
|
|
|
cmd := plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "start")
|
|
plugin.AddMiddleware(NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true }))
|
|
|
|
bot.AddPlugins(plugin)
|
|
|
|
cmd.SetDescription("mutated after registration")
|
|
plugin.NewCommand(func(ctx *MsgContext, db NoData) error { return nil }, "late")
|
|
plugin.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
|
|
|
|
registered := bot.plugins[0]
|
|
if _, exists := registered.commands["late"]; exists {
|
|
t.Fatal("late command leaked into registered plugin snapshot")
|
|
}
|
|
if registered.commands["start"].description != "" {
|
|
t.Fatalf("registered command description unexpectedly mutated: %q", registered.commands["start"].description)
|
|
}
|
|
if len(registered.middlewares) != 1 {
|
|
t.Fatalf("registered middlewares unexpectedly mutated: got %d want 1", len(registered.middlewares))
|
|
}
|
|
}
|
|
|
|
func TestBotPayloadTypeConfiguration(t *testing.T) {
|
|
bot := &Bot[NoData]{payloadType: BotPayloadBase64}
|
|
|
|
if got := bot.GetPayloadType(); got != BotPayloadBase64 {
|
|
t.Fatalf("unexpected initial payload type: %q", got)
|
|
}
|
|
bot.SetPayloadType(BotPayloadJson)
|
|
if got := bot.GetPayloadType(); got != BotPayloadJson {
|
|
t.Fatalf("unexpected updated payload type: %q", got)
|
|
}
|
|
bot.SetStrictPayloadType(true)
|
|
if !bot.strictPayloadType {
|
|
t.Fatal("expected strict payload type to be enabled")
|
|
}
|
|
}
|
|
|
|
func TestAddPluginsSkipsNilPlugin(t *testing.T) {
|
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
|
plugin := NewPlugin[NoData]("demo")
|
|
|
|
bot.AddPlugins(nil, plugin)
|
|
|
|
if len(bot.plugins) != 1 {
|
|
t.Fatalf("expected exactly one registered plugin, got %d", len(bot.plugins))
|
|
}
|
|
if bot.plugins[0].name != "demo" {
|
|
t.Fatalf("unexpected plugin name: %q", bot.plugins[0].name)
|
|
}
|
|
}
|
|
|
|
func TestInitLoggersFallsBackToStdoutLoggerOnFileError(t *testing.T) {
|
|
bot := &Bot[NoData]{}
|
|
|
|
bot.initLoggers(&BotOpts{
|
|
Debug: true,
|
|
WriteToFile: true,
|
|
UseRequestLogger: true,
|
|
LoggerBasePath: filepath.Join(t.TempDir(), "missing", "nested"),
|
|
})
|
|
|
|
if bot.logger == nil {
|
|
t.Fatal("expected main logger fallback")
|
|
}
|
|
if bot.RequestLogger == nil {
|
|
t.Fatal("expected request logger fallback")
|
|
}
|
|
if err := bot.RequestLogger.Close(); err != nil {
|
|
t.Fatalf("failed to close request logger: %v", err)
|
|
}
|
|
if err := bot.logger.Close(); err != nil {
|
|
t.Fatalf("failed to close main logger: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNextPollRetryDelay(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
prev time.Duration
|
|
want time.Duration
|
|
}{
|
|
{name: "initial", prev: 0, want: time.Second},
|
|
{name: "double", prev: 2 * time.Second, want: 4 * time.Second},
|
|
{name: "cap", prev: 20 * time.Second, want: 30 * time.Second},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := nextPollRetryDelay(tt.prev); got != tt.want {
|
|
t.Fatalf("nextPollRetryDelay(%s) = %s, want %s", tt.prev, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsUnset(t *testing.T) {
|
|
bot := &Bot[NoData]{logger: slog.CreateLogger()}
|
|
called := false
|
|
|
|
bot.AddAppDataLoggerWriter(func(db NoData) slog.LoggerWriter {
|
|
called = true
|
|
return nil
|
|
})
|
|
|
|
if called {
|
|
t.Fatal("expected app-data logger writer to be skipped when app data is unset")
|
|
}
|
|
}
|
|
|
|
func TestAddDatabaseLoggerWriterSkipsWhenAppDataIsNil(t *testing.T) {
|
|
type testDB struct{}
|
|
|
|
bot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
|
var db *testDB
|
|
bot.SetAppData(db)
|
|
|
|
called := false
|
|
bot.AddAppDataLoggerWriter(func(db *testDB) slog.LoggerWriter {
|
|
called = true
|
|
return nil
|
|
})
|
|
|
|
if called {
|
|
t.Fatal("expected app-data logger writer to be skipped when app data is nil")
|
|
}
|
|
}
|
|
|
|
func TestShouldWarnOnValueAppData(t *testing.T) {
|
|
type testDB struct{}
|
|
type dbIface interface{ Ping() error }
|
|
|
|
tests := []struct {
|
|
name string
|
|
got bool
|
|
want bool
|
|
}{
|
|
{name: "NoData", got: shouldWarnOnValueAppData[NoData](), want: false},
|
|
{name: "pointer", got: shouldWarnOnValueAppData[*testDB](), want: false},
|
|
{name: "interface", got: shouldWarnOnValueAppData[dbIface](), want: false},
|
|
{name: "map", got: shouldWarnOnValueAppData[map[string]int](), want: false},
|
|
{name: "struct", got: shouldWarnOnValueAppData[testDB](), want: true},
|
|
{name: "int", got: shouldWarnOnValueAppData[int](), want: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.got != tt.want {
|
|
t.Fatalf("shouldWarnOnValueAppData = %v, want %v", tt.got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetAppDataMarksValueWarningOnce(t *testing.T) {
|
|
type testDB struct{}
|
|
|
|
bot := &Bot[testDB]{logger: slog.CreateLogger()}
|
|
bot.SetAppData(testDB{})
|
|
if !bot.warnedValueData {
|
|
t.Fatal("expected value-typed app data to mark warning state")
|
|
}
|
|
|
|
ptrBot := &Bot[*testDB]{logger: slog.CreateLogger()}
|
|
ptrBot.SetAppData(&testDB{})
|
|
if ptrBot.warnedValueData {
|
|
t.Fatal("did not expect pointer-typed app data to mark warning state")
|
|
}
|
|
}
|
|
|
|
func TestRunWithContextRejectsSecondRun(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
plugins: []Plugin[NoData]{{name: "demo"}},
|
|
updateQueue: make(chan *tgapi.Update, 1),
|
|
maxWorkers: 1,
|
|
}
|
|
|
|
if err := bot.RunWithContext(ctx); err != nil {
|
|
t.Fatalf("first RunWithContext returned error: %v", err)
|
|
}
|
|
if err := bot.RunWithContext(ctx); !errors.Is(err, ErrBotAlreadyRun) {
|
|
t.Fatalf("expected ErrBotAlreadyRun on second run, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBotConfigurationFreezesAfterRunStarts(t *testing.T) {
|
|
type testDB struct{ Name string }
|
|
|
|
makeBot := func() *Bot[*testDB] {
|
|
return &Bot[*testDB]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
updateTypes: []tgapi.UpdateType{tgapi.UpdateTypeMessage},
|
|
payloadType: BotPayloadBase64,
|
|
strictPayloadType: false,
|
|
errorTemplate: "%s",
|
|
l10n: &L10n{},
|
|
draftProvider: &DraftProvider{},
|
|
sessionStore: NewMemorySessionStore(),
|
|
sceneScopePriority: []SceneScope{SceneScopeUserChat, SceneScopeChat, SceneScopeUser},
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
check func(t *testing.T, bot *Bot[*testDB])
|
|
}{
|
|
{
|
|
name: "SetAppData",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := &testDB{Name: "before"}
|
|
bot.SetAppData(original)
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
later := &testDB{Name: "after"}
|
|
bot.SetAppData(later)
|
|
if bot.appData != original {
|
|
t.Fatal("SetAppData mutated after configuration freeze")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "UpdateTypes",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetUpdateTypes(tgapi.UpdateTypePoll)
|
|
if !reflect.DeepEqual(bot.updateTypes, original) {
|
|
t.Fatalf("UpdateTypes mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "AddUpdateType",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := append([]tgapi.UpdateType(nil), bot.updateTypes...)
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.AddUpdateType(tgapi.UpdateTypePoll)
|
|
if !reflect.DeepEqual(bot.updateTypes, original) {
|
|
t.Fatalf("AddUpdateType mutated after configuration freeze: got %v want %v", bot.updateTypes, original)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SetPayloadType",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetPayloadType(BotPayloadJson)
|
|
if bot.payloadType != BotPayloadBase64 {
|
|
t.Fatalf("payloadType mutated after configuration freeze: got %q want %q", bot.payloadType, BotPayloadBase64)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SetStrictPayloadType",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetStrictPayloadType(true)
|
|
if bot.strictPayloadType {
|
|
t.Fatal("strictPayloadType mutated after configuration freeze")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "AddPrefixes",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := append([]string(nil), bot.prefixes...)
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.AddPrefixes("!")
|
|
if !reflect.DeepEqual(bot.prefixes, original) {
|
|
t.Fatalf("prefixes mutated after configuration freeze: got %v want %v", bot.prefixes, original)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ErrorTemplate",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetErrorTemplate("changed")
|
|
if bot.errorTemplate != "%s" {
|
|
t.Fatalf("errorTemplate mutated after configuration freeze: got %q want %q", bot.errorTemplate, "%s")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SetDraftProvider",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := bot.draftProvider
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetDraftProvider(&DraftProvider{})
|
|
if bot.draftProvider != original {
|
|
t.Fatal("draftProvider mutated after configuration freeze")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SetSessionStore",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := bot.sessionStore
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetSessionStore(NewMemorySessionStore())
|
|
if bot.sessionStore != original {
|
|
t.Fatal("sessionStore mutated after configuration freeze")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "SetSceneScopePriority",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := append([]SceneScope(nil), bot.sceneScopePriority...)
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetSceneScopePriority([]SceneScope{SceneScopeUser})
|
|
if !reflect.DeepEqual(bot.sceneScopePriority, original) {
|
|
t.Fatalf("sceneScopePriority mutated after configuration freeze: got %v want %v", bot.sceneScopePriority, original)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "AddL10n",
|
|
check: func(t *testing.T, bot *Bot[*testDB]) {
|
|
original := bot.l10n
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
t.Cleanup(bot.finishRun)
|
|
|
|
bot.SetL10n(&L10n{})
|
|
if bot.l10n != original {
|
|
t.Fatal("l10n mutated after configuration freeze")
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.check(t, makeBot())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddPluginsAndRuntimeRegistrationsNoOpAfterRunStarts(t *testing.T) {
|
|
bot := &Bot[NoData]{
|
|
logger: slog.CreateLogger(),
|
|
prefixes: []string{"/"},
|
|
middlewares: []Middleware[NoData]{NewMiddleware("base", func(ctx *MsgContext, db NoData) bool { return true })},
|
|
runners: []Runner[NoData]{NewRunner("base", func(bot *Bot[NoData]) error { return nil })},
|
|
}
|
|
plugin := NewPlugin[NoData]("late")
|
|
|
|
if err := bot.beginRun(); err != nil {
|
|
t.Fatalf("beginRun returned error: %v", err)
|
|
}
|
|
defer bot.finishRun()
|
|
|
|
bot.AddPlugins(plugin)
|
|
bot.AddMiddleware(NewMiddleware("late", func(ctx *MsgContext, db NoData) bool { return true }))
|
|
bot.AddRunner(NewRunner("late", func(bot *Bot[NoData]) error { return nil }))
|
|
|
|
if len(bot.plugins) != 0 {
|
|
t.Fatalf("expected AddPlugins to be ignored after configuration freeze, got %d plugins", len(bot.plugins))
|
|
}
|
|
if len(bot.middlewares) != 1 {
|
|
t.Fatalf("expected AddMiddleware to be ignored after configuration freeze, got %d middlewares", len(bot.middlewares))
|
|
}
|
|
if len(bot.runners) != 1 {
|
|
t.Fatalf("expected AddRunner to be ignored after configuration freeze, got %d runners", len(bot.runners))
|
|
}
|
|
}
|