aa330b726e
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Has been skipped
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s
The main bot answered /start with a single English line ("Tap to open Scrabble.").
Localize it: Russian or English by the sender's reported Telegram language
(Message.from.language_code, which the Bot API carries on the message itself — there is
no separate user-update event — English fallback), with the longer welcome copy and a
localized launch button ("Открыть «Эрудит»" / "Open “Erudite”").
The welcome links the game channel and the discussion chat by their public @username,
resolved once at startup from the configured TELEGRAM_GAME_CHANNEL_ID / TELEGRAM_CHAT_ID
via getChat and cached. A handle that is unset, private, or unreadable degrades to a
generic noun ("the channel" / "our chat") rather than a dangling "@", so the paragraph
always reads cleanly (the bot's info screen still lists the real links). Adds
GameChannelID to bot.Config (wired from the existing config) for the channel handle.
Tests: startText localization + handle embedding + per-slot generic fallback; handleStart
language selection; resolveWelcomeHandles. README updated.
206 lines
7.0 KiB
Go
206 lines
7.0 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}}`)
|
|
case strings.HasSuffix(r.URL.Path, "/getChat"):
|
|
// Echo the requested id into the username so a resolver test can tell the
|
|
// channel lookup from the chat lookup.
|
|
io.WriteString(w, `{"ok":true,"result":{"id":-100,"type":"channel","username":"u`+r.FormValue("chat_id")+`"}}`)
|
|
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 in english by default", 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)
|
|
}
|
|
// No reported language -> English welcome + English button.
|
|
if !strings.Contains(api.text, "Hi!") {
|
|
t.Errorf("text = %q, want the English welcome", api.text)
|
|
}
|
|
if !strings.Contains(api.replyMarkup, "Open") {
|
|
t.Errorf("reply_markup = %q, want the English button", api.replyMarkup)
|
|
}
|
|
})
|
|
t.Run("uses the sender's reported language", 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",
|
|
From: &models.User{ID: 7, LanguageCode: "ru"},
|
|
}})
|
|
if !strings.Contains(api.text, "Привет!") {
|
|
t.Errorf("text = %q, want the Russian welcome for a ru sender", api.text)
|
|
}
|
|
})
|
|
t.Run("embeds resolved follow handles", func(t *testing.T) {
|
|
api := &fakeBotAPI{}
|
|
b := newTestBot(t, api)
|
|
b.channelUsername, b.chatUsername = "erudit", "erudite_chat"
|
|
b.handleStart(context.Background(), b.api, &models.Update{Message: &models.Message{
|
|
Chat: models.Chat{ID: 42, Type: models.ChatTypePrivate}, Text: "/start",
|
|
}})
|
|
if !strings.Contains(api.text, "@erudit") || !strings.Contains(api.text, "@erudite_chat") {
|
|
t.Errorf("text = %q, want the follow handles", api.text)
|
|
}
|
|
})
|
|
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 TestResolveWelcomeHandles(t *testing.T) {
|
|
api := &fakeBotAPI{}
|
|
b := newTestBot(t, api)
|
|
b.channelID, b.chatID = 111, 222
|
|
b.resolveWelcomeHandles(context.Background())
|
|
// The fake echoes the requested id into the username, so each lookup is independent.
|
|
if b.channelUsername != "u111" {
|
|
t.Errorf("channelUsername = %q, want u111", b.channelUsername)
|
|
}
|
|
if b.chatUsername != "u222" {
|
|
t.Errorf("chatUsername = %q, want u222", b.chatUsername)
|
|
}
|
|
// An unset id resolves to no handle (and makes no getChat call).
|
|
b.channelID = 0
|
|
b.resolveWelcomeHandles(context.Background())
|
|
if b.channelUsername != "" {
|
|
t.Errorf("channelUsername = %q, want empty for id 0", b.channelUsername)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|