Files
pulse/internal/updates/updater_test.go
T
rcourtman 33d318a29e Delete the unreachable adapter Execute/Rollback update path and the unused UpdateQueue
The only production consumer of the UpdaterRegistry is the plan endpoint
(GET /api/updates/plan); every real apply runs through the in-Go pipeline
in manager.go ApplyUpdate, with concurrency held by updateMu/updateInFlight.
The InstallShAdapter Execute/Rollback path (install.sh piping, rollback
binary download, health wait) and the UpdateQueue were never called outside
tests, so fixes made there silently did nothing. Adapters are now plan
providers only: the Updater interface keeps SupportsApply, PrepareUpdate,
and GetDeploymentType, pinned by the new registry proof in
internal/api/updates_test.go.

The orphaned fetchAndVerifyReleaseSignature helper goes with it; its
fail-closed sidecar tests now exercise the manager's
downloadAndVerifyReleaseSignature directly. The deployment-installability
sshsig invariant now names the manager pipeline as the only surface that
fetches release artifacts, and api-contracts, storage-recovery, and
agent-lifecycle pin the same plan-provider-only shape at their update
transport boundaries. Stale adapter references in build-release.sh,
validate-release.sh, and the installtests comments are updated to match.
2026-07-10 01:14:40 +01:00

258 lines
6.6 KiB
Go

