mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 14:55:02 +03:00
a403faee23
Admins who run the bot in a forum supergroup got every notification in the General topic. An admin chat ID may now carry a topic, chatId:topicId, and the bot's admin messages and reports go to that topic. Repeated chats collapse into one entry and a second, different topic for the same chat is rejected, in the panel and in the docs setup helper alike. The chat ID hint in every locale and the English, Persian, Russian and Chinese guides describe the form.
225 lines
6.5 KiB
Go
225 lines
6.5 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"io"
|
|
"net"
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mymmrac/telego"
|
|
tu "github.com/mymmrac/telego/telegoutil"
|
|
)
|
|
|
|
func TestLoginAttemptDoesNotCarryPassword(t *testing.T) {
|
|
typ := reflect.TypeFor[LoginAttempt]()
|
|
if _, ok := typ.FieldByName("Password"); ok {
|
|
t.Fatal("LoginAttempt must not carry attempted passwords")
|
|
}
|
|
}
|
|
|
|
func TestIsSupportedBotProxyScheme(t *testing.T) {
|
|
supported := []string{
|
|
"socks5://127.0.0.1:1080",
|
|
"http://127.0.0.1:8080",
|
|
"https://127.0.0.1:8080",
|
|
}
|
|
for _, p := range supported {
|
|
if !isSupportedBotProxyScheme(p) {
|
|
t.Errorf("expected %q to be supported", p)
|
|
}
|
|
}
|
|
unsupported := []string{"", "ftp://x", "127.0.0.1:1080", "socks4://1.2.3.4:1080"}
|
|
for _, p := range unsupported {
|
|
if isSupportedBotProxyScheme(p) {
|
|
t.Errorf("expected %q to be unsupported", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func recordingDialTarget(t *testing.T, n int) (addr string, got chan []byte) {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got = make(chan []byte, 1)
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
go func() {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
buf := make([]byte, n)
|
|
m, _ := io.ReadFull(conn, buf)
|
|
got <- buf[:m]
|
|
}()
|
|
return ln.Addr().String(), got
|
|
}
|
|
|
|
func TestTgbotProxyDialerSelectsHTTPForHTTPScheme(t *testing.T) {
|
|
addr, got := recordingDialTarget(t, len("CONNECT "))
|
|
tg := &Tgbot{}
|
|
client := tg.createRobustFastHTTPClient("http://"+addr, nil)
|
|
if client.Dial == nil {
|
|
t.Fatal("Dial must be set for an http:// proxy")
|
|
}
|
|
go func() { _, _ = client.Dial("example.com:443") }()
|
|
select {
|
|
case b := <-got:
|
|
if string(b) != "CONNECT " {
|
|
t.Fatalf("expected HTTP CONNECT to the proxy, got %q", b)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("proxy never received a connection")
|
|
}
|
|
}
|
|
|
|
func TestTgbotProxyDialerSelectsSOCKSForSocks5Scheme(t *testing.T) {
|
|
addr, got := recordingDialTarget(t, 1)
|
|
tg := &Tgbot{}
|
|
client := tg.createRobustFastHTTPClient("socks5://"+addr, nil)
|
|
if client.Dial == nil {
|
|
t.Fatal("Dial must be set for a socks5:// proxy")
|
|
}
|
|
go func() { _, _ = client.Dial("example.com:443") }()
|
|
select {
|
|
case b := <-got:
|
|
if len(b) != 1 || b[0] != 0x05 {
|
|
t.Fatalf("expected SOCKS5 greeting (0x05), got %v", b)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("proxy never received a connection")
|
|
}
|
|
}
|
|
|
|
func TestTgbotPanelEgressBridgeAppearingAfterStartIsUsed(t *testing.T) {
|
|
addr, got := recordingDialTarget(t, 1)
|
|
bridge := ""
|
|
tg := &Tgbot{}
|
|
client := tg.createRobustFastHTTPClient("", func() string { return bridge })
|
|
bridge = "socks5://" + addr
|
|
go func() { _, _ = client.Dial("example.com:443") }()
|
|
select {
|
|
case b := <-got:
|
|
if len(b) != 1 || b[0] != 0x05 {
|
|
t.Fatalf("expected SOCKS5 greeting (0x05) on the late bridge, got %v", b)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("bridge that came up after bot start never received a connection")
|
|
}
|
|
}
|
|
|
|
func TestTgbotPanelEgressDialsDirectWithoutBridge(t *testing.T) {
|
|
addr, got := recordingDialTarget(t, 1)
|
|
tg := &Tgbot{}
|
|
client := tg.createRobustFastHTTPClient("", func() string { return "" })
|
|
conn, err := client.Dial(addr)
|
|
if err != nil {
|
|
t.Fatalf("direct dial: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
if _, err := conn.Write([]byte{0x42}); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
select {
|
|
case b := <-got:
|
|
if len(b) != 1 || b[0] != 0x42 {
|
|
t.Fatalf("expected the payload byte on a direct connection, got %v", b)
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("target never received the direct connection")
|
|
}
|
|
}
|
|
|
|
func TestIsCommandForBotAllowsUntargetedCommand(t *testing.T) {
|
|
if !isCommandForBot("/status", "panel_bot") {
|
|
t.Fatal("untargeted commands must remain accepted")
|
|
}
|
|
}
|
|
|
|
func TestIsCommandForBotAllowsMatchingUsername(t *testing.T) {
|
|
if !isCommandForBot("/status@panel_bot", "Panel_Bot") {
|
|
t.Fatal("commands targeted to this bot must be accepted")
|
|
}
|
|
}
|
|
|
|
func TestIsCommandForBotRejectsOtherUsername(t *testing.T) {
|
|
if isCommandForBot("/status@other_bot", "panel_bot") {
|
|
t.Fatal("commands targeted to another bot must be ignored")
|
|
}
|
|
}
|
|
|
|
func TestIsCommandForBotKeepsLegacyBehaviorWhenUsernameUnavailable(t *testing.T) {
|
|
if !isCommandForBot("/status@panel_bot", "") {
|
|
t.Fatal("commands must remain accepted when the current bot username is unavailable")
|
|
}
|
|
}
|
|
|
|
func TestParseAdminChats(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
ids []int64
|
|
topics map[int64]int
|
|
}{
|
|
{name: "empty", raw: "", ids: []int64{}, topics: map[int64]int{}},
|
|
{name: "plain ids", raw: "123,-1001", ids: []int64{123, -1001}, topics: map[int64]int{}},
|
|
{name: "topic", raw: "-1003855200872:42", ids: []int64{-1003855200872}, topics: map[int64]int{-1003855200872: 42}},
|
|
{name: "mixed with spaces", raw: " 123 , -1001 : 7 ", ids: []int64{123, -1001}, topics: map[int64]int{-1001: 7}},
|
|
{name: "general topic means no thread", raw: "-1001:1,123", ids: []int64{-1001, 123}, topics: map[int64]int{}},
|
|
{name: "duplicate chat collapses", raw: "-1001:7,-1001:7,-1001", ids: []int64{-1001}, topics: map[int64]int{-1001: 7}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ids, topics, err := parseAdminChats(tc.raw)
|
|
if err != nil {
|
|
t.Fatalf("parseAdminChats(%q) error: %v", tc.raw, err)
|
|
}
|
|
if !slices.Equal(ids, tc.ids) {
|
|
t.Fatalf("ids = %v, want %v", ids, tc.ids)
|
|
}
|
|
if len(topics) != len(tc.topics) {
|
|
t.Fatalf("topics = %v, want %v", topics, tc.topics)
|
|
}
|
|
for chat, topic := range tc.topics {
|
|
if topics[chat] != topic {
|
|
t.Fatalf("topics[%d] = %d, want %d", chat, topics[chat], topic)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseAdminChatsRejectsInvalid(t *testing.T) {
|
|
for _, raw := range []string{"abc", "123:", "123:abc", "123:0", "123:-5", "-1001:7,-1001:8", "123,"} {
|
|
if _, _, err := parseAdminChats(raw); err == nil {
|
|
t.Errorf("parseAdminChats(%q) accepted invalid input", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInAdminTopicRoutesConfiguredChat(t *testing.T) {
|
|
tgBotMutex.Lock()
|
|
orig := adminTopics
|
|
adminTopics = map[int64]int{-1001: 42}
|
|
tgBotMutex.Unlock()
|
|
t.Cleanup(func() {
|
|
tgBotMutex.Lock()
|
|
adminTopics = orig
|
|
tgBotMutex.Unlock()
|
|
})
|
|
|
|
doc := inAdminTopic(tu.Document(tu.ID(-1001), tu.FileFromBytes([]byte("x"), "x.db")))
|
|
if doc.MessageThreadID != 42 {
|
|
t.Fatalf("MessageThreadID = %d, want 42", doc.MessageThreadID)
|
|
}
|
|
doc = inAdminTopic(&telego.SendDocumentParams{ChatID: tu.ID(123)})
|
|
if doc.MessageThreadID != 0 {
|
|
t.Fatalf("MessageThreadID = %d for chat without topic, want 0", doc.MessageThreadID)
|
|
}
|
|
}
|