1
0

backup_test.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package database
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "testing"
  11. "time"
  12. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  13. )
  14. func TestBackupSQLiteProducesValidSnapshotDuringWrites(t *testing.T) {
  15. t.Setenv("XUI_DB_JOURNAL_MODE", "")
  16. dbPath := filepath.Join(t.TempDir(), "x-ui.db")
  17. if err := InitDB(dbPath); err != nil {
  18. t.Fatalf("InitDB: %v", err)
  19. }
  20. t.Cleanup(func() { _ = CloseDB() })
  21. seed := make([]model.Setting, 128)
  22. value := strings.Repeat("x", 1024)
  23. for i := range seed {
  24. seed[i] = model.Setting{Key: fmt.Sprintf("backup-seed-%d", i), Value: value}
  25. }
  26. if err := db.Create(&seed).Error; err != nil {
  27. t.Fatalf("seed database: %v", err)
  28. }
  29. stop := make(chan struct{})
  30. firstWrite := make(chan error, 1)
  31. writesDone := make(chan error, 1)
  32. go func() {
  33. for i := 0; i < 128; i++ {
  34. if err := db.Create(&model.Setting{Key: fmt.Sprintf("backup-write-%d", i), Value: value}).Error; err != nil {
  35. if i == 0 {
  36. firstWrite <- err
  37. }
  38. writesDone <- err
  39. return
  40. }
  41. if i == 0 {
  42. firstWrite <- nil
  43. }
  44. select {
  45. case <-stop:
  46. writesDone <- nil
  47. return
  48. default:
  49. }
  50. }
  51. writesDone <- nil
  52. }()
  53. if err := <-firstWrite; err != nil {
  54. t.Fatalf("first concurrent write: %v", err)
  55. }
  56. backupPath := filepath.Join(t.TempDir(), "backup.db")
  57. if err := BackupSQLite(backupPath); err != nil {
  58. close(stop)
  59. <-writesDone
  60. t.Fatalf("BackupSQLite: %v", err)
  61. }
  62. close(stop)
  63. if err := <-writesDone; err != nil {
  64. t.Fatalf("concurrent write: %v", err)
  65. }
  66. if err := ValidateSQLiteDB(backupPath); err != nil {
  67. t.Fatalf("validate backup: %v", err)
  68. }
  69. backup, err := sql.Open("sqlite3", backupPath)
  70. if err != nil {
  71. t.Fatalf("open backup: %v", err)
  72. }
  73. defer backup.Close()
  74. var seedCount int
  75. if err := backup.QueryRow("SELECT count(*) FROM settings WHERE key LIKE 'backup-seed-%'").Scan(&seedCount); err != nil {
  76. t.Fatalf("count seeded rows: %v", err)
  77. }
  78. if seedCount != 128 {
  79. t.Fatalf("seeded row count = %d, want 128", seedCount)
  80. }
  81. var firstWriteCount int
  82. if err := backup.QueryRow("SELECT count(*) FROM settings WHERE key = 'backup-write-0'").Scan(&firstWriteCount); err != nil {
  83. t.Fatalf("count first concurrent write: %v", err)
  84. }
  85. if firstWriteCount != 1 {
  86. t.Fatalf("first concurrent write count = %d, want 1", firstWriteCount)
  87. }
  88. }
  89. func TestBackupSQLiteTimesOutWaitingForSourceConnection(t *testing.T) {
  90. t.Setenv("XUI_DB_JOURNAL_MODE", "")
  91. if err := InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  92. t.Fatalf("InitDB: %v", err)
  93. }
  94. t.Cleanup(func() { _ = CloseDB() })
  95. sqlDB, err := db.DB()
  96. if err != nil {
  97. t.Fatalf("get database connection pool: %v", err)
  98. }
  99. sqlDB.SetMaxOpenConns(1)
  100. held, err := sqlDB.Conn(context.Background())
  101. if err != nil {
  102. t.Fatalf("hold source connection: %v", err)
  103. }
  104. defer held.Close()
  105. previousTimeout := backupSQLiteTimeout
  106. backupSQLiteTimeout = 20 * time.Millisecond
  107. t.Cleanup(func() { backupSQLiteTimeout = previousTimeout })
  108. err = BackupSQLite(filepath.Join(t.TempDir(), "backup.db"))
  109. if !errors.Is(err, context.DeadlineExceeded) {
  110. t.Fatalf("BackupSQLite error = %v, want context deadline exceeded", err)
  111. }
  112. }
  113. func TestBackupSQLiteRefusesExistingDestination(t *testing.T) {
  114. t.Setenv("XUI_DB_JOURNAL_MODE", "")
  115. if err := InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  116. t.Fatalf("InitDB: %v", err)
  117. }
  118. t.Cleanup(func() { _ = CloseDB() })
  119. backupPath := filepath.Join(t.TempDir(), "backup.db")
  120. if err := os.WriteFile(backupPath, []byte("existing backup"), 0o600); err != nil {
  121. t.Fatalf("create existing destination: %v", err)
  122. }
  123. err := BackupSQLite(backupPath)
  124. want := fmt.Sprintf("sqlite backup destination already exists: %s", backupPath)
  125. if err == nil || err.Error() != want {
  126. t.Fatalf("BackupSQLite error = %v, want %q", err, want)
  127. }
  128. data, err := os.ReadFile(backupPath)
  129. if err != nil {
  130. t.Fatalf("read existing destination: %v", err)
  131. }
  132. if string(data) != "existing backup" {
  133. t.Fatalf("existing destination = %q, want %q", data, "existing backup")
  134. }
  135. }
  136. func TestBackupSQLiteStepPages(t *testing.T) {
  137. t.Setenv("XUI_DB_JOURNAL_MODE", "")
  138. if got := backupSQLiteStepPages(); got != -1 {
  139. t.Fatalf("WAL backup step pages = %d, want -1", got)
  140. }
  141. t.Setenv("XUI_DB_JOURNAL_MODE", "DELETE")
  142. if got := backupSQLiteStepPages(); got != 128 {
  143. t.Fatalf("DELETE backup step pages = %d, want 128", got)
  144. }
  145. }
  146. func TestInitDBCleansBackupDirectories(t *testing.T) {
  147. t.Setenv("XUI_DB_JOURNAL_MODE", "")
  148. dbDir := t.TempDir()
  149. orphanDir := filepath.Join(dbDir, sqliteBackupDirPrefix+"orphan")
  150. if err := os.Mkdir(orphanDir, 0o700); err != nil {
  151. t.Fatalf("create orphan backup directory: %v", err)
  152. }
  153. if err := os.WriteFile(filepath.Join(orphanDir, "backup.db"), []byte("backup"), 0o600); err != nil {
  154. t.Fatalf("write orphan backup: %v", err)
  155. }
  156. regularDir := filepath.Join(dbDir, ".x-ui-keep")
  157. if err := os.Mkdir(regularDir, 0o700); err != nil {
  158. t.Fatalf("create regular directory: %v", err)
  159. }
  160. if err := InitDB(filepath.Join(dbDir, "x-ui.db")); err != nil {
  161. t.Fatalf("InitDB: %v", err)
  162. }
  163. t.Cleanup(func() { _ = CloseDB() })
  164. if _, err := os.Stat(orphanDir); !errors.Is(err, os.ErrNotExist) {
  165. t.Fatalf("orphan backup directory error = %v, want not exist", err)
  166. }
  167. if info, err := os.Stat(regularDir); err != nil || !info.IsDir() {
  168. t.Fatalf("regular directory info = %v, %v; want existing directory", info, err)
  169. }
  170. }