tgbot_client_draft_per_chat_test.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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/dbtest"
  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. dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))
  80. const (
  81. chatA = int64(7101)
  82. chatB = int64(7202)
  83. )
  84. url, textsFor := draftTexts(t)
  85. swapTestBot(t, url)
  86. origRunning := isRunning
  87. t.Cleanup(func() { isRunning = origRunning })
  88. isRunning = true
  89. callback := func(chatID int64, data string) {
  90. t.Helper()
  91. (&Tgbot{}).answerCallback(&telego.CallbackQuery{
  92. ID: "q1",
  93. From: telego.User{ID: 1},
  94. Data: data,
  95. Message: &telego.Message{MessageID: 7, Chat: telego.Chat{ID: chatID}},
  96. }, true)
  97. }
  98. // Both admins start a client; each card carries the email the wizard just
  99. // generated for that chat.
  100. callback(chatA, "add_client_to 1")
  101. callback(chatB, "add_client_to 2")
  102. emailA := cardEmail(t, lastDraftCard(t, textsFor(chatA)))
  103. emailB := cardEmail(t, lastDraftCard(t, textsFor(chatB)))
  104. if emailA == "" || emailA == emailB {
  105. t.Fatalf("drafts start with the same email %q, want one per chat", emailA)
  106. }
  107. // Chat A renders its card again, with chat B's wizard already past its start.
  108. callback(chatA, "add_client_default_traffic_exp")
  109. if got := cardEmail(t, lastDraftCard(t, textsFor(chatA))); got != emailA {
  110. t.Errorf("chat A's card shows email %q, want its own %q from chat B's draft", got, emailA)
  111. }
  112. if got := cardEmail(t, lastDraftCard(t, textsFor(chatB))); got != emailB {
  113. t.Errorf("chat B's card shows email %q, want %q", got, emailB)
  114. }
  115. }
  116. // Regression test: the draft's lock and map were reached before the admin gate, so
  117. // a report tap queued behind a wizard and any chat a tap came from got stored.
  118. func TestNonWizardCallbackTakesNoDraftLock(t *testing.T) {
  119. const (
  120. heldChat = int64(7303)
  121. spareChat = int64(7404)
  122. )
  123. decliningServer(t)
  124. held := addClientDrafts.forActor(chatUser{chatID: heldChat, userID: 1})
  125. held.Lock()
  126. defer held.Unlock()
  127. tap := func(chatID int64, isAdmin bool, data string) {
  128. (&Tgbot{}).answerCallback(&telego.CallbackQuery{
  129. ID: "q1",
  130. From: telego.User{ID: 1},
  131. Data: data,
  132. Message: &telego.Message{Chat: telego.Chat{ID: chatID}},
  133. }, isAdmin)
  134. }
  135. returns := func(what string, tap func()) {
  136. t.Helper()
  137. done := make(chan struct{})
  138. go func() {
  139. defer close(done)
  140. tap()
  141. }()
  142. select {
  143. case <-done:
  144. case <-time.After(2 * time.Second):
  145. t.Fatalf("%s waited on the draft lock it never reads", what)
  146. }
  147. }
  148. returns("an admin report tap", func() { tap(heldChat, true, "no_such_admin_action 5") })
  149. returns("a non-admin wizard tap", func() { tap(heldChat, false, "add_client_to 1") })
  150. tap(spareChat, false, "add_client_to 1")
  151. addClientDrafts.mu.Lock()
  152. _, stored := addClientDrafts.drafts[chatUser{chatID: spareChat, userID: 1}]
  153. addClientDrafts.mu.Unlock()
  154. if stored {
  155. t.Errorf("draft stored for chat %d, want none until its wizard starts", spareChat)
  156. }
  157. }