1
0

setting_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package controller
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "net/http/httptest"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "testing"
  10. "github.com/gin-gonic/gin"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. "github.com/mhsanaei/3x-ui/v3/internal/util/crypto"
  14. "github.com/mhsanaei/3x-ui/v3/internal/web/locale"
  15. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  16. "github.com/mhsanaei/3x-ui/v3/internal/web/service/discord"
  17. )
  18. func TestValidateRegex(t *testing.T) {
  19. gin.SetMode(gin.TestMode)
  20. router := gin.New()
  21. NewSettingController(router.Group("/panel/api"))
  22. tests := []struct {
  23. name string
  24. body string
  25. success bool
  26. }{
  27. {name: "Go RE2 inline flag", body: `{"regex":"(?m)^general-purpose$"}`, success: true},
  28. {name: "invalid expression", body: `{"regex":"["}`, success: false},
  29. }
  30. for _, tt := range tests {
  31. t.Run(tt.name, func(t *testing.T) {
  32. req := httptest.NewRequest(http.MethodPost, "/panel/api/setting/validateRegex", strings.NewReader(tt.body))
  33. req.Header.Set("Content-Type", "application/json")
  34. resp := httptest.NewRecorder()
  35. router.ServeHTTP(resp, req)
  36. if resp.Code != http.StatusOK {
  37. t.Fatalf("status = %d, want 200; body=%s", resp.Code, resp.Body.String())
  38. }
  39. needle := `"success":true`
  40. if !tt.success {
  41. needle = `"success":false`
  42. }
  43. if !strings.Contains(resp.Body.String(), needle) {
  44. t.Fatalf("body = %s, want %s", resp.Body.String(), needle)
  45. }
  46. })
  47. }
  48. }
  49. func TestAPITokenMutationRoutesEnforceExpectedScope(t *testing.T) {
  50. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  51. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  52. t.Fatalf("InitDB: %v", err)
  53. }
  54. t.Cleanup(func() { _ = database.CloseDB() })
  55. row := &model.ApiToken{Name: "route-scope", Token: crypto.HashTokenSHA256("token"), Enabled: true, Scope: model.ApiScopeNodeSync}
  56. if err := database.GetDB().Create(row).Error; err != nil {
  57. t.Fatalf("seed token: %v", err)
  58. }
  59. gin.SetMode(gin.TestMode)
  60. router := gin.New()
  61. NewSettingController(router.Group("/panel/api"))
  62. for _, path := range []string{
  63. "/panel/api/setting/apiTokens/delete/" + strconv.Itoa(row.Id),
  64. "/panel/api/setting/apiTokens/setEnabled/" + strconv.Itoa(row.Id),
  65. } {
  66. body := `{"expectedScope":"admin","enabled":false}`
  67. req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
  68. req.Header.Set("Content-Type", "application/json")
  69. resp := httptest.NewRecorder()
  70. router.ServeHTTP(resp, req)
  71. if !strings.Contains(resp.Body.String(), `"success":false`) {
  72. t.Fatalf("%s accepted wrong expected scope: %s", path, resp.Body.String())
  73. }
  74. }
  75. var stored model.ApiToken
  76. if err := database.GetDB().First(&stored, row.Id).Error; err != nil {
  77. t.Fatalf("token was deleted by wrong scope: %v", err)
  78. }
  79. if !stored.Enabled {
  80. t.Fatal("token was disabled by wrong scope")
  81. }
  82. }
  83. // GHSA-xqqw-jqqv-99h6: a save that keeps 2FA enabled must not be able to
  84. // rebind the authenticator without presenting a current code.
  85. func TestUpdateSettingRequiresCodeToReplaceTwoFactorToken(t *testing.T) {
  86. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  87. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  88. t.Fatalf("InitDB: %v", err)
  89. }
  90. t.Cleanup(func() { _ = database.CloseDB() })
  91. settingService := service.SettingService{}
  92. if err := settingService.SetTwoFactorToken("ORIGINALSECRET234567"); err != nil {
  93. t.Fatalf("seed token: %v", err)
  94. }
  95. if err := settingService.SetTwoFactorEnable(true); err != nil {
  96. t.Fatalf("seed enable: %v", err)
  97. }
  98. post := func(t *testing.T, mutate func(map[string]any)) string {
  99. t.Helper()
  100. base, err := settingService.GetAllSetting()
  101. if err != nil {
  102. t.Fatalf("GetAllSetting: %v", err)
  103. }
  104. raw, err := json.Marshal(base)
  105. if err != nil {
  106. t.Fatalf("marshal: %v", err)
  107. }
  108. body := map[string]any{}
  109. if err := json.Unmarshal(raw, &body); err != nil {
  110. t.Fatalf("unmarshal: %v", err)
  111. }
  112. mutate(body)
  113. payload, err := json.Marshal(body)
  114. if err != nil {
  115. t.Fatalf("marshal payload: %v", err)
  116. }
  117. gin.SetMode(gin.TestMode)
  118. router := gin.New()
  119. NewSettingController(router.Group("/panel/api"))
  120. req := httptest.NewRequest(http.MethodPost, "/panel/api/setting/update", strings.NewReader(string(payload)))
  121. req.Header.Set("Content-Type", "application/json")
  122. resp := httptest.NewRecorder()
  123. router.ServeHTTP(resp, req)
  124. return resp.Body.String()
  125. }
  126. t.Run("rebind without code is rejected", func(t *testing.T) {
  127. got := post(t, func(body map[string]any) {
  128. body["twoFactorEnable"] = true
  129. body["twoFactorToken"] = "ATTACKERSECRET567890"
  130. })
  131. if !strings.Contains(got, `"success":false`) {
  132. t.Fatalf("rebind without a 2FA code was accepted: %s", got)
  133. }
  134. stored, err := settingService.GetTwoFactorToken()
  135. if err != nil {
  136. t.Fatalf("GetTwoFactorToken: %v", err)
  137. }
  138. if stored != "ORIGINALSECRET234567" {
  139. t.Fatalf("stored 2FA secret = %q, want it unchanged", stored)
  140. }
  141. })
  142. t.Run("ordinary save with redacted token still succeeds", func(t *testing.T) {
  143. got := post(t, func(body map[string]any) {
  144. body["twoFactorEnable"] = true
  145. body["twoFactorToken"] = ""
  146. })
  147. if !strings.Contains(got, `"success":true`) {
  148. t.Fatalf("normal settings save was rejected: %s", got)
  149. }
  150. stored, err := settingService.GetTwoFactorToken()
  151. if err != nil {
  152. t.Fatalf("GetTwoFactorToken: %v", err)
  153. }
  154. if stored != "ORIGINALSECRET234567" {
  155. t.Fatalf("stored 2FA secret = %q, want it preserved", stored)
  156. }
  157. })
  158. }
  159. func TestTestDiscordEndpoint(t *testing.T) {
  160. gin.SetMode(gin.TestMode)
  161. // 1. Service not initialized
  162. SetDiscordService(nil)
  163. router := gin.New()
  164. router.Use(func(c *gin.Context) {
  165. c.Set("I18n", func(_ locale.I18nType, key string, _ ...string) string { return key })
  166. c.Next()
  167. })
  168. NewSettingController(router.Group("/panel/api"))
  169. req := httptest.NewRequest(http.MethodPost, "/panel/api/setting/testDiscord", nil)
  170. resp := httptest.NewRecorder()
  171. router.ServeHTTP(resp, req)
  172. if !strings.Contains(resp.Body.String(), `"success":false`) || !strings.Contains(resp.Body.String(), "pages.settings.discordNotInitialized") {
  173. t.Fatalf("expected uninitialized error, got %s", resp.Body.String())
  174. }
  175. // Setup DB
  176. t.Setenv("XUI_DB_FOLDER", t.TempDir())
  177. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  178. t.Fatalf("InitDB: %v", err)
  179. }
  180. t.Cleanup(func() {
  181. _ = database.CloseDB()
  182. SetDiscordService(nil)
  183. })
  184. settingService := service.SettingService{}
  185. svc := discord.NewDiscordService(settingService)
  186. SetDiscordService(svc)
  187. // 2. Discord bot disabled
  188. _ = settingService.SetDiscordBotEnable(false)
  189. req = httptest.NewRequest(http.MethodPost, "/panel/api/setting/testDiscord", nil)
  190. resp = httptest.NewRecorder()
  191. router.ServeHTTP(resp, req)
  192. if !strings.Contains(resp.Body.String(), `"success":false`) || !strings.Contains(resp.Body.String(), "pages.settings.discordBotNotEnabled") {
  193. t.Fatalf("expected disabled error, got %s", resp.Body.String())
  194. }
  195. // 3. Discord bot enabled but missing config
  196. _ = settingService.SetDiscordBotEnable(true)
  197. req = httptest.NewRequest(http.MethodPost, "/panel/api/setting/testDiscord", nil)
  198. resp = httptest.NewRecorder()
  199. router.ServeHTTP(resp, req)
  200. if !strings.Contains(resp.Body.String(), `"success":false`) || !strings.Contains(resp.Body.String(), "pages.settings.discordTestFailed") {
  201. t.Fatalf("expected send failure error, got %s", resp.Body.String())
  202. }
  203. // 4. Discord bot enabled with working server
  204. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  205. w.WriteHeader(http.StatusOK)
  206. _, _ = w.Write([]byte(`{"id": "msg-1"}`))
  207. }))
  208. defer server.Close()
  209. _ = settingService.SetDiscordBotToken("test-bot-token")
  210. _ = settingService.SetDiscordChannelId("123456789")
  211. svc.SetBaseURL(server.URL)
  212. svc.SetHTTPClient(server.Client())
  213. req = httptest.NewRequest(http.MethodPost, "/panel/api/setting/testDiscord", nil)
  214. resp = httptest.NewRecorder()
  215. router.ServeHTTP(resp, req)
  216. if !strings.Contains(resp.Body.String(), `"success":true`) || !strings.Contains(resp.Body.String(), "pages.settings.discordTestSuccess") {
  217. t.Fatalf("expected success, got %s", resp.Body.String())
  218. }
  219. }