269 lines
9.4 KiB
Go
269 lines
9.4 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|