mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
- 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
214 lines
5 KiB
Go
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)
|
|
}
|
|
}
|