tgbot_stale_button_test.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. package tgbot
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "path/filepath"
  7. "sync"
  8. "testing"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mymmrac/telego"
  12. )
  13. // staleButtonServer serves canned Telegram API responses so tests can drive
  14. // bot-dependent paths; the returned func reports per-method call counts.
  15. func staleButtonServer(t *testing.T, responses map[string]any) (*httptest.Server, func(string) int) {
  16. t.Helper()
  17. var mu sync.Mutex
  18. counts := map[string]int{}
  19. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  20. for method, body := range responses {
  21. if r.URL.Path == "/bot"+testBotToken+"/"+method {
  22. mu.Lock()
  23. counts[method]++
  24. mu.Unlock()
  25. w.Header().Set("Content-Type", "application/json")
  26. json.NewEncoder(w).Encode(body)
  27. return
  28. }
  29. }
  30. w.WriteHeader(http.StatusNotFound)
  31. }))
  32. return srv, func(method string) int {
  33. mu.Lock()
  34. defer mu.Unlock()
  35. return counts[method]
  36. }
  37. }
  38. func swapTestBot(t *testing.T, url string) {
  39. t.Helper()
  40. origBot := bot
  41. origPool := messageWorkerPool
  42. t.Cleanup(func() {
  43. bot = origBot
  44. messageWorkerPool = origPool
  45. })
  46. var err error
  47. bot, err = telego.NewBot(testBotToken, telego.WithAPIServer(url))
  48. if err != nil {
  49. t.Fatalf("NewBot: %v", err)
  50. }
  51. messageWorkerPool = make(chan struct{}, 10)
  52. }
  53. func newStaleButtonTgbot(t *testing.T) *Tgbot {
  54. t.Helper()
  55. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  56. t.Fatalf("InitDB: %v", err)
  57. }
  58. t.Cleanup(func() { _ = database.CloseDB() })
  59. return &Tgbot{}
  60. }
  61. // Regression test: a stale get_clients_for_* tap on a deleted inbound must
  62. // answer an error, not panic on inbound.Remark; removing the guard fails here.
  63. func TestChooseInboundClientStaleInbound(t *testing.T) {
  64. mock, calls := staleButtonServer(t, map[string]any{
  65. "answerCallbackQuery": map[string]any{"ok": true, "result": true},
  66. })
  67. swapTestBot(t, mock.URL)
  68. defer mock.Close()
  69. tb := newStaleButtonTgbot(t)
  70. q := &telego.CallbackQuery{
  71. ID: "q1",
  72. From: telego.User{ID: 999999},
  73. Message: &telego.Message{Chat: telego.Chat{ID: 1}},
  74. }
  75. tb.chooseInboundClient(q, 1, 42, "client_sub_links")
  76. if n := calls("answerCallbackQuery"); n != 1 {
  77. t.Errorf("answerCallbackQuery calls = %d, want 1: a stale tap must be answered with an error", n)
  78. }
  79. }
  80. // The keyboard builder must consume the caller's inbound row; a second DB read
  81. // reintroduces the stale-row window the guard closed.
  82. func TestGetInboundClientsForUsesProvidedInbound(t *testing.T) {
  83. mock, _ := staleButtonServer(t, map[string]any{
  84. "answerCallbackQuery": map[string]any{"ok": true, "result": true},
  85. })
  86. swapTestBot(t, mock.URL)
  87. defer mock.Close()
  88. tb := newStaleButtonTgbot(t)
  89. inbound := &model.Inbound{Id: 7, Remark: "in-7", Settings: `{"clients":[{"email":"[email protected]"}]}`}
  90. kb, err := tb.getInboundClientsFor(inbound, "client_sub_links")
  91. if err != nil {
  92. t.Fatalf("getInboundClientsFor: %v", err)
  93. }
  94. if kb == nil || len(kb.InlineKeyboard) == 0 || len(kb.InlineKeyboard[0]) == 0 {
  95. t.Fatalf("getInboundClientsFor returned no keyboard")
  96. }
  97. if email := kb.InlineKeyboard[0][0].Text; email != "[email protected]" {
  98. t.Errorf("keyboard button text = %q, want %q", email, "[email protected]")
  99. }
  100. }
  101. // A panicking handler must be contained by runBotHandler; without the
  102. // recover() the panic escapes and fails this test.
  103. func TestRunBotHandlerRecoversPanic(t *testing.T) {
  104. origPool := messageWorkerPool
  105. t.Cleanup(func() { messageWorkerPool = origPool })
  106. messageWorkerPool = make(chan struct{}, 10)
  107. ran := false
  108. func() {
  109. defer func() {
  110. if r := recover(); r != nil {
  111. t.Fatalf("panic escaped runBotHandler: %v", r)
  112. }
  113. }()
  114. runBotHandler(func() {
  115. ran = true
  116. panic("boom")
  117. })
  118. }()
  119. if !ran {
  120. t.Errorf("handler body did not run")
  121. }
  122. }