mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
* fix(projects): return 400 (not 500) for invalid project status/priority CreateProject/UpdateProject passed an unvalidated status/priority straight to the INSERT, so an unknown value (e.g. --status active) tripped the table's CHECK constraint and surfaced as a blanket 500 'failed to create project' with no server-side log to diagnose it (#3925). Pre-validate both enums against the column CHECK lists and return a 400 with the allowed values. Back it with isCheckViolation -> 400 for any other constrained column, and log the underlying error on genuine 500s so transient DB failures are diagnosable. MUL-3153 Co-authored-by: multica-agent <github@multica.ai> * fix(cli): validate project --status in create/update project create and project update forwarded --status to the server without checking it, while project status already validated. Share a single validateProjectStatus helper across all three so a typo fails fast with the valid list instead of a server round-trip. MUL-3153 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
94 lines
3.2 KiB
Go
94 lines
3.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// An unknown project status must fail fast with a 400 and the valid list, not
|
|
// surface the DB CHECK violation as a 500 (#3925: `--status active`).
|
|
func TestCreateProjectInvalidStatusReturns400(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("POST", "/api/projects?workspace_id="+testWorkspaceID, map[string]any{
|
|
"title": "invalid status project",
|
|
"status": "active",
|
|
})
|
|
testHandler.CreateProject(w, req)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid status, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
if body := w.Body.String(); !strings.Contains(body, "planned") {
|
|
t.Errorf("expected error to list valid statuses, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestCreateProjectInvalidPriorityReturns400(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("POST", "/api/projects?workspace_id="+testWorkspaceID, map[string]any{
|
|
"title": "invalid priority project",
|
|
"priority": "critical",
|
|
})
|
|
testHandler.CreateProject(w, req)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid priority, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// A valid status still creates the project (the validation does not over-reject).
|
|
func TestCreateProjectValidStatusReturns201(t *testing.T) {
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("POST", "/api/projects?workspace_id="+testWorkspaceID, map[string]any{
|
|
"title": "valid status project",
|
|
"status": "in_progress",
|
|
})
|
|
testHandler.CreateProject(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 for valid status, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var project ProjectResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&project); err != nil {
|
|
t.Fatalf("decode CreateProject: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
req := newRequest("DELETE", "/api/projects/"+project.ID, nil)
|
|
req = withURLParam(req, "id", project.ID)
|
|
testHandler.DeleteProject(httptest.NewRecorder(), req)
|
|
})
|
|
if project.Status != "in_progress" {
|
|
t.Errorf("expected status in_progress, got %q", project.Status)
|
|
}
|
|
}
|
|
|
|
// Updating to an unknown status is a 400, not a 500.
|
|
func TestUpdateProjectInvalidStatusReturns400(t *testing.T) {
|
|
// Seed a project to update.
|
|
w := httptest.NewRecorder()
|
|
req := newRequest("POST", "/api/projects?workspace_id="+testWorkspaceID, map[string]any{
|
|
"title": "update validation project",
|
|
})
|
|
testHandler.CreateProject(w, req)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("seed CreateProject: %d %s", w.Code, w.Body.String())
|
|
}
|
|
var project ProjectResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&project); err != nil {
|
|
t.Fatalf("decode CreateProject: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
req := newRequest("DELETE", "/api/projects/"+project.ID, nil)
|
|
req = withURLParam(req, "id", project.ID)
|
|
testHandler.DeleteProject(httptest.NewRecorder(), req)
|
|
})
|
|
|
|
w = httptest.NewRecorder()
|
|
req = newRequest("PUT", "/api/projects/"+project.ID, map[string]any{"status": "active"})
|
|
req = withURLParam(req, "id", project.ID)
|
|
testHandler.UpdateProject(w, req)
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid update status, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|