client_partial_apply_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. package controller
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "path/filepath"
  8. "testing"
  9. "github.com/gin-gonic/gin"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database"
  11. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  12. "github.com/mhsanaei/3x-ui/v3/internal/web/entity"
  13. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  14. )
  15. // seedPartlyApplyingClient puts one client on two inbounds and corrupts the second
  16. // one's settings, so a later op succeeds on one inbound and fails on the other.
  17. func seedPartlyApplyingClient(t *testing.T, email string, basePort int) (healthyID, brokenID int) {
  18. t.Helper()
  19. dbDir := t.TempDir()
  20. t.Setenv("XUI_DB_FOLDER", dbDir)
  21. if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  22. t.Fatalf("InitDB: %v", err)
  23. }
  24. t.Cleanup(func() { _ = database.CloseDB() })
  25. db := database.GetDB()
  26. ids := make([]int, 0, 2)
  27. for i := range 2 {
  28. ib := &model.Inbound{
  29. UserId: 1, Enable: true, Port: basePort + i,
  30. Tag: "in-" + string(rune('a'+i)) + "-partial",
  31. Protocol: model.VLESS, Settings: `{"clients": []}`,
  32. StreamSettings: `{"network":"tcp","security":"none"}`,
  33. }
  34. if err := db.Create(ib).Error; err != nil {
  35. t.Fatalf("create inbound %d: %v", i, err)
  36. }
  37. ids = append(ids, ib.Id)
  38. }
  39. if _, err := (&service.ClientService{}).Create(&service.InboundService{}, &service.ClientCreatePayload{
  40. Client: model.Client{Email: email, ID: "11111111-2222-3333-4444-555555555555", SubID: "sub-" + email, Enable: true},
  41. InboundIds: ids,
  42. }); err != nil {
  43. t.Fatalf("seed Create across both inbounds: %v", err)
  44. }
  45. if err := db.Model(&model.Inbound{}).Where("id = ?", ids[1]).
  46. Update("settings", `{"clients":`).Error; err != nil {
  47. t.Fatalf("corrupt inbound %d settings: %v", ids[1], err)
  48. }
  49. return ids[0], ids[1]
  50. }
  51. func postCtx(t *testing.T, email string, body any) (*gin.Context, *httptest.ResponseRecorder) {
  52. t.Helper()
  53. gin.SetMode(gin.TestMode)
  54. w := httptest.NewRecorder()
  55. c, _ := gin.CreateTestContext(w)
  56. c.Params = gin.Params{{Key: "email", Value: email}}
  57. payload := []byte("{}")
  58. if body != nil {
  59. var err error
  60. if payload, err = json.Marshal(body); err != nil {
  61. t.Fatalf("marshal body: %v", err)
  62. }
  63. }
  64. c.Request = httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(payload))
  65. c.Request.Header.Set("Content-Type", "application/json")
  66. return c, w
  67. }
  68. // assertPartialApply pins that the op really failed on one inbound, so a green
  69. // test cannot be a plain full success that never exercised the error path.
  70. func assertPartialApply(t *testing.T, w *httptest.ResponseRecorder) {
  71. t.Helper()
  72. var msg entity.Msg
  73. if err := json.Unmarshal(w.Body.Bytes(), &msg); err != nil {
  74. t.Fatalf("decode response %q: %v", w.Body.String(), err)
  75. }
  76. if msg.Success {
  77. t.Fatalf("response reports success=true, want the partial apply to report failure: %q", w.Body.String())
  78. }
  79. }
  80. // TestUpdateHandlerFlagsRestartOnPartialApply pins that an edit committed on some
  81. // inbounds and failed on others still flags Xray, as create/attach already did.
  82. func TestUpdateHandlerFlagsRestartOnPartialApply(t *testing.T) {
  83. const email = "[email protected]"
  84. seedPartlyApplyingClient(t, email, 43310)
  85. a := &ClientController{}
  86. a.xrayService.IsNeedRestartAndSetFalse()
  87. c, w := postCtx(t, email, map[string]any{
  88. "email": email, "id": "11111111-2222-3333-4444-555555555555",
  89. "subId": "sub-" + email, "enable": true, "comment": "edited",
  90. })
  91. a.update(c)
  92. assertPartialApply(t, w)
  93. if !a.xrayService.IsNeedRestartAndSetFalse() {
  94. t.Fatal("a partly-applied client edit left Xray unflagged for restart")
  95. }
  96. }
  97. // TestDeleteHandlerFlagsRestartOnPartialApply is the delete-side twin: the
  98. // removals that landed still need the restart the error path used to discard.
  99. func TestDeleteHandlerFlagsRestartOnPartialApply(t *testing.T) {
  100. const email = "[email protected]"
  101. seedPartlyApplyingClient(t, email, 43320)
  102. a := &ClientController{}
  103. a.xrayService.IsNeedRestartAndSetFalse()
  104. c, w := postCtx(t, email, nil)
  105. a.delete(c)
  106. assertPartialApply(t, w)
  107. if !a.xrayService.IsNeedRestartAndSetFalse() {
  108. t.Fatal("a partly-applied client delete left Xray unflagged for restart")
  109. }
  110. }
  111. // TestDetachHandlerFlagsRestartOnPartialApply covers the third converted path.
  112. func TestDetachHandlerFlagsRestartOnPartialApply(t *testing.T) {
  113. const email = "[email protected]"
  114. healthyID, brokenID := seedPartlyApplyingClient(t, email, 43330)
  115. a := &ClientController{}
  116. a.xrayService.IsNeedRestartAndSetFalse()
  117. c, w := postCtx(t, email, attachDetachBody{InboundIds: []int{healthyID, brokenID}})
  118. a.detach(c)
  119. assertPartialApply(t, w)
  120. if !a.xrayService.IsNeedRestartAndSetFalse() {
  121. t.Fatal("a partly-applied client detach left Xray unflagged for restart")
  122. }
  123. }