mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
fac9009398
Every bot message is sent HTML-parsed while the intended markup lives in the translation templates, yet the values filled into them were escaped only at some call sites. Remarks and e-mails, which a node chooses for the inbounds it reports, reached the scheduled report and the client info unescaped and turned into markup, and the reports that escaped their whole text instead showed the templates' own tags. I18nBot now escapes every parameter value, the call sites drop their own escaping and the whole-report escapes, the two values concatenated outside a template are escaped where they are written, and callback answers, which are plain text, are unescaped again.
175 lines
5.3 KiB
Go
175 lines
5.3 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
|
"github.com/mymmrac/telego"
|
|
tu "github.com/mymmrac/telego/telegoutil"
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
)
|
|
|
|
func TestIsTelegramNotModifiedError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{"nil error", nil, false},
|
|
{"not modified", errors.New("Bad Request: message is not modified"), true},
|
|
{"No fields to modify", errors.New("Bad Request: No fields to modify"), true},
|
|
{"unrelated error", errors.New("Bad Request: message to edit not found"), false},
|
|
{"network error", errors.New("connection reset"), false},
|
|
{"empty string", errors.New(""), false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := isTelegramNotModifiedError(tt.err)
|
|
if got != tt.want {
|
|
t.Errorf("isTelegramNotModifiedError(%v) = %v, want %v", tt.err, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// telego.NewBot rejects any token that does not match `^\d+:[\w-]{35}$`.
|
|
var testBotToken = "123456:" + strings.Repeat("a", 35)
|
|
|
|
func TestEditMessageTgBotSkipsNotModified(t *testing.T) {
|
|
// Mock Telegram API that always returns "message is not modified".
|
|
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"ok": false,
|
|
"error_code": 400,
|
|
"description": "Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message.",
|
|
})
|
|
}))
|
|
defer mock.Close()
|
|
|
|
// Point the package-level bot at the mock.
|
|
origBot := liveBot()
|
|
t.Cleanup(func() { setTestBot(origBot) })
|
|
var err error
|
|
bot, err = telego.NewBot(testBotToken, telego.WithAPIServer(mock.URL))
|
|
if err != nil {
|
|
t.Fatalf("NewBot: %v", err)
|
|
}
|
|
|
|
// Snapshot warning count before the edit call.
|
|
before := logger.GetLogs(100, "warning")
|
|
|
|
tb := &Tgbot{}
|
|
tb.editMessageTgBot(123, 456, "<b>hello</b>")
|
|
|
|
after := logger.GetLogs(100, "warning")
|
|
if len(after) > len(before) {
|
|
t.Errorf("editMessageTgBot logged %d new warnings, want 0; new entries: %v",
|
|
len(after)-len(before), after[len(before):])
|
|
}
|
|
}
|
|
|
|
func TestEditMessageCallbackTgBotSkipsNotModified(t *testing.T) {
|
|
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"ok": false,
|
|
"error_code": 400,
|
|
"description": "Bad Request: message is not modified",
|
|
})
|
|
}))
|
|
defer mock.Close()
|
|
|
|
origBot := liveBot()
|
|
t.Cleanup(func() { setTestBot(origBot) })
|
|
var err error
|
|
bot, err = telego.NewBot(testBotToken, telego.WithAPIServer(mock.URL))
|
|
if err != nil {
|
|
t.Fatalf("NewBot: %v", err)
|
|
}
|
|
|
|
before := logger.GetLogs(100, "warning")
|
|
|
|
tb := &Tgbot{}
|
|
kb := tu.InlineKeyboard(tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton("btn").WithCallbackData("test"),
|
|
))
|
|
tb.editMessageCallbackTgBot(123, 456, kb)
|
|
|
|
after := logger.GetLogs(100, "warning")
|
|
if len(after) > len(before) {
|
|
t.Errorf("editMessageCallbackTgBot logged %d new warnings, want 0; new entries: %v",
|
|
len(after)-len(before), after[len(before):])
|
|
}
|
|
}
|
|
|
|
func TestPageMessageSplitsLinkListWithoutBlankLines(t *testing.T) {
|
|
var message strings.Builder
|
|
message.WriteString("Individual links:\r\n")
|
|
for range 50 {
|
|
message.WriteString("<code>vless://" + strings.Repeat("a", 300) + "</code>\r\n")
|
|
}
|
|
|
|
pages := pageMessage(message.String(), telegramPageLimit)
|
|
if len(pages) < 2 {
|
|
t.Fatalf("pageMessage() returned %d page, want multiple", len(pages))
|
|
}
|
|
|
|
links := 0
|
|
for index, page := range pages {
|
|
if len(page) > telegramPageLimit {
|
|
t.Errorf("page %d has %d bytes, want at most %d", index, len(page), telegramPageLimit)
|
|
}
|
|
openingTags := strings.Count(page, "<code>")
|
|
closingTags := strings.Count(page, "</code>")
|
|
if openingTags != closingTags {
|
|
t.Errorf("page %d has %d opening tags and %d closing tags", index, openingTags, closingTags)
|
|
}
|
|
links += openingTags
|
|
}
|
|
if links != 50 {
|
|
t.Errorf("pages contain %d links, want 50", links)
|
|
}
|
|
}
|
|
|
|
// Regression test: callback answers are shown as plain text, so the entities
|
|
// I18nBot escapes for HTML-parsed messages must not surface in them.
|
|
func TestCallbackAnswerIsPlainText(t *testing.T) {
|
|
var mu sync.Mutex
|
|
var texts []string
|
|
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/bot"+testBotToken+"/answerCallbackQuery" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
var body struct {
|
|
Text string `json:"text"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
mu.Lock()
|
|
texts = append(texts, body.Text)
|
|
mu.Unlock()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": true})
|
|
}))
|
|
defer mock.Close()
|
|
swapTestBot(t, mock.URL)
|
|
draftLocalizer(t, &i18n.Message{ID: "tgbot.answers.canceled", Other: "❌ {{ .Email }}: Operation canceled."})
|
|
|
|
tb := &Tgbot{}
|
|
tb.sendCallbackAnswerTgBot("q1", tb.I18nBot("tgbot.answers.canceled", "Email==a&b<c>"))
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(texts) != 1 || texts[0] != "❌ a&b<c>: Operation canceled." {
|
|
t.Errorf("answerCallbackQuery texts = %q, want the e-mail shown verbatim", texts)
|
|
}
|
|
}
|