tgbot_report_batching_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package tgbot
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "testing"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database"
  13. "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  15. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  16. "github.com/mymmrac/telego"
  17. )
  18. // seedReportClients writes one inbound holding every email plus the traffic row
  19. // each of them needs to appear in the sorted usage report.
  20. func seedReportClients(t *testing.T, remark string, emails []string) {
  21. t.Helper()
  22. settings := make([]string, 0, len(emails))
  23. for _, email := range emails {
  24. settings = append(settings, fmt.Sprintf(`{"email":%q,"subId":"sub-%s"}`, email, email))
  25. }
  26. inbound := &model.Inbound{
  27. UserId: 1,
  28. Remark: remark,
  29. Port: 8443,
  30. Protocol: model.VLESS,
  31. Enable: true,
  32. Settings: `{"clients":[` + strings.Join(settings, ",") + `]}`,
  33. }
  34. if err := database.GetDB().Create(inbound).Error; err != nil {
  35. t.Fatalf("seed inbound: %v", err)
  36. }
  37. for _, email := range emails {
  38. if err := database.GetDB().Create(&xray.ClientTraffic{
  39. InboundId: inbound.Id,
  40. Email: email,
  41. Enable: true,
  42. Up: 1,
  43. Down: 1,
  44. }).Error; err != nil {
  45. t.Fatalf("seed traffic for %s: %v", email, err)
  46. }
  47. record := (&model.Client{Email: email, Enable: true, SubID: "sub-" + email}).ToRecord()
  48. if err := database.GetDB().Create(record).Error; err != nil {
  49. t.Fatalf("seed client %s: %v", email, err)
  50. }
  51. if err := database.GetDB().Create(&model.ClientInbound{ClientId: record.Id, InboundId: inbound.Id}).Error; err != nil {
  52. t.Fatalf("seed client_inbounds for %s: %v", email, err)
  53. }
  54. }
  55. }
  56. func initReportDB(t *testing.T) *Tgbot {
  57. t.Helper()
  58. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  59. origRunning := isRunning
  60. t.Cleanup(func() { isRunning = origRunning })
  61. isRunning = true
  62. return &Tgbot{}
  63. }
  64. type sentMessage struct {
  65. Text string `json:"text"`
  66. ReplyMarkup json.RawMessage `json:"reply_markup"`
  67. }
  68. // captureReportServer records every sendMessage call so a test can assert on
  69. // what Telegram would have received, not merely how many calls were made.
  70. func captureReportServer(t *testing.T) (*httptest.Server, func() []sentMessage) {
  71. t.Helper()
  72. var mu sync.Mutex
  73. var sent []sentMessage
  74. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  75. body, _ := io.ReadAll(r.Body)
  76. result := any(true)
  77. if r.URL.Path == "/bot"+testBotToken+"/sendMessage" {
  78. var payload sentMessage
  79. _ = json.Unmarshal(body, &payload)
  80. mu.Lock()
  81. sent = append(sent, payload)
  82. mu.Unlock()
  83. result = map[string]any{"message_id": 1, "date": 0, "chat": map[string]any{"id": 1, "type": "private"}}
  84. }
  85. w.Header().Set("Content-Type", "application/json")
  86. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": result})
  87. }))
  88. return srv, func() []sentMessage {
  89. mu.Lock()
  90. defer mu.Unlock()
  91. return append([]sentMessage(nil), sent...)
  92. }
  93. }
  94. // Regression test: the sorted usage report must reach Telegram as one message
  95. // whatever the client count; per-client sends burst past the rate limit.
  96. func TestTrafficUsageReportIsOneMessage(t *testing.T) {
  97. mock, calls := staleButtonServer(t, map[string]any{
  98. "sendMessage": map[string]any{"ok": true, "result": map[string]any{
  99. "message_id": 1,
  100. "date": 0,
  101. "chat": map[string]any{"id": 1, "type": "private"},
  102. }},
  103. "deleteMessage": map[string]any{"ok": true, "result": true},
  104. })
  105. swapTestBot(t, mock.URL)
  106. defer mock.Close()
  107. tb := initReportDB(t)
  108. seedReportClients(t, "report", []string{"a@x", "b@x", "c@x"})
  109. tb.answerCallback(&telego.CallbackQuery{
  110. ID: "q1",
  111. From: telego.User{ID: 1},
  112. Data: "get_sorted_traffic_usage_report",
  113. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  114. }, true) // admin
  115. if n := calls("sendMessage"); n != 1 {
  116. t.Errorf("sendMessage calls = %d, want 1: one report per tap, not one per client", n)
  117. }
  118. }
  119. // Regression test: batching must not swallow the reply on a panel with no
  120. // clients, where the old code still answered FinishProcess.
  121. func TestResetAllTrafficsAnswersWithNoClients(t *testing.T) {
  122. mock, sent := captureReportServer(t)
  123. swapTestBot(t, mock.URL)
  124. defer mock.Close()
  125. tb := initReportDB(t)
  126. tb.answerCallback(&telego.CallbackQuery{
  127. ID: "q1",
  128. From: telego.User{ID: 1},
  129. Data: "reset_all_traffics_c",
  130. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  131. }, true) // admin
  132. got := sent()
  133. if len(got) != 1 {
  134. t.Fatalf("sendMessage calls = %d, want 1: an empty panel must still answer the tap", len(got))
  135. }
  136. if got[0].Text == "" {
  137. t.Error("reset report text is empty, want the finish-process message")
  138. }
  139. if !strings.Contains(string(got[0].ReplyMarkup), `"remove_keyboard":true`) {
  140. t.Errorf("reply_markup = %s, want the reply keyboard removed", got[0].ReplyMarkup)
  141. }
  142. }
  143. // Regression test: the report leaves as one HTML-parsed message, so a remark
  144. // holding "<" must reach Telegram escaped instead of dropping the whole page.
  145. func TestTrafficUsageReportEscapesHtml(t *testing.T) {
  146. mock, sent := captureReportServer(t)
  147. swapTestBot(t, mock.URL)
  148. defer mock.Close()
  149. tb := initReportDB(t)
  150. seedReportClients(t, "DE <fast>", []string{"a@x"})
  151. tb.answerCallback(&telego.CallbackQuery{
  152. ID: "q1",
  153. From: telego.User{ID: 1},
  154. Data: "get_sorted_traffic_usage_report",
  155. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  156. }, true) // admin
  157. got := sent()
  158. if len(got) != 1 {
  159. t.Fatalf("sendMessage calls = %d, want 1", len(got))
  160. }
  161. if strings.Contains(got[0].Text, "<fast>") {
  162. t.Errorf("report text = %q, want the remark escaped", got[0].Text)
  163. }
  164. if !strings.Contains(got[0].Text, "&lt;fast&gt;") {
  165. t.Errorf("report text = %q, want the remark escaped as &lt;fast&gt;", got[0].Text)
  166. }
  167. }