+144
-3
@@ -3,6 +3,9 @@ package laniakea
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
|
||||
@@ -1378,14 +1381,14 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
|
||||
name: "error value",
|
||||
panicWith: errors.New("boom"),
|
||||
matchErr: func(err error) bool {
|
||||
return err != nil && err.Error() == "boom"
|
||||
return errors.Is(err, ErrHandlerPanic) && strings.Contains(err.Error(), "boom")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "string value",
|
||||
panicWith: "kaboom",
|
||||
matchErr: func(err error) bool {
|
||||
return err != nil && err.Error() == "kaboom"
|
||||
return errors.Is(err, ErrHandlerPanic) && strings.Contains(err.Error(), "kaboom")
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1419,7 +1422,7 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
|
||||
panicEvent := (*ErrorEvent)(nil)
|
||||
for i := range observer.errors {
|
||||
ev := observer.errors[i]
|
||||
if ev.Plugin == "" && ev.HandlerKind == "" && ev.UpdateID == 100 {
|
||||
if ev.Plugin == "test" && ev.HandlerKind == HandlerCommandKind && ev.UpdateID == 100 {
|
||||
panicEvent = &ev
|
||||
break
|
||||
}
|
||||
@@ -1436,10 +1439,148 @@ func TestHandlePanicEmitsErrorEvent(t *testing.T) {
|
||||
if !tt.matchErr(panicEvent.Err) {
|
||||
t.Fatalf("unexpected panic Err: %v", panicEvent.Err)
|
||||
}
|
||||
if len(observer.finished) != 1 || !errors.Is(observer.finished[0].Err, ErrHandlerPanic) {
|
||||
t.Fatalf("expected balanced HandlerFinishedEvent, got %#v", observer.finished)
|
||||
}
|
||||
if len(observer.handled) != 1 || !observer.handled[0].Handled {
|
||||
t.Fatalf("expected completed handled update, got %#v", observer.handled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSceneUserErrorIsDeliveredOnce(t *testing.T) {
|
||||
failing := func(*SceneContext, NoData) (SceneResult, error) {
|
||||
return SceneResult{}, AsUserError(errors.New("try again"))
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
step string
|
||||
kind HandlerEventKind
|
||||
setup func(*Scene[NoData])
|
||||
update func() tgapi.Update
|
||||
}{
|
||||
{
|
||||
name: "step", step: "start", kind: HandlerSceneStepKind,
|
||||
setup: func(scene *Scene[NoData]) { scene.OnStep("start", failing) },
|
||||
update: func() tgapi.Update { return sceneMessageUpdate(101, "hello") },
|
||||
},
|
||||
{
|
||||
name: "command", step: "start", kind: HandlerSceneCommandKind,
|
||||
setup: func(scene *Scene[NoData]) { scene.OnCommand("fail", failing) },
|
||||
update: func() tgapi.Update { return sceneMessageUpdate(102, "/fail") },
|
||||
},
|
||||
{
|
||||
name: "payload", step: "start", kind: HandlerScenePayloadKind,
|
||||
setup: func(scene *Scene[NoData]) { scene.OnPayload("fail", failing) },
|
||||
update: func() tgapi.Update {
|
||||
data, err := encodeJSONPayload(CallbackData{Command: "fail"})
|
||||
if err != nil {
|
||||
t.Fatalf("encodeJSONPayload returned error: %v", err)
|
||||
}
|
||||
return tgapi.Update{
|
||||
UpdateID: 103,
|
||||
Type: tgapi.UpdateTypeCallbackQuery,
|
||||
CallbackQuery: &tgapi.CallbackQuery{
|
||||
ID: "callback", Data: data, From: tgapi.User{ID: 1},
|
||||
Message: &tgapi.Message{MessageID: 1, Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate}},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "message fallback", step: "missing", kind: HandlerSceneMessageKind,
|
||||
setup: func(scene *Scene[NoData]) { scene.OnMessage(failing) },
|
||||
update: func() tgapi.Update { return sceneMessageUpdate(104, "hello") },
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
requests := 0
|
||||
client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
requests++
|
||||
result := `true`
|
||||
if !strings.HasSuffix(req.URL.Path, "/answerCallbackQuery") {
|
||||
result = `{"message_id":1,"date":0,"chat":{"id":42,"type":"private"}}`
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":` + result + `}`)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
api := tgapi.NewAPI(tgapi.NewAPIOpts("token").SetAPIURL("https://example.test").SetHTTPClient(client))
|
||||
defer func() { _ = api.Close() }()
|
||||
|
||||
scene := NewScene[NoData]("signup")
|
||||
tt.setup(scene)
|
||||
plugin := NewPlugin[NoData]("scene-plugin")
|
||||
plugin.AddScene(scene)
|
||||
store := NewMemorySessionStore()
|
||||
if err := store.Set("user_id:1:chat_id:42", SceneSession{Scene: "signup", Step: tt.step}); err != nil {
|
||||
t.Fatalf("Set returned error: %v", err)
|
||||
}
|
||||
observer := &recordingObserver{}
|
||||
bot := &Bot[NoData]{
|
||||
api: api, logger: sneklog.NewLogger(), errorTemplate: "error: %s",
|
||||
plugins: []Plugin[NoData]{clonePlugin(plugin)}, observer: observer,
|
||||
sessionStore: store, sceneScopePriority: []SceneScope{SceneScopeUserChat},
|
||||
prefixes: []string{"/"}, payloadType: BotPayloadJSON,
|
||||
}
|
||||
defer func() { _ = bot.logger.Close() }()
|
||||
|
||||
update := tt.update()
|
||||
bot.handle(context.Background(), &update)
|
||||
|
||||
if requests != 1 {
|
||||
t.Fatalf("expected one user-facing reply, got %d requests", requests)
|
||||
}
|
||||
if len(observer.errors) != 1 {
|
||||
t.Fatalf("expected one ErrorEvent, got %#v", observer.errors)
|
||||
}
|
||||
if event := observer.errors[0]; event.Plugin != "scene-plugin" || event.HandlerKind != tt.kind || !event.UserFacing {
|
||||
t.Fatalf("unexpected scene ErrorEvent: %#v", event)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPolicyEventsUseConfiguredEmitter(t *testing.T) {
|
||||
directObserver := &recordingObserver{}
|
||||
emitted := 0
|
||||
ctx := &MessageContext{
|
||||
observer: directObserver,
|
||||
eventEmitter: func(_ context.Context, event Event) {
|
||||
if _, ok := event.(PolicyCheckedEvent); !ok {
|
||||
t.Fatalf("unexpected event type %T", event)
|
||||
}
|
||||
emitted++
|
||||
},
|
||||
}
|
||||
|
||||
ctx.emitPolicyChecked(PolicyCheckedEvent{Name: "admin", Passed: true})
|
||||
if emitted != 1 {
|
||||
t.Fatalf("emitter call count = %d, want 1", emitted)
|
||||
}
|
||||
if len(directObserver.policies) != 0 {
|
||||
t.Fatalf("policy bypassed configured emitter: %#v", directObserver.policies)
|
||||
}
|
||||
}
|
||||
|
||||
func sceneMessageUpdate(updateID int, text string) tgapi.Update {
|
||||
return tgapi.Update{
|
||||
UpdateID: updateID,
|
||||
Type: tgapi.UpdateTypeMessage,
|
||||
Message: &tgapi.Message{
|
||||
MessageID: 1,
|
||||
Text: text,
|
||||
From: &tgapi.User{ID: 1},
|
||||
Chat: &tgapi.Chat{ID: 42, Type: tgapi.ChatTypePrivate},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleCallbackObserverEmitsDecodeErrors(t *testing.T) {
|
||||
observer := &recordingObserver{}
|
||||
bot := &Bot[NoData]{
|
||||
|
||||
Reference in New Issue
Block a user