tgbot_client_draft_per_chat_test.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. package tgbot
  2. import (
  3. "encoding/json"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. "testing"
  11. "time"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database"
  13. "github.com/mymmrac/telego"
  14. )
  15. // draftTexts serves the methods the add-client wizard touches and records the
  16. // text of every sendMessage and editMessageText per chat.
  17. func draftTexts(t *testing.T) (string, func(int64) []string) {
  18. t.Helper()
  19. var mu sync.Mutex
  20. texts := map[int64][]string{}
  21. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  22. body, _ := io.ReadAll(r.Body)
  23. result := any(true)
  24. if r.URL.Path == "/bot"+testBotToken+"/sendMessage" || r.URL.Path == "/bot"+testBotToken+"/editMessageText" {
  25. var payload struct {
  26. ChatID any `json:"chat_id"`
  27. Text string `json:"text"`
  28. }
  29. _ = json.Unmarshal(body, &payload)
  30. chatID := int64(0)
  31. switch v := payload.ChatID.(type) {
  32. case float64:
  33. chatID = int64(v)
  34. }
  35. mu.Lock()
  36. texts[chatID] = append(texts[chatID], payload.Text)
  37. mu.Unlock()
  38. result = map[string]any{"message_id": 1, "date": 0, "chat": map[string]any{"id": chatID, "type": "private"}}
  39. }
  40. w.Header().Set("Content-Type", "application/json")
  41. _ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "result": result})
  42. }))
  43. t.Cleanup(srv.Close)
  44. return srv.URL, func(chatID int64) []string {
  45. mu.Lock()
  46. defer mu.Unlock()
  47. return append([]string(nil), texts[chatID]...)
  48. }
  49. }
  50. // cardEmail reads the email off a rendered draft card, which is the field the
  51. // wizard assigns when the flow starts.
  52. func cardEmail(t *testing.T, card string) string {
  53. t.Helper()
  54. const marker = "Email: <code>"
  55. start := strings.Index(card, marker)
  56. if start < 0 {
  57. t.Fatalf("not a draft card: %q", card)
  58. }
  59. rest := card[start+len(marker):]
  60. end := strings.Index(rest, "</code>")
  61. if end < 0 {
  62. t.Fatalf("card has an unterminated email: %q", card)
  63. }
  64. return rest[:end]
  65. }
  66. func lastDraftCard(t *testing.T, texts []string) string {
  67. t.Helper()
  68. for i := len(texts) - 1; i >= 0; i-- {
  69. if strings.Contains(texts[i], "Email: <code>") {
  70. return texts[i]
  71. }
  72. }
  73. t.Fatal("no draft card reached the chat")
  74. return ""
  75. }
  76. // Regression test: one package-level draft per bot meant an admin's new client
  77. // was filled in by another chat's steps.
  78. func TestAddClientDraftIsPerChat(t *testing.T) {
  79. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  80. t.Fatalf("InitDB: %v", err)
  81. }
  82. t.Cleanup(func() { _ = database.CloseDB() })
  83. const (
  84. chatA = int64(7101)
  85. chatB = int64(7202)
  86. )
  87. url, textsFor := draftTexts(t)
  88. swapTestBot(t, url)
  89. origRunning := isRunning
  90. t.Cleanup(func() { isRunning = origRunning })
  91. isRunning = true
  92. callback := func(chatID int64, data string) {
  93. t.Helper()
  94. (&Tgbot{}).answerCallback(&telego.CallbackQuery{
  95. ID: "q1",
  96. From: telego.User{ID: 1},
  97. Data: data,
  98. Message: &telego.Message{MessageID: 7, Chat: telego.Chat{ID: chatID}},
  99. }, true)
  100. }
  101. // Both admins start a client; each card carries the email the wizard just
  102. // generated for that chat.
  103. callback(chatA, "add_client_to 1")
  104. callback(chatB, "add_client_to 2")
  105. emailA := cardEmail(t, lastDraftCard(t, textsFor(chatA)))
  106. emailB := cardEmail(t, lastDraftCard(t, textsFor(chatB)))
  107. if emailA == "" || emailA == emailB {
  108. t.Fatalf("drafts start with the same email %q, want one per chat", emailA)
  109. }
  110. // Chat A renders its card again, with chat B's wizard already past its start.
  111. callback(chatA, "add_client_default_traffic_exp")
  112. if got := cardEmail(t, lastDraftCard(t, textsFor(chatA))); got != emailA {
  113. t.Errorf("chat A's card shows email %q, want its own %q from chat B's draft", got, emailA)
  114. }
  115. if got := cardEmail(t, lastDraftCard(t, textsFor(chatB))); got != emailB {
  116. t.Errorf("chat B's card shows email %q, want %q", got, emailB)
  117. }
  118. }
  119. // Regression test: the draft's lock and map were reached before the admin gate, so
  120. // a report tap queued behind a wizard and any chat a tap came from got stored.
  121. func TestNonWizardCallbackTakesNoDraftLock(t *testing.T) {
  122. const (
  123. heldChat = int64(7303)
  124. spareChat = int64(7404)
  125. )
  126. decliningServer(t)
  127. held := addClientDrafts.forChat(heldChat)
  128. held.Lock()
  129. defer held.Unlock()
  130. tap := func(chatID int64, isAdmin bool, data string) {
  131. (&Tgbot{}).answerCallback(&telego.CallbackQuery{
  132. ID: "q1",
  133. From: telego.User{ID: 1},
  134. Data: data,
  135. Message: &telego.Message{Chat: telego.Chat{ID: chatID}},
  136. }, isAdmin)
  137. }
  138. returns := func(what string, tap func()) {
  139. t.Helper()
  140. done := make(chan struct{})
  141. go func() {
  142. defer close(done)
  143. tap()
  144. }()
  145. select {
  146. case <-done:
  147. case <-time.After(2 * time.Second):
  148. t.Fatalf("%s waited on the draft lock it never reads", what)
  149. }
  150. }
  151. returns("an admin report tap", func() { tap(heldChat, true, "no_such_admin_action 5") })
  152. returns("a non-admin wizard tap", func() { tap(heldChat, false, "add_client_to 1") })
  153. tap(spareChat, false, "add_client_to 1")
  154. addClientDrafts.mu.Lock()
  155. _, stored := addClientDrafts.drafts[spareChat]
  156. addClientDrafts.mu.Unlock()
  157. if stored {
  158. t.Errorf("draft stored for chat %d, want none until its wizard starts", spareChat)
  159. }
  160. }