report_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package discord
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "testing"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  11. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  12. )
  13. type mockServerProvider struct {
  14. status *service.Status
  15. dbData []byte
  16. dbErr error
  17. filename string
  18. }
  19. func (m *mockServerProvider) GetStatus(lastStatus *service.Status) *service.Status {
  20. return m.status
  21. }
  22. func (m *mockServerProvider) GetDb() ([]byte, error) {
  23. return m.dbData, m.dbErr
  24. }
  25. func (m *mockServerProvider) BackupFilename(requestHost string) string {
  26. if m.filename != "" {
  27. return m.filename
  28. }
  29. return "x-ui_test.db"
  30. }
  31. type mockInboundProvider struct {
  32. inbounds []*model.Inbound
  33. err error
  34. }
  35. func (m *mockInboundProvider) GetAllInbounds() ([]*model.Inbound, error) {
  36. return m.inbounds, m.err
  37. }
  38. func TestBuildReport_NoBackup(t *testing.T) {
  39. settingService := setupTestDB(t)
  40. _ = settingService.SetDiscordBotToken("test-bot-token")
  41. _ = settingService.SetDiscordChannelId("12345")
  42. _ = settingService.SetDiscordBotBackup(false)
  43. _ = settingService.SetDiscordRunTime("@daily")
  44. mockStatus := &service.Status{
  45. Uptime: 172800,
  46. Loads: []float64{0.5, 0.4, 0.3},
  47. TcpCount: 15,
  48. UdpCount: 5,
  49. }
  50. mockStatus.Xray.State = service.Running
  51. mockStatus.Xray.Version = "25.1.0"
  52. mockServer := &mockServerProvider{
  53. status: mockStatus,
  54. dbData: []byte("sqlite-backup-bytes"),
  55. }
  56. mockInbound := &mockInboundProvider{
  57. inbounds: []*model.Inbound{
  58. {
  59. Id: 1,
  60. Remark: "VLESS-TCP",
  61. Enable: true,
  62. Port: 443,
  63. ClientStats: []xray.ClientTraffic{
  64. {Email: "[email protected]", Enable: true, Up: 100, Down: 200},
  65. {Email: "[email protected]", Enable: false},
  66. },
  67. },
  68. },
  69. }
  70. svc := NewDiscordService(settingService)
  71. payload, files, err := svc.BuildReport(context.Background(), mockServer, mockInbound)
  72. if err != nil {
  73. t.Fatalf("BuildReport failed: %v", err)
  74. }
  75. if len(payload.Embeds) != 1 {
  76. t.Fatalf("expected 1 embed, got %d", len(payload.Embeds))
  77. }
  78. embed := payload.Embeds[0]
  79. if embed.Color != ColorBlue {
  80. t.Errorf("expected ColorBlue, got %X", embed.Color)
  81. }
  82. if len(files) != 0 {
  83. t.Errorf("expected 0 files when backup is disabled, got %d", len(files))
  84. }
  85. foundHost, foundUptime, foundXray := false, false, false
  86. for _, field := range embed.Fields {
  87. if field.Name == "Host" {
  88. foundHost = true
  89. }
  90. if field.Name == "Uptime" && field.Value == "2d 0h" {
  91. foundUptime = true
  92. }
  93. if field.Name == "Xray Core" && field.Value == "25.1.0 (running)" {
  94. foundXray = true
  95. }
  96. }
  97. if !foundHost || !foundUptime || !foundXray {
  98. t.Errorf("expected fields not found in embed: %+v", embed.Fields)
  99. }
  100. }
  101. func TestBuildReport_WithBackup(t *testing.T) {
  102. settingService := setupTestDB(t)
  103. _ = settingService.SetDiscordBotToken("test-bot-token")
  104. _ = settingService.SetDiscordChannelId("12345")
  105. _ = settingService.SetDiscordBotBackup(true)
  106. mockStatus := &service.Status{
  107. Uptime: 3600,
  108. }
  109. mockStatus.Xray.State = service.Running
  110. mockStatus.Xray.Version = "25.1.0"
  111. mockServer := &mockServerProvider{
  112. status: mockStatus,
  113. dbData: []byte("test-db-content"),
  114. filename: "x-ui_backup.db",
  115. }
  116. svc := NewDiscordService(settingService)
  117. _, files, err := svc.BuildReport(context.Background(), mockServer, nil)
  118. if err != nil {
  119. t.Fatalf("BuildReport failed: %v", err)
  120. }
  121. if len(files) == 0 {
  122. t.Fatal("expected at least 1 backup file, got 0")
  123. }
  124. if files[0].Filename != "x-ui_backup.db" {
  125. t.Errorf("expected filename 'x-ui_backup.db', got %q", files[0].Filename)
  126. }
  127. if string(files[0].Data) != "test-db-content" {
  128. t.Errorf("expected db content 'test-db-content', got %q", string(files[0].Data))
  129. }
  130. }
  131. func TestSendReport_Integration(t *testing.T) {
  132. settingService := setupTestDB(t)
  133. _ = settingService.SetDiscordBotToken("test-token")
  134. _ = settingService.SetDiscordChannelId("998877")
  135. _ = settingService.SetDiscordBotBackup(true)
  136. var receivedRequest bool
  137. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  138. receivedRequest = true
  139. w.WriteHeader(http.StatusOK)
  140. _, _ = w.Write([]byte(`{"id": "msg-123"}`))
  141. }))
  142. defer server.Close()
  143. svc := NewDiscordService(settingService)
  144. svc.SetBaseURL(server.URL)
  145. svc.SetHTTPClient(server.Client())
  146. mockStatus := &service.Status{
  147. Uptime: 86400,
  148. }
  149. mockStatus.Xray.State = service.Running
  150. mockStatus.Xray.Version = "25.1.0"
  151. mockServer := &mockServerProvider{
  152. status: mockStatus,
  153. dbData: []byte("sqlite-data"),
  154. }
  155. err := svc.SendReport(context.Background(), mockServer, nil)
  156. if err != nil {
  157. t.Fatalf("SendReport failed: %v", err)
  158. }
  159. if !receivedRequest {
  160. t.Error("expected server to receive report request")
  161. }
  162. }
  163. func TestSendReport_DeliversEmbedWhenBackupUploadIsRejected(t *testing.T) {
  164. settingService := setupTestDB(t)
  165. _ = settingService.SetDiscordBotToken("test-token")
  166. _ = settingService.SetDiscordChannelId("998877")
  167. _ = settingService.SetDiscordBotBackup(true)
  168. embeds := make(chan int, 4)
  169. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  170. if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/") {
  171. w.WriteHeader(http.StatusRequestEntityTooLarge)
  172. _, _ = w.Write([]byte(`{"message": "Request entity too large", "code": 40005}`))
  173. return
  174. }
  175. var p MessagePayload
  176. _ = json.NewDecoder(r.Body).Decode(&p)
  177. embeds <- len(p.Embeds)
  178. w.WriteHeader(http.StatusOK)
  179. }))
  180. defer server.Close()
  181. svc := NewDiscordService(settingService)
  182. svc.SetBaseURL(server.URL)
  183. svc.SetHTTPClient(server.Client())
  184. mockServer := &mockServerProvider{
  185. status: &service.Status{Uptime: 86400},
  186. dbData: []byte("sqlite-data-over-the-upload-cap"),
  187. }
  188. err := svc.SendReport(context.Background(), mockServer, nil)
  189. if err == nil || !strings.Contains(err.Error(), "(413)") {
  190. t.Fatalf("SendReport error = %v, want the rejected backup upload (413)", err)
  191. }
  192. select {
  193. case n := <-embeds:
  194. if n != 1 {
  195. t.Fatalf("report message carried %d embeds, want 1", n)
  196. }
  197. default:
  198. t.Fatal("report embed was never delivered: it rode on the rejected backup upload")
  199. }
  200. }