Ctrl K
ComunidadGuías…

main_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"database/sql"
	"os"
	"path/filepath"
	"testing"

	"github.com/naibabiji/server-panel/config"
	"github.com/naibabiji/server-panel/database"
)

// newMinimalBackupDB creates a trivial-but-valid sqlite file (VerifyDBBackup
// requires it to open and pass PRAGMA integrity_check) to stand in for a
// real database backup snapshot.
func newMinimalBackupDB(t *testing.T, path string) {
	t.Helper()
	db, err := sql.Open("sqlite", path)
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	if _, err := db.Exec("CREATE TABLE settings (skey TEXT PRIMARY KEY, svalue TEXT)"); err != nil {
		t.Fatalf("create table: %v", err)
	}
	if err := db.Close(); err != nil {
		t.Fatalf("close db: %v", err)
	}
}

func TestCopyFileLeavesDestinationUntouchedOnWriteFailure(t *testing.T) {
	dir := t.TempDir()
	src := filepath.Join(dir, "src")
	if err := os.WriteFile(src, []byte("new content"), 0600); err != nil {
		t.Fatalf("write src: %v", err)
	}

	dstDir := filepath.Join(dir, "dstdir")
	if err := os.Mkdir(dstDir, 0700); err != nil {
		t.Fatalf("mkdir dstdir: %v", err)
	}
	dst := filepath.Join(dstDir, "dst")
	if err := os.WriteFile(dst, []byte("original content"), 0600); err != nil {
		t.Fatalf("write dst: %v", err)
	}

	// Read-only directory: copyFile can't create its temp file, so the
	// existing dst must survive completely unmodified - not truncated by
	// an in-place os.WriteFile(dst, ...).
	if err := os.Chmod(dstDir, 0500); err != nil {
		t.Fatalf("chmod dstdir: %v", err)
	}
	t.Cleanup(func() { _ = os.Chmod(dstDir, 0700) })

	if err := copyFile(src, dst); err == nil {
		t.Fatal("expected copyFile to fail when its directory is read-only")
	}

	data, err := os.ReadFile(dst)
	if err != nil {
		t.Fatalf("read dst after failed copy: %v", err)
	}
	if string(data) != "original content" {
		t.Errorf("dst after failed copy = %q, want untouched %q", data, "original content")
	}
}

func TestRunRestoreBackupRemovesStaleKeyWhenArchiveHasNone(t *testing.T) {
	srcDir := t.TempDir()
	dbPath := filepath.Join(srcDir, "server-panel.db")
	newMinimalBackupDB(t, dbPath)

	archiveDir := t.TempDir()
	archivePath, err := database.CreateFullBackupArchive(dbPath, filepath.Join(srcDir, "no-such-secret.key"), archiveDir)
	if err != nil {
		t.Fatalf("CreateFullBackupArchive: %v", err)
	}

	// Target ("this panel", freshly reinstalled) already has its own
	// secret.key from install - this is the exact scenario the restore path
	// must not leave stale: handlers.GetSecretEncryptionKey prefers an
	// existing file over any DB-stored legacy key, so leaving this file in
	// place would silently defeat the legacy-key fallback after restore.
	targetDir := t.TempDir()
	liveSecretKeyPath := filepath.Join(targetDir, "secret.key")
	if err := os.WriteFile(liveSecretKeyPath, []byte("stale-target-key"), 0600); err != nil {
		t.Fatalf("seed target secret.key: %v", err)
	}

	cfg := &config.Config{
		Panel:  config.PanelConfig{DataDir: targetDir},
		SQLite: config.SQLiteConfig{Path: filepath.Join(targetDir, "server-panel.db")},
	}

	if err := runRestoreBackup(cfg, archivePath); err != nil {
		t.Fatalf("runRestoreBackup: %v", err)
	}

	if _, err := os.Stat(liveSecretKeyPath); !os.IsNotExist(err) {
		t.Errorf("expected stale secret.key removed from live path, stat err = %v", err)
	}

	matches, _ := filepath.Glob(liveSecretKeyPath + ".*.pre-restore")
	if len(matches) != 1 {
		t.Fatalf("expected exactly one .pre-restore copy of the stale key, got %v", matches)
	}
	data, err := os.ReadFile(matches[0])
	if err != nil {
		t.Fatalf("read pre-restore key copy: %v", err)
	}
	if string(data) != "stale-target-key" {
		t.Errorf("pre-restore key copy content = %q, want %q", data, "stale-target-key")
	}

	if _, err := os.Stat(cfg.SQLite.Path); err != nil {
		t.Errorf("expected restored db at live path: %v", err)
	}
}

