axe/internal/telegram/telegram_test.go
2026-10-18 22:07:25 +03:00

269 lines
9.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package telegram
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
)
// newTestBot — клиент на фейковом Bot API: сервер видит метод и форму,
// клиент видит только свой APIURL. Токен обязателен, иначе бот «выключен».
func newTestBot(t *testing.T, handler http.HandlerFunc) *Bot {
t.Helper()
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
return New(Config{Token: "test-token", APIURL: srv.URL, HTTP: srv.Client()})
}
// formHandler — роутер по последнему сегменту пути (…/sendMessage и т.д.)
// с доступом к отправленной форме из теста.
func formHandler(t *testing.T, method *string, form *url.Values, reply string) http.HandlerFunc {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
*method = strings.TrimPrefix(r.URL.Path, "/bottest-token/")
if err := r.ParseForm(); err != nil {
t.Errorf("parse form: %v", err)
}
*form = r.PostForm
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(reply))
}
}
// TestDisabled — пустой токен: фича выключена, сетевых вызовов нет,
// ошибки распознаются как «постоянная» (повтор бессмыслен).
func TestDisabled(t *testing.T) {
b := New(Config{})
if b.Enabled() {
t.Fatal("Enabled = true, want false (no token)")
}
ctx := context.Background()
if err := b.SendMessage(ctx, "123", "hi"); !errors.Is(err, ErrDisabled) {
t.Errorf("SendMessage err = %v, want ErrDisabled", err)
}
if err := b.GetChat(ctx, "@group"); !errors.Is(err, ErrDisabled) {
t.Errorf("GetChat err = %v, want ErrDisabled", err)
}
if _, err := b.GetUpdates(ctx, 0, time.Second); !errors.Is(err, ErrDisabled) {
t.Errorf("GetUpdates err = %v, want ErrDisabled", err)
}
if err := b.Init(ctx); !errors.Is(err, ErrDisabled) {
t.Errorf("Init err = %v, want ErrDisabled", err)
}
if !Permanent(ErrDisabled) {
t.Error("Permanent(ErrDisabled) = false, want true")
}
}
// TestSendMessage — форма уходит POST'ом (текст не в URL), путь — с токеном.
func TestSendMessage(t *testing.T) {
var method string
var form url.Values
b := newTestBot(t, formHandler(t, &method, &form,
`{"ok":true,"result":{"message_id":1}}`))
if err := b.SendMessage(context.Background(), "-1001", "<b>hi</b>"); err != nil {
t.Fatalf("SendMessage: %v", err)
}
if method != "sendMessage" {
t.Errorf("method = %q, want sendMessage", method)
}
if got := form.Get("chat_id"); got != "-1001" {
t.Errorf("chat_id = %q, want -1001", got)
}
if got := form.Get("text"); got != "<b>hi</b>" {
t.Errorf("text = %q", got)
}
if got := form.Get("parse_mode"); got != "HTML" {
t.Errorf("parse_mode = %q, want HTML", got)
}
}
// TestSendMessageErrors — классификация ошибок Bot API: «чат недоступен»
// и «бот заблокирован» — постоянные (повтор не поможет), 429 — временная.
func TestSendMessageErrors(t *testing.T) {
for _, tc := range []struct {
name string
reply string
permanent bool
want error
}{
{"chat not found", `{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`, true, ErrChatNotFound},
{"bot blocked", `{"ok":false,"error_code":403,"description":"Forbidden: bot was blocked by the user"}`, true, ErrForbidden},
{"too many", `{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 10"}`, false, nil},
{"server error", `{"ok":false,"error_code":500,"description":"Internal Server Error"}`, false, nil},
} {
t.Run(tc.name, func(t *testing.T) {
b := newTestBot(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.reply))
})
err := b.SendMessage(context.Background(), "1", "text")
if err == nil {
t.Fatal("err = nil, want error")
}
if tc.want != nil && !errors.Is(err, tc.want) {
t.Errorf("err = %v, want %v", err, tc.want)
}
if got := Permanent(err); got != tc.permanent {
t.Errorf("Permanent = %v, want %v (err: %v)", got, tc.permanent, err)
}
})
}
}
// TestSendMessageEmptyChat — пустая цель не должна уходить в API:
// это ошибка вызывающего кода, и она сразу «постоянная».
func TestSendMessageEmptyChat(t *testing.T) {
called := false
b := newTestBot(t, func(_ http.ResponseWriter, _ *http.Request) {
called = true
})
err := b.SendMessage(context.Background(), " ", "text")
if !errors.Is(err, ErrChatNotFound) {
t.Errorf("err = %v, want ErrChatNotFound", err)
}
if called {
t.Error("API called for empty chat")
}
}
// TestGetChat — проверка вручную введённой группы: путь, параметр и
// ошибка «чата нет» для несуществующей цели.
func TestGetChat(t *testing.T) {
var method string
var form url.Values
b := newTestBot(t, formHandler(t, &method, &form,
`{"ok":true,"result":{"id":-1001,"type":"group","username":"buildings"}}`))
if err := b.GetChat(context.Background(), "@buildings"); err != nil {
t.Fatalf("GetChat: %v", err)
}
if method != "getChat" || form.Get("chat_id") != "@buildings" {
t.Errorf("method = %q, chat_id = %q", method, form.Get("chat_id"))
}
bad := newTestBot(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"ok":false,"error_code":400,"description":"Bad Request: chat not found"}`))
})
if err := bad.GetChat(context.Background(), "@nope"); !errors.Is(err, ErrChatNotFound) {
t.Errorf("err = %v, want ErrChatNotFound", err)
}
}
// TestGetUpdates — разбор очереди: offset, таймаут и фильтр allowed_updates
// уходят формой, апдейты приходят структурой.
func TestGetUpdates(t *testing.T) {
var method string
var form url.Values
b := newTestBot(t, formHandler(t, &method, &form,
`{"ok":true,"result":[
{"update_id":41,"message":{"text":"/start ABCD2345","chat":{"id":777,"type":"private"}}},
{"update_id":42,"message":{"text":"просто сообщение","chat":{"id":-1002,"type":"group"}}}
]}`))
updates, err := b.GetUpdates(context.Background(), 40, 30*time.Second)
if err != nil {
t.Fatalf("GetUpdates: %v", err)
}
if method != "getUpdates" {
t.Errorf("method = %q, want getUpdates", method)
}
if got := form.Get("offset"); got != "40" {
t.Errorf("offset = %q, want 40", got)
}
if got := form.Get("timeout"); got != "30" {
t.Errorf("timeout = %q, want 30", got)
}
if got := form.Get("allowed_updates"); got != `["message"]` {
t.Errorf("allowed_updates = %q", got)
}
if len(updates) != 2 {
t.Fatalf("updates = %d, want 2", len(updates))
}
if updates[0].Message == nil || updates[0].Message.Text != "/start ABCD2345" {
t.Errorf("first message = %+v", updates[0].Message)
}
if updates[0].Message.Chat.ID != 777 {
t.Errorf("chat id = %d, want 777", updates[0].Message.Chat.ID)
}
if updates[0].UpdateID != 41 {
t.Errorf("update_id = %d, want 41", updates[0].UpdateID)
}
}
// TestGetUpdatesEmpty — пустая очередь (длинный поллинг истёк): не ошибка,
// пустой срез.
func TestGetUpdatesEmpty(t *testing.T) {
b := newTestBot(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"ok":true,"result":[]}`))
})
updates, err := b.GetUpdates(context.Background(), 0, time.Second)
if err != nil {
t.Fatalf("GetUpdates: %v", err)
}
if len(updates) != 0 {
t.Errorf("updates = %d, want 0", len(updates))
}
}
// TestInitAndGetLink — getMe даёт имя бота, из него собирается ссылка
// привязки; мусорный ответ — ошибка, а не пустое имя.
func TestInitAndGetLink(t *testing.T) {
var method string
var form url.Values
b := newTestBot(t, formHandler(t, &method, &form,
`{"ok":true,"result":{"id":1,"username":"my_build_bot"}}`))
if err := b.Init(context.Background()); err != nil {
t.Fatalf("Init: %v", err)
}
if method != "getMe" {
t.Errorf("method = %q, want getMe", method)
}
if got := b.Username(); got != "my_build_bot" {
t.Errorf("Username = %q, want my_build_bot", got)
}
if got := Link(b.Username(), "ABCD2345"); got != "https://t.me/my_build_bot?start=ABCD2345" {
t.Errorf("Link = %q", got)
}
if got := Link("", "ABCD2345"); got != "" {
t.Errorf("Link without username = %q, want empty", got)
}
broken := newTestBot(t, func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`not a json`))
})
if err := broken.Init(context.Background()); err == nil {
t.Error("Init on garbage response: err = nil, want error")
}
if broken.Username() != "" {
t.Errorf("Username after failure = %q, want empty", broken.Username())
}
}
// TestCodeHash — хеш стабилен, 64 hex-знака: в базу кладётся он.
func TestCodeHash(t *testing.T) {
h := CodeHash("ABCD2345")
if len(h) != 64 {
t.Errorf("len = %d, want 64", len(h))
}
if h != CodeHash("ABCD2345") {
t.Error("hash is not stable")
}
if h == CodeHash("ABCD2346") {
t.Error("different codes give the same hash")
}
for _, r := range h {
if !strings.ContainsRune("0123456789abcdef", r) {
t.Fatalf("non-hex character %q in %q", r, h)
}
}
}