inbound_durable_postgres_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package service
  2. import (
  3. "os"
  4. "strings"
  5. "testing"
  6. "github.com/op/go-logging"
  7. "gorm.io/gorm"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  11. )
  12. func durablePostgresDB(t *testing.T) *gorm.DB {
  13. t.Helper()
  14. if os.Getenv("XUI_DB_TYPE") != "postgres" || strings.TrimSpace(os.Getenv("XUI_DB_DSN")) == "" {
  15. t.Skip("set XUI_DB_TYPE=postgres and XUI_DB_DSN to run commit-failure injection")
  16. }
  17. portConflictLoggerOnce.Do(func() { xuilogger.InitLogger(logging.ERROR) })
  18. if err := database.InitDB(""); err != nil {
  19. t.Fatalf("InitDB: %v", err)
  20. }
  21. t.Cleanup(func() { _ = database.CloseDB() })
  22. return database.GetDB()
  23. }
  24. func durableTestInbound(nodeID *int, tag string, port int) *model.Inbound {
  25. return &model.Inbound{
  26. UserId: 1,
  27. NodeID: nodeID,
  28. Tag: tag,
  29. Remark: tag,
  30. Enable: true,
  31. Port: port,
  32. Protocol: model.VLESS,
  33. StreamSettings: `{"network":"tcp","security":"tls"}`,
  34. Settings: `{"clients":[],"decryption":"none"}`,
  35. }
  36. }
  37. func installDeferredCommitFailure(t *testing.T, db *gorm.DB, callbackKind, callbackName, triggerTable, parentTable, childTable string) {
  38. t.Helper()
  39. _ = db.Exec("DROP TABLE IF EXISTS " + childTable).Error
  40. _ = db.Exec("DROP TABLE IF EXISTS " + parentTable).Error
  41. if err := db.Exec("CREATE TABLE " + parentTable + " (id bigint PRIMARY KEY)").Error; err != nil {
  42. t.Fatal(err)
  43. }
  44. if err := db.Exec("CREATE TABLE " + childTable + " (id bigint PRIMARY KEY, parent_id bigint REFERENCES " + parentTable + "(id) DEFERRABLE INITIALLY DEFERRED)").Error; err != nil {
  45. t.Fatal(err)
  46. }
  47. t.Cleanup(func() {
  48. _ = db.Exec("DROP TABLE IF EXISTS " + childTable).Error
  49. _ = db.Exec("DROP TABLE IF EXISTS " + parentTable).Error
  50. })
  51. cb := func(tx *gorm.DB) {
  52. if tx.Statement == nil || tx.Statement.Table != triggerTable {
  53. return
  54. }
  55. res := tx.Session(&gorm.Session{NewDB: true}).Exec("INSERT INTO " + childTable + " (id, parent_id) VALUES (1, 999999)")
  56. if res.Error != nil {
  57. tx.AddError(res.Error)
  58. }
  59. }
  60. switch callbackKind {
  61. case "create":
  62. if err := db.Callback().Create().After("gorm:create").Register(callbackName, cb); err != nil {
  63. t.Fatal(err)
  64. }
  65. t.Cleanup(func() { _ = db.Callback().Create().Remove(callbackName) })
  66. case "update":
  67. if err := db.Callback().Update().After("gorm:update").Register(callbackName, cb); err != nil {
  68. t.Fatal(err)
  69. }
  70. t.Cleanup(func() { _ = db.Callback().Update().Remove(callbackName) })
  71. default:
  72. t.Fatalf("unknown callback kind %q", callbackKind)
  73. }
  74. }
  75. func cleanupDurableInboundFixtures(t *testing.T, db *gorm.DB, tags ...string) {
  76. t.Helper()
  77. if len(tags) == 0 {
  78. return
  79. }
  80. cleanup := func() {
  81. _ = db.Exec("DELETE FROM hosts WHERE inbound_id IN (SELECT id FROM inbounds WHERE tag IN ?)", tags).Error
  82. _ = db.Exec("DELETE FROM client_inbounds WHERE inbound_id IN (SELECT id FROM inbounds WHERE tag IN ?)", tags).Error
  83. _ = db.Where("tag IN ?", tags).Delete(&model.Inbound{}).Error
  84. }
  85. cleanup()
  86. t.Cleanup(cleanup)
  87. }
  88. func TestAddInbound_PostgresCommitFailureMakesNoRuntimeCall(t *testing.T) {
  89. db := durablePostgresDB(t)
  90. nodeID, fake := setupNodeRuntime(t)
  91. tag := "durable-add-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name())
  92. cleanupDurableInboundFixtures(t, db, tag)
  93. installDeferredCommitFailure(t, db, "create", "durable:add_inbound_commit_failure", "inbounds", "durable_add_parent", "durable_add_child")
  94. inbound := durableTestInbound(&nodeID, tag, 25443)
  95. _, _, err := (&InboundService{}).AddInbound(inbound)
  96. if err == nil || !strings.Contains(strings.ToLower(err.Error()), "foreign key") {
  97. t.Fatalf("AddInbound error = %v, want deferred foreign-key commit failure", err)
  98. }
  99. if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
  100. t.Fatalf("runtime calls before failed commit: add=%d update=%d del=%d, want zero", fake.addInbound.Load(), fake.updateInbound.Load(), fake.delInbound.Load())
  101. }
  102. var rows int64
  103. if err := db.Model(&model.Inbound{}).Where("tag = ?", inbound.Tag).Count(&rows).Error; err != nil {
  104. t.Fatal(err)
  105. }
  106. if rows != 0 {
  107. t.Fatalf("rolled-back inbound rows = %d, want 0", rows)
  108. }
  109. var persisted model.Node
  110. if err := db.First(&persisted, nodeID).Error; err != nil {
  111. t.Fatal(err)
  112. }
  113. if persisted.ConfigDirty {
  114. t.Fatal("node dirty flag changed across failed commit")
  115. }
  116. }
  117. func TestUpdateInbound_PostgresCommitFailureMakesNoRuntimeCall(t *testing.T) {
  118. db := durablePostgresDB(t)
  119. nodeID, fake := setupNodeRuntime(t)
  120. tag := "durable-update-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name())
  121. cleanupDurableInboundFixtures(t, db, tag)
  122. original := durableTestInbound(&nodeID, tag, 25444)
  123. original.Remark = "before"
  124. if err := db.Create(original).Error; err != nil {
  125. t.Fatal(err)
  126. }
  127. installDeferredCommitFailure(t, db, "update", "durable:update_inbound_commit_failure", "inbounds", "durable_update_parent", "durable_update_child")
  128. update := durableTestInbound(&nodeID, original.Tag, original.Port)
  129. update.Id = original.Id
  130. update.Remark = "after"
  131. _, _, err := (&InboundService{}).UpdateInbound(update)
  132. if err == nil || !strings.Contains(strings.ToLower(err.Error()), "foreign key") {
  133. t.Fatalf("UpdateInbound error = %v, want deferred foreign-key commit failure", err)
  134. }
  135. if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
  136. t.Fatalf("runtime calls before failed commit: add=%d update=%d del=%d, want zero", fake.addInbound.Load(), fake.updateInbound.Load(), fake.delInbound.Load())
  137. }
  138. var persisted model.Inbound
  139. if err := db.First(&persisted, original.Id).Error; err != nil {
  140. t.Fatal(err)
  141. }
  142. if persisted.Remark != "before" {
  143. t.Fatalf("remark after failed commit = %q, want before", persisted.Remark)
  144. }
  145. var persistedNode model.Node
  146. if err := db.First(&persistedNode, nodeID).Error; err != nil {
  147. t.Fatal(err)
  148. }
  149. if persistedNode.ConfigDirty {
  150. t.Fatal("node dirty flag changed across failed commit")
  151. }
  152. }