client_update_no_inbound_test.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package service
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. func TestUpdate_PersistsFields_NoInbound(t *testing.T) {
  9. cases := []struct {
  10. name string
  11. mutate func(c *model.Client)
  12. readBack func(rec *model.ClientRecord) any
  13. want any
  14. }{
  15. {
  16. name: "subId",
  17. mutate: func(c *model.Client) { c.SubID = "new-sub-id" },
  18. readBack: func(rec *model.ClientRecord) any { return rec.SubID },
  19. want: "new-sub-id",
  20. },
  21. {
  22. name: "totalGB cleared to zero",
  23. mutate: func(c *model.Client) { c.TotalGB = 0 },
  24. readBack: func(rec *model.ClientRecord) any { return rec.TotalGB },
  25. want: int64(0),
  26. },
  27. {
  28. name: "expiryTime",
  29. mutate: func(c *model.Client) { c.ExpiryTime = 1700000000 },
  30. readBack: func(rec *model.ClientRecord) any { return rec.ExpiryTime },
  31. want: int64(1700000000),
  32. },
  33. {
  34. name: "limitIp",
  35. mutate: func(c *model.Client) { c.LimitIP = 7 },
  36. readBack: func(rec *model.ClientRecord) any { return rec.LimitIP },
  37. want: 7,
  38. },
  39. {
  40. name: "tgId",
  41. mutate: func(c *model.Client) { c.TgID = 9876543210 },
  42. readBack: func(rec *model.ClientRecord) any { return rec.TgID },
  43. want: int64(9876543210),
  44. },
  45. {
  46. name: "comment cleared to empty",
  47. mutate: func(c *model.Client) { c.Comment = "" },
  48. readBack: func(rec *model.ClientRecord) any { return rec.Comment },
  49. want: "",
  50. },
  51. {
  52. name: "reset",
  53. mutate: func(c *model.Client) { c.Reset = 30 },
  54. readBack: func(rec *model.ClientRecord) any { return rec.Reset },
  55. want: 30,
  56. },
  57. {
  58. name: "flow",
  59. mutate: func(c *model.Client) { c.Flow = "xtls-rprx-vision" },
  60. readBack: func(rec *model.ClientRecord) any { return rec.Flow },
  61. want: "xtls-rprx-vision",
  62. },
  63. {
  64. name: "security",
  65. mutate: func(c *model.Client) { c.Security = "aes-128-gcm" },
  66. readBack: func(rec *model.ClientRecord) any { return rec.Security },
  67. want: "aes-128-gcm",
  68. },
  69. {
  70. name: "uuid rotated",
  71. mutate: func(c *model.Client) { c.ID = "22222222-2222-2222-2222-222222222222" },
  72. readBack: func(rec *model.ClientRecord) any { return rec.UUID },
  73. want: "22222222-2222-2222-2222-222222222222",
  74. },
  75. }
  76. for _, tc := range cases {
  77. t.Run(tc.name, func(t *testing.T) {
  78. setupBulkDB(t)
  79. svc := &ClientService{}
  80. inboundSvc := &InboundService{}
  81. email := "noib-" + strings.ReplaceAll(strings.ToLower(tc.name), " ", "-") + "@x"
  82. rec := &model.ClientRecord{
  83. Email: email,
  84. UUID: "11111111-1111-1111-1111-111111111111",
  85. SubID: email,
  86. TotalGB: 5,
  87. ExpiryTime: 1000,
  88. LimitIP: 1,
  89. TgID: 1,
  90. Comment: "seeded",
  91. Reset: 1,
  92. Flow: "seeded-flow",
  93. Security: "seeded-sec",
  94. }
  95. if err := database.GetDB().Create(rec).Error; err != nil {
  96. t.Fatalf("create record: %v", err)
  97. }
  98. updated := rec.ToClient()
  99. tc.mutate(updated)
  100. if _, err := svc.Update(inboundSvc, rec.Id, *updated); err != nil {
  101. t.Fatalf("Update: %v", err)
  102. }
  103. got, err := svc.GetByID(rec.Id)
  104. if err != nil {
  105. t.Fatalf("GetByID: %v", err)
  106. }
  107. if tc.readBack(got) != tc.want {
  108. t.Fatalf("%s: not persisted for no-inbound client, got %v, want %v", tc.name, tc.readBack(got), tc.want)
  109. }
  110. })
  111. }
  112. }
  113. func TestUpdate_NoInbound_PreservesCredentialsWhenOmitted(t *testing.T) {
  114. setupBulkDB(t)
  115. svc := &ClientService{}
  116. inboundSvc := &InboundService{}
  117. email := "noib-preserve@x"
  118. rec := &model.ClientRecord{
  119. Email: email,
  120. UUID: "11111111-1111-1111-1111-111111111111",
  121. SubID: email,
  122. Password: "seeded-pw",
  123. Auth: "seeded-auth",
  124. Secret: "seeded-secret",
  125. }
  126. if err := database.GetDB().Create(rec).Error; err != nil {
  127. t.Fatalf("create record: %v", err)
  128. }
  129. updated := rec.ToClient()
  130. updated.ID = ""
  131. updated.Password = ""
  132. updated.Auth = ""
  133. updated.Secret = ""
  134. updated.Comment = "only comment changed"
  135. if _, err := svc.Update(inboundSvc, rec.Id, *updated); err != nil {
  136. t.Fatalf("Update: %v", err)
  137. }
  138. got, err := svc.GetByID(rec.Id)
  139. if err != nil {
  140. t.Fatalf("GetByID: %v", err)
  141. }
  142. if got.UUID != "11111111-1111-1111-1111-111111111111" {
  143. t.Fatalf("uuid wiped on partial update, got %q", got.UUID)
  144. }
  145. if got.Password != "seeded-pw" {
  146. t.Fatalf("password wiped on partial update, got %q", got.Password)
  147. }
  148. if got.Auth != "seeded-auth" {
  149. t.Fatalf("auth wiped on partial update, got %q", got.Auth)
  150. }
  151. if got.Secret != "seeded-secret" {
  152. t.Fatalf("secret wiped on partial update, got %q", got.Secret)
  153. }
  154. if got.Comment != "only comment changed" {
  155. t.Fatalf("comment not persisted, got %q", got.Comment)
  156. }
  157. }
  158. func TestApplyClientRecordMerge_MirrorsSyncInboundRules(t *testing.T) {
  159. row := &model.ClientRecord{
  160. UUID: "kept-uuid",
  161. Password: "kept-pw",
  162. Flow: "kept-flow",
  163. TotalGB: 9,
  164. Group: "kept-group",
  165. Comment: "kept-comment",
  166. }
  167. incoming := &model.ClientRecord{
  168. Password: "new-pw",
  169. TotalGB: 0,
  170. Comment: "new-comment",
  171. }
  172. applyClientRecordMerge(row, incoming)
  173. if row.UUID != "kept-uuid" {
  174. t.Fatalf("empty incoming UUID should preserve stored UUID, got %q", row.UUID)
  175. }
  176. if row.Password != "new-pw" {
  177. t.Fatalf("non-empty incoming Password should overwrite, got %q", row.Password)
  178. }
  179. if row.Flow != "" {
  180. t.Fatalf("incoming Flow is unconditional and should overwrite with empty, got %q", row.Flow)
  181. }
  182. if row.TotalGB != 0 {
  183. t.Fatalf("incoming TotalGB is unconditional and should overwrite with zero, got %v", row.TotalGB)
  184. }
  185. if row.Group != "kept-group" {
  186. t.Fatalf("empty incoming Group should preserve stored group, got %q", row.Group)
  187. }
  188. if row.Comment != "new-comment" {
  189. t.Fatalf("incoming Comment is unconditional and should overwrite, got %q", row.Comment)
  190. }
  191. }