3x-ui/database/db_test.go
Sora39831 09f84782b0 test: add unit tests for critical modules and fix flaky redirect middleware
- Add tests for config, database, model, util/common, util/crypto,
  util/random, web/middleware, web/service, and xray packages
- Fix redirect middleware using slice instead of map to guarantee
  deterministic longest-prefix-first matching order
2026-04-03 08:44:51 +08:00

214 lines
5 KiB
Go

package database
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/database/model"
)
func setupTestDB(t *testing.T) {
t.Helper()
tmpDir := t.TempDir()
t.Setenv("XUI_DEBUG", "")
dbPath := filepath.Join(tmpDir, "test.db")
if err := InitDB(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
t.Cleanup(func() {
CloseDB()
})
}
func TestIsSQLiteDB_ValidFile(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "valid.db")
if err := InitDB(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
defer CloseDB()
f, err := os.Open(dbPath)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if !ok {
t.Error("IsSQLiteDB should return true for a valid SQLite file")
}
}
func TestIsSQLiteDB_InvalidFile(t *testing.T) {
tmpDir := t.TempDir()
notADB := filepath.Join(tmpDir, "notdb.txt")
if err := os.WriteFile(notADB, []byte("this is not a database"), 0644); err != nil {
t.Fatal(err)
}
f, err := os.Open(notADB)
if err != nil {
t.Fatal(err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if ok {
t.Error("IsSQLiteDB should return false for a non-SQLite file")
}
}
func TestIsSQLiteDB_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
empty := filepath.Join(tmpDir, "empty.db")
if err := os.WriteFile(empty, []byte{}, 0644); err != nil {
t.Fatal(err)
}
f, err := os.Open(empty)
if err != nil {
t.Fatal(err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
// Empty file returns EOF since there aren't enough bytes to read
if err == nil && ok {
t.Error("IsSQLiteDB should return false for an empty file")
}
}
func TestIsSQLiteDB_WrongContent(t *testing.T) {
// File with 16 bytes (matching SQLite header length) but wrong signature content
r := bytes.NewReader([]byte("SQLite for !!"))
ok, err := IsSQLiteDB(r)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if ok {
t.Error("IsSQLiteDB should return false for wrong signature content")
}
}
func TestInitDB_CreatesTables(t *testing.T) {
setupTestDB(t)
// Verify all tables exist by querying them
tables := []string{"users", "inbounds", "outbound_traffics", "settings", "inbound_client_ips", "client_traffics", "history_of_seeders"}
for _, table := range tables {
var count int64
if err := db.Table(table).Count(&count).Error; err != nil {
t.Errorf("table %q should exist but got error: %v", table, err)
}
}
}
func TestInitDB_CreatesDefaultUser(t *testing.T) {
setupTestDB(t)
var user model.User
if err := db.First(&user).Error; err != nil {
t.Fatalf("should have a default user: %v", err)
}
if user.Username != "admin" {
t.Errorf("default username should be 'admin', got %q", user.Username)
}
if user.Role != "admin" {
t.Errorf("default role should be 'admin', got %q", user.Role)
}
// Password should be a bcrypt hash, not plaintext
if user.Password == "admin" {
t.Error("default password should be hashed, not plaintext")
}
}
func TestInitDB_Idempotent(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XUI_DEBUG", "")
dbPath := filepath.Join(tmpDir, "idempotent.db")
// First init
if err := InitDB(dbPath); err != nil {
t.Fatalf("first InitDB failed: %v", err)
}
CloseDB()
// Second init on the same file should not fail
if err := InitDB(dbPath); err != nil {
t.Fatalf("second InitDB failed: %v", err)
}
defer CloseDB()
// Should still have exactly one default user
var count int64
db.Model(&model.User{}).Count(&count)
if count != 1 {
t.Errorf("expected 1 user after second init, got %d", count)
}
}
func TestValidateSQLiteDB_ValidDB(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "valid.db")
if err := InitDB(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
CloseDB()
if err := ValidateSQLiteDB(dbPath); err != nil {
t.Errorf("ValidateSQLiteDB should pass for valid DB: %v", err)
}
}
func TestValidateSQLiteDB_NonExistent(t *testing.T) {
err := ValidateSQLiteDB("/tmp/does-not-exist-12345.db")
if err == nil {
t.Error("ValidateSQLiteDB should fail for non-existent file")
}
}
func TestValidateSQLiteDB_CorruptDB(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "corrupt.db")
// Write garbage that looks like SQLite header but is corrupt
garbage := make([]byte, 4096)
copy(garbage, []byte("SQLite format 3\x00"))
if err := os.WriteFile(dbPath, garbage, 0644); err != nil {
t.Fatal(err)
}
err := ValidateSQLiteDB(dbPath)
if err == nil {
t.Error("ValidateSQLiteDB should fail for corrupt DB")
}
}
func TestIsNotFound(t *testing.T) {
if IsNotFound(nil) {
t.Error("IsNotFound should return false for nil")
}
}
func TestInitUser_OnlyOnce(t *testing.T) {
setupTestDB(t)
// initUser should not create a second user when table is not empty
if err := initUser(); err != nil {
t.Fatalf("initUser error: %v", err)
}
var count int64
db.Model(&model.User{}).Count(&count)
if count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}