diff --git a/main.go b/main.go index df5924f..618d96d 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,10 @@ func main() { } if *latestPtr || *cleanPtr { + if err := app.validateVersionExists(versions, cfg.Version); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } app.updateLifecycle(versions, *latestPtr, *cleanPtr) } } @@ -237,6 +241,21 @@ func (a *App) displayVersions(projects []Project) { fmt.Fprintln(a.Stdout, "------------------------") } +func (a *App) validateVersionExists(projects []Project, version string) error { + var available []string + for _, p := range projects { + if p.Name != a.Config.ProjectName { + continue + } + if p.Version == version { + return nil + } + available = append(available, p.Version) + } + return fmt.Errorf("version %q not found in project %q. Available versions: %s", + version, a.Config.ProjectName, strings.Join(available, ", ")) +} + func (a *App) updateLifecycle(projects []Project, updateLatest bool, cleanInactive bool) { fmt.Fprintln(a.Stdout, "Updating Lifecycle...") for _, p := range projects { diff --git a/main_test.go b/main_test.go index be3668f..f4456f7 100644 --- a/main_test.go +++ b/main_test.go @@ -408,3 +408,88 @@ func TestDisplayVersions(t *testing.T) { t.Errorf("expected projects with different name to be filtered out; got: %s", out) } } + +func TestValidateVersionExists_Found(t *testing.T) { + app, _, _ := newTestApp(t, func(w http.ResponseWriter, r *http.Request) {}) + projects := []Project{ + {Name: "test-project", Version: "v0.9.0"}, + {Name: "test-project", Version: "v1.0.0"}, + } + if err := app.validateVersionExists(projects, "v1.0.0"); err != nil { + t.Fatalf("expected nil error, got %v", err) + } +} + +func TestValidateVersionExists_NotFound(t *testing.T) { + app, _, _ := newTestApp(t, func(w http.ResponseWriter, r *http.Request) {}) + projects := []Project{ + {Name: "test-project", Version: "v1.5.7"}, + {Name: "test-project", Version: "v1.5.8"}, + } + err := app.validateVersionExists(projects, "1.5.8") + if err == nil { + t.Fatal("expected error for missing version") + } + msg := err.Error() + if !strings.Contains(msg, `"1.5.8"`) { + t.Errorf("expected requested version %q in error, got: %s", "1.5.8", msg) + } + if !strings.Contains(msg, "not found") { + t.Errorf("expected 'not found' in error, got: %s", msg) + } + if !strings.Contains(msg, "v1.5.8") { + t.Errorf("expected available version 'v1.5.8' in error to make typo obvious, got: %s", msg) + } +} + +func TestValidateVersionExists_IgnoresOtherProjects(t *testing.T) { + app, _, _ := newTestApp(t, func(w http.ResponseWriter, r *http.Request) {}) + projects := []Project{ + {Name: "other-project", Version: "v1.0.0"}, + } + err := app.validateVersionExists(projects, "v1.0.0") + if err == nil { + t.Fatal("expected error: matching version under a different project name should not satisfy validation") + } + if !strings.Contains(err.Error(), "test-project") { + t.Errorf("expected configured project name in error, got: %s", err.Error()) + } +} + +func TestUpdateLifecycle_VersionNotInList(t *testing.T) { + projects := []Project{ + {UUID: "u-1", Name: "test-project", Version: "v1.5.7", Active: true, IsLatest: false}, + {UUID: "u-2", Name: "test-project", Version: "v1.5.8", Active: true, IsLatest: true}, + } + + { + app, _, _ := newTestApp(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("validation phase must not send any HTTP requests; got %s %s", r.Method, r.URL.Path) + }) + app.Config.Version = "1.5.8" + if err := app.validateVersionExists(projects, app.Config.Version); err == nil { + t.Fatal("expected validateVersionExists to reject typo'd version") + } + } + + handler, records, mu := newPatchRecorder() + app, _, _ := newTestApp(t, handler) + app.Config.Version = "1.5.8" + + app.updateLifecycle(projects, true, true) + + mu.Lock() + defer mu.Unlock() + if len(*records) != 2 { + t.Fatalf("expected 2 destructive PATCHes (one per project) when validation is bypassed; got %d: %+v", + len(*records), *records) + } + for _, r := range *records { + if r.payload["active"] != false { + t.Errorf("expected active=false in destructive PATCH for %s, got %+v", r.uuid, r.payload) + } + if r.payload["isLatest"] != false && r.uuid == "u-2" { + t.Errorf("expected isLatest=false in destructive PATCH for %s, got %+v", r.uuid, r.payload) + } + } +}