diff --git a/internal/updates/updater_test.go b/internal/updates/updater_test.go new file mode 100644 index 0000000..77505bb --- /dev/null +++ b/internal/updates/updater_test.go @@ -0,0 +1,328 @@ +package updates + +import ( + "context" + "errors" + "testing" +) + +// mockUpdater implements the Updater interface for testing +type mockUpdater struct { + deploymentType string + supportsApply bool + prepareErr error + executeErr error + rollbackErr 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) Execute(ctx context.Context, request UpdateRequest, progressCb ProgressCallback) error { + return m.executeErr +} + +func (m *mockUpdater) Rollback(ctx context.Context, eventID string) error { + return m.rollbackErr +} + +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 TestUpdateProgress_Fields(t *testing.T) { + progress := UpdateProgress{ + Stage: "downloading", + Progress: 50, + Message: "Downloading update...", + IsComplete: false, + Error: "", + } + + if progress.Stage != "downloading" { + t.Errorf("Stage = %q, want %q", progress.Stage, "downloading") + } + if progress.Progress != 50 { + t.Errorf("Progress = %d, want 50", progress.Progress) + } + if progress.Message != "Downloading update..." { + t.Errorf("Message = %q, want %q", progress.Message, "Downloading update...") + } + if progress.IsComplete { + t.Error("IsComplete should be false") + } + if progress.Error != "" { + t.Errorf("Error = %q, want empty string", progress.Error) + } +} + +func TestUpdateProgress_WithError(t *testing.T) { + progress := UpdateProgress{ + Stage: "failed", + Progress: 0, + Message: "Update failed", + IsComplete: true, + Error: "connection timeout", + } + + if !progress.IsComplete { + t.Error("IsComplete should be true for failed state") + } + if progress.Error != "connection timeout" { + t.Errorf("Error = %q, want %q", progress.Error, "connection timeout") + } +} + +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_Execute(t *testing.T) { + ctx := context.Background() + req := UpdateRequest{Version: "1.0.0"} + progressCb := func(p UpdateProgress) {} + + t.Run("success", func(t *testing.T) { + updater := &mockUpdater{} + err := updater.Execute(ctx, req, progressCb) + if err != nil { + t.Errorf("Execute() error = %v, want nil", err) + } + }) + + t.Run("error", func(t *testing.T) { + expectedErr := errors.New("execute failed") + updater := &mockUpdater{executeErr: expectedErr} + err := updater.Execute(ctx, req, progressCb) + if err != expectedErr { + t.Errorf("Execute() error = %v, want %v", err, expectedErr) + } + }) +} + +func TestMockUpdater_Rollback(t *testing.T) { + ctx := context.Background() + + t.Run("success", func(t *testing.T) { + updater := &mockUpdater{} + err := updater.Rollback(ctx, "event-123") + if err != nil { + t.Errorf("Rollback() error = %v, want nil", err) + } + }) + + t.Run("error", func(t *testing.T) { + expectedErr := errors.New("rollback failed") + updater := &mockUpdater{rollbackErr: expectedErr} + err := updater.Rollback(ctx, "event-123") + if err != expectedErr { + t.Errorf("Rollback() 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") + } +}