Rename module github.com/johnellis/stalwart-migrator -> github.com/LINUXexpert-org/stalwart-migrator to match the repository location, so the module resolves under `go get`. The initial commit's .gitignore listed the compiled binary as a bare `stalwart-migrate` pattern, which Git matches at any depth -- so it also excluded the cmd/stalwart-migrate/ source directory, and the initial commit shipped without the CLI entrypoint. Anchor the pattern to the repo root as /stalwart-migrate and add the four missing files. go build, go vet, and go test ./... all pass.
218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
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")
|
|
}
|
|
}
|