Files
stalwart-migrator/internal/backup/backup_test.go
T
jcoffey-dev 77e80cb9f2 Change the copyright holder to Coffey Labs
119 SPDX-FileCopyrightText headers and the README's licence line.

The distinction that matters here: LINUXexpert-org appears in this repository
in two completely different roles. As a copyright holder in the SPDX headers,
which is what changes, and as the GitHub organisation in the module path and 64
import statements, which does not -- the repository still lives at
github.com/LINUXexpert-org/stalwart-migrator, and rewriting that would not be a
licence change, it would break the build.

Both replacements are anchored to their copyright forms, so an import path
cannot match either. Import count is 64 before and after, and go.mod is
untouched.

LICENSE untouched: the FSF's copyright on the GPL text and the "<name of
author>" placeholders are not ours to edit.

go vet, go build and go test all clean.
2026-08-30 01:28:23 -07:00

221 lines
6.9 KiB
Go

// SPDX-FileCopyrightText: 2026 Coffey Labs
// SPDX-License-Identifier: GPL-3.0-or-later
package backup
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/LINUXexpert-org/stalwart-migrator/internal/checkpoint"
)
func TestBackupRunEndToEndAndResume(t *testing.T) {
dir := t.TempDir()
binaryPath := filepath.Join(dir, "stalwart")
if err := os.WriteFile(binaryPath, []byte("fake binary"), 0o755); err != nil {
t.Fatal(err)
}
dataDir := filepath.Join(dir, "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dataDir, "db.bin"), []byte("some rocksdb-shaped data"), 0o644); err != nil {
t.Fatal(err)
}
backupDir := filepath.Join(dir, "data-backup")
settingsPath := filepath.Join(dir, "settings.json")
principalsPath := filepath.Join(dir, "principals.json")
pythonScript := fmt.Sprintf("#!/bin/sh\necho fake-settings > %q\necho fake-principals > %q\n", settingsPath, principalsPath)
pythonDir := withFakeExecutable(t, "python3", pythonScript)
pythonScriptPath := filepath.Join(pythonDir, "python3")
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("print('fake migrate_v016.py')\n"))
}))
defer srv.Close()
store := checkpoint.NewStore(t.TempDir())
rs, err := store.Create("0.15.5", "0.16.14")
if err != nil {
t.Fatalf("store.Create: %v", err)
}
rs.Topology = checkpoint.Topology{StoreBackend: "rocksdb"}
opts := Options{
BinaryPath: binaryPath,
DataDir: dataDir,
BackupDir: backupDir,
MigrationScriptURL: srv.URL,
ScriptDestPath: filepath.Join(dir, "migrate_v016.py"),
AdminURL: "https://mail.example.com",
AdminUser: "admin",
AdminPassword: "hunter2",
SettingsDumpPath: settingsPath,
PrincipalsDumpPath: principalsPath,
}
report, err := Run(context.Background(), store, rs, opts)
if err != nil {
t.Fatalf("Run #1: %v", err)
}
for _, r := range report.Results {
if r.Status == StatusFail {
t.Errorf("Run #1: step %s failed: %s", r.Name, r.Detail)
}
}
for _, name := range []string{"old-binary", "fs-backup", "settings-dump", "principals-dump"} {
if _, ok := rs.Artifacts[name]; !ok {
t.Errorf("Run #1: expected artifact %q to be recorded, got %v", name, rs.Artifacts)
}
}
if _, err := os.Stat(binaryPath + ".v0.15.5"); err != nil {
t.Errorf("preserved binary missing: %v", err)
}
// -- simulate a crash and resume, making every non-idempotent step's
// -- inputs unusable so a re-execution (rather than a skip) fails loudly.
if err := os.RemoveAll(dataDir); err != nil {
t.Fatal(err)
}
srv.Close()
if err := os.WriteFile(pythonScriptPath, []byte("#!/bin/sh\necho should-not-run-again >&2\nexit 1\n"), 0o755); err != nil {
t.Fatal(err)
}
resumed, err := store.Load(rs.RunID)
if err != nil {
t.Fatalf("store.Load (resume): %v", err)
}
report2, err := Run(context.Background(), store, resumed, opts)
if err != nil {
t.Fatalf("Run #2 (resume) should succeed without redoing completed steps: %v", err)
}
if len(report2.Results) != len(report.Results) {
t.Errorf("resumed report has %d results, want %d (same as first run)", len(report2.Results), len(report.Results))
}
for _, r := range report2.Results {
if r.Status == StatusFail {
t.Errorf("Run #2 (resume): step %s unexpectedly failed: %s", r.Name, r.Detail)
}
}
}
func TestBackupRunSkipsVandelayWhenNoAccountsGiven(t *testing.T) {
dir := t.TempDir()
binaryPath := filepath.Join(dir, "stalwart")
os.WriteFile(binaryPath, []byte("fake binary"), 0o755)
dataDir := filepath.Join(dir, "data")
os.MkdirAll(dataDir, 0o755)
os.WriteFile(filepath.Join(dataDir, "db.bin"), []byte("x"), 0o644)
withFakeExecutable(t, "python3", fmt.Sprintf(
"#!/bin/sh\necho x > %q\necho x > %q\n",
filepath.Join(dir, "settings.json"), filepath.Join(dir, "principals.json"),
))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("script"))
}))
defer srv.Close()
store := checkpoint.NewStore(t.TempDir())
rs, err := store.Create("0.15.5", "0.16.14")
if err != nil {
t.Fatal(err)
}
rs.Topology = checkpoint.Topology{StoreBackend: "rocksdb"}
opts := Options{
BinaryPath: binaryPath,
DataDir: dataDir,
BackupDir: filepath.Join(dir, "data-backup"),
MigrationScriptURL: srv.URL,
ScriptDestPath: filepath.Join(dir, "migrate_v016.py"),
AdminURL: "https://mail.example.com",
SettingsDumpPath: filepath.Join(dir, "settings.json"),
PrincipalsDumpPath: filepath.Join(dir, "principals.json"),
// Accounts intentionally left empty.
}
report, err := Run(context.Background(), store, rs, opts)
if err != nil {
t.Fatalf("Run: %v", err)
}
found := false
for _, r := range report.Results {
if r.Name == "vandelay-export" {
found = true
if r.Status != StatusSkipped {
t.Errorf("vandelay-export status = %s, want skipped", r.Status)
}
}
}
if !found {
t.Error("report missing a vandelay-export entry")
}
}
func TestBackupRunSkipsBinaryPreservationForDryRun(t *testing.T) {
dir := t.TempDir()
binaryPath := filepath.Join(dir, "stalwart")
if err := os.WriteFile(binaryPath, []byte("fake binary"), 0o755); err != nil {
t.Fatal(err)
}
dataDir := filepath.Join(dir, "data")
os.MkdirAll(dataDir, 0o755)
os.WriteFile(filepath.Join(dataDir, "db.bin"), []byte("x"), 0o644)
withFakeExecutable(t, "python3", fmt.Sprintf(
"#!/bin/sh\necho x > %q\necho x > %q\n",
filepath.Join(dir, "settings.json"), filepath.Join(dir, "principals.json"),
))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("script"))
}))
defer srv.Close()
store := checkpoint.NewStore(t.TempDir())
rs, err := store.Create("0.15.5", "0.16.14")
if err != nil {
t.Fatal(err)
}
rs.Topology = checkpoint.Topology{StoreBackend: "rocksdb"}
opts := Options{
BinaryPath: binaryPath,
SkipBinaryPreservation: true,
DataDir: dataDir,
BackupDir: filepath.Join(dir, "data-backup"),
MigrationScriptURL: srv.URL,
ScriptDestPath: filepath.Join(dir, "migrate_v016.py"),
AdminURL: "https://mail.example.com",
SettingsDumpPath: filepath.Join(dir, "settings.json"),
PrincipalsDumpPath: filepath.Join(dir, "principals.json"),
}
report, err := Run(context.Background(), store, rs, opts)
if err != nil {
t.Fatalf("Run: %v", err)
}
for _, r := range report.Results {
if r.Name == "preserve-binary" && r.Status != StatusSkipped {
t.Errorf("preserve-binary status = %s, want skipped", r.Status)
}
}
if _, err := os.Stat(binaryPath); err != nil {
t.Errorf("production binary at %s should be untouched, but stat failed: %v", binaryPath, err)
}
if _, ok := rs.Artifacts["old-binary"]; ok {
t.Error("no old-binary artifact should be recorded when preservation is skipped")
}
}