func TestRunRestoreBackupWritesArchiveKeyToLivePath(t *testing.T) {
	srcDir := t.TempDir()
	dbPath := filepath.Join(srcDir, "server-panel.db")
	newMinimalBackupDB(t, dbPath)

	secretKeyPath := filepath.Join(srcDir, "secret.key")
	if err := os.WriteFile(secretKeyPath, []byte("archive-key-value"), 0600); err != nil {
		t.Fatalf("write source secret.key: %v", err)
	}

	archiveDir := t.TempDir()
	archivePath, err := database.CreateFullBackupArchive(dbPath, secretKeyPath, archiveDir)
	if err != nil {
		t.Fatalf("CreateFullBackupArchive: %v", err)
	}

	targetDir := t.TempDir()
	cfg := &config.Config{
		Panel:  config.PanelConfig{DataDir: targetDir},
		SQLite: config.SQLiteConfig{Path: filepath.Join(targetDir, "server-panel.db")},
	}

	if err := runRestoreBackup(cfg, archivePath); err != nil {
		t.Fatalf("runRestoreBackup: %v", err)
	}

	data, err := os.ReadFile(filepath.Join(targetDir, "secret.key"))
	if err != nil {
		t.Fatalf("read restored secret.key: %v", err)
	}
	if string(data) != "archive-key-value" {
		t.Errorf("restored secret.key content = %q, want %q", data, "archive-key-value")
	}
}

// TestRunRestoreBackupRollsBackKeyWhenDatabaseWriteFails forces the final
// database.RestoreDatabaseFile step to fail (by making its target directory
// read-only) after the secret-key step has already succeeded, and checks
// that the live secret.key is rolled back to its pre-restore content rather
// than left paired with the database that was never actually replaced.
func TestRunRestoreBackupRollsBackKeyWhenDatabaseWriteFails(t *testing.T) {
	srcDir := t.TempDir()
	dbPath := filepath.Join(srcDir, "server-panel.db")
	newMinimalBackupDB(t, dbPath)

	secretKeyPath := filepath.Join(srcDir, "secret.key")
	if err := os.WriteFile(secretKeyPath, []byte("archive-key-value"), 0600); err != nil {
		t.Fatalf("write source secret.key: %v", err)
	}

	archiveDir := t.TempDir()
	archivePath, err := database.CreateFullBackupArchive(dbPath, secretKeyPath, archiveDir)
	if err != nil {
		t.Fatalf("CreateFullBackupArchive: %v", err)
	}

	targetDir := t.TempDir()
	liveSecretKeyPath := filepath.Join(targetDir, "secret.key")
	if err := os.WriteFile(liveSecretKeyPath, []byte("stale-target-key"), 0600); err != nil {
		t.Fatalf("seed target secret.key: %v", err)
	}

	// secret.key lives directly in targetDir (writable); the database lives
	// in a separate, pre-created, read-only subdirectory so only the final
	// RestoreDatabaseFile write fails - the earlier secret-key step must
	// still succeed for this to actually test the rollback.
	dbDir := filepath.Join(targetDir, "dbdir")
	if err := os.Mkdir(dbDir, 0500); err != nil {
		t.Fatalf("mkdir read-only dbdir: %v", err)
	}
	t.Cleanup(func() { _ = os.Chmod(dbDir, 0700) })

	cfg := &config.Config{
		Panel:  config.PanelConfig{DataDir: targetDir},
		SQLite: config.SQLiteConfig{Path: filepath.Join(dbDir, "server-panel.db")},
	}

	if err := runRestoreBackup(cfg, archivePath); err == nil {
		t.Fatal("expected runRestoreBackup to fail when the database directory is read-only")
	}

	data, err := os.ReadFile(liveSecretKeyPath)
	if err != nil {
		t.Fatalf("read live secret.key after failed restore: %v", err)
	}
	if string(data) != "stale-target-key" {
		t.Errorf("secret.key after failed database write = %q, want rollback to original %q", data, "stale-target-key")
	}
}