Initial commit: stalwart-migrator design and scaffolding

In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with
checkpointed rollback and post-migration validation. Design stage; see
ARCHITECTURE.md.
This commit is contained in:
2026-08-22 18:17:17 -07:00
commit 719a945d64
71 changed files with 6677 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
package recovery
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"testing"
"time"
)
// freePort asks the OS for an unused TCP port by binding to :0 and
// immediately releasing it. There's a small window where something else
// could grab it before the fake server binds, but that's an acceptable,
// standard tradeoff for tests.
func freePort(t *testing.T) int {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
return ln.Addr().(*net.TCPAddr).Port
}
func helperProcessEnv(port int, extra ...string) []string {
env := []string{
"STALWART_MIGRATOR_TEST_HELPER=1",
fmt.Sprintf("STALWART_MIGRATOR_TEST_PORT=%d", port),
}
return append(env, extra...)
}
func testBinaryPath(t *testing.T) string {
t.Helper()
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return self
}
func TestProcessStartWaitHealthyStop(t *testing.T) {
port := freePort(t)
configPath := filepath.Join(t.TempDir(), "config.json")
os.WriteFile(configPath, []byte("{}"), 0o644)
proc := &Process{}
err := proc.Start(context.Background(), ProcessOptions{
BinaryPath: testBinaryPath(t),
ConfigPath: configPath,
ExtraEnv: helperProcessEnv(port),
})
if err != nil {
t.Fatalf("Start: %v", err)
}
url := fmt.Sprintf("http://127.0.0.1:%d/", port)
if err := WaitForHealthy(context.Background(), nil, url, 5*time.Second); err != nil {
t.Fatalf("WaitForHealthy: %v", err)
}
if err := proc.Stop(5 * time.Second); err != nil {
t.Fatalf("Stop: %v", err)
}
}
func TestProcessStopEscalatesToSIGKILL(t *testing.T) {
port := freePort(t)
configPath := filepath.Join(t.TempDir(), "config.json")
os.WriteFile(configPath, []byte("{}"), 0o644)
proc := &Process{}
err := proc.Start(context.Background(), ProcessOptions{
BinaryPath: testBinaryPath(t),
ConfigPath: configPath,
ExtraEnv: helperProcessEnv(port, "STALWART_MIGRATOR_TEST_IGNORE_SIGTERM=1"),
})
if err != nil {
t.Fatalf("Start: %v", err)
}
url := fmt.Sprintf("http://127.0.0.1:%d/", port)
if err := WaitForHealthy(context.Background(), nil, url, 5*time.Second); err != nil {
t.Fatalf("WaitForHealthy: %v", err)
}
err = proc.Stop(500 * time.Millisecond)
if err == nil {
t.Fatal("Stop should report an error when it had to escalate to SIGKILL")
}
}
func TestWaitForHealthyTimesOut(t *testing.T) {
// Nothing listens on this port.
port := freePort(t)
url := fmt.Sprintf("http://127.0.0.1:%d/", port)
err := WaitForHealthy(context.Background(), nil, url, 500*time.Millisecond)
if err == nil {
t.Fatal("WaitForHealthy should time out when nothing is listening")
}
}