node_bulk_dispatch_test.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "sync/atomic"
  7. "testing"
  8. "github.com/google/uuid"
  9. "gorm.io/gorm"
  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/runtime"
  13. )
  14. // fakeNodeRuntime is a runtime.Runtime stub that counts the per-client dispatch
  15. // calls so a test can assert a bulk op does NOT stream one RPC per client.
  16. type fakeNodeRuntime struct {
  17. addInbound atomic.Int32
  18. delInbound atomic.Int32
  19. addClient atomic.Int32
  20. deleteClient atomic.Int32
  21. deleteUser atomic.Int32
  22. updateInbound atomic.Int32
  23. updateSubSort atomic.Int32
  24. updateUser atomic.Int32
  25. }
  26. func (f *fakeNodeRuntime) Name() string { return "fake-node" }
  27. func (f *fakeNodeRuntime) AddInbound(context.Context, *model.Inbound) error {
  28. f.addInbound.Add(1)
  29. return nil
  30. }
  31. func (f *fakeNodeRuntime) DelInbound(context.Context, *model.Inbound) error {
  32. f.delInbound.Add(1)
  33. return nil
  34. }
  35. func (f *fakeNodeRuntime) UpdateInbound(context.Context, *model.Inbound, *model.Inbound) error {
  36. f.updateInbound.Add(1)
  37. return nil
  38. }
  39. func (f *fakeNodeRuntime) SetInboundSubSortIndex(context.Context, *model.Inbound, int) error {
  40. f.updateSubSort.Add(1)
  41. return nil
  42. }
  43. func (f *fakeNodeRuntime) AddUser(context.Context, *model.Inbound, map[string]any) error { return nil }
  44. func (f *fakeNodeRuntime) RemoveUser(context.Context, *model.Inbound, string) error { return nil }
  45. func (f *fakeNodeRuntime) UpdateUser(context.Context, *model.Inbound, string, model.Client) error {
  46. f.updateUser.Add(1)
  47. return nil
  48. }
  49. func (f *fakeNodeRuntime) DeleteUser(context.Context, *model.Inbound, string) error {
  50. f.deleteUser.Add(1)
  51. return nil
  52. }
  53. func (f *fakeNodeRuntime) DeleteClient(context.Context, string) error {
  54. f.deleteClient.Add(1)
  55. return nil
  56. }
  57. func (f *fakeNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error {
  58. f.addClient.Add(1)
  59. return nil
  60. }
  61. func (f *fakeNodeRuntime) RestartXray(context.Context) error { return nil }
  62. func (f *fakeNodeRuntime) ResetClientTraffic(context.Context, *model.Inbound, string) error {
  63. return nil
  64. }
  65. func (f *fakeNodeRuntime) ResetInboundTraffic(context.Context, *model.Inbound) error { return nil }
  66. func (f *fakeNodeRuntime) ResetAllTraffics(context.Context) error { return nil }
  67. // startSerializedWriter runs the single traffic-writer goroutine for the test, so
  68. // concurrent service writes take the serialized path production uses.
  69. func startSerializedWriter(t *testing.T) {
  70. t.Helper()
  71. resetTrafficWriterForTest(t)
  72. StartTrafficWriter()
  73. }
  74. // useTestRuntimeManager swaps in a fresh runtime.Manager for the test and puts
  75. // the previous one back afterwards, so overrides can't leak between tests.
  76. func useTestRuntimeManager(t *testing.T) *runtime.Manager {
  77. t.Helper()
  78. prev := runtime.GetManager()
  79. mgr := runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})
  80. runtime.SetManager(mgr)
  81. t.Cleanup(func() { runtime.SetManager(prev) })
  82. return mgr
  83. }
  84. // panicNodeRuntime panics on the per-client push, standing in for a bug in the
  85. // apply path that would otherwise unwind straight out of a fanout goroutine.
  86. type panicNodeRuntime struct{ fakeNodeRuntime }
  87. func (p *panicNodeRuntime) AddClient(context.Context, *model.Inbound, model.Client) error {
  88. panic("boom from node runtime")
  89. }
  90. // setupNodeRuntime wires an online node + a fake runtime override so a test can
  91. // drive the service node-dispatch path without a network node.
  92. func setupNodeRuntime(t *testing.T) (int, *fakeNodeRuntime) {
  93. t.Helper()
  94. mgr := useTestRuntimeManager(t)
  95. node := &model.Node{Name: "n1-" + t.Name(), Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true, Status: "online"}
  96. if err := database.GetDB().Create(node).Error; err != nil {
  97. t.Fatalf("create node: %v", err)
  98. }
  99. t.Cleanup(func() {
  100. _ = database.GetDB().Where("id = ?", node.Id).Delete(&model.Node{}).Error
  101. })
  102. fake := &fakeNodeRuntime{}
  103. mgr.SetRuntimeOverride(node.Id, fake)
  104. return node.Id, fake
  105. }
  106. func nodeInbound(t *testing.T, nodeID, port int, clients []model.Client) *model.Inbound {
  107. t.Helper()
  108. if clients == nil {
  109. clients = []model.Client{}
  110. }
  111. ib := &model.Inbound{
  112. UserId: 1, NodeID: &nodeID, Tag: fmt.Sprintf("in-%d", port), Enable: true,
  113. Port: port, Protocol: model.VLESS, Settings: clientsSettings(t, clients),
  114. }
  115. if err := database.GetDB().Create(ib).Error; err != nil {
  116. t.Fatalf("create node inbound: %v", err)
  117. }
  118. if err := (&ClientService{}).SyncInbound(nil, ib.Id, clients); err != nil {
  119. t.Fatalf("seed SyncInbound: %v", err)
  120. }
  121. return ib
  122. }
  123. func makeNodeClients(n int) []model.Client {
  124. out := make([]model.Client, n)
  125. for i := range n {
  126. out[i] = model.Client{ID: uuid.NewString(), Email: fmt.Sprintf("nu-%05d@x", i), Enable: true}
  127. }
  128. return out
  129. }
  130. // TestNodeBulk_LargeAddFoldsToDirty: adding more than the threshold of clients to
  131. // an online node inbound must NOT stream one AddClient RPC per client; it marks
  132. // the node dirty so a single reconcile push converges it instead.
  133. func TestNodeBulk_LargeAddFoldsToDirty(t *testing.T) {
  134. setupBulkDB(t)
  135. nodeID, fake := setupNodeRuntime(t)
  136. ib := nodeInbound(t, nodeID, 30001, nil)
  137. svc := &ClientService{}
  138. inboundSvc := &InboundService{}
  139. add := makeNodeClients(nodeBulkPushThreshold + 10)
  140. if _, err := svc.AddInboundClient(inboundSvc, &model.Inbound{Id: ib.Id, Protocol: model.VLESS, Settings: clientsSettings(t, add)}); err != nil {
  141. t.Fatalf("AddInboundClient: %v", err)
  142. }
  143. if got := fake.addClient.Load(); got != 0 {
  144. t.Fatalf("large add streamed %d AddClient RPCs, want 0 (should fold to dirty)", got)
  145. }
  146. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  147. t.Fatalf("NodeSyncState: %v", err)
  148. } else if !dirty {
  149. t.Fatal("large add must mark the node dirty")
  150. }
  151. }
  152. // TestNodeBulk_SmallAddPushesLive: a small add stays on the live per-client path.
  153. func TestNodeBulk_SmallAddPushesLive(t *testing.T) {
  154. setupBulkDB(t)
  155. nodeID, fake := setupNodeRuntime(t)
  156. ib := nodeInbound(t, nodeID, 30002, nil)
  157. svc := &ClientService{}
  158. inboundSvc := &InboundService{}
  159. const small = 3
  160. add := makeNodeClients(small)
  161. if _, err := svc.AddInboundClient(inboundSvc, &model.Inbound{Id: ib.Id, Protocol: model.VLESS, Settings: clientsSettings(t, add)}); err != nil {
  162. t.Fatalf("AddInboundClient: %v", err)
  163. }
  164. if got := fake.addClient.Load(); got != int32(small) {
  165. t.Fatalf("small add streamed %d AddClient RPCs, want %d", got, small)
  166. }
  167. }
  168. func TestNodeBulkAdjustDoesNotPushBeforeFailedCommit(t *testing.T) {
  169. setupBulkDB(t)
  170. nodeID, fake := setupNodeRuntime(t)
  171. client := model.Client{
  172. ID: uuid.NewString(),
  173. Email: "txfail-adjust@x",
  174. Enable: true,
  175. ExpiryTime: 1_900_000_000_000,
  176. }
  177. nodeInbound(t, nodeID, 30022, []model.Client{client})
  178. db := database.GetDB()
  179. const callbackName = "bulk-adjust:fail-inbound-update"
  180. if err := db.Callback().Update().After("gorm:update").Register(callbackName, func(tx *gorm.DB) {
  181. if tx.Statement != nil && tx.Statement.Table == "inbounds" {
  182. tx.AddError(errors.New("injected bulk-adjust transaction failure"))
  183. }
  184. }); err != nil {
  185. t.Fatalf("register callback: %v", err)
  186. }
  187. t.Cleanup(func() { _ = db.Callback().Update().Remove(callbackName) })
  188. result, _, err := (&ClientService{}).BulkAdjust(&InboundService{}, []string{client.Email}, 1, 0, "")
  189. if err != nil {
  190. t.Fatalf("BulkAdjust: %v", err)
  191. }
  192. if result.Adjusted != 0 || len(result.Skipped) != 1 {
  193. t.Fatalf("BulkAdjust result = %+v, want one skipped client after injected failure", result)
  194. }
  195. if got := fake.updateUser.Load(); got != 0 {
  196. t.Fatalf("failed transaction pushed %d UpdateUser call(s) to the node, want 0", got)
  197. }
  198. }
  199. func TestNodeBulkDeleteDoesNotPushBeforeFailedCommit(t *testing.T) {
  200. setupBulkDB(t)
  201. nodeID, fake := setupNodeRuntime(t)
  202. client := model.Client{ID: uuid.NewString(), Email: "txfail-delete@x", Enable: true}
  203. nodeInbound(t, nodeID, 30023, []model.Client{client})
  204. db := database.GetDB()
  205. const callbackName = "bulk-delete:fail-inbound-update"
  206. if err := db.Callback().Update().After("gorm:update").Register(callbackName, func(tx *gorm.DB) {
  207. if tx.Statement != nil && tx.Statement.Table == "inbounds" {
  208. tx.AddError(errors.New("injected bulk-delete transaction failure"))
  209. }
  210. }); err != nil {
  211. t.Fatalf("register callback: %v", err)
  212. }
  213. t.Cleanup(func() { _ = db.Callback().Update().Remove(callbackName) })
  214. result, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{client.Email}, true)
  215. if err != nil {
  216. t.Fatalf("BulkDelete: %v", err)
  217. }
  218. if result.Deleted != 0 || len(result.Skipped) != 1 {
  219. t.Fatalf("BulkDelete result = %+v, want one skipped client after injected failure", result)
  220. }
  221. if got := fake.deleteClient.Load() + fake.deleteUser.Load(); got != 0 {
  222. t.Fatalf("failed transaction pushed %d delete call(s) to the node, want 0", got)
  223. }
  224. if isClientEmailTombstoned(client.Email) {
  225. t.Fatal("failed bulk delete left a live tombstone")
  226. }
  227. }
  228. func TestNodeBulkSmallDeleteRemovesWholeRemoteClient(t *testing.T) {
  229. setupBulkDB(t)
  230. nodeID, fake := setupNodeRuntime(t)
  231. client := model.Client{ID: uuid.NewString(), Email: "full-delete@x", Enable: true}
  232. nodeInbound(t, nodeID, 30024, []model.Client{client})
  233. var record model.ClientRecord
  234. if err := database.GetDB().Where("email = ?", client.Email).First(&record).Error; err != nil {
  235. t.Fatalf("load client record: %v", err)
  236. }
  237. result, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{client.Email}, true)
  238. if err != nil {
  239. t.Fatalf("BulkDelete: %v", err)
  240. }
  241. if result.Deleted != 1 || len(result.Skipped) != 0 {
  242. t.Fatalf("BulkDelete result = %+v, want one deleted client", result)
  243. }
  244. if got := fake.deleteClient.Load(); got != 1 {
  245. t.Fatalf("remote DeleteClient calls = %d, want 1", got)
  246. }
  247. if got := fake.deleteUser.Load(); got != 0 {
  248. t.Fatalf("remote DeleteUser detach calls = %d, want 0 for full deletion", got)
  249. }
  250. var records, links int64
  251. if err := database.GetDB().Model(&model.ClientRecord{}).Where("email = ?", client.Email).Count(&records).Error; err != nil {
  252. t.Fatalf("count client records: %v", err)
  253. }
  254. if err := database.GetDB().Model(&model.ClientInbound{}).Where("client_id = ?", record.Id).Count(&links).Error; err != nil {
  255. t.Fatalf("count client links: %v", err)
  256. }
  257. if records != 0 || links != 0 {
  258. t.Fatalf("bulk delete left records=%d links=%d, want 0/0", records, links)
  259. }
  260. }
  261. func TestNodeBulkDeleteTreatsMissingSettingsClientAsAlreadyDeleted(t *testing.T) {
  262. setupBulkDB(t)
  263. nodeID, fake := setupNodeRuntime(t)
  264. client := model.Client{ID: uuid.NewString(), Email: "drifted-delete@x", Enable: true}
  265. ib := nodeInbound(t, nodeID, 30025, []model.Client{client})
  266. // Simulate a stale normalized link after the client has already disappeared
  267. // from the inbound settings JSON.
  268. if err := database.GetDB().Model(&model.Inbound{}).Where("id = ?", ib.Id).
  269. Update("settings", clientsSettings(t, nil)).Error; err != nil {
  270. t.Fatalf("drift inbound settings: %v", err)
  271. }
  272. result, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{client.Email}, true)
  273. if err != nil {
  274. t.Fatalf("BulkDelete: %v", err)
  275. }
  276. if result.Deleted != 1 || len(result.Skipped) != 0 {
  277. t.Fatalf("BulkDelete result = %+v, want one deleted client", result)
  278. }
  279. if got := fake.deleteClient.Load(); got != 1 {
  280. t.Fatalf("remote DeleteClient calls = %d, want 1", got)
  281. }
  282. var records, links int64
  283. if err := database.GetDB().Model(&model.ClientRecord{}).Where("email = ?", client.Email).Count(&records).Error; err != nil {
  284. t.Fatalf("count client records: %v", err)
  285. }
  286. if err := database.GetDB().Model(&model.ClientInbound{}).Where("inbound_id = ?", ib.Id).Count(&links).Error; err != nil {
  287. t.Fatalf("count client links: %v", err)
  288. }
  289. if records != 0 || links != 0 {
  290. t.Fatalf("bulk delete left records=%d links=%d, want 0/0", records, links)
  291. }
  292. if !isClientEmailTombstoned(client.Email) {
  293. t.Fatal("successful bulk delete withdrew the client tombstone")
  294. }
  295. t.Cleanup(func() { withdrawClientTombstones(client.Email) })
  296. }
  297. func TestNodeBulkDeleteCompletesAcrossPresentAndMissingSettings(t *testing.T) {
  298. setupBulkDB(t)
  299. nodeID, fake := setupNodeRuntime(t)
  300. client := model.Client{ID: uuid.NewString(), Email: "mixed-delete@x", Enable: true}
  301. drifted := nodeInbound(t, nodeID, 30026, []model.Client{client})
  302. nodeInbound(t, nodeID, 30027, []model.Client{client})
  303. if err := database.GetDB().Model(&model.Inbound{}).Where("id = ?", drifted.Id).
  304. Update("settings", clientsSettings(t, nil)).Error; err != nil {
  305. t.Fatalf("drift inbound settings: %v", err)
  306. }
  307. result, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{client.Email}, true)
  308. if err != nil {
  309. t.Fatalf("BulkDelete: %v", err)
  310. }
  311. if result.Deleted != 1 || len(result.Skipped) != 0 {
  312. t.Fatalf("BulkDelete result = %+v, want one deleted client", result)
  313. }
  314. if got := fake.deleteClient.Load(); got != 2 {
  315. t.Fatalf("remote DeleteClient calls = %d, want one per node inbound", got)
  316. }
  317. }
  318. func TestNodeBulkDeleteMalformedSettingsWithdrawsTombstone(t *testing.T) {
  319. setupBulkDB(t)
  320. nodeID, fake := setupNodeRuntime(t)
  321. client := model.Client{ID: uuid.NewString(), Email: "malformed-delete@x", Enable: true}
  322. ib := nodeInbound(t, nodeID, 30028, []model.Client{client})
  323. if err := database.GetDB().Model(&model.Inbound{}).Where("id = ?", ib.Id).
  324. Update("settings", `{"clients":[`).Error; err != nil {
  325. t.Fatalf("break inbound settings: %v", err)
  326. }
  327. result, _, err := (&ClientService{}).BulkDelete(&InboundService{}, []string{client.Email}, true)
  328. if err != nil {
  329. t.Fatalf("BulkDelete: %v", err)
  330. }
  331. if result.Deleted != 0 || len(result.Skipped) != 1 {
  332. t.Fatalf("BulkDelete result = %+v, want one skipped client", result)
  333. }
  334. if got := fake.deleteClient.Load() + fake.deleteUser.Load(); got != 0 {
  335. t.Fatalf("malformed settings pushed %d delete call(s) to the node, want 0", got)
  336. }
  337. if isClientEmailTombstoned(client.Email) {
  338. t.Fatal("failed bulk delete left a live tombstone")
  339. }
  340. }
  341. func TestNodeUpdateInboundClientNoopSkipsRuntimeAndDirty(t *testing.T) {
  342. setupBulkDB(t)
  343. nodeID, fake := setupNodeRuntime(t)
  344. client := model.Client{
  345. ID: uuid.NewString(),
  346. Email: "noop@x",
  347. SubID: "sub-noop",
  348. Enable: true,
  349. CreatedAt: 111,
  350. UpdatedAt: 222,
  351. }
  352. ib := nodeInbound(t, nodeID, 30020, []model.Client{client})
  353. svc := &ClientService{}
  354. inboundSvc := &InboundService{}
  355. if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{
  356. Id: ib.Id,
  357. Protocol: model.VLESS,
  358. Settings: clientsSettings(t, []model.Client{client}),
  359. }, client.Email); err != nil {
  360. t.Fatalf("UpdateInboundClient: %v", err)
  361. }
  362. if got := fake.updateUser.Load(); got != 0 {
  363. t.Fatalf("no-op update streamed %d UpdateUser RPCs, want 0", got)
  364. }
  365. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  366. t.Fatalf("NodeSyncState: %v", err)
  367. } else if dirty {
  368. t.Fatal("no-op update must not mark the node dirty")
  369. }
  370. reloaded, err := inboundSvc.GetInbound(ib.Id)
  371. if err != nil {
  372. t.Fatalf("GetInbound: %v", err)
  373. }
  374. if reloaded.Settings != ib.Settings {
  375. t.Fatal("no-op update rewrote inbound settings")
  376. }
  377. }
  378. func TestNodeUpdateInboundClientLivePushKeepsDirtyBackup(t *testing.T) {
  379. setupBulkDB(t)
  380. nodeID, fake := setupNodeRuntime(t)
  381. client := model.Client{
  382. ID: uuid.NewString(),
  383. Email: "edit@x",
  384. SubID: "sub-edit",
  385. Enable: true,
  386. CreatedAt: 111,
  387. UpdatedAt: 222,
  388. }
  389. ib := nodeInbound(t, nodeID, 30021, []model.Client{client})
  390. edited := client
  391. edited.Comment = "changed"
  392. svc := &ClientService{}
  393. inboundSvc := &InboundService{}
  394. if _, err := svc.UpdateInboundClient(inboundSvc, &model.Inbound{
  395. Id: ib.Id,
  396. Protocol: model.VLESS,
  397. Settings: clientsSettings(t, []model.Client{edited}),
  398. }, client.Email); err != nil {
  399. t.Fatalf("UpdateInboundClient: %v", err)
  400. }
  401. if got := fake.updateUser.Load(); got != 1 {
  402. t.Fatalf("edit streamed %d UpdateUser RPCs, want 1", got)
  403. }
  404. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  405. t.Fatalf("NodeSyncState: %v", err)
  406. } else if !dirty {
  407. t.Fatal("successful live update should keep node dirty as reconcile backup")
  408. }
  409. }
  410. // TestNodeBulk_LargeDeleteFoldsToDirty: deleting more than the threshold from an
  411. // online node inbound must fold into a reconcile rather than per-client deletes.
  412. func TestNodeBulk_LargeDeleteFoldsToDirty(t *testing.T) {
  413. setupBulkDB(t)
  414. nodeID, fake := setupNodeRuntime(t)
  415. seed := makeNodeClients(nodeBulkPushThreshold + 10)
  416. nodeInbound(t, nodeID, 30003, seed)
  417. svc := &ClientService{}
  418. inboundSvc := &InboundService{}
  419. emails := make([]string, len(seed))
  420. for i := range seed {
  421. emails[i] = seed[i].Email
  422. }
  423. if _, _, err := svc.BulkDelete(inboundSvc, emails, false); err != nil {
  424. t.Fatalf("BulkDelete: %v", err)
  425. }
  426. if got := fake.deleteClient.Load() + fake.deleteUser.Load(); got != 0 {
  427. t.Fatalf("large delete streamed %d delete RPCs, want 0 (should fold to dirty)", got)
  428. }
  429. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  430. t.Fatalf("NodeSyncState: %v", err)
  431. } else if !dirty {
  432. t.Fatal("large delete must mark the node dirty")
  433. }
  434. }
  435. func TestDelInbound_NodeSelectedModeDeletesRemoteImmediately(t *testing.T) {
  436. setupBulkDB(t)
  437. nodeID, fake := setupNodeRuntime(t)
  438. if err := database.GetDB().Model(&model.Node{}).Where("id = ?", nodeID).
  439. Updates(map[string]any{
  440. "inbound_sync_mode": "selected",
  441. "inbound_tags": []string{"other-tag"},
  442. }).Error; err != nil {
  443. t.Fatalf("set selected mode: %v", err)
  444. }
  445. ib := nodeInbound(t, nodeID, 30004, makeNodeClients(1))
  446. needRestart, err := (&InboundService{}).DelInbound(ib.Id)
  447. if err != nil {
  448. t.Fatalf("DelInbound: %v", err)
  449. }
  450. if needRestart {
  451. t.Fatal("node-owned delete should not request local restart")
  452. }
  453. if got := fake.delInbound.Load(); got != 1 {
  454. t.Fatalf("node-owned delete streamed %d DelInbound RPCs, want 1", got)
  455. }
  456. var count int64
  457. if err := database.GetDB().Model(&model.Inbound{}).Where("id = ?", ib.Id).Count(&count).Error; err != nil {
  458. t.Fatalf("count inbound: %v", err)
  459. }
  460. if count != 0 {
  461. t.Fatalf("deleted inbound row count = %d, want 0", count)
  462. }
  463. if _, _, dirty, _, err := (&NodeService{}).NodeSyncState(nodeID); err != nil {
  464. t.Fatalf("NodeSyncState: %v", err)
  465. } else if !dirty {
  466. t.Fatal("node-owned delete should still mark the node dirty as reconcile backup")
  467. }
  468. }