client_link_delta_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. )
  7. // stampLinkCreatedAt marks every link of an inbound with a sentinel timestamp.
  8. // A row that still carries it afterwards was not deleted and re-inserted.
  9. func stampLinkCreatedAt(t *testing.T, inboundId int) {
  10. t.Helper()
  11. err := database.GetDB().Model(&model.ClientInbound{}).
  12. Where("inbound_id = ?", inboundId).
  13. UpdateColumn("created_at", 1).Error
  14. if err != nil {
  15. t.Fatalf("stamp created_at: %v", err)
  16. }
  17. }
  18. func linksOf(t *testing.T, inboundId int) map[int]model.ClientInbound {
  19. t.Helper()
  20. var rows []model.ClientInbound
  21. if err := database.GetDB().Where("inbound_id = ?", inboundId).Find(&rows).Error; err != nil {
  22. t.Fatalf("load links: %v", err)
  23. }
  24. out := make(map[int]model.ClientInbound, len(rows))
  25. for _, r := range rows {
  26. out[r.ClientId] = r
  27. }
  28. return out
  29. }
  30. func recordID(t *testing.T, email string) int {
  31. t.Helper()
  32. var rec model.ClientRecord
  33. if err := database.GetDB().Where("email = ?", email).First(&rec).Error; err != nil {
  34. t.Fatalf("record %q: %v", email, err)
  35. }
  36. return rec.Id
  37. }
  38. // A re-sync that changes one client's flow must leave the other links in place
  39. // and UPDATE the changed one, not rebuild the whole membership set (#6252).
  40. func TestSyncInboundReusesUnchangedLinkRows(t *testing.T) {
  41. setupBulkDB(t)
  42. cs := &ClientService{}
  43. seed := []model.Client{
  44. {ID: "id-a", Email: "a@x", Enable: true, SubID: "s-a"},
  45. {ID: "id-b", Email: "b@x", Enable: true, SubID: "s-b", Flow: "xtls-rprx-vision"},
  46. {ID: "id-c", Email: "c@x", Enable: true, SubID: "s-c"},
  47. }
  48. ib := mkInbound(t, 21001, model.VLESS, clientsSettings(t, seed))
  49. if err := cs.SyncInbound(nil, ib.Id, seed); err != nil {
  50. t.Fatalf("seed SyncInbound: %v", err)
  51. }
  52. stampLinkCreatedAt(t, ib.Id)
  53. changed := make([]model.Client, len(seed))
  54. copy(changed, seed)
  55. changed[1].Flow = ""
  56. if err := cs.SyncInbound(nil, ib.Id, changed); err != nil {
  57. t.Fatalf("re-sync: %v", err)
  58. }
  59. links := linksOf(t, ib.Id)
  60. if len(links) != 3 {
  61. t.Fatalf("link count = %d, want 3", len(links))
  62. }
  63. for _, email := range []string{"a@x", "b@x", "c@x"} {
  64. link, ok := links[recordID(t, email)]
  65. if !ok {
  66. t.Fatalf("%s lost its link", email)
  67. }
  68. if link.CreatedAt != 1 {
  69. t.Errorf("%s link created_at = %d, want the 1 sentinel: the row was deleted and re-inserted", email, link.CreatedAt)
  70. }
  71. }
  72. if got := links[recordID(t, "b@x")].FlowOverride; got != "" {
  73. t.Errorf("b@x flow_override = %q, want \"\" (cleared in place)", got)
  74. }
  75. // Dropping a client must still remove exactly that one link.
  76. if err := cs.SyncInbound(nil, ib.Id, []model.Client{seed[0], seed[2]}); err != nil {
  77. t.Fatalf("prune sync: %v", err)
  78. }
  79. links = linksOf(t, ib.Id)
  80. if len(links) != 2 {
  81. t.Fatalf("after prune link count = %d, want 2", len(links))
  82. }
  83. if _, still := links[recordID(t, "b@x")]; still {
  84. t.Error("b@x link survived a full sync that dropped it")
  85. }
  86. for _, email := range []string{"a@x", "c@x"} {
  87. if links[recordID(t, email)].CreatedAt != 1 {
  88. t.Errorf("%s link was rebuilt by the prune sync", email)
  89. }
  90. }
  91. }
  92. // Adding a client must not re-merge its bystanders' records from the settings
  93. // JSON; comment lives only in the clients table, so a full sync erases it.
  94. func TestAddInboundClientLeavesBystanderRecordsUntouched(t *testing.T) {
  95. setupBulkDB(t)
  96. cs := &ClientService{}
  97. is := &InboundService{}
  98. seed := []model.Client{
  99. {ID: "id-a", Email: "a@x", Enable: true, SubID: "s-a"},
  100. {ID: "id-b", Email: "b@x", Enable: true, SubID: "s-b"},
  101. }
  102. ib := mkInbound(t, 21002, model.VLESS, clientsSettings(t, seed))
  103. if err := cs.SyncInbound(nil, ib.Id, seed); err != nil {
  104. t.Fatalf("seed SyncInbound: %v", err)
  105. }
  106. db := database.GetDB()
  107. if err := db.Model(&model.ClientRecord{}).Where("email = ?", "a@x").
  108. UpdateColumn("comment", "operator note").Error; err != nil {
  109. t.Fatalf("set comment: %v", err)
  110. }
  111. stampLinkCreatedAt(t, ib.Id)
  112. add := []model.Client{{ID: "id-c", Email: "c@x", Enable: true, SubID: "s-c"}}
  113. if _, err := cs.AddInboundClient(is, &model.Inbound{Id: ib.Id, Settings: clientsSettings(t, add)}); err != nil {
  114. t.Fatalf("AddInboundClient: %v", err)
  115. }
  116. var bystander model.ClientRecord
  117. if err := db.Where("email = ?", "a@x").First(&bystander).Error; err != nil {
  118. t.Fatalf("reload a@x: %v", err)
  119. }
  120. if bystander.Comment != "operator note" {
  121. t.Errorf("bystander comment = %q, want %q: the add re-merged an unrelated record from settings JSON",
  122. bystander.Comment, "operator note")
  123. }
  124. links := linksOf(t, ib.Id)
  125. if len(links) != 3 {
  126. t.Fatalf("link count = %d, want 3", len(links))
  127. }
  128. for _, email := range []string{"a@x", "b@x"} {
  129. if links[recordID(t, email)].CreatedAt != 1 {
  130. t.Errorf("%s link was rebuilt by an unrelated add", email)
  131. }
  132. }
  133. newLink, ok := links[recordID(t, "c@x")]
  134. if !ok {
  135. t.Fatal("c@x got no link")
  136. }
  137. if newLink.CreatedAt == 1 {
  138. t.Error("c@x link carries the sentinel; it should be freshly inserted")
  139. }
  140. }
  141. // Deleting one client detaches only that client; the others keep both their
  142. // link rows and the record fields that live only in the clients table.
  143. func TestDelInboundClientDetachesOnlyTheRemovedClient(t *testing.T) {
  144. setupBulkDB(t)
  145. cs := &ClientService{}
  146. is := &InboundService{}
  147. seed := []model.Client{
  148. {ID: "id-a", Email: "a@x", Enable: true, SubID: "s-a"},
  149. {ID: "id-b", Email: "b@x", Enable: true, SubID: "s-b"},
  150. {ID: "id-c", Email: "c@x", Enable: true, SubID: "s-c"},
  151. }
  152. ib := mkInbound(t, 21003, model.VLESS, clientsSettings(t, seed))
  153. if err := cs.SyncInbound(nil, ib.Id, seed); err != nil {
  154. t.Fatalf("seed SyncInbound: %v", err)
  155. }
  156. db := database.GetDB()
  157. if err := db.Model(&model.ClientRecord{}).Where("email = ?", "c@x").
  158. UpdateColumn("comment", "keep me").Error; err != nil {
  159. t.Fatalf("set comment: %v", err)
  160. }
  161. removedID := recordID(t, "b@x")
  162. stampLinkCreatedAt(t, ib.Id)
  163. if _, err := cs.DelInboundClientByEmail(is, ib.Id, "b@x", true, false); err != nil {
  164. t.Fatalf("DelInboundClientByEmail: %v", err)
  165. }
  166. links := linksOf(t, ib.Id)
  167. if _, still := links[removedID]; still {
  168. t.Error("b@x link survived the delete")
  169. }
  170. if len(links) != 2 {
  171. t.Fatalf("link count = %d, want 2", len(links))
  172. }
  173. for _, email := range []string{"a@x", "c@x"} {
  174. if links[recordID(t, email)].CreatedAt != 1 {
  175. t.Errorf("%s link was rebuilt by an unrelated delete", email)
  176. }
  177. }
  178. // Detach must not delete the record itself.
  179. var removed model.ClientRecord
  180. if err := db.Where("email = ?", "b@x").First(&removed).Error; err != nil {
  181. t.Fatalf("b@x record should survive a detach: %v", err)
  182. }
  183. var kept model.ClientRecord
  184. if err := db.Where("email = ?", "c@x").First(&kept).Error; err != nil {
  185. t.Fatalf("reload c@x: %v", err)
  186. }
  187. if kept.Comment != "keep me" {
  188. t.Errorf("bystander comment = %q, want %q", kept.Comment, "keep me")
  189. }
  190. }