Files
felhom-controller/controller/internal/backup/restore_db_test.go
T
admin 80e6ad8c47
gates / gates (push) Successful in 26s
controller v0.267.0: tests off DooPlex's Docker, cut-off copies refused, two pages true
R-650: internal/dockerexec — every docker exec routed through it; under
go test a real docker is refused (opt-in FELHOM_TEST_REAL_DOCKER=1; a stub
under the temp dir is allowed). api/stacks/web tests run under a silent
stub (TestMain). TestR650_NoBareDockerExec pins it repo-wide.
R-640: a dump without its engine's completion marker is refused before
the first mutation (unit + off-site restore) and again before any load.
R-499: the Tier-2 page's system-disk sentence has four true branches.
R-518: the backup button states the measured ~8 min stop.
R-626: measured on 9202, not reproduced.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 20:25:28 +02:00

133 lines
4.5 KiB
Go

package backup
import (
"context"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
)
func newReimportTestManager() *Manager {
return &Manager{logger: log.New(io.Discard, "", 0)}
}
func writeDump(t *testing.T, nsRoot, stack string, dbType DBType) string {
t.Helper()
dir := AppDBDumpPath(nsRoot, stack)
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
p := filepath.Join(dir, fmt.Sprintf("%s-%s.sql", stack, dbType))
// R-640: a complete dump ends with its engine's marker, as the real tools write it.
end := "-- PostgreSQL database dump complete\n"
if dbType == appbackup.DBTypeMariaDB {
end = "-- Dump completed on 2026-09-23 21:00:00\n"
}
if err := os.WriteFile(p, []byte("-- dump\nDROP TABLE IF EXISTS t;\n"+end), 0o644); err != nil {
t.Fatal(err)
}
return p
}
// TestReimportDBDumps_ImportsWhenDumpAndDBPresent asserts F17: when a captured .sql dump exists and a
// matching running DB is discovered, reimportDBDumps replays it. The PRE-FIX restore never called any
// import — this orchestration is the fix.
func TestReimportDBDumps_ImportsWhenDumpAndDBPresent(t *testing.T) {
nsRoot := t.TempDir()
wantPath := writeDump(t, nsRoot, "app", DBTypeMariaDB)
m := newReimportTestManager()
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
return []DiscoveredDB{
{StackName: "other", DBType: DBTypePostgres, ContainerName: "other-db"},
{StackName: "app", DBType: DBTypeMariaDB, ContainerName: "app-db", ContainerID: "cid"},
}, nil
}
var gotPath string
var gotDB DiscoveredDB
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
gotPath, gotDB = dumpPath, db
return nil
}
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
if err != nil {
t.Fatalf("reimportDBDumps: %v", err)
}
if n != 1 {
t.Fatalf("imported = %d, want 1", n)
}
if gotPath != wantPath {
t.Fatalf("imported path = %q, want %q", gotPath, wantPath)
}
if gotDB.ContainerName != "app-db" {
t.Fatalf("imported into %q, want app-db (must match the stack's own DB)", gotDB.ContainerName)
}
}
// TestReimportDBDumps_FailureSurfaces asserts an import failure is RETURNED, not swallowed (a failed
// data restore must not read as success).
func TestReimportDBDumps_FailureSurfaces(t *testing.T) {
nsRoot := t.TempDir()
writeDump(t, nsRoot, "app", DBTypeMariaDB)
m := newReimportTestManager()
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
return []DiscoveredDB{{StackName: "app", DBType: DBTypeMariaDB, ContainerName: "app-db"}}, nil
}
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
return fmt.Errorf("boom")
}
if _, err := m.reimportDBDumps(context.Background(), "app", nsRoot); err == nil || !strings.Contains(err.Error(), "boom") {
t.Fatalf("expected the import failure to surface, got %v", err)
}
}
// TestReimportDBDumps_NoDumpNoImport asserts apps with no .sql dump never trigger discovery/import.
func TestReimportDBDumps_NoDumpNoImport(t *testing.T) {
nsRoot := t.TempDir()
m := newReimportTestManager()
discoverCalled := false
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
discoverCalled = true
return nil, nil
}
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
t.Fatal("importDBDump must not be called when there is no dump")
return nil
}
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
if err != nil || n != 0 {
t.Fatalf("reimportDBDumps with no dump = (%d, %v), want (0, nil)", n, err)
}
if discoverCalled {
t.Fatalf("discovery should be skipped when there is no .sql dump")
}
}
// TestReimportDBDumps_DumpButNoMatchingDB asserts that a dump with no matching running DB container is a
// non-fatal skip (logged), returning 0 imported and no error — the app is up, just no DB matched.
func TestReimportDBDumps_DumpButNoMatchingDB(t *testing.T) {
nsRoot := t.TempDir()
writeDump(t, nsRoot, "app", DBTypeMariaDB)
m := newReimportTestManager()
m.discoverDBs = func(ctx context.Context) ([]DiscoveredDB, error) {
return []DiscoveredDB{{StackName: "different", DBType: DBTypeMariaDB}}, nil
}
m.importDBDump = func(ctx context.Context, db DiscoveredDB, dumpPath string) error {
t.Fatal("must not import when no DB matches the stack")
return nil
}
n, err := m.reimportDBDumps(context.Background(), "app", nsRoot)
if err != nil || n != 0 {
t.Fatalf("= (%d, %v), want (0, nil)", n, err)
}
}