104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/klarkxy/nekonest-cloud/relay/internal/controlplane"
|
|
"github.com/klarkxy/nekonest-cloud/relay/internal/tenantbackup"
|
|
"github.com/klarkxy/nekonest-cloud/relay/internal/tenantfs"
|
|
"github.com/klarkxy/nekonest-cloud/relay/internal/tenantstore"
|
|
"github.com/klarkxy/nekonest/relaycore/protocol"
|
|
)
|
|
|
|
type fakeRegistry struct {
|
|
paths tenantfs.Paths
|
|
err error
|
|
}
|
|
|
|
func (registry fakeRegistry) Quiesce(string, int64) (tenantfs.Paths, error) {
|
|
return registry.paths, registry.err
|
|
}
|
|
|
|
type fakeControl struct {
|
|
updates []controlplane.MigrationAdvance
|
|
}
|
|
|
|
func (control *fakeControl) AdvanceMigration(_ context.Context, update controlplane.MigrationAdvance) error {
|
|
control.updates = append(control.updates, update)
|
|
return nil
|
|
}
|
|
|
|
func createSource(t *testing.T, root, tenantID string) tenantfs.Paths {
|
|
t.Helper()
|
|
paths, err := tenantfs.Resolve(root, tenantID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store, err := tenantstore.NewWithTransportMode(paths.Database, string(protocol.TransportSealed))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.RegisterDevice("host_0123456789abcdef0123456789abcdef", "Host", "linux"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func TestManagerCreatesFencedBackupAndRestoresTarget(t *testing.T) {
|
|
const tenantID = "tenant_0123456789abcdef0123456789abcdef"
|
|
sourceRoot := t.TempDir()
|
|
paths := createSource(t, sourceRoot, tenantID)
|
|
backupRoot := t.TempDir()
|
|
control := &fakeControl{}
|
|
now := time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC)
|
|
source, err := New(Config{
|
|
DataRoot: sourceRoot, BackupRoot: backupRoot,
|
|
Registry: fakeRegistry{paths: paths}, ControlPlane: control, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assignment := controlplane.MigrationAssignment{
|
|
MigrationID: "migration_0123456789abcdef0123456789abcdef", TenantID: tenantID,
|
|
Role: "source", State: "quiescing", SourceGeneration: 3, TargetGeneration: 4,
|
|
}
|
|
if err := source.process(context.Background(), assignment); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(control.updates) != 1 || control.updates[0].Action != "quiesced" || control.updates[0].BackupRef == "" {
|
|
t.Fatalf("source update = %#v", control.updates)
|
|
}
|
|
backupPath, err := tenantbackup.ResolveReference(backupRoot, control.updates[0].BackupRef)
|
|
if err != nil || filepath.Base(backupPath) == "" {
|
|
t.Fatalf("backup path = %q err=%v", backupPath, err)
|
|
}
|
|
targetRoot := t.TempDir()
|
|
targetControl := &fakeControl{}
|
|
target, err := New(Config{
|
|
DataRoot: targetRoot, BackupRoot: backupRoot,
|
|
Registry: fakeRegistry{}, ControlPlane: targetControl, Now: func() time.Time { return now },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assignment.Role = "target"
|
|
assignment.State = "copying"
|
|
assignment.BackupRef = control.updates[0].BackupRef
|
|
assignment.ManifestSHA256 = control.updates[0].ManifestSHA256
|
|
if err := target.process(context.Background(), assignment); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := target.process(context.Background(), assignment); err != nil {
|
|
t.Fatal("copy retry was not idempotent:", err)
|
|
}
|
|
if len(targetControl.updates) != 2 || targetControl.updates[0].Action != "copied" {
|
|
t.Fatalf("target updates = %#v", targetControl.updates)
|
|
}
|
|
}
|