| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- package service
- import (
- "os"
- "strings"
- "testing"
- "github.com/op/go-logging"
- "gorm.io/gorm"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
- )
- func durablePostgresDB(t *testing.T) *gorm.DB {
- t.Helper()
- if os.Getenv("XUI_DB_TYPE") != "postgres" || strings.TrimSpace(os.Getenv("XUI_DB_DSN")) == "" {
- t.Skip("set XUI_DB_TYPE=postgres and XUI_DB_DSN to run commit-failure injection")
- }
- portConflictLoggerOnce.Do(func() { xuilogger.InitLogger(logging.ERROR) })
- if err := database.InitDB(""); err != nil {
- t.Fatalf("InitDB: %v", err)
- }
- t.Cleanup(func() { _ = database.CloseDB() })
- return database.GetDB()
- }
- func durableTestInbound(nodeID *int, tag string, port int) *model.Inbound {
- return &model.Inbound{
- UserId: 1,
- NodeID: nodeID,
- Tag: tag,
- Remark: tag,
- Enable: true,
- Port: port,
- Protocol: model.VLESS,
- StreamSettings: `{"network":"tcp","security":"tls"}`,
- Settings: `{"clients":[],"decryption":"none"}`,
- }
- }
- func installDeferredCommitFailure(t *testing.T, db *gorm.DB, callbackKind, callbackName, triggerTable, parentTable, childTable string) {
- t.Helper()
- _ = db.Exec("DROP TABLE IF EXISTS " + childTable).Error
- _ = db.Exec("DROP TABLE IF EXISTS " + parentTable).Error
- if err := db.Exec("CREATE TABLE " + parentTable + " (id bigint PRIMARY KEY)").Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Exec("CREATE TABLE " + childTable + " (id bigint PRIMARY KEY, parent_id bigint REFERENCES " + parentTable + "(id) DEFERRABLE INITIALLY DEFERRED)").Error; err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() {
- _ = db.Exec("DROP TABLE IF EXISTS " + childTable).Error
- _ = db.Exec("DROP TABLE IF EXISTS " + parentTable).Error
- })
- cb := func(tx *gorm.DB) {
- if tx.Statement == nil || tx.Statement.Table != triggerTable {
- return
- }
- res := tx.Session(&gorm.Session{NewDB: true}).Exec("INSERT INTO " + childTable + " (id, parent_id) VALUES (1, 999999)")
- if res.Error != nil {
- tx.AddError(res.Error)
- }
- }
- switch callbackKind {
- case "create":
- if err := db.Callback().Create().After("gorm:create").Register(callbackName, cb); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { _ = db.Callback().Create().Remove(callbackName) })
- case "update":
- if err := db.Callback().Update().After("gorm:update").Register(callbackName, cb); err != nil {
- t.Fatal(err)
- }
- t.Cleanup(func() { _ = db.Callback().Update().Remove(callbackName) })
- default:
- t.Fatalf("unknown callback kind %q", callbackKind)
- }
- }
- func cleanupDurableInboundFixtures(t *testing.T, db *gorm.DB, tags ...string) {
- t.Helper()
- if len(tags) == 0 {
- return
- }
- cleanup := func() {
- _ = db.Exec("DELETE FROM hosts WHERE inbound_id IN (SELECT id FROM inbounds WHERE tag IN ?)", tags).Error
- _ = db.Exec("DELETE FROM client_inbounds WHERE inbound_id IN (SELECT id FROM inbounds WHERE tag IN ?)", tags).Error
- _ = db.Where("tag IN ?", tags).Delete(&model.Inbound{}).Error
- }
- cleanup()
- t.Cleanup(cleanup)
- }
- func TestAddInbound_PostgresCommitFailureMakesNoRuntimeCall(t *testing.T) {
- db := durablePostgresDB(t)
- nodeID, fake := setupNodeRuntime(t)
- tag := "durable-add-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name())
- cleanupDurableInboundFixtures(t, db, tag)
- installDeferredCommitFailure(t, db, "create", "durable:add_inbound_commit_failure", "inbounds", "durable_add_parent", "durable_add_child")
- inbound := durableTestInbound(&nodeID, tag, 25443)
- _, _, err := (&InboundService{}).AddInbound(inbound)
- if err == nil || !strings.Contains(strings.ToLower(err.Error()), "foreign key") {
- t.Fatalf("AddInbound error = %v, want deferred foreign-key commit failure", err)
- }
- if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
- t.Fatalf("runtime calls before failed commit: add=%d update=%d del=%d, want zero", fake.addInbound.Load(), fake.updateInbound.Load(), fake.delInbound.Load())
- }
- var rows int64
- if err := db.Model(&model.Inbound{}).Where("tag = ?", inbound.Tag).Count(&rows).Error; err != nil {
- t.Fatal(err)
- }
- if rows != 0 {
- t.Fatalf("rolled-back inbound rows = %d, want 0", rows)
- }
- var persisted model.Node
- if err := db.First(&persisted, nodeID).Error; err != nil {
- t.Fatal(err)
- }
- if persisted.ConfigDirty {
- t.Fatal("node dirty flag changed across failed commit")
- }
- }
- func TestUpdateInbound_PostgresCommitFailureMakesNoRuntimeCall(t *testing.T) {
- db := durablePostgresDB(t)
- nodeID, fake := setupNodeRuntime(t)
- tag := "durable-update-" + strings.NewReplacer("/", "-", " ", "-").Replace(t.Name())
- cleanupDurableInboundFixtures(t, db, tag)
- original := durableTestInbound(&nodeID, tag, 25444)
- original.Remark = "before"
- if err := db.Create(original).Error; err != nil {
- t.Fatal(err)
- }
- installDeferredCommitFailure(t, db, "update", "durable:update_inbound_commit_failure", "inbounds", "durable_update_parent", "durable_update_child")
- update := durableTestInbound(&nodeID, original.Tag, original.Port)
- update.Id = original.Id
- update.Remark = "after"
- _, _, err := (&InboundService{}).UpdateInbound(update)
- if err == nil || !strings.Contains(strings.ToLower(err.Error()), "foreign key") {
- t.Fatalf("UpdateInbound error = %v, want deferred foreign-key commit failure", err)
- }
- if fake.addInbound.Load() != 0 || fake.updateInbound.Load() != 0 || fake.delInbound.Load() != 0 {
- t.Fatalf("runtime calls before failed commit: add=%d update=%d del=%d, want zero", fake.addInbound.Load(), fake.updateInbound.Load(), fake.delInbound.Load())
- }
- var persisted model.Inbound
- if err := db.First(&persisted, original.Id).Error; err != nil {
- t.Fatal(err)
- }
- if persisted.Remark != "before" {
- t.Fatalf("remark after failed commit = %q, want before", persisted.Remark)
- }
- var persistedNode model.Node
- if err := db.First(&persistedNode, nodeID).Error; err != nil {
- t.Fatal(err)
- }
- if persistedNode.ConfigDirty {
- t.Fatal("node dirty flag changed across failed commit")
- }
- }
|