Files
3x-ui-agent/internal/web/service/setting_clash_test.go
T
Muse ceca96571d P9(5): restore upstream Telegram bot (settings, backup, notifications)
Per user decision 2026-10-21 (reverses the 2026-10-20 drop-Telegram
call): port the upstream 3x-ui v3.7.0-generation Telegram bot wholesale
instead of rewriting a minimal set, adapted to this fork.

- new internal/web/service/tgbot package (telego): bot lifecycle,
  commands, reports, callback router, send helpers; token validation via
  telego; custom API server URL passes the fork's SSRF sanitizer
  (loopback/private bases fall back to the default endpoint, logged)
- settings: full upstream key set (tgBotEnable/Token/Proxy/APIServer/
  ChatId, tgRunTime, tgBotBackup, tgCpu/tgMemory, tgLang,
  tgEnabledEvents) with getters/setters; settings page gains a Telegram
  tab + sidebar entry; token shown masked, clearTgBotToken honored
- wiring (web.go): stats-notify cron registered from tgRunTime when the
  bot is enabled; CheckHashStorage job; CPU/memory alarm jobs gated on
  tgEnabledEvents + thresholds; login notifications via event bus;
  POST /panel/api/setting/testTgBot and /panel/api/backuptotgbot
- lite adaptation: StatsNotifyJob no longer gates on a local Xray core
  (this panel never runs one) so the periodic report and the scheduled
  DB backup (sendDocument) can actually fire
- translations: upstream TG keys restored across all locales

End-to-end smoke (real binary + fake Telegram API over HTTPS at a
public-looking address, browser form wire format): bot starts on
settings change (getMe), testTgBot delivers, panel restart re-arms the
bot, tgRunTime='@every 1m' cron really fires sendDocument carrying the
panel SQLite DB (magic verified) plus the report sendMessage, login
notification delivered, dead-endpoint failure is logged not silent.
13/13 PASS — see P9-REPORT.md.
2026-10-21 17:16:32 +00:00

200 lines
6.1 KiB
Go

package service
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestValidateRegex(t *testing.T) {
for _, pattern := range []string{"", `(?i)^jsonclient([ /]|$)`, `(?m)^general-purpose$`} {
if err := ValidateRegex(pattern); err != nil {
t.Errorf("ValidateRegex(%q) returned %v", pattern, err)
}
}
for _, pattern := range []string{"[", strings.Repeat("a", 2049)} {
if err := ValidateRegex(pattern); err == nil {
t.Errorf("ValidateRegex(%q) accepted an invalid pattern", pattern)
}
}
}
func TestSubscriptionAutoDetectDefaultsWithoutStoredRows(t *testing.T) {
setupSettingTestDB(t)
keys := []string{"subClashAutoDetect", "subClashUserAgentRegex", "subJsonAutoDetect", "subJsonAlwaysArray", "subJsonUserAgentRegex"}
if err := database.GetDB().Where("key IN ?", keys).Delete(&model.Setting{}).Error; err != nil {
t.Fatal(err)
}
s := &SettingService{}
clashEnabled, err := s.GetSubClashAutoDetect()
if err != nil {
t.Fatal(err)
}
jsonEnabled, err := s.GetSubJsonAutoDetect()
if err != nil {
t.Fatal(err)
}
jsonAlwaysArray, err := s.GetSubJsonAlwaysArray()
if err != nil {
t.Fatal(err)
}
clashRegex, err := s.GetSubClashUserAgentRegex()
if err != nil {
t.Fatal(err)
}
jsonRegex, err := s.GetSubJsonUserAgentRegex()
if err != nil {
t.Fatal(err)
}
if clashEnabled || jsonEnabled || jsonAlwaysArray {
t.Fatalf("missing subscription flags must default off: clashAuto=%v jsonAuto=%v jsonAlwaysArray=%v", clashEnabled, jsonEnabled, jsonAlwaysArray)
}
if clashRegex != "" {
t.Fatalf("missing Clash regex = %q, want empty inherited value", clashRegex)
}
if jsonRegex != "" {
t.Fatalf("missing JSON regex = %q, want empty inherited value", jsonRegex)
}
var count int64
if err := database.GetDB().Model(&model.Setting{}).Where("key IN ?", keys).Count(&count).Error; err != nil {
t.Fatal(err)
}
if count != 0 {
t.Fatalf("default lookup unexpectedly persisted %d setting rows", count)
}
}
func TestUpdateAllSettingPreservesEmptyUserAgentRegexes(t *testing.T) {
setupSettingTestDB(t)
s := &SettingService{}
settings, err := s.GetAllSetting()
if err != nil {
t.Fatal(err)
}
settings.SubJsonUserAgentRegex = " "
settings.SubClashUserAgentRegex = ""
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
t.Fatal(err)
}
for _, key := range []string{"subJsonUserAgentRegex", "subClashUserAgentRegex"} {
var stored model.Setting
if err := database.GetDB().Where("key = ?", key).First(&stored).Error; err != nil {
t.Fatal(err)
}
if stored.Value != "" {
t.Fatalf("%s stored value = %q, want empty inherited value", key, stored.Value)
}
}
}
func TestUpdateAllSettingPersistsClashSubscriptionSettings(t *testing.T) {
setupSettingTestDB(t)
s := &SettingService{}
settings, err := s.GetAllSetting()
if err != nil {
t.Fatal(err)
}
if settings.SubClashAutoDetect {
t.Fatal("subClashAutoDetect default = true, want false")
}
if settings.SubJsonAutoDetect {
t.Fatal("subJsonAutoDetect default = true, want false")
}
if settings.SubJsonAlwaysArray {
t.Fatal("subJsonAlwaysArray default = true, want false")
}
if settings.SubJsonUserAgentRegex != "" {
t.Fatalf("subJsonUserAgentRegex = %q, want empty inherited value", settings.SubJsonUserAgentRegex)
}
if settings.SubClashUserAgentRegex != "" {
t.Fatalf("subClashUserAgentRegex = %q, want empty inherited value", settings.SubClashUserAgentRegex)
}
settings.SubClashAutoDetect = true
settings.SubClashUserAgentRegex = `(?i)^custom-clash/`
settings.SubJsonAutoDetect = true
settings.SubJsonAlwaysArray = true
settings.SubJsonUserAgentRegex = `(?i)^custom-json/`
settings.SubJsonEnable = true
settings.SubJsonPath = "/json-custom/"
settings.SubJsonURI = "https://subscriptions.example.com/json-custom/"
settings.SubClashEnable = true
settings.SubClashPath = "/clash-custom/"
settings.SubClashURI = "https://subscriptions.example.com/clash-custom/"
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
t.Fatal(err)
}
got, err := s.GetAllSetting()
if err != nil {
t.Fatal(err)
}
if !got.SubClashEnable {
t.Fatal("subClashEnable = false, want true")
}
if !got.SubClashAutoDetect {
t.Fatal("subClashAutoDetect = false, want true")
}
if !got.SubJsonAutoDetect {
t.Fatal("subJsonAutoDetect = false, want true")
}
if !got.SubJsonAlwaysArray {
t.Fatal("subJsonAlwaysArray = false, want true")
}
if !got.SubJsonEnable {
t.Fatal("subJsonEnable = false, want true")
}
if got.SubJsonPath != "/json-custom/" {
t.Fatalf("subJsonPath = %q, want %q", got.SubJsonPath, "/json-custom/")
}
if got.SubJsonURI != "https://subscriptions.example.com/json-custom/" {
t.Fatalf("subJsonURI = %q, want %q", got.SubJsonURI, "https://subscriptions.example.com/json-custom/")
}
if got.SubJsonUserAgentRegex != `(?i)^custom-json/` {
t.Fatalf("subJsonUserAgentRegex = %q, want %q", got.SubJsonUserAgentRegex, `(?i)^custom-json/`)
}
if got.SubClashUserAgentRegex != `(?i)^custom-clash/` {
t.Fatalf("subClashUserAgentRegex = %q, want %q", got.SubClashUserAgentRegex, `(?i)^custom-clash/`)
}
if got.SubClashPath != "/clash-custom/" {
t.Fatalf("subClashPath = %q, want %q", got.SubClashPath, "/clash-custom/")
}
if got.SubClashURI != "https://subscriptions.example.com/clash-custom/" {
t.Fatalf("subClashURI = %q, want %q", got.SubClashURI, "https://subscriptions.example.com/clash-custom/")
}
}
func TestUpdateAllSettingRejectsInvalidClashUserAgentRegex(t *testing.T) {
setupSettingTestDB(t)
s := &SettingService{}
settings, err := s.GetAllSetting()
if err != nil {
t.Fatal(err)
}
settings.SubClashUserAgentRegex = "["
if err := s.UpdateAllSetting(settings, SecretClears{}); err == nil {
t.Fatal("UpdateAllSetting accepted an invalid Clash/Mihomo User-Agent regex")
}
}
func TestUpdateAllSettingRejectsInvalidJsonUserAgentRegex(t *testing.T) {
setupSettingTestDB(t)
s := &SettingService{}
settings, err := s.GetAllSetting()
if err != nil {
t.Fatal(err)
}
settings.SubJsonUserAgentRegex = "["
if err := s.UpdateAllSetting(settings, SecretClears{}); err == nil {
t.Fatal("UpdateAllSetting accepted an invalid Xray JSON User-Agent regex")
}
}