From 38be7021889496c7ba838b2b0675761e99a4397e Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 20:53:48 +0400 Subject: [PATCH] a fake bot API to test the poller against (V-637) An httptest server that hands out one batch of updates per getUpdates call and records everything else, plus a recorder for what the poller asked the daemon to do. --- .../telegramsink/intakeharness_test.go | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 internal/delivery/telegramsink/intakeharness_test.go diff --git a/internal/delivery/telegramsink/intakeharness_test.go b/internal/delivery/telegramsink/intakeharness_test.go new file mode 100644 index 0000000..b255f4b --- /dev/null +++ b/internal/delivery/telegramsink/intakeharness_test.go @@ -0,0 +1,123 @@ +// intakeharness_test.go — a fake bot API and a recorder for what the poller +// asked the daemon to do. Shared by the intake tests beside it. +package telegramsink + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" +) + +// fakeBot stands in for the bot API. It hands out queued updates once, records +// every other call, and answers the ok=true envelope the poller checks. +type fakeBot struct { + mu sync.Mutex + updates [][]update // one batch per getUpdates call, then empty + calls []botCall + srv *httptest.Server +} + +type botCall struct { + method string + body map[string]any +} + +func newFakeBot(t *testing.T, batches ...[]update) *fakeBot { + t.Helper() + b := &fakeBot{updates: batches} + b.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + method := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] + raw, _ := io.ReadAll(r.Body) + var body map[string]any + _ = json.Unmarshal(raw, &body) + + b.mu.Lock() + b.calls = append(b.calls, botCall{method: method, body: body}) + var batch []update + if method == "getUpdates" && len(b.updates) > 0 { + batch, b.updates = b.updates[0], b.updates[1:] + } + b.mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": batch}) + })) + t.Cleanup(b.srv.Close) + return b +} + +func (b *fakeBot) called(method string) []botCall { + b.mu.Lock() + defer b.mu.Unlock() + var out []botCall + for _, c := range b.calls { + if c.method == method { + out = append(out, c) + } + } + return out +} + +// recorder collects what the poller asked the daemon to do. +type recorder struct { + mu sync.Mutex + turns []string + conversation string + traceID int64 + corrections []correction + reply string + err error + correctErr error +} + +type correction struct { + traceID int64 + shouldBe string +} + +func (r *recorder) turn(_ context.Context, conversation, text string) (string, int64, error) { + r.mu.Lock() + defer r.mu.Unlock() + r.turns = append(r.turns, text) + r.conversation = conversation + return r.reply, r.traceID, r.err +} + +func (r *recorder) correct(_ context.Context, traceID int64, shouldBe string) error { + r.mu.Lock() + defer r.mu.Unlock() + r.corrections = append(r.corrections, correction{traceID, shouldBe}) + return r.correctErr +} + +func (r *recorder) took() []string { + r.mu.Lock() + defer r.mu.Unlock() + return append([]string(nil), r.turns...) +} + +const ownerChat = "4242" + +func newTestPoller(t *testing.T, b *fakeBot, rec *recorder) *Poller { + t.Helper() + sink, err := New(Config{BotToken: "secret-token", ChatID: ownerChat, BaseURL: b.srv.URL}) + if err != nil { + t.Fatal(err) + } + p, err := NewPoller(sink, rec.turn, rec.correct) + if err != nil { + t.Fatal(err) + } + return p +} + +func msg(chatID, text string) update { + return update{UpdateID: 7, Message: &message{ + MessageID: 11, Text: text, Chat: chat{ID: json.Number(chatID)}, + }} +}