| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- package discord
- import (
- "context"
- "encoding/json"
- "io"
- "net/http"
- "net/http/httptest"
- "path/filepath"
- "strings"
- "testing"
- "time"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/web/service"
- )
- func setupTestDB(t *testing.T) service.SettingService {
- t.Helper()
- dbPath := filepath.Join(t.TempDir(), "x-ui.db")
- if err := database.InitDB(dbPath); err != nil {
- t.Fatalf("init db: %v", err)
- }
- t.Cleanup(func() { _ = database.CloseDB() })
- return service.SettingService{}
- }
- func TestSendMessage_Success(t *testing.T) {
- settingService := setupTestDB(t)
- if err := settingService.SetDiscordBotToken("test-bot-token"); err != nil {
- t.Fatal(err)
- }
- if err := settingService.SetDiscordChannelId("123456789012345678"); err != nil {
- t.Fatal(err)
- }
- var reqMethod, reqPath, reqAuth, reqUA, reqCT string
- var reqPayload MessagePayload
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- reqMethod = r.Method
- reqPath = r.URL.Path
- reqAuth = r.Header.Get("Authorization")
- reqUA = r.Header.Get("User-Agent")
- reqCT = r.Header.Get("Content-Type")
- body, _ := io.ReadAll(r.Body)
- _ = json.Unmarshal(body, &reqPayload)
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"id": "msg-123"}`))
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- payload := MessagePayload{
- Content: "Hello Discord!",
- Embeds: []Embed{
- {
- Title: "Test Embed",
- Description: "Desc",
- Color: ColorGreen,
- },
- },
- }
- if err := svc.SendMessage(context.Background(), payload); err != nil {
- t.Fatalf("SendMessage failed: %v", err)
- }
- if reqMethod != http.MethodPost {
- t.Errorf("expected POST, got %s", reqMethod)
- }
- expectedPath := "/channels/123456789012345678/messages"
- if reqPath != expectedPath {
- t.Errorf("expected path %s, got %s", expectedPath, reqPath)
- }
- if reqAuth != "Bot test-bot-token" {
- t.Errorf("expected auth 'Bot test-bot-token', got %s", reqAuth)
- }
- if reqUA != discordUserAgent {
- t.Errorf("expected User-Agent %s, got %s", discordUserAgent, reqUA)
- }
- if reqCT != "application/json" {
- t.Errorf("expected Content-Type application/json, got %s", reqCT)
- }
- if reqPayload.Content != "Hello Discord!" || len(reqPayload.Embeds) != 1 {
- t.Errorf("payload mismatch: %+v", reqPayload)
- }
- }
- func TestSendMessage_CreatedStatus(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("token")
- _ = settingService.SetDiscordChannelId("ch-1")
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusCreated)
- _, _ = w.Write([]byte(`{}`))
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- if err := svc.SendEmbed(context.Background(), Embed{Title: "Title"}); err != nil {
- t.Fatalf("SendEmbed failed: %v", err)
- }
- }
- func TestSendMessage_StatusCodes(t *testing.T) {
- cases := []struct {
- name string
- statusCode int
- respBody string
- wantErrSub string
- }{
- {"Bad Request", http.StatusBadRequest, `{"message": "Invalid Form Body"}`, "discord bad request (400)"},
- {"Unauthorized", http.StatusUnauthorized, `{"message": "401: Unauthorized"}`, "discord unauthorized (401)"},
- {"Forbidden", http.StatusForbidden, `{"message": "Missing Permissions"}`, "discord forbidden (403)"},
- {"NotFound", http.StatusNotFound, `{"message": "Unknown Channel"}`, "discord not found (404)"},
- {"RateLimited", http.StatusTooManyRequests, `{"retry_after": 1.5}`, "discord rate limited (429)"},
- {"InternalError", http.StatusInternalServerError, `{"message": "Server Error"}`, "discord API error (500)"},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("token")
- _ = settingService.SetDiscordChannelId("ch-1")
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(tc.statusCode)
- _, _ = w.Write([]byte(tc.respBody))
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- err := svc.SendEmbed(context.Background(), Embed{Title: "Test"})
- if err == nil {
- t.Fatalf("expected error for status %d, got nil", tc.statusCode)
- }
- if !strings.Contains(err.Error(), tc.wantErrSub) {
- t.Errorf("expected error containing %q, got %q", tc.wantErrSub, err.Error())
- }
- })
- }
- }
- func TestSendMessage_MissingConfig(t *testing.T) {
- settingService := setupTestDB(t)
- svc := NewDiscordService(settingService)
- // Both empty
- err := svc.SendMessage(context.Background(), MessagePayload{Content: "Hi"})
- if err == nil || !strings.Contains(err.Error(), "token is not configured") {
- t.Fatalf("expected token not configured error, got %v", err)
- }
- // Token set, channel empty
- _ = settingService.SetDiscordBotToken("some-token")
- err = svc.SendMessage(context.Background(), MessagePayload{Content: "Hi"})
- if err == nil || !strings.Contains(err.Error(), "channel id is not configured") {
- t.Fatalf("expected channel id not configured error, got %v", err)
- }
- }
- func TestSendTest(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("test-bot-token")
- _ = settingService.SetDiscordChannelId("999888777")
- var receivedPayload MessagePayload
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- body, _ := io.ReadAll(r.Body)
- _ = json.Unmarshal(body, &receivedPayload)
- w.WriteHeader(http.StatusOK)
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- if err := svc.SendTest(context.Background()); err != nil {
- t.Fatalf("SendTest failed: %v", err)
- }
- if len(receivedPayload.Embeds) != 1 {
- t.Fatalf("expected 1 embed, got %d", len(receivedPayload.Embeds))
- }
- embed := receivedPayload.Embeds[0]
- if embed.Color != ColorGreen {
- t.Errorf("expected ColorGreen (0x%X), got 0x%X", ColorGreen, embed.Color)
- }
- if embed.Timestamp == "" {
- t.Error("expected non-empty timestamp")
- } else {
- parsed, err := time.Parse(time.RFC3339, embed.Timestamp)
- if err != nil {
- t.Errorf("timestamp is not RFC3339: %v", err)
- }
- if parsed.Location() != time.UTC {
- t.Errorf("expected UTC timestamp location, got %v", parsed.Location())
- }
- }
- if len(embed.Fields) == 0 {
- t.Error("expected test embed to have fields")
- }
- }
- func TestSendMessage_BotPrefixHandling(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("Bot prefixed-token")
- _ = settingService.SetDiscordChannelId("ch-100")
- var receivedAuth string
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- receivedAuth = r.Header.Get("Authorization")
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{}`))
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- if err := svc.SendEmbed(context.Background(), Embed{Title: "Prefix Test"}); err != nil {
- t.Fatalf("SendEmbed failed: %v", err)
- }
- if receivedAuth != "Bot prefixed-token" {
- t.Errorf("expected 'Bot prefixed-token', got %q", receivedAuth)
- }
- }
- func TestSendMessage_NoContentStatus(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("token")
- _ = settingService.SetDiscordChannelId("ch-204")
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusNoContent)
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- if err := svc.SendEmbed(context.Background(), Embed{Title: "204 Test"}); err != nil {
- t.Fatalf("SendEmbed failed for 204: %v", err)
- }
- }
- func TestSendMessage_ContextCancelled(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("token")
- _ = settingService.SetDiscordChannelId("ch-1")
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- time.Sleep(100 * time.Millisecond)
- w.WriteHeader(http.StatusOK)
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
- err := svc.SendMessage(ctx, MessagePayload{Content: "Cancelled"})
- if err == nil {
- t.Fatal("expected error with cancelled context, got nil")
- }
- }
- func TestSendMessageWithFiles_Success(t *testing.T) {
- settingService := setupTestDB(t)
- _ = settingService.SetDiscordBotToken("test-bot-token")
- _ = settingService.SetDiscordChannelId("ch-multipart")
- var receivedCT string
- var receivedPayload MessagePayload
- receivedFiles := make(map[string][]byte)
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- receivedCT = r.Header.Get("Content-Type")
- mr, err := r.MultipartReader()
- if err != nil {
- t.Fatalf("MultipartReader error: %v", err)
- }
- for {
- part, err := mr.NextPart()
- if err == io.EOF {
- break
- }
- if err != nil {
- t.Fatalf("NextPart error: %v", err)
- }
- data, _ := io.ReadAll(part)
- formName := part.FormName()
- if formName == "payload_json" {
- _ = json.Unmarshal(data, &receivedPayload)
- } else {
- receivedFiles[part.FileName()] = data
- }
- }
- w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(`{"id": "msg-files"}`))
- }))
- defer server.Close()
- svc := NewDiscordService(settingService)
- svc.SetBaseURL(server.URL)
- svc.SetHTTPClient(server.Client())
- payload := MessagePayload{
- Content: "Report message",
- Embeds: []Embed{{Title: "Report Embed"}},
- }
- files := []FileAttachment{
- {Filename: "x-ui.db", Data: []byte("sqlite-db-binary")},
- {Filename: "config.json", Data: []byte(`{"log":{}}`)},
- }
- if err := svc.SendMessageWithFiles(context.Background(), payload, files...); err != nil {
- t.Fatalf("SendMessageWithFiles failed: %v", err)
- }
- if !strings.HasPrefix(receivedCT, "multipart/form-data; boundary=") {
- t.Errorf("expected multipart/form-data content type, got %s", receivedCT)
- }
- if receivedPayload.Content != "Report message" || len(receivedPayload.Embeds) != 1 {
- t.Errorf("payload mismatch: %+v", receivedPayload)
- }
- if string(receivedFiles["x-ui.db"]) != "sqlite-db-binary" {
- t.Errorf("x-ui.db mismatch: %s", string(receivedFiles["x-ui.db"]))
- }
- if string(receivedFiles["config.json"]) != `{"log":{}}` {
- t.Errorf("config.json mismatch: %s", string(receivedFiles["config.json"]))
- }
- }
|