0a5211b038
The cases that matter: the turn runs with the chat as its dialogue id, the reply carries the gesture, a turn nothing persisted carries no buttons, a stranger gets no answer at all, the first tap writes nothing, and a write that failed says so on the button instead of going quiet.
202 lines
6.7 KiB
Go
202 lines
6.7 KiB
Go
package telegramsink
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The turn he types in the chat is the turn the web would run, and the reply
|
|
// carries the one gesture beside it.
|
|
func TestIntakeRunsTheTurnAndOffersTheCorrection(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
rec := &recorder{reply: "поняла", traceID: 91}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.handle(context.Background(), msg(ownerChat, " поужинал "))
|
|
|
|
if got := rec.took(); len(got) != 1 || got[0] != "поужинал" {
|
|
t.Fatalf("turns %q, want the trimmed utterance once", got)
|
|
}
|
|
// The dialogue is keyed per chat, so a clarify asked here is not answered on
|
|
// the web.
|
|
if rec.conversation != "telegram:"+ownerChat {
|
|
t.Errorf("conversation %q does not name the chat", rec.conversation)
|
|
}
|
|
sends := b.called("sendMessage")
|
|
if len(sends) != 1 {
|
|
t.Fatalf("%d sends, want 1", len(sends))
|
|
}
|
|
if sends[0].body["text"] != "поняла" {
|
|
t.Errorf("sent %v, want the reply", sends[0].body["text"])
|
|
}
|
|
if sends[0].body["protect_content"] != true {
|
|
t.Error("his own data went out forwardable")
|
|
}
|
|
kb, _ := json.Marshal(sends[0].body["reply_markup"])
|
|
if !strings.Contains(string(kb), "w:91") {
|
|
t.Errorf("keyboard %s does not point at the turn's trace", kb)
|
|
}
|
|
}
|
|
|
|
// A turn nothing persisted has no row to correct, and a button that would name
|
|
// one reports a failure he cannot act on.
|
|
func TestIntakeSkipsTheGestureWithNoTrace(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
p := newTestPoller(t, b, &recorder{reply: "поняла", traceID: 0})
|
|
|
|
p.handle(context.Background(), msg(ownerChat, "привет"))
|
|
|
|
sends := b.called("sendMessage")
|
|
if len(sends) != 1 {
|
|
t.Fatalf("%d sends, want 1", len(sends))
|
|
}
|
|
if _, ok := sends[0].body["reply_markup"]; ok {
|
|
t.Error("offered a correction on a turn with no trace")
|
|
}
|
|
}
|
|
|
|
// One chat, and a stranger is not answered at all: a reply confirms the bot
|
|
// exists and whose it is.
|
|
func TestIntakeIgnoresAnyOtherChat(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
rec := &recorder{reply: "поняла", traceID: 5}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.handle(context.Background(), msg("9999", "включи свет"))
|
|
p.handle(context.Background(), update{UpdateID: 8, CallbackQuery: &callbackQuery{
|
|
ID: "cb", Data: "t:5:note", Message: message{Chat: chat{ID: json.Number("9999")}},
|
|
}})
|
|
|
|
if got := rec.took(); len(got) != 0 {
|
|
t.Errorf("ran %q for a chat that is not the owner's", got)
|
|
}
|
|
if len(rec.corrections) != 0 {
|
|
t.Errorf("wrote %v from a chat that is not the owner's", rec.corrections)
|
|
}
|
|
if len(b.calls) != 0 {
|
|
t.Errorf("answered a stranger: %v", b.calls)
|
|
}
|
|
}
|
|
|
|
// Tapping "не то" opens the seven and writes nothing yet. The target is worth
|
|
// much more than the negative, so it must not be spent before he can name it.
|
|
func TestIntakeFirstTapOnlyOpensTheTargets(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
rec := &recorder{}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
|
|
ID: "cb", Data: "w:77", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
|
|
}})
|
|
|
|
if len(rec.corrections) != 0 {
|
|
t.Fatalf("wrote %v before he named a target", rec.corrections)
|
|
}
|
|
if len(b.called("answerCallbackQuery")) != 1 {
|
|
t.Error("left the clock spinning on the button")
|
|
}
|
|
edits := b.called("editMessageReplyMarkup")
|
|
if len(edits) != 1 {
|
|
t.Fatalf("%d edits, want the target row", len(edits))
|
|
}
|
|
kb, _ := json.Marshal(edits[0].body["reply_markup"])
|
|
for _, want := range CorrectionTargets {
|
|
if !strings.Contains(string(kb), `"`+want+`"`) {
|
|
t.Errorf("target row %s is missing %s", kb, want)
|
|
}
|
|
}
|
|
// And the way out, because he opened the row without knowing he had to name
|
|
// anything.
|
|
if !strings.Contains(string(kb), `"t:77:"`) {
|
|
t.Errorf("target row %s prices out the untargeted negative", kb)
|
|
}
|
|
}
|
|
|
|
func TestIntakeWritesTheCorrection(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, data, want string
|
|
}{
|
|
{"with a target", "t:77:note", "note"},
|
|
{"untargeted", "t:77:", ""},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
rec := &recorder{}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
|
|
ID: "cb", Data: tc.data, Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
|
|
}})
|
|
|
|
if len(rec.corrections) != 1 || rec.corrections[0] != (correction{77, tc.want}) {
|
|
t.Fatalf("corrections %v, want trace 77 → %q", rec.corrections, tc.want)
|
|
}
|
|
// The buttons come off once the gesture is given.
|
|
edits := b.called("editMessageReplyMarkup")
|
|
if len(edits) != 1 {
|
|
t.Fatalf("%d edits, want the keyboard cleared", len(edits))
|
|
}
|
|
kb, _ := json.Marshal(edits[0].body["reply_markup"])
|
|
if strings.Contains(string(kb), "t:77") {
|
|
t.Errorf("keyboard %s still invites a second correction", kb)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A write that failed says so on the button. Silence would read as recorded.
|
|
func TestIntakeSaysWhenTheLabelDidNotLand(t *testing.T) {
|
|
b := newFakeBot(t)
|
|
rec := &recorder{correctErr: errors.New("no such routing trace")}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.handle(context.Background(), update{UpdateID: 9, CallbackQuery: &callbackQuery{
|
|
ID: "cb", Data: "t:77:fact", Message: message{MessageID: 11, Chat: chat{ID: json.Number(ownerChat)}},
|
|
}})
|
|
|
|
answers := b.called("answerCallbackQuery")
|
|
if len(answers) != 1 || answers[0].body["text"] == "" {
|
|
t.Fatalf("answers %v, want a toast saying it did not land", answers)
|
|
}
|
|
if len(b.called("editMessageReplyMarkup")) != 0 {
|
|
t.Error("cleared the buttons after a failed write, so he cannot try again")
|
|
}
|
|
}
|
|
|
|
// A question asked while the daemon was down has been answered by him or has
|
|
// stopped mattering, and a reminder set from it would land at the wrong time.
|
|
func TestIntakeDiscardsTheBacklog(t *testing.T) {
|
|
b := newFakeBot(t, []update{msg(ownerChat, "напомни в 7 позвонить маме")})
|
|
rec := &recorder{reply: "поняла", traceID: 3}
|
|
p := newTestPoller(t, b, rec)
|
|
|
|
p.discardBacklog(context.Background())
|
|
|
|
if got := rec.took(); len(got) != 0 {
|
|
t.Errorf("answered %q from the overnight queue", got)
|
|
}
|
|
// And the offset moved past it, so the next poll does not see it again.
|
|
if p.offset != 8 {
|
|
t.Errorf("offset %d, want the skipped update acknowledged", p.offset)
|
|
}
|
|
}
|
|
|
|
// The poller does not start without somewhere to send the turn.
|
|
func TestNewPollerNeedsATurn(t *testing.T) {
|
|
sink, err := New(Config{BotToken: "t", ChatID: ownerChat})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := NewPoller(sink, nil, nil); err == nil {
|
|
t.Error("built a poller that reads the chat and answers nothing")
|
|
}
|
|
if _, err := NewPoller(nil, func(context.Context, string, string) (string, int64, error) {
|
|
return "", 0, nil
|
|
}, nil); err == nil {
|
|
t.Error("built a poller with no sink to answer through")
|
|
}
|
|
}
|