mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 22:26:09 +00:00
f431e9cc03
* fix(inbounds): apply runtime changes after commit * ci: fix staticcheck findings
165 lines
5.9 KiB
Go
165 lines
5.9 KiB
Go
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")
|
|
}
|
|
}
|