Files
scrabble-game/platform/telegram/internal/bot/bot_test.go
T
Ilia Denisov c494da553a
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m7s
fix(telegram): reply to /start only in private chats
The main bot is now an admin in the moderated discussion group and receives its
messages (allowed_updates includes message). Its default handler replied to
every message with a Mini App launch button — an inline web_app button, which
Telegram permits only in private chats — so replying in the group failed with
BUTTON_TYPE_INVALID (silently: the send fails, no user-facing error). Reply only
in a private chat; in the group the bot only manages permissions. The promo bot
gets the same guard.
2026-06-21 17:28:01 +02:00

153 lines
4.8 KiB
Go

package bot
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/go-telegram/bot/models"
"go.uber.org/zap"
)
// fakeBotAPI answers getMe (so bot.New succeeds offline) and records the last
// sendMessage form fields.
type fakeBotAPI struct {
chatID string
text string
replyMarkup string
}
func (f *fakeBotAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasSuffix(r.URL.Path, "/getMe"):
io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"test","username":"testbot"}}`)
case strings.HasSuffix(r.URL.Path, "/sendMessage"):
f.chatID = r.FormValue("chat_id")
f.text = r.FormValue("text")
f.replyMarkup = r.FormValue("reply_markup")
io.WriteString(w, `{"ok":true,"result":{"message_id":1}}`)
default:
io.WriteString(w, `{"ok":true,"result":true}`)
}
}
func newTestBot(t *testing.T, api http.Handler) *Bot {
t.Helper()
srv := httptest.NewServer(api)
t.Cleanup(srv.Close)
b, err := New(Config{Token: "123:ABC", APIBaseURL: srv.URL, MiniAppURL: "https://example.com/telegram/"}, zap.NewNop())
if err != nil {
t.Fatalf("new bot: %v", err)
}
return b
}
func TestNotifyBuildsLaunchButton(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
if err := b.Notify(context.Background(), 12345, "It's your turn.", "Open game", "g7c9e"); err != nil {
t.Fatalf("notify: %v", err)
}
if api.chatID != "12345" {
t.Errorf("chat_id = %q, want 12345", api.chatID)
}
if api.text != "It's your turn." {
t.Errorf("text = %q", api.text)
}
if !strings.Contains(api.replyMarkup, "web_app") || !strings.Contains(api.replyMarkup, "startapp=g7c9e") {
t.Errorf("reply_markup = %q, want a web_app button with startapp=g7c9e", api.replyMarkup)
}
}
func TestSendTextHasNoMarkup(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
if err := b.SendText(context.Background(), 999, "plain"); err != nil {
t.Fatalf("send text: %v", err)
}
if api.chatID != "999" || api.text != "plain" {
t.Errorf("chat_id=%q text=%q, want 999/plain", api.chatID, api.text)
}
if api.replyMarkup != "" {
t.Errorf("reply_markup = %q, want empty", api.replyMarkup)
}
}
// getMePathFor captures the path bot.New's getMe call hits for the given TestEnv,
// so the test environment routing is covered (a misroute is exactly what makes a
// test-environment token fail with "getMe unauthorized").
func getMePathFor(t *testing.T, testEnv bool) string {
t.Helper()
var path string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/getMe") {
path = r.URL.Path
}
io.WriteString(w, `{"ok":true,"result":{"id":1,"is_bot":true,"first_name":"t","username":"tb"}}`)
}))
t.Cleanup(srv.Close)
if _, err := New(Config{Token: "123:ABC", APIBaseURL: srv.URL, TestEnv: testEnv, MiniAppURL: "https://example.com/"}, zap.NewNop()); err != nil {
t.Fatalf("new bot (testEnv=%v): %v", testEnv, err)
}
return path
}
func TestTestEnvironmentRoutesGetMe(t *testing.T) {
if got, want := getMePathFor(t, true), "/bot123:ABC/test/getMe"; got != want {
t.Errorf("TestEnv getMe path = %q, want %q", got, want)
}
if got, want := getMePathFor(t, false), "/bot123:ABC/getMe"; got != want {
t.Errorf("prod getMe path = %q, want %q", got, want)
}
}
func TestHandleStartRepliesPrivateOnly(t *testing.T) {
t.Run("private replies", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate}, Text: "/start g7",
}})
if api.chatID != "42" || !strings.Contains(api.replyMarkup, "web_app") {
t.Errorf("private /start: chat=%q markup=%q, want a web_app reply", api.chatID, api.replyMarkup)
}
})
t.Run("group ignored", func(t *testing.T) {
api := &fakeBotAPI{}
b := newTestBot(t, api)
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
Chat: models.Chat{ID: -100, Type: models.ChatTypeSupergroup}, Text: "/start",
}})
if api.chatID != "" {
t.Errorf("group /start got a reply (chat=%q); an inline web_app button is invalid in groups", api.chatID)
}
})
}
func TestStartPayload(t *testing.T) {
cases := map[string]string{
"/start g123": "g123",
"/start": "",
"/start f99 ": "f99",
"hello": "",
}
for in, want := range cases {
if got := startPayload(in); got != want {
t.Errorf("startPayload(%q) = %q, want %q", in, got, want)
}
}
}
func TestLaunchURL(t *testing.T) {
b := &Bot{miniAppURL: "https://example.com/telegram/"}
if got := b.launchURL(""); got != "https://example.com/telegram/" {
t.Errorf("empty start param = %q, want the base URL", got)
}
if got := b.launchURL("g123"); !strings.Contains(got, "startapp=g123") {
t.Errorf("launchURL = %q, want startapp=g123", got)
}
}