mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-01 12:52:14 +03:00
Frontend deps: @hookform/resolvers 5.4.3 -> 5.5.7, Storybook 10.5.4 -> 10.5.5 across the four packages we declare, globals 17.7.0 -> 17.8.0, and jsdom 29.1.1 -> 30.0.1. The jsdom major replaces its CSS and selector stack -- @asamuzakjp/css-color 5 -> 6, @asamuzakjp/dom-selector 7 -> 8, undici 7 -> 8, nwsapi and generational-cache folded into their parents, whatwg-url 17 nested underneath. Nothing in the Vitest suites reaches those directly and the whole frontend gate (typecheck, lint, tests, build, Storybook compile) is green. Panel frontend version to 0.6.0. Backend deps: mattn/go-sqlite3 1.14.48 -> 1.14.49 and valyala/fasthttp 1.72.0 -> 1.73.0, plus the golang.org/x/exp and genproto/googleapis/rpc indirect bumps that came with them. Go tests: modernize -fix output, covering range-over-int, sync.WaitGroup.Go in place of manual Add/Done pairs, maps.Copy, and Go 1.26 new(expr) for pointer-to-value in the forwarded-trust table. The storedAs helper is deleted instead of being left behind a //go:fix inline directive -- keeping it that way fails govet on the one call site the rewrite did not reach, and every caller now takes new(...) directly. Behaviour is unchanged. DnsTab: the hosts-sync effect tested dns while declaring dnsEnabled in its dependency array. Both carry the same truth value, so this is exhaustive-deps hygiene rather than a behaviour change.
187 lines
5.3 KiB
Go
187 lines
5.3 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
)
|
|
|
|
func TestBackupSQLiteProducesValidSnapshotDuringWrites(t *testing.T) {
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "")
|
|
dbPath := filepath.Join(t.TempDir(), "x-ui.db")
|
|
if err := InitDB(dbPath); err != nil {
|
|
t.Fatalf("InitDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = CloseDB() })
|
|
|
|
seed := make([]model.Setting, 128)
|
|
value := strings.Repeat("x", 1024)
|
|
for i := range seed {
|
|
seed[i] = model.Setting{Key: fmt.Sprintf("backup-seed-%d", i), Value: value}
|
|
}
|
|
if err := db.Create(&seed).Error; err != nil {
|
|
t.Fatalf("seed database: %v", err)
|
|
}
|
|
|
|
stop := make(chan struct{})
|
|
firstWrite := make(chan error, 1)
|
|
writesDone := make(chan error, 1)
|
|
go func() {
|
|
for i := range 128 {
|
|
if err := db.Create(&model.Setting{Key: fmt.Sprintf("backup-write-%d", i), Value: value}).Error; err != nil {
|
|
if i == 0 {
|
|
firstWrite <- err
|
|
}
|
|
writesDone <- err
|
|
return
|
|
}
|
|
if i == 0 {
|
|
firstWrite <- nil
|
|
}
|
|
select {
|
|
case <-stop:
|
|
writesDone <- nil
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
writesDone <- nil
|
|
}()
|
|
|
|
if err := <-firstWrite; err != nil {
|
|
t.Fatalf("first concurrent write: %v", err)
|
|
}
|
|
backupPath := filepath.Join(t.TempDir(), "backup.db")
|
|
if err := BackupSQLite(backupPath); err != nil {
|
|
close(stop)
|
|
<-writesDone
|
|
t.Fatalf("BackupSQLite: %v", err)
|
|
}
|
|
close(stop)
|
|
if err := <-writesDone; err != nil {
|
|
t.Fatalf("concurrent write: %v", err)
|
|
}
|
|
if err := ValidateSQLiteDB(backupPath); err != nil {
|
|
t.Fatalf("validate backup: %v", err)
|
|
}
|
|
|
|
backup, err := sql.Open("sqlite3", backupPath)
|
|
if err != nil {
|
|
t.Fatalf("open backup: %v", err)
|
|
}
|
|
defer backup.Close()
|
|
|
|
var seedCount int
|
|
if err := backup.QueryRow("SELECT count(*) FROM settings WHERE key LIKE 'backup-seed-%'").Scan(&seedCount); err != nil {
|
|
t.Fatalf("count seeded rows: %v", err)
|
|
}
|
|
if seedCount != 128 {
|
|
t.Fatalf("seeded row count = %d, want 128", seedCount)
|
|
}
|
|
var firstWriteCount int
|
|
if err := backup.QueryRow("SELECT count(*) FROM settings WHERE key = 'backup-write-0'").Scan(&firstWriteCount); err != nil {
|
|
t.Fatalf("count first concurrent write: %v", err)
|
|
}
|
|
if firstWriteCount != 1 {
|
|
t.Fatalf("first concurrent write count = %d, want 1", firstWriteCount)
|
|
}
|
|
}
|
|
|
|
func TestBackupSQLiteTimesOutWaitingForSourceConnection(t *testing.T) {
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "")
|
|
if err := InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
|
t.Fatalf("InitDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = CloseDB() })
|
|
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
t.Fatalf("get database connection pool: %v", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(1)
|
|
held, err := sqlDB.Conn(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("hold source connection: %v", err)
|
|
}
|
|
defer held.Close()
|
|
|
|
previousTimeout := backupSQLiteTimeout
|
|
backupSQLiteTimeout = 20 * time.Millisecond
|
|
t.Cleanup(func() { backupSQLiteTimeout = previousTimeout })
|
|
err = BackupSQLite(filepath.Join(t.TempDir(), "backup.db"))
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("BackupSQLite error = %v, want context deadline exceeded", err)
|
|
}
|
|
}
|
|
|
|
func TestBackupSQLiteRefusesExistingDestination(t *testing.T) {
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "")
|
|
if err := InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
|
|
t.Fatalf("InitDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = CloseDB() })
|
|
|
|
backupPath := filepath.Join(t.TempDir(), "backup.db")
|
|
if err := os.WriteFile(backupPath, []byte("existing backup"), 0o600); err != nil {
|
|
t.Fatalf("create existing destination: %v", err)
|
|
}
|
|
err := BackupSQLite(backupPath)
|
|
want := fmt.Sprintf("sqlite backup destination already exists: %s", backupPath)
|
|
if err == nil || err.Error() != want {
|
|
t.Fatalf("BackupSQLite error = %v, want %q", err, want)
|
|
}
|
|
data, err := os.ReadFile(backupPath)
|
|
if err != nil {
|
|
t.Fatalf("read existing destination: %v", err)
|
|
}
|
|
if string(data) != "existing backup" {
|
|
t.Fatalf("existing destination = %q, want %q", data, "existing backup")
|
|
}
|
|
}
|
|
|
|
func TestBackupSQLiteStepPages(t *testing.T) {
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "")
|
|
if got := backupSQLiteStepPages(); got != -1 {
|
|
t.Fatalf("WAL backup step pages = %d, want -1", got)
|
|
}
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "DELETE")
|
|
if got := backupSQLiteStepPages(); got != 128 {
|
|
t.Fatalf("DELETE backup step pages = %d, want 128", got)
|
|
}
|
|
}
|
|
|
|
func TestInitDBCleansBackupDirectories(t *testing.T) {
|
|
t.Setenv("XUI_DB_JOURNAL_MODE", "")
|
|
dbDir := t.TempDir()
|
|
orphanDir := filepath.Join(dbDir, sqliteBackupDirPrefix+"orphan")
|
|
if err := os.Mkdir(orphanDir, 0o700); err != nil {
|
|
t.Fatalf("create orphan backup directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(orphanDir, "backup.db"), []byte("backup"), 0o600); err != nil {
|
|
t.Fatalf("write orphan backup: %v", err)
|
|
}
|
|
regularDir := filepath.Join(dbDir, ".x-ui-keep")
|
|
if err := os.Mkdir(regularDir, 0o700); err != nil {
|
|
t.Fatalf("create regular directory: %v", err)
|
|
}
|
|
|
|
if err := InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
|
|
t.Fatalf("InitDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = CloseDB() })
|
|
if _, err := os.Stat(orphanDir); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("orphan backup directory error = %v, want not exist", err)
|
|
}
|
|
if info, err := os.Stat(regularDir); err != nil || !info.IsDir() {
|
|
t.Fatalf("regular directory info = %v, %v; want existing directory", info, err)
|
|
}
|
|
}
|