Files
jcoffey-dev 12ec0c3fd4 Rename the module to the Coffey-Labs organisation
The repositories moved off LINUXexpert-org. Here that is not a
documentation change: the old organisation was the module path, so it is
declared in go.mod and repeated in every internal import.

Leaving it would have been worse than a stale link. GitHub redirects the
repository, but a go.mod whose module line disagrees with the path it was
fetched from is an error rather than a redirect, so `go get` on the new
address would have failed against the old declaration.

go.mod, 34 files of imports, and the repository links in README and
ARCHITECTURE. go mod tidy leaves go.sum untouched -- no dependency moved,
only our own path.
2026-08-30 15:24:18 -07:00

124 lines
4.0 KiB
Go

// SPDX-FileCopyrightText: 2026 Coffey Labs
// SPDX-License-Identifier: GPL-3.0-or-later
package recovery
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/Coffey-Labs/stalwart-migrator/internal/checkpoint"
)
// recordingLauncher stands in for a deployment that is not a local binary -
// a container, when that lands. It records what it was asked for and
// delegates the actual process to BinaryLauncher, so the recovery cycle
// still has something real to talk to.
type recordingLauncher struct {
inner Launcher
calls int
lastOp LaunchOptions
}
func (r *recordingLauncher) Launch(ctx context.Context, o LaunchOptions) (Supervised, error) {
r.calls++
r.lastOp = o
return r.inner.Launch(ctx, o)
}
// The whole point of the seam: a deployment that is not a host binary can
// supply its own way of starting the target version, and everything the
// recovery cycle does afterwards is unchanged.
func TestRunUsesTheSuppliedLauncher(t *testing.T) {
port := freePort(t)
configPath := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configPath, []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
applyDir := t.TempDir()
withFakeExecutable(t, "stalwart-cli", fmt.Sprintf("#!/bin/sh\necho \"$@\" >> %q\nexit 0\n", argsFile(t, applyDir)))
exportFile := filepath.Join(t.TempDir(), "export.json")
if err := os.WriteFile(exportFile, []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
store := checkpoint.NewStore(t.TempDir())
rs, err := store.Create("0.15.5", "0.16.14")
if err != nil {
t.Fatal(err)
}
launcher := &recordingLauncher{inner: BinaryLauncher{BinaryPath: testBinaryPath(t)}}
_, err = Run(context.Background(), store, rs, Options{
// Deliberately empty: a supplied Launcher owns how the target
// version is started, so BinaryPath must not be consulted at all.
BinaryPath: "",
ConfigPath: configPath,
ListenURL: fmt.Sprintf("http://127.0.0.1:%d/", port),
AdminUser: "admin",
ApplyFiles: []string{exportFile},
ExtraEnv: helperProcessEnv(port),
StartupTimeout: 5 * time.Second,
StopGrace: 5 * time.Second,
Launcher: launcher,
})
if err != nil {
t.Fatalf("Run with a supplied launcher: %v", err)
}
if launcher.calls != 1 {
t.Errorf("launcher called %d times, want 1", launcher.calls)
}
if !launcher.lastOp.RecoveryMode {
t.Error("recovery cycle launched without RecoveryMode set")
}
if launcher.lastOp.AdminPassword == "" {
t.Error("recovery cycle launched without a generated admin password")
}
if launcher.lastOp.ConfigPath != configPath {
t.Errorf("ConfigPath = %q, want %q", launcher.lastOp.ConfigPath, configPath)
}
}
// A nil Launcher has to keep meaning exactly what it meant before this
// seam existed, or every existing caller changes behaviour silently.
func TestNilLauncherStillRunsTheBinaryPath(t *testing.T) {
port := freePort(t)
configPath := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(configPath, []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
applyDir := t.TempDir()
applyLog := argsFile(t, applyDir)
withFakeExecutable(t, "stalwart-cli", fmt.Sprintf("#!/bin/sh\necho \"$@\" >> %q\nexit 0\n", applyLog))
exportFile := filepath.Join(t.TempDir(), "export.json")
if err := os.WriteFile(exportFile, []byte("{}"), 0o644); err != nil {
t.Fatal(err)
}
store := checkpoint.NewStore(t.TempDir())
rs, err := store.Create("0.15.5", "0.16.14")
if err != nil {
t.Fatal(err)
}
if _, err := Run(context.Background(), store, rs, Options{
BinaryPath: testBinaryPath(t),
ConfigPath: configPath,
ListenURL: fmt.Sprintf("http://127.0.0.1:%d/", port),
AdminUser: "admin",
ApplyFiles: []string{exportFile},
ExtraEnv: helperProcessEnv(port),
StartupTimeout: 5 * time.Second,
StopGrace: 5 * time.Second,
// Launcher deliberately unset.
}); err != nil {
t.Fatalf("Run with a nil launcher: %v", err)
}
if got := readArgsFile(t, applyLog); got == "" {
t.Fatal("stalwart-cli apply was never invoked, so the cycle did not run")
}
}