From 12d51d7195afecde284c82dbe533ed6c6d19eb26 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sun, 27 Sep 2026 03:04:50 +0200 Subject: [PATCH] perf(tests): copy a migrated template DB instead of migrating per test Most tests opened a throwaway panel DB with database.InitDB, which runs the full AutoMigrate + seed on an empty file every time: ~230ms, and ~850ms under -race because GORM's reflection-heavy migration is what the detector slows most. internal/web/service does this in ~550 of its 830 tests, so the CI race job spent ~10 of its ~14.6 minutes re-migrating empty databases. internal/database/dbtest.InitDB migrates once per test process, then hands each test its own copy of that file (~130ms under -race) and registers the CloseDB cleanup. The copy then goes through InitDB like a panel restart, so every test still starts from the state a fresh install has. Tests that reopen an existing file, migrate a hand-built legacy DB or target Postgres keep calling database.InitDB. Locally under -race: internal/web/service 626s (last CI run) -> 114s, internal/sub 246s -> 35s. --- .github/claude/issue-analyst-context.md | 4 +- CLAUDE.md | 8 ++- CONTRIBUTING.md | 2 +- api_token_cli_test.go | 6 +- internal/database/dbtest/dbtest.go | 67 +++++++++++++++++++ internal/database/dbtest/dbtest_test.go | 36 ++++++++++ internal/sub/build_urls_test.go | 8 +-- internal/sub/external_hwid_test.go | 7 +- internal/sub/external_subscription_test.go | 11 +-- internal/sub/host_sub_test.go | 6 +- internal/sub/hwid_controller_test.go | 6 +- internal/sub/mutation_audit_test.go | 6 +- internal/sub/remote_routing_test.go | 6 +- internal/sub/service_dedup_test.go | 11 +-- internal/sub/service_info_node_test.go | 8 +-- internal/sub/service_orphaned_stats_test.go | 6 +- internal/sub/service_sort_test.go | 6 +- internal/sub/service_userinfo_test.go | 6 +- internal/sub/sub_scale_test.go | 6 +- internal/web/controller/api_auth_test.go | 6 +- .../controller/client_partial_apply_test.go | 11 +-- .../controller/client_renewal_preview_test.go | 6 +- internal/web/controller/host_test.go | 6 +- .../web/controller/inbound_node_sync_test.go | 6 +- .../node_credentials_writeonly_test.go | 6 +- internal/web/controller/setting_test.go | 20 ++---- internal/web/controller/sub_balancer_test.go | 7 +- .../web/controller/xray_setting_warp_test.go | 6 +- .../check_client_ip_job_integration_test.go | 11 +-- internal/web/job/check_client_ip_job_test.go | 6 +- .../web/job/check_client_ip_scale_test.go | 6 +- internal/web/job/ldap_sync_job_test.go | 6 +- .../job/node_heartbeat_descendants_test.go | 6 +- .../web/job/node_traffic_sync_ip_push_test.go | 6 +- .../node_traffic_sync_online_prune_test.go | 6 +- .../web/job/node_transition_burst_test.go | 6 +- .../job/periodic_traffic_reset_client_test.go | 6 +- internal/web/routes_contract_test.go | 7 +- internal/web/service/bulk_clients_test.go | 6 +- .../web/service/client_apply_field_test.go | 11 +-- .../web/service/client_effective_flow_test.go | 6 +- .../web/service/client_flow_isolation_test.go | 26 ++----- .../service/client_group_node_sync_test.go | 16 ++--- internal/web/service/client_hwid_test.go | 6 +- internal/web/service/client_hwid_tx_test.go | 6 +- .../web/service/client_sync_mtproto_test.go | 6 +- .../service/client_sync_multiprotocol_test.go | 11 +-- internal/web/service/discord/discord_test.go | 7 +- internal/web/service/email/email_test.go | 12 +--- internal/web/service/happ_test.go | 6 +- .../service/inbound_client_ips_merge_test.go | 6 +- .../service/inbound_client_traffic_test.go | 16 ++--- .../web/service/inbound_disable_flow_test.go | 6 +- .../web/service/inbound_flow_restore_test.go | 6 +- .../web/service/inbound_migration_test.go | 16 ++--- internal/web/service/inbound_subsort_test.go | 11 +-- .../web/service/inbound_tgbot_lookup_test.go | 6 +- internal/web/service/integration/pia_test.go | 7 +- .../integration/warp_change_ip_test.go | 11 +-- .../service/integration/warp_response_test.go | 7 +- .../service/node_client_traffic_sum_test.go | 6 +- .../web/service/panel/api_token_scope_test.go | 10 +-- internal/web/service/panel/api_token_test.go | 16 ++--- internal/web/service/panel/sponsor_test.go | 7 +- internal/web/service/port_conflict_test.go | 10 +-- internal/web/service/scale_helpers_test.go | 6 +- .../web/service/server_cold_status_test.go | 7 +- internal/web/service/server_geofile_test.go | 7 +- .../web/service/server_import_sniff_test.go | 6 +- .../web/service/server_import_stage_test.go | 6 +- .../service/server_public_ip_async_test.go | 7 +- internal/web/service/setting_mtls_test.go | 6 +- internal/web/service/setting_security_test.go | 10 +-- internal/web/service/sub_balancer_test.go | 11 +-- .../tgbot/tgbot_add_client_picker_test.go | 6 +- .../tgbot_client_draft_per_admin_test.go | 7 +- .../tgbot/tgbot_client_draft_per_chat_test.go | 7 +- .../service/tgbot/tgbot_client_expiry_test.go | 7 +- .../tgbot/tgbot_client_links_authz_test.go | 6 +- .../tgbot/tgbot_report_batching_test.go | 6 +- .../service/tgbot/tgbot_stale_button_test.go | 7 +- internal/web/service/xray_metrics_test.go | 12 +--- 82 files changed, 289 insertions(+), 451 deletions(-) create mode 100644 internal/database/dbtest/dbtest.go create mode 100644 internal/database/dbtest/dbtest_test.go diff --git a/.github/claude/issue-analyst-context.md b/.github/claude/issue-analyst-context.md index 759538d8f..042fd34e2 100644 --- a/.github/claude/issue-analyst-context.md +++ b/.github/claude/issue-analyst-context.md @@ -100,8 +100,8 @@ question it already answers. subtests and `t.Helper()` on helpers. An assertion must pin the exact value, typed error or emitted string — `err != nil` and `len(x) > 0` are findings, not nits. Prefer real dependencies: a throwaway DB via - `database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` with `t.Cleanup`, and - `httptest` for HTTP. `internal/sub`'s `initSubDB(t)` is the template. + `dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))` + (`internal/database/dbtest`), and `httptest` for HTTP. `internal/sub`'s `initSubDB(t)` is the template. A test must FAIL without its fix; one that passes either way certifies nothing and then gets cited as proof the fix works. diff --git a/CLAUDE.md b/CLAUDE.md index 56bb624ab..cd3298d56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,9 +154,11 @@ file locations when it can answer in one hop. - Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests, `t.Helper()` on helpers. Assert the exact value / typed error / emitted string, never just `err != nil`. Prefer real deps over mocks: throwaway DB via - `database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` + - `t.Cleanup(func() { _ = database.CloseDB() })`; `httptest` for HTTP. - `internal/sub`'s `initSubDB(t)` is the template. + `dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))` + (`internal/database/dbtest`: copies a once-migrated template and registers + `CloseDB` cleanup; a fresh `database.InitDB` costs ~7x more, ~850ms under + `-race`); `httptest` for HTTP. Keep `database.InitDB` for reopening a file or + migrating a hand-built legacy DB. `internal/sub`'s `initSubDB(t)` is the template. - Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`. - Postgres, xray-gRPC-e2e and scale tests `t.Skip` unless `XUI_TEST_PG_DSN`, `XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY` or `XUI_SCALE_TEST` is set — a diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f45bbeb2..79991fa73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -253,7 +253,7 @@ Tests live next to the code (`foo.go` ↔ `foo_test.go`); frontend specs and gol - **Stdlib `testing` only** — no testify. Table-driven with `t.Run` subtests and `t.Helper()` on helpers. - **Assert the contract, not internals.** Pin the exact value / typed error / emitted string — not `err != nil` or `len > 0`. A test that still passes when the behavior is broken is worse than no test. -- **Real dependencies over mocks.** Get a throwaway DB with `database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` + `t.Cleanup(func() { _ = database.CloseDB() })` (Windows-safe), and use `httptest` servers for HTTP. The `internal/sub` suite's `initSubDB(t)` is the template. +- **Real dependencies over mocks.** Get a throwaway DB with `dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db"))` from `internal/database/dbtest`: it copies a once-migrated template (migrating from scratch per test is ~7x slower, worst under `-race`) and closes the DB before `t.TempDir` cleanup (Windows-safe). Keep `database.InitDB` for reopening an existing file or migrating a hand-built legacy DB. Use `httptest` servers for HTTP. The `internal/sub` suite's `initSubDB(t)` is the template. ### Running diff --git a/api_token_cli_test.go b/api_token_cli_test.go index 2a2e168d4..73d88aa05 100644 --- a/api_token_cli_test.go +++ b/api_token_cli_test.go @@ -9,6 +9,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel" ) @@ -16,10 +17,7 @@ import ( func newTokenCLIEnv(t *testing.T) { t.Helper() t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(config.GetDBPath()); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, config.GetDBPath()) } func tokenNames(t *testing.T) []string { diff --git a/internal/database/dbtest/dbtest.go b/internal/database/dbtest/dbtest.go new file mode 100644 index 000000000..71e93e4a9 --- /dev/null +++ b/internal/database/dbtest/dbtest.go @@ -0,0 +1,67 @@ +// Package dbtest opens throwaway panel databases for tests. Migrating a new +// SQLite file costs ~850ms under -race; copying a migrated template ~130ms. +package dbtest + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/config" + "github.com/mhsanaei/3x-ui/v3/internal/database" +) + +var migrated struct { + once sync.Once + data []byte + err error +} + +// InitDB opens a new, fully migrated panel database at path and closes it when +// t ends. Reopen an existing file with database.InitDB instead. +func InitDB(t testing.TB, path string) { + t.Helper() + if config.GetDBKind() != "postgres" { + if _, err := os.Stat(path); err == nil { + t.Fatalf("dbtest.InitDB would overwrite existing %s; reopen it with database.InitDB", path) + } + data, err := migratedTemplate() + if err != nil { + t.Fatalf("build template database: %v", err) + } + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + t.Fatalf("create database dir: %v", err) + } + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatalf("copy template database: %v", err) + } + } + if err := database.InitDB(path); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) +} + +func migratedTemplate() ([]byte, error) { + migrated.once.Do(func() { + dir, err := os.MkdirTemp("", "xui-dbtest-") + if err != nil { + migrated.err = err + return + } + defer os.RemoveAll(dir) + path := filepath.Join(dir, "template.db") + if err := database.InitDB(path); err != nil { + migrated.err = err + return + } + // Closing the last connection checkpoints the WAL into the main file. + if err := database.CloseDB(); err != nil { + migrated.err = err + return + } + migrated.data, migrated.err = os.ReadFile(path) + }) + return migrated.data, migrated.err +} diff --git a/internal/database/dbtest/dbtest_test.go b/internal/database/dbtest/dbtest_test.go new file mode 100644 index 000000000..3f33ee722 --- /dev/null +++ b/internal/database/dbtest/dbtest_test.go @@ -0,0 +1,36 @@ +package dbtest + +import ( + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" +) + +func TestInitDBGivesEachTestItsOwnDatabase(t *testing.T) { + t.Run("first test writes", func(t *testing.T) { + InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) + if err := database.GetDB().Create(&model.Setting{Key: "dbtestProbe", Value: "first"}).Error; err != nil { + t.Fatalf("write probe: %v", err) + } + }) + + t.Run("next test starts clean", func(t *testing.T) { + InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) + var leaked int64 + if err := database.GetDB().Model(&model.Setting{}).Where("key = ?", "dbtestProbe").Count(&leaked).Error; err != nil { + t.Fatalf("count probe: %v", err) + } + if leaked != 0 { + t.Fatalf("database holds %d probe rows written by the previous test; copies must not share state", leaked) + } + var admins int64 + if err := database.GetDB().Model(&model.User{}).Count(&admins).Error; err != nil { + t.Fatalf("count users: %v", err) + } + if admins != 1 { + t.Fatalf("users = %d, want the 1 seeded admin a fresh install has", admins) + } + }) +} diff --git a/internal/sub/build_urls_test.go b/internal/sub/build_urls_test.go index f29400aa9..c6ccd890e 100644 --- a/internal/sub/build_urls_test.go +++ b/internal/sub/build_urls_test.go @@ -6,17 +6,13 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) func initSubDB(t *testing.T) { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - // Close the handle before t.TempDir cleanup so Windows doesn't refuse to - // remove the still-open sqlite file. - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) } // The subscription page's Copy URL must be built from the same host the diff --git a/internal/sub/external_hwid_test.go b/internal/sub/external_hwid_test.go index 702f5abd7..ff39b1a95 100644 --- a/internal/sub/external_hwid_test.go +++ b/internal/sub/external_hwid_test.go @@ -6,17 +6,14 @@ import ( "path/filepath" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) // #6559: the Master panel must send a stable X-HWID when fetching external // subscriptions, otherwise an HWID-limited donor answers 404. func TestFetchSendsStableHwid(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) var gotHwid string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/sub/external_subscription_test.go b/internal/sub/external_subscription_test.go index 1e887cb6d..1b0508d1e 100644 --- a/internal/sub/external_subscription_test.go +++ b/internal/sub/external_subscription_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -302,10 +303,7 @@ func TestExpandEntryCacheHitWritesNothing(t *testing.T) { func TestFetchUsesConfiguredExternalSubUserAgent(t *testing.T) { resetSubscriptionCache(t) - if err := database.InitDB(filepath.Join(t.TempDir(), "ua.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "ua.db")) const customUA = "Happ/4.2.1" if err := database.GetDB().Create(&model.Setting{ @@ -333,10 +331,7 @@ func TestFetchUsesConfiguredExternalSubUserAgent(t *testing.T) { func TestFetchFallsBackToDefaultExternalSubUserAgent(t *testing.T) { resetSubscriptionCache(t) - if err := database.InitDB(filepath.Join(t.TempDir(), "ua-default.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "ua-default.db")) var gotUA string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/sub/host_sub_test.go b/internal/sub/host_sub_test.go index 6d3461364..c36fc1d65 100644 --- a/internal/sub/host_sub_test.go +++ b/internal/sub/host_sub_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -15,10 +16,7 @@ func seedSubDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } // seedSubInbound creates a VLESS inbound with one client wired into the diff --git a/internal/sub/hwid_controller_test.go b/internal/sub/hwid_controller_test.go index 1468d4a2a..76ab4bafd 100644 --- a/internal/sub/hwid_controller_test.go +++ b/internal/sub/hwid_controller_test.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -27,10 +28,7 @@ func initHwidSubRouter(t *testing.T, limit int) (*gin.Engine, string) { } t.Setenv("XUI_DB_FOLDER", tmp) - if err := database.InitDB(filepath.Join(tmp, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(tmp, "x-ui.db")) const subID = "sub-hwid-route" const email = "route@example.com" diff --git a/internal/sub/mutation_audit_test.go b/internal/sub/mutation_audit_test.go index 2af2bb7a2..1577766cb 100644 --- a/internal/sub/mutation_audit_test.go +++ b/internal/sub/mutation_audit_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -19,10 +20,7 @@ func initMutDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } // --- json_service.go:40 — rules are merged into routing only when non-empty --- diff --git a/internal/sub/remote_routing_test.go b/internal/sub/remote_routing_test.go index df7387923..2182620ba 100644 --- a/internal/sub/remote_routing_test.go +++ b/internal/sub/remote_routing_test.go @@ -17,6 +17,7 @@ import ( yaml "github.com/goccy/go-yaml" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) func mergeRemoteClashRulesYAML(base map[string]any, raw string) error { @@ -607,10 +608,7 @@ func TestRemoteRoutingHTTPClientRejectsLoopback(t *testing.T) { func TestRemoteRoutingPersistedLoadRetriesAfterDatabaseBecomesReady(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "x-ui.db") - if err := database.InitDB(dbPath); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, dbPath) deeplink, err := normalizeHappRouting([]byte(`{"Name":"persisted-after-ready"}`)) if err != nil { diff --git a/internal/sub/service_dedup_test.go b/internal/sub/service_dedup_test.go index 83242a48b..6407d8c88 100644 --- a/internal/sub/service_dedup_test.go +++ b/internal/sub/service_dedup_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -18,10 +19,7 @@ import ( func TestGetSubs_DuplicateSettingsClients_Deduped(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) const subId = "sub-dup" const email = "dup@example.com" @@ -76,10 +74,7 @@ func TestGetSubs_DuplicateSettingsClients_Deduped(t *testing.T) { func TestMatchingClients_DedupsCaseInsensitiveEmail(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) const subId = "s1" const uuid = "11111111-2222-4333-8444-555555555555" diff --git a/internal/sub/service_info_node_test.go b/internal/sub/service_info_node_test.go index 274f2881d..00674c1e6 100644 --- a/internal/sub/service_info_node_test.go +++ b/internal/sub/service_info_node_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -15,12 +16,7 @@ import ( func setupInfoNodeTestDB(t *testing.T) { t.Helper() - if err := database.InitDB(t.TempDir() + "/test_infonode.db"); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { - _ = database.CloseDB() - }) + dbtest.InitDB(t, t.TempDir()+"/test_infonode.db") db := database.GetDB() if err := db.AutoMigrate( &model.Inbound{}, diff --git a/internal/sub/service_orphaned_stats_test.go b/internal/sub/service_orphaned_stats_test.go index e095794e5..b1990a5ca 100644 --- a/internal/sub/service_orphaned_stats_test.go +++ b/internal/sub/service_orphaned_stats_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -17,10 +18,7 @@ import ( func TestStatsForClient_OrphanedInboundIdFallback(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) const email = "old-client@example.com" const total = int64(100) * gb diff --git a/internal/sub/service_sort_test.go b/internal/sub/service_sort_test.go index b6614e2b7..0de1aa04f 100644 --- a/internal/sub/service_sort_test.go +++ b/internal/sub/service_sort_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -16,10 +17,7 @@ import ( func TestGetSubs_OrdersBySubSortIndexThenId(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) const subId = "sub-sort" db := database.GetDB() diff --git a/internal/sub/service_userinfo_test.go b/internal/sub/service_userinfo_test.go index 74c16893f..8bc7c0cd4 100644 --- a/internal/sub/service_userinfo_test.go +++ b/internal/sub/service_userinfo_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -12,10 +13,7 @@ import ( func TestAggregateTrafficByEmails_FallsBackToClientLimits(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) const email = "node-client@example.com" const totalBytes = int64(300) * 1024 * 1024 * 1024 diff --git a/internal/sub/sub_scale_test.go b/internal/sub/sub_scale_test.go index 7e87509a4..2fdef62a0 100644 --- a/internal/sub/sub_scale_test.go +++ b/internal/sub/sub_scale_test.go @@ -16,6 +16,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -38,10 +39,7 @@ func setupScaleSubDB(t *testing.T) { } switch strings.ToLower(strings.TrimSpace(os.Getenv("XUI_SCALE_TEST"))) { case "1", "true", "yes": - if err := database.InitDB(filepath.Join(t.TempDir(), "scale.db")); err != nil { - t.Fatalf("InitDB(sqlite): %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "scale.db")) return } t.Skip("set XUI_SCALE_TEST=1 (sqlite) or XUI_DB_TYPE=postgres + XUI_DB_DSN (postgres) to run the scale benchmark") diff --git a/internal/web/controller/api_auth_test.go b/internal/web/controller/api_auth_test.go index 5bd4c64fc..44e7cc7a1 100644 --- a/internal/web/controller/api_auth_test.go +++ b/internal/web/controller/api_auth_test.go @@ -15,6 +15,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" "github.com/mhsanaei/3x-ui/v3/internal/web/session" @@ -31,10 +32,7 @@ func newAPIAuthTestEngine(t *testing.T) (*gin.Engine, *APIController) { gin.SetMode(gin.TestMode) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) engine := gin.New() store := cookie.NewStore([]byte("api-auth-test-secret")) engine.Use(sessions.Sessions("3x-ui", store)) diff --git a/internal/web/controller/client_partial_apply_test.go b/internal/web/controller/client_partial_apply_test.go index 33c0001df..eb73e617b 100644 --- a/internal/web/controller/client_partial_apply_test.go +++ b/internal/web/controller/client_partial_apply_test.go @@ -11,6 +11,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/entity" "github.com/mhsanaei/3x-ui/v3/internal/web/service" @@ -22,10 +23,7 @@ func seedPartlyApplyingClient(t *testing.T, email string, basePort int) (healthy t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() ids := make([]int, 0, 2) @@ -129,10 +127,7 @@ func TestDeleteHandlerFlagsRestartOnPartialApply(t *testing.T) { func TestImportHandlerFlagsRestartWhenTrafficRestoreFails(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() ib := &model.Inbound{ UserId: 1, Enable: true, Port: 43340, Tag: "in-import-partial", diff --git a/internal/web/controller/client_renewal_preview_test.go b/internal/web/controller/client_renewal_preview_test.go index c67953dab..9ed4000bc 100644 --- a/internal/web/controller/client_renewal_preview_test.go +++ b/internal/web/controller/client_renewal_preview_test.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -45,10 +46,7 @@ func TestClientRenewalPreviewHTTP(t *testing.T) { {name: "negative count", zone: "UTC", count: -1, invalid: true, wantError: "renewal preview reset and resetCount must not be negative\n"}, } { t.Run(tt.name, func(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) db := database.GetDB() if err := db.Create(&model.Setting{Key: "timeLocation", Value: tt.zone}).Error; err != nil { t.Fatal(err) diff --git a/internal/web/controller/host_test.go b/internal/web/controller/host_test.go index 419d8ec23..9b094f171 100644 --- a/internal/web/controller/host_test.go +++ b/internal/web/controller/host_test.go @@ -14,6 +14,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/web/entity" @@ -25,10 +26,7 @@ func newHostTestDB(t *testing.T) { gin.SetMode(gin.TestMode) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } type hostEnvelope struct { diff --git a/internal/web/controller/inbound_node_sync_test.go b/internal/web/controller/inbound_node_sync_test.go index f09be717f..9b86da12e 100644 --- a/internal/web/controller/inbound_node_sync_test.go +++ b/internal/web/controller/inbound_node_sync_test.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" @@ -23,10 +24,7 @@ func TestNodeSyncPushSkipsOperatorTLSGuard(t *testing.T) { gin.SetMode(gin.TestMode) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) prev := runtime.GetManager() runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})) t.Cleanup(func() { runtime.SetManager(prev) }) diff --git a/internal/web/controller/node_credentials_writeonly_test.go b/internal/web/controller/node_credentials_writeonly_test.go index 195447022..2dc105a6f 100644 --- a/internal/web/controller/node_credentials_writeonly_test.go +++ b/internal/web/controller/node_credentials_writeonly_test.go @@ -16,6 +16,7 @@ import ( "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/locale" ) @@ -25,10 +26,7 @@ func newNodeCredentialTestEngine(t *testing.T) *gin.Engine { gin.SetMode(gin.TestMode) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) engine := gin.New() engine.Use(func(c *gin.Context) { diff --git a/internal/web/controller/setting_test.go b/internal/web/controller/setting_test.go index 2b9f5ee0f..adc08c3cf 100644 --- a/internal/web/controller/setting_test.go +++ b/internal/web/controller/setting_test.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" "github.com/mhsanaei/3x-ui/v3/internal/web/locale" @@ -57,10 +58,7 @@ func TestValidateRegex(t *testing.T) { func TestAPITokenMutationRoutesEnforceExpectedScope(t *testing.T) { t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) row := &model.ApiToken{Name: "route-scope", Token: crypto.HashTokenSHA256("token"), Enabled: true, Scope: model.ApiScopeNodeSync} if err := database.GetDB().Create(row).Error; err != nil { t.Fatalf("seed token: %v", err) @@ -95,10 +93,7 @@ func TestAPITokenMutationRoutesEnforceExpectedScope(t *testing.T) { // rebind the authenticator without presenting a current code. func TestUpdateSettingRequiresCodeToReplaceTwoFactorToken(t *testing.T) { t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) settingService := service.SettingService{} if err := settingService.SetTwoFactorToken("ORIGINALSECRET234567"); err != nil { @@ -195,13 +190,8 @@ func TestTestDiscordEndpoint(t *testing.T) { // Setup DB t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { - _ = database.CloseDB() - SetDiscordService(nil) - }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) + t.Cleanup(func() { SetDiscordService(nil) }) settingService := service.SettingService{} svc := discord.NewDiscordService(settingService) diff --git a/internal/web/controller/sub_balancer_test.go b/internal/web/controller/sub_balancer_test.go index 12f908caf..051ef8668 100644 --- a/internal/web/controller/sub_balancer_test.go +++ b/internal/web/controller/sub_balancer_test.go @@ -11,16 +11,13 @@ import ( "github.com/gin-gonic/gin" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) func setupSubBalancerRouter(t *testing.T) *gin.Engine { t.Helper() t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) gin.SetMode(gin.TestMode) router := gin.New() NewSubBalancerController(router.Group("/panel/api")) diff --git a/internal/web/controller/xray_setting_warp_test.go b/internal/web/controller/xray_setting_warp_test.go index 200a0f609..45749708a 100644 --- a/internal/web/controller/xray_setting_warp_test.go +++ b/internal/web/controller/xray_setting_warp_test.go @@ -13,6 +13,7 @@ import ( "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/locale" ) @@ -21,10 +22,7 @@ func TestWarpIntervalReportsClockPersistenceFailure(t *testing.T) { gin.SetMode(gin.TestMode) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() for _, setting := range []*model.Setting{ {Key: "warpUpdateInterval", Value: "0"}, diff --git a/internal/web/job/check_client_ip_job_integration_test.go b/internal/web/job/check_client_ip_job_integration_test.go index a25870f93..da3997c5d 100644 --- a/internal/web/job/check_client_ip_job_integration_test.go +++ b/internal/web/job/check_client_ip_job_integration_test.go @@ -12,6 +12,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) @@ -47,15 +48,7 @@ func setupIntegrationDB(t *testing.T) { log.SetFlags(origLogFlags) }) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("database.InitDB failed: %v", err) - } - // LIFO cleanup order: this runs before t.TempDir's own cleanup. - t.Cleanup(func() { - if err := database.CloseDB(); err != nil { - t.Logf("database.CloseDB warning: %v", err) - } - }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } // enforceIpLimitForTest runs the same two steps processObserved does: select diff --git a/internal/web/job/check_client_ip_job_test.go b/internal/web/job/check_client_ip_job_test.go index 7670b55b8..211d68cd9 100644 --- a/internal/web/job/check_client_ip_job_test.go +++ b/internal/web/job/check_client_ip_job_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -197,10 +198,7 @@ func TestPartitionLiveIps_ConcurrentLiveIpsSortedAscending(t *testing.T) { func TestGetInboundByEmailFallbackIgnoresProtocolScalarFields(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) inbound := &model.Inbound{ UserId: 1, diff --git a/internal/web/job/check_client_ip_scale_test.go b/internal/web/job/check_client_ip_scale_test.go index d57f1cd58..a6d62cf27 100644 --- a/internal/web/job/check_client_ip_scale_test.go +++ b/internal/web/job/check_client_ip_scale_test.go @@ -15,6 +15,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) @@ -35,10 +36,7 @@ func setupScaleJobDB(t *testing.T) { } switch strings.ToLower(strings.TrimSpace(os.Getenv("XUI_SCALE_TEST"))) { case "1", "true", "yes": - if err := database.InitDB(filepath.Join(t.TempDir(), "scale.db")); err != nil { - t.Fatalf("InitDB(sqlite): %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "scale.db")) return } t.Skip("set XUI_SCALE_TEST=1 (sqlite) or XUI_DB_TYPE=postgres + XUI_DB_DSN (postgres) to run the scale benchmark") diff --git a/internal/web/job/ldap_sync_job_test.go b/internal/web/job/ldap_sync_job_test.go index 9f2463ea0..bdc5a77b1 100644 --- a/internal/web/job/ldap_sync_job_test.go +++ b/internal/web/job/ldap_sync_job_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) @@ -13,10 +14,7 @@ func initLdapJobDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func TestBuildClient_ConvertsDefaultTotalGBToBytes(t *testing.T) { diff --git a/internal/web/job/node_heartbeat_descendants_test.go b/internal/web/job/node_heartbeat_descendants_test.go index f9882dac4..5c035ffbe 100644 --- a/internal/web/job/node_heartbeat_descendants_test.go +++ b/internal/web/job/node_heartbeat_descendants_test.go @@ -11,6 +11,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" @@ -53,10 +54,7 @@ func TestHeartbeatDropsSubNodesOfNodesItNoLongerProbes(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { xuilogger.InitLogger(logging.ERROR) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})) t.Cleanup(func() { runtime.SetManager(nil) }) diff --git a/internal/web/job/node_traffic_sync_ip_push_test.go b/internal/web/job/node_traffic_sync_ip_push_test.go index 82ab5ac8a..8db156d02 100644 --- a/internal/web/job/node_traffic_sync_ip_push_test.go +++ b/internal/web/job/node_traffic_sync_ip_push_test.go @@ -16,6 +16,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" @@ -26,10 +27,7 @@ import ( // whole table made every node store and echo back the entire fleet's IPs. func TestNodeTrafficSyncPushesOnlyHostedClientIps(t *testing.T) { xuilogger.InitLogger(logging.ERROR) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) service.StartTrafficWriter() t.Cleanup(service.StopTrafficWriter) runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})) diff --git a/internal/web/job/node_traffic_sync_online_prune_test.go b/internal/web/job/node_traffic_sync_online_prune_test.go index 907a6aa31..29e706627 100644 --- a/internal/web/job/node_traffic_sync_online_prune_test.go +++ b/internal/web/job/node_traffic_sync_online_prune_test.go @@ -7,6 +7,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" @@ -18,10 +19,7 @@ import ( // must drop their online sets itself: a disabled node here, a deleted one below. func TestNodeTrafficSyncDropsOnlineClientsOfUnsyncedNodes(t *testing.T) { xuilogger.InitLogger(logging.ERROR) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})) t.Cleanup(func() { runtime.SetManager(nil) }) process := xray.NewTestProcess(nil, "") diff --git a/internal/web/job/node_transition_burst_test.go b/internal/web/job/node_transition_burst_test.go index c195dfcd8..0b31a8c4b 100644 --- a/internal/web/job/node_transition_burst_test.go +++ b/internal/web/job/node_transition_burst_test.go @@ -13,6 +13,7 @@ import ( "github.com/op/go-logging" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/eventbus" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" @@ -24,10 +25,7 @@ import ( func goingDownNodes(t *testing.T, n int) { t.Helper() xuilogger.InitLogger(logging.ERROR) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) runtime.SetManager(runtime.NewManager(runtime.LocalDeps{APIPort: func() int { return 0 }, SetNeedRestart: func() {}})) t.Cleanup(func() { runtime.SetManager(nil) }) srv := httptest.NewServer(nil) diff --git a/internal/web/job/periodic_traffic_reset_client_test.go b/internal/web/job/periodic_traffic_reset_client_test.go index d9c3ded3b..66a872306 100644 --- a/internal/web/job/periodic_traffic_reset_client_test.go +++ b/internal/web/job/periodic_traffic_reset_client_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -15,10 +16,7 @@ func initResetJobDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } type seededClient struct { diff --git a/internal/web/routes_contract_test.go b/internal/web/routes_contract_test.go index bcf7bec99..0364c25a5 100644 --- a/internal/web/routes_contract_test.go +++ b/internal/web/routes_contract_test.go @@ -12,7 +12,7 @@ import ( "github.com/robfig/cron/v3" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/web/global" ) @@ -44,10 +44,7 @@ func inContractScope(method, path string) bool { func registeredContractRoutes(t *testing.T) map[string]bool { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) previous := global.GetWebServer() s := NewServer() diff --git a/internal/web/service/bulk_clients_test.go b/internal/web/service/bulk_clients_test.go index 334ab5121..af6ceb048 100644 --- a/internal/web/service/bulk_clients_test.go +++ b/internal/web/service/bulk_clients_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -14,10 +15,7 @@ func setupBulkDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func clientsSettings(t *testing.T, clients []model.Client) string { diff --git a/internal/web/service/client_apply_field_test.go b/internal/web/service/client_apply_field_test.go index 3f90d3ba7..fd68e71a6 100644 --- a/internal/web/service/client_apply_field_test.go +++ b/internal/web/service/client_apply_field_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -16,10 +17,7 @@ import ( func TestResetClientExpiryTimeByEmail_MultiInbound(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -91,10 +89,7 @@ func TestResetClientExpiryTimeByEmail_MultiInbound(t *testing.T) { func TestSetClientEnableByEmail_MultiInbound(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/client_effective_flow_test.go b/internal/web/service/client_effective_flow_test.go index 8a09f5c6b..9bcffb3bc 100644 --- a/internal/web/service/client_effective_flow_test.go +++ b/internal/web/service/client_effective_flow_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -14,10 +15,7 @@ import ( func TestEffectiveFlowsByEmails(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() const vision = "xtls-rprx-vision" diff --git a/internal/web/service/client_flow_isolation_test.go b/internal/web/service/client_flow_isolation_test.go index f379d3a0e..52b18454e 100644 --- a/internal/web/service/client_flow_isolation_test.go +++ b/internal/web/service/client_flow_isolation_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -46,10 +47,7 @@ func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) { func TestFlowIsolation_VisionDoesNotLeakToWsInbound(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -95,10 +93,7 @@ func TestFlowIsolation_VisionDoesNotLeakToWsInbound(t *testing.T) { func TestEffectiveFlow_NonFlowInboundSyncedLastDoesNotHideVision(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() reality := &model.Inbound{Tag: "vless-reality", Enable: true, Port: 40001, Protocol: model.VLESS, StreamSettings: `{"network":"tcp","security":"reality"}`} @@ -145,10 +140,7 @@ func TestEffectiveFlow_NonFlowInboundSyncedLastDoesNotHideVision(t *testing.T) { func TestEffectiveFlow_ClearedFlowStaysCleared(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() reality := &model.Inbound{Tag: "vless-reality", Enable: true, Port: 41001, Protocol: model.VLESS, StreamSettings: `{"network":"tcp","security":"reality"}`} @@ -191,10 +183,7 @@ func TestEffectiveFlow_ClearedFlowStaysCleared(t *testing.T) { func TestAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -267,10 +256,7 @@ func TestAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) { func TestBulkAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/client_group_node_sync_test.go b/internal/web/service/client_group_node_sync_test.go index 9a37f3f10..28fe18f3e 100644 --- a/internal/web/service/client_group_node_sync_test.go +++ b/internal/web/service/client_group_node_sync_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" ) @@ -13,10 +14,7 @@ import ( func TestSetRemoteTraffic_PreservesPanelLocalGroupAndComment(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -82,10 +80,7 @@ func TestSetRemoteTraffic_PreservesPanelLocalGroupAndComment(t *testing.T) { func TestSyncInbound_KeepsGroupWhenIncomingEmpty(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -125,10 +120,7 @@ func TestSyncInbound_KeepsGroupWhenIncomingEmpty(t *testing.T) { func TestClientUpdate_ClearsGroup(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/client_hwid_test.go b/internal/web/service/client_hwid_test.go index 7c8792886..2d742d5e8 100644 --- a/internal/web/service/client_hwid_test.go +++ b/internal/web/service/client_hwid_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -12,10 +13,7 @@ func initClientHwidTestDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func seedHwidClient(t *testing.T, limit int) *model.ClientRecord { diff --git a/internal/web/service/client_hwid_tx_test.go b/internal/web/service/client_hwid_tx_test.go index e6d33050d..0f83822f4 100644 --- a/internal/web/service/client_hwid_tx_test.go +++ b/internal/web/service/client_hwid_tx_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "gorm.io/gorm" @@ -188,10 +189,7 @@ func TestSetClientLimitHwidIsSerializedWithSyncInbound(t *testing.T) { func BenchmarkSetClientLimitHwidSerialized(b *testing.B) { dbDir := b.TempDir() b.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - b.Fatalf("InitDB: %v", err) - } - b.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(b, filepath.Join(dbDir, "x-ui.db")) StartTrafficWriter() b.Cleanup(StopTrafficWriter) db := database.GetDB() diff --git a/internal/web/service/client_sync_mtproto_test.go b/internal/web/service/client_sync_mtproto_test.go index cb03c08b9..e2c2561d8 100644 --- a/internal/web/service/client_sync_mtproto_test.go +++ b/internal/web/service/client_sync_mtproto_test.go @@ -5,16 +5,14 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) func TestSyncInbound_UpdatesMtprotoSecretAndAdTag(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/client_sync_multiprotocol_test.go b/internal/web/service/client_sync_multiprotocol_test.go index 2045fb87f..4397eaf7e 100644 --- a/internal/web/service/client_sync_multiprotocol_test.go +++ b/internal/web/service/client_sync_multiprotocol_test.go @@ -5,16 +5,14 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) func TestSyncInbound_PreservesCredentialsAcrossProtocols(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -74,10 +72,7 @@ func TestSyncInbound_PreservesCredentialsAcrossProtocols(t *testing.T) { func TestSyncInbound_AllowsClearingFlow(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/discord/discord_test.go b/internal/web/service/discord/discord_test.go index 104c2a63d..db4fef338 100644 --- a/internal/web/service/discord/discord_test.go +++ b/internal/web/service/discord/discord_test.go @@ -11,17 +11,14 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) func setupTestDB(t *testing.T) service.SettingService { t.Helper() dbPath := filepath.Join(t.TempDir(), "x-ui.db") - if err := database.InitDB(dbPath); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, dbPath) return service.SettingService{} } diff --git a/internal/web/service/email/email_test.go b/internal/web/service/email/email_test.go index 3c56498a5..5bcfa8150 100644 --- a/internal/web/service/email/email_test.go +++ b/internal/web/service/email/email_test.go @@ -14,7 +14,7 @@ import ( "sync" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/web/service" ) @@ -132,10 +132,7 @@ func startFakeSMTPServer(t *testing.T) (string, func() []string) { } func TestSendUsesBareAddressFromNameAddrSmtpFrom(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) addr, recordedLines := startFakeSMTPServer(t) host, portStr, err := net.SplitHostPort(addr) @@ -182,10 +179,7 @@ func TestSendUsesBareAddressFromNameAddrSmtpFrom(t *testing.T) { } func TestConnectionReportsMissingFrom(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) settingService := service.SettingService{} mustSet := func(name string, err error) { diff --git a/internal/web/service/happ_test.go b/internal/web/service/happ_test.go index ed1c2b77b..487110ddc 100644 --- a/internal/web/service/happ_test.go +++ b/internal/web/service/happ_test.go @@ -18,6 +18,7 @@ import ( "golang.org/x/crypto/chacha20poly1305" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/logger" ) @@ -30,10 +31,7 @@ func initHappTestDB(t *testing.T) { if err := os.WriteFile(filepath.Join(dbDir, "config.json"), []byte(`{"log":{}}`), 0o600); err != nil { t.Fatalf("write Xray config: %v", err) } - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func seedHappClient(t *testing.T, subID string) *model.ClientRecord { diff --git a/internal/web/service/inbound_client_ips_merge_test.go b/internal/web/service/inbound_client_ips_merge_test.go index 304dfc7f3..3470fac49 100644 --- a/internal/web/service/inbound_client_ips_merge_test.go +++ b/internal/web/service/inbound_client_ips_merge_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "gorm.io/gorm" @@ -18,10 +19,7 @@ func setupClientIpTestDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func marshalIps(t *testing.T, entries ...clientIpEntry) string { diff --git a/internal/web/service/inbound_client_traffic_test.go b/internal/web/service/inbound_client_traffic_test.go index c22672618..6845b4028 100644 --- a/internal/web/service/inbound_client_traffic_test.go +++ b/internal/web/service/inbound_client_traffic_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -26,10 +27,7 @@ import ( func TestAddClientTraffic_MatchesByEmail(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -97,10 +95,7 @@ func TestAddClientTraffic_MatchesByEmail(t *testing.T) { func TestAdjustTraffics_DelayedStartConvertsDespiteStaleInboundId(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -166,10 +161,7 @@ func TestAdjustTraffics_DelayedStartConvertsDespiteStaleInboundId(t *testing.T) func TestAddClientTraffic_ExpiryWriteOnlyForConvertedClients(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/inbound_disable_flow_test.go b/internal/web/service/inbound_disable_flow_test.go index 14a5c132b..6bfd0aedf 100644 --- a/internal/web/service/inbound_disable_flow_test.go +++ b/internal/web/service/inbound_disable_flow_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "gorm.io/gorm" @@ -95,10 +96,7 @@ func initFlowTestDB(t *testing.T) *gorm.DB { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) return database.GetDB() } diff --git a/internal/web/service/inbound_flow_restore_test.go b/internal/web/service/inbound_flow_restore_test.go index 21f178634..e52819dc0 100644 --- a/internal/web/service/inbound_flow_restore_test.go +++ b/internal/web/service/inbound_flow_restore_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -16,10 +17,7 @@ import ( func TestRestoreVisionFlowForEligibleInbound(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() const vision = "xtls-rprx-vision" diff --git a/internal/web/service/inbound_migration_test.go b/internal/web/service/inbound_migration_test.go index b30867edc..ae58a0127 100644 --- a/internal/web/service/inbound_migration_test.go +++ b/internal/web/service/inbound_migration_test.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -22,10 +23,7 @@ import ( func TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() @@ -96,10 +94,7 @@ func TestMigrationRequirements_BackfillsClientTrafficsWithMultiDomainInbound(t * func TestMigrationRequirementsReturnsAddClientStatFailure(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() first := &model.Inbound{UserId: 1, Tag: "first", Port: 31001, Protocol: model.VLESS, Settings: `{"clients":[{"email":"first@example.test","id":"id-1"}]}`, StreamSettings: `{}`} if err := db.Create(first).Error; err != nil { @@ -136,10 +131,7 @@ func TestMigrationRequirementsReturnsAddClientStatFailure(t *testing.T) { func TestMigrationRequirements_CleansLegacyZeroAddrTag(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() legacy := &model.Inbound{ diff --git a/internal/web/service/inbound_subsort_test.go b/internal/web/service/inbound_subsort_test.go index 6adbff822..43fccaa65 100644 --- a/internal/web/service/inbound_subsort_test.go +++ b/internal/web/service/inbound_subsort_test.go @@ -5,14 +5,12 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) func TestSetInboundSubSortIndexLeavesSettingsUntouched(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) const settings = `{"clients":[{"email":"a@example.test","id":"11111111-1111-1111-1111-111111111111"}]}` ib := &model.Inbound{UserId: 1, Remark: "r", Port: 21001, Protocol: model.VLESS, Settings: settings, SubSortIndex: 1, Enable: true} @@ -57,10 +55,7 @@ func TestSetInboundSubSortIndexUsesNarrowNodeUpdate(t *testing.T) { } func TestSetInboundSubSortIndexPreservesNegative(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) ib := &model.Inbound{UserId: 1, Remark: "r", Port: 21003, Protocol: model.VLESS, Settings: `{"clients":[]}`, SubSortIndex: 1, Enable: true} if err := database.GetDB().Create(ib).Error; err != nil { diff --git a/internal/web/service/inbound_tgbot_lookup_test.go b/internal/web/service/inbound_tgbot_lookup_test.go index 95cd056c2..f90546848 100644 --- a/internal/web/service/inbound_tgbot_lookup_test.go +++ b/internal/web/service/inbound_tgbot_lookup_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" ) @@ -17,10 +18,7 @@ import ( func TestGetClientTrafficTgBot_SettingsSerializationStyles(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) db := database.GetDB() diff --git a/internal/web/service/integration/pia_test.go b/internal/web/service/integration/pia_test.go index 7d0e4f724..96479cc22 100644 --- a/internal/web/service/integration/pia_test.go +++ b/internal/web/service/integration/pia_test.go @@ -12,7 +12,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" piaprotocol "github.com/mhsanaei/3x-ui/v3/internal/pia" ) @@ -48,10 +48,7 @@ func (f *fakePiaRegistrar) RegisterKey(_ context.Context, server piaprotocol.Wir func setupPiaService(t *testing.T) *PiaService { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) payload := []byte(`{"version":6,"groups":{"wg":[{"name":"wireguard","ports":[1337]}]},"regions":[{"id":"us-east","name":"US East","country":"US","geo":false,"offline":false,"port_forward":true,"servers":{"wg":[{"ip":"198.51.100.10","cn":"useast1"},{"ip":"198.51.100.20","cn":"useast2"}]}},{"id":"de-berlin","name":"Berlin","country":"DE","geo":false,"offline":false,"port_forward":false,"servers":{"wg":[{"ip":"203.0.113.10","cn":"berlin1"}]}}]}`) svc := NewPiaService() svc.Auth = fakePiaAuth{token: "tokentokentokentoken12"} diff --git a/internal/web/service/integration/warp_change_ip_test.go b/internal/web/service/integration/warp_change_ip_test.go index ee00a02c7..68ebfe8cd 100644 --- a/internal/web/service/integration/warp_change_ip_test.go +++ b/internal/web/service/integration/warp_change_ip_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -69,10 +70,7 @@ func withWarpAPIBase(t *testing.T, base string) { } func TestChangeWarpIPPreservesLicenseKey(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) const license = "WARPPLLUS-KEY-0123456789abcdefgh" // 32 chars, >= 26 gate seedWarp(t, license) @@ -124,10 +122,7 @@ func TestChangeWarpIPPreservesLicenseKey(t *testing.T) { } func TestChangeWarpIPKeepsLicenseWhenReapplyFails(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) const license = "WARPPLLUS-KEY-0123456789abcdefgh" seedWarp(t, license) diff --git a/internal/web/service/integration/warp_response_test.go b/internal/web/service/integration/warp_response_test.go index da2ed01b4..5ff1f33ea 100644 --- a/internal/web/service/integration/warp_response_test.go +++ b/internal/web/service/integration/warp_response_test.go @@ -7,14 +7,11 @@ import ( "path/filepath" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) func TestDoWarpRequestCapsResponseBody(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) oversize := maxResponseSize + 4096 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/service/node_client_traffic_sum_test.go b/internal/web/service/node_client_traffic_sum_test.go index f13c030a8..ff804525f 100644 --- a/internal/web/service/node_client_traffic_sum_test.go +++ b/internal/web/service/node_client_traffic_sum_test.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -18,10 +19,7 @@ func initTrafficTestDB(t *testing.T) *gorm.DB { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) return database.GetDB() } diff --git a/internal/web/service/panel/api_token_scope_test.go b/internal/web/service/panel/api_token_scope_test.go index 563020381..ff7518dd7 100644 --- a/internal/web/service/panel/api_token_scope_test.go +++ b/internal/web/service/panel/api_token_scope_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" ) @@ -14,14 +15,7 @@ func setupAPITokenTestDB(t *testing.T) { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { - if err := database.CloseDB(); err != nil { - t.Fatalf("CloseDB: %v", err) - } - }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func TestAPITokenScopeExpiryAndExpectedRevoke(t *testing.T) { diff --git a/internal/web/service/panel/api_token_test.go b/internal/web/service/panel/api_token_test.go index f81fa9688..bcb252244 100644 --- a/internal/web/service/panel/api_token_test.go +++ b/internal/web/service/panel/api_token_test.go @@ -9,6 +9,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -36,10 +37,7 @@ func TestApiTokenCreatedAtSeconds(t *testing.T) { func TestRecreateByNamePreservesTokenWhenReplacementFails(t *testing.T) { t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(config.GetDBPath()); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, config.GetDBPath()) svc := ApiTokenService{} first, err := svc.RecreateByName("cli-fallback") @@ -73,10 +71,7 @@ func TestRecreateByNamePreservesTokenWhenReplacementFails(t *testing.T) { // and now takes operator input from -tokenName, so it must cap it too. func TestRecreateByNameRejectsOverlongName(t *testing.T) { t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(config.GetDBPath()); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, config.GetDBPath()) const wantErr = "token name must be 64 characters or fewer" @@ -95,10 +90,7 @@ func TestRecreateByNameRejectsOverlongName(t *testing.T) { func TestRecreateByNameKeepsOneToken(t *testing.T) { t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(config.GetDBPath()); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, config.GetDBPath()) svc := ApiTokenService{} first, err := svc.RecreateByName("cli-fallback") diff --git a/internal/web/service/panel/sponsor_test.go b/internal/web/service/panel/sponsor_test.go index e213664ad..a7066206b 100644 --- a/internal/web/service/panel/sponsor_test.go +++ b/internal/web/service/panel/sponsor_test.go @@ -13,7 +13,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/config" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) var sponsorTestNow = time.Date(2026, 10, 15, 0, 0, 0, 0, time.UTC) @@ -131,10 +131,7 @@ func TestActiveSponsorsResolvesLogoAndSlots(t *testing.T) { func setupSponsorServer(t *testing.T, body string) *atomic.Int32 { t.Helper() t.Setenv("XUI_DB_FOLDER", t.TempDir()) - if err := database.InitDB(config.GetDBPath()); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, config.GetDBPath()) var hits atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/service/port_conflict_test.go b/internal/web/service/port_conflict_test.go index 31be5e2aa..085092436 100644 --- a/internal/web/service/port_conflict_test.go +++ b/internal/web/service/port_conflict_test.go @@ -10,6 +10,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) @@ -27,14 +28,7 @@ func setupConflictDB(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { - if err := database.CloseDB(); err != nil { - t.Logf("CloseDB warning: %v", err) - } - }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func seedInboundConflict(t *testing.T, tag, listen string, port int, protocol model.Protocol, streamSettings, settings string) { diff --git a/internal/web/service/scale_helpers_test.go b/internal/web/service/scale_helpers_test.go index 87a2888a3..dcd4eaef8 100644 --- a/internal/web/service/scale_helpers_test.go +++ b/internal/web/service/scale_helpers_test.go @@ -11,6 +11,7 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/config" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -41,10 +42,7 @@ func setupScaleDB(t *testing.T) { if dbPath == "" { dbPath = filepath.Join(t.TempDir(), "scale.db") } - if err := database.InitDB(dbPath); err != nil { - t.Fatalf("InitDB(sqlite): %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, dbPath) return } diff --git a/internal/web/service/server_cold_status_test.go b/internal/web/service/server_cold_status_test.go index 650e64ce7..fd83803a9 100644 --- a/internal/web/service/server_cold_status_test.go +++ b/internal/web/service/server_cold_status_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) // A panel restarts with an empty snapshot until the @2s ticker fires, and a @@ -12,10 +12,7 @@ import ( func TestCurrentStatusSamplesBeforeFirstTick(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) svc := &ServerService{} if svc.LastStatus() != nil { diff --git a/internal/web/service/server_geofile_test.go b/internal/web/service/server_geofile_test.go index d2ace06f1..2b58778cb 100644 --- a/internal/web/service/server_geofile_test.go +++ b/internal/web/service/server_geofile_test.go @@ -12,7 +12,7 @@ import ( "sync" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) // Loyalsoldier and runetfreedom write " geoip.dat"; chocolate4u writes @@ -141,10 +141,7 @@ func geofileTestEnv(t *testing.T, entries map[string]geofileEntry) string { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) binFolder := t.TempDir() t.Setenv("XUI_BIN_FOLDER", binFolder) diff --git a/internal/web/service/server_import_sniff_test.go b/internal/web/service/server_import_sniff_test.go index d2a02f4b5..dbba0511e 100644 --- a/internal/web/service/server_import_sniff_test.go +++ b/internal/web/service/server_import_sniff_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) func TestSniffImportKind(t *testing.T) { @@ -30,10 +31,7 @@ func TestSniffImportKind(t *testing.T) { t.Run("panel migration dump", func(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "x-ui.db") - if err := database.InitDB(dbPath); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, dbPath) dump, err := database.DumpSQLiteToBytes(dbPath) if err != nil { t.Fatalf("DumpSQLiteToBytes: %v", err) diff --git a/internal/web/service/server_import_stage_test.go b/internal/web/service/server_import_stage_test.go index f59d48233..bc57d82df 100644 --- a/internal/web/service/server_import_stage_test.go +++ b/internal/web/service/server_import_stage_test.go @@ -6,15 +6,13 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) func TestStageSQLiteUploadRebuildsFromDump(t *testing.T) { dir := t.TempDir() dbPath := filepath.Join(dir, "x-ui.db") - if err := database.InitDB(dbPath); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, dbPath) dump, err := database.DumpSQLiteToBytes(dbPath) if err != nil { t.Fatalf("DumpSQLiteToBytes: %v", err) diff --git a/internal/web/service/server_public_ip_async_test.go b/internal/web/service/server_public_ip_async_test.go index 2acaf9961..6608e96df 100644 --- a/internal/web/service/server_public_ip_async_test.go +++ b/internal/web/service/server_public_ip_async_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" ) // A box with no IPv6 route spends 3s per lookup service, and a status sample @@ -15,10 +15,7 @@ import ( func TestStatusSampleDoesNotWaitOnPublicIPLookup(t *testing.T) { dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) release := make(chan struct{}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/web/service/setting_mtls_test.go b/internal/web/service/setting_mtls_test.go index 55d28c504..b0104ae63 100644 --- a/internal/web/service/setting_mtls_test.go +++ b/internal/web/service/setting_mtls_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/util/crypto" ) @@ -16,10 +17,7 @@ func setupSettingMtlsDB(t *testing.T) *SettingService { t.Helper() dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) return &SettingService{} } diff --git a/internal/web/service/setting_security_test.go b/internal/web/service/setting_security_test.go index 338aa65aa..1b0777303 100644 --- a/internal/web/service/setting_security_test.go +++ b/internal/web/service/setting_security_test.go @@ -9,6 +9,7 @@ import ( "github.com/xlzd/gotp" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" ) @@ -52,14 +53,7 @@ func TestResetSettingsRegeneratesSubscriptionPaths(t *testing.T) { func setupSettingTestDB(t *testing.T) { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - if err := database.CloseDB(); err != nil { - t.Fatal(err) - } - }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) } func TestGetAllSettingViewRedactsSecrets(t *testing.T) { diff --git a/internal/web/service/sub_balancer_test.go b/internal/web/service/sub_balancer_test.go index 6dfdb52dc..7000f6875 100644 --- a/internal/web/service/sub_balancer_test.go +++ b/internal/web/service/sub_balancer_test.go @@ -8,7 +8,7 @@ import ( "github.com/op/go-logging" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger" ) @@ -20,14 +20,7 @@ func setupSubBalancerDB(t *testing.T) { subBalancerLoggerOnce.Do(func() { xuilogger.InitLogger(logging.ERROR) }) dbDir := t.TempDir() t.Setenv("XUI_DB_FOLDER", dbDir) - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { - if err := database.CloseDB(); err != nil { - t.Logf("CloseDB warning: %v", err) - } - }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) } func TestSubBalancerServiceCRUD(t *testing.T) { diff --git a/internal/web/service/tgbot/tgbot_add_client_picker_test.go b/internal/web/service/tgbot/tgbot_add_client_picker_test.go index c1679b37e..36a4e06ef 100644 --- a/internal/web/service/tgbot/tgbot_add_client_picker_test.go +++ b/internal/web/service/tgbot/tgbot_add_client_picker_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mymmrac/telego" @@ -15,10 +16,7 @@ import ( func seedPickerInbounds(t *testing.T, protocols ...model.Protocol) { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) for i, protocol := range protocols { port := 20000 + i ib := &model.Inbound{Remark: string(protocol), Enable: true, Port: port, Protocol: protocol, Tag: fmt.Sprintf("inbound-%d", port), Settings: `{}`} diff --git a/internal/web/service/tgbot/tgbot_client_draft_per_admin_test.go b/internal/web/service/tgbot/tgbot_client_draft_per_admin_test.go index a5f9c83fc..4d270377e 100644 --- a/internal/web/service/tgbot/tgbot_client_draft_per_admin_test.go +++ b/internal/web/service/tgbot/tgbot_client_draft_per_admin_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mymmrac/telego" ) @@ -12,10 +12,7 @@ import ( // Regression test: keying the add-client wizard by chat alone left the two // admins of a group chat filling in one client between them. func TestAddClientDraftIsPerAdminInGroupChat(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) const ( groupChat = int64(-1001234567890) diff --git a/internal/web/service/tgbot/tgbot_client_draft_per_chat_test.go b/internal/web/service/tgbot/tgbot_client_draft_per_chat_test.go index e08e02fbc..3d62a0ba5 100644 --- a/internal/web/service/tgbot/tgbot_client_draft_per_chat_test.go +++ b/internal/web/service/tgbot/tgbot_client_draft_per_chat_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mymmrac/telego" ) @@ -84,10 +84,7 @@ func lastDraftCard(t *testing.T, texts []string) string { // Regression test: one package-level draft per bot meant an admin's new client // was filled in by another chat's steps. func TestAddClientDraftIsPerChat(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) const ( chatA = int64(7101) diff --git a/internal/web/service/tgbot/tgbot_client_expiry_test.go b/internal/web/service/tgbot/tgbot_client_expiry_test.go index 745fc8951..efa9b8c92 100644 --- a/internal/web/service/tgbot/tgbot_client_expiry_test.go +++ b/internal/web/service/tgbot/tgbot_client_expiry_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/web/locale" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -35,10 +35,7 @@ func clientInfoLocalizer(t *testing.T) { // Regression test: a start-after-first-use client is stored as a negative duration, // and a disabled one rendered it as a 1969 date. func TestClientInfoShowsStartAfterFirstUseWhenDisabled(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) clientInfoLocalizer(t) traffic := &xray.ClientTraffic{ diff --git a/internal/web/service/tgbot/tgbot_client_links_authz_test.go b/internal/web/service/tgbot/tgbot_client_links_authz_test.go index e8549d0b0..8de9e9100 100644 --- a/internal/web/service/tgbot/tgbot_client_links_authz_test.go +++ b/internal/web/service/tgbot/tgbot_client_links_authz_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/global" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -33,10 +34,7 @@ func newLinksCallbackTgbot(t *testing.T, email string) (*Tgbot, func(string) int swapTestBot(t, mock.URL) t.Cleanup(mock.Close) - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) inbound := &model.Inbound{ UserId: 1, diff --git a/internal/web/service/tgbot/tgbot_report_batching_test.go b/internal/web/service/tgbot/tgbot_report_batching_test.go index fa0116d00..324f63415 100644 --- a/internal/web/service/tgbot/tgbot_report_batching_test.go +++ b/internal/web/service/tgbot/tgbot_report_batching_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/xray" @@ -59,10 +60,7 @@ func seedReportClients(t *testing.T, remark string, emails []string) { func initReportDB(t *testing.T) *Tgbot { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) origRunning := isRunning t.Cleanup(func() { isRunning = origRunning }) diff --git a/internal/web/service/tgbot/tgbot_stale_button_test.go b/internal/web/service/tgbot/tgbot_stale_button_test.go index 91b6c04e3..90feddff4 100644 --- a/internal/web/service/tgbot/tgbot_stale_button_test.go +++ b/internal/web/service/tgbot/tgbot_stale_button_test.go @@ -8,7 +8,7 @@ import ( "sync" "testing" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mymmrac/telego" @@ -58,10 +58,7 @@ func swapTestBot(t *testing.T, url string) { func newStaleButtonTgbot(t *testing.T) *Tgbot { t.Helper() - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) return &Tgbot{} } diff --git a/internal/web/service/xray_metrics_test.go b/internal/web/service/xray_metrics_test.go index afecf1d5f..ff1660f4c 100644 --- a/internal/web/service/xray_metrics_test.go +++ b/internal/web/service/xray_metrics_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/dbtest" "github.com/mhsanaei/3x-ui/v3/internal/eventbus" ) @@ -61,10 +61,7 @@ func runObservatory(t *testing.T, threshold int, seq []probe) []eventbus.EventTy } func TestApplyObservatoryDebounce(t *testing.T) { - if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil { - t.Fatalf("init db: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(t.TempDir(), "x-ui.db")) tests := []struct { name string @@ -150,10 +147,7 @@ func TestValidObsTag(t *testing.T) { func TestApplyObservatoryKeepsUnicodeTags(t *testing.T) { dbDir := t.TempDir() - if err := database.InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil { - t.Fatalf("InitDB: %v", err) - } - t.Cleanup(func() { _ = database.CloseDB() }) + dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db")) s := &XrayMetricsService{settingService: SettingService{}} s.applyObservatory(time.Unix(1000, 0), map[string]rawObsEntry{