package updates
import (
"context"
"errors"
"testing"
)
// mockUpdater implements the Updater interface for testing
type mockUpdater struct {
deploymentType string
supportsApply bool
prepareErr error
}
func (m *mockUpdater) SupportsApply() bool {
return m.supportsApply
}
func (m *mockUpdater) PrepareUpdate(ctx context.Context, request UpdateRequest) (*UpdatePlan, error) {
if m.prepareErr != nil {
return nil, m.prepareErr
}
return &UpdatePlan{
CanAutoUpdate: m.supportsApply,
}, nil
}
func (m *mockUpdater) GetDeploymentType() string {
return m.deploymentType
}
func TestNewUpdaterRegistry(t *testing.T) {
registry := NewUpdaterRegistry()
if registry == nil {
t.Fatal("NewUpdaterRegistry() returned nil")
}
if registry.updaters == nil {
t.Fatal("NewUpdaterRegistry() did not initialize updaters map")
}
}
func TestUpdaterRegistry_Register(t *testing.T) {
registry := NewUpdaterRegistry()
updater := &mockUpdater{deploymentType: "docker"}
registry.Register("docker", updater)
// Verify the updater was registered
got, err := registry.Get("docker")
if err != nil {
t.Fatalf("Get() after Register() failed: %v", err)
}
if got != updater {
t.Error("Get() returned different updater than registered")
}
}
func TestUpdaterRegistry_Register_Overwrites(t *testing.T) {
registry := NewUpdaterRegistry()
updater1 := &mockUpdater{deploymentType: "docker", supportsApply: false}
updater2 := &mockUpdater{deploymentType: "docker", supportsApply: true}
registry.Register("docker", updater1)
registry.Register("docker", updater2)
got, err := registry.Get("docker")
if err != nil {
t.Fatalf("Get() failed: %v", err)
}
if got != updater2 {
t.Error("Register() did not overwrite previous updater")
}
if !got.SupportsApply() {
t.Error("Expected second updater with SupportsApply=true")
}
}
func TestUpdaterRegistry_Get_NotFound(t *testing.T) {
registry := NewUpdaterRegistry()
_, err := registry.Get("nonexistent")
if err == nil {
t.Fatal("Get() should return error for unregistered deployment type")
}
expectedMsg := "no updater registered for deployment type: nonexistent"
if err.Error() != expectedMsg {
t.Errorf("Get() error = %q, want %q", err.Error(), expectedMsg)
}
}
func TestUpdaterRegistry_MultipleDeploymentTypes(t *testing.T) {
registry := NewUpdaterRegistry()
dockerUpdater := &mockUpdater{deploymentType: "docker"}
installshUpdater := &mockUpdater{deploymentType: "install.sh"}
aurUpdater := &mockUpdater{deploymentType: "aur"}
registry.Register("docker", dockerUpdater)
registry.Register("install.sh", installshUpdater)
registry.Register("aur", aurUpdater)
tests := []struct {
deploymentType string
expectedType string
}{
{"docker", "docker"},
{"install.sh", "install.sh"},
{"aur", "aur"},
}
for _, tc := range tests {
t.Run(tc.deploymentType, func(t *testing.T) {
got, err := registry.Get(tc.deploymentType)
if err != nil {
t.Fatalf("Get(%q) failed: %v", tc.deploymentType, err)
}
if got.GetDeploymentType() != tc.expectedType {
t.Errorf("GetDeploymentType() = %q, want %q", got.GetDeploymentType(), tc.expectedType)
}
})
}
}
func TestUpdateRequest_Fields(t *testing.T) {
req := UpdateRequest{
Version: "1.2.3",
Channel: "stable",
Force: true,
DryRun: false,
}
if req.Version != "1.2.3" {
t.Errorf("Version = %q, want %q", req.Version, "1.2.3")
}
if req.Channel != "stable" {
t.Errorf("Channel = %q, want %q", req.Channel, "stable")
}
if !req.Force {
t.Error("Force should be true")
}
if req.DryRun {
t.Error("DryRun should be false")
}
}
func TestUpdatePlan_Fields(t *testing.T) {
plan := UpdatePlan{
CanAutoUpdate: true,
Instructions: []string{"step1", "step2"},
Prerequisites: []string{"prereq1"},
EstimatedTime: "5 minutes",
RequiresRoot: true,
RollbackSupport: true,
DownloadURL: "https://example.com/download",
}
if !plan.CanAutoUpdate {
t.Error("CanAutoUpdate should be true")
}
if len(plan.Instructions) != 2 {
t.Errorf("Instructions length = %d, want 2", len(plan.Instructions))
}
if len(plan.Prerequisites) != 1 {
t.Errorf("Prerequisites length = %d, want 1", len(plan.Prerequisites))
}
if plan.EstimatedTime != "5 minutes" {
t.Errorf("EstimatedTime = %q, want %q", plan.EstimatedTime, "5 minutes")
}
if !plan.RequiresRoot {
t.Error("RequiresRoot should be true")
}
if !plan.RollbackSupport {
t.Error("RollbackSupport should be true")
}
if plan.DownloadURL != "https://example.com/download" {
t.Errorf("DownloadURL = %q, want %q", plan.DownloadURL, "https://example.com/download")
}
}
func TestUpdatePlan_UsesCanonicalEmptyCollections(t *testing.T) {
plan := EmptyUpdatePlan()
if plan.Instructions == nil {
t.Fatal("expected instructions to normalize to an empty slice")
}
if plan.Prerequisites == nil {
t.Fatal("expected prerequisites to normalize to an empty slice")
}
}
func TestUpdatePlan_NormalizeCollectionsInitializesReadinessChecks(t *testing.T) {
plan := UpdatePlan{
Readiness: &UpdateReadiness{
Status: "ready",
Summary: "Upgrade checks passed.",
},
}.NormalizeCollections()
if plan.Readiness == nil {
t.Fatal("expected readiness to remain attached")
}
if plan.Readiness.Checks == nil {
t.Fatal("expected readiness checks to normalize to an empty slice")
}
}
func TestMockUpdater_SupportsApply(t *testing.T) {
tests := []struct {
name string
supports bool
}{
{"supports apply", true},
{"does not support apply", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
updater := &mockUpdater{supportsApply: tc.supports}
if updater.SupportsApply() != tc.supports {
t.Errorf("SupportsApply() = %v, want %v", updater.SupportsApply(), tc.supports)
}
})
}
}
func TestMockUpdater_PrepareUpdate(t *testing.T) {
ctx := context.Background()
req := UpdateRequest{Version: "1.0.0"}
t.Run("success", func(t *testing.T) {
updater := &mockUpdater{supportsApply: true}
plan, err := updater.PrepareUpdate(ctx, req)
if err != nil {
t.Fatalf("PrepareUpdate() failed: %v", err)
}
if !plan.CanAutoUpdate {
t.Error("CanAutoUpdate should be true")
}
})
t.Run("error", func(t *testing.T) {
expectedErr := errors.New("prepare failed")
updater := &mockUpdater{prepareErr: expectedErr}
_, err := updater.PrepareUpdate(ctx, req)
if err != expectedErr {
t.Errorf("PrepareUpdate() error = %v, want %v", err, expectedErr)
}
})
}
func TestMockUpdater_GetDeploymentType(t *testing.T) {
updater := &mockUpdater{deploymentType: "custom-type"}
if got := updater.GetDeploymentType(); got != "custom-type" {
t.Errorf("GetDeploymentType() = %q, want %q", got, "custom-type")
}
}