diff options
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/server.go | 49 | ||||
| -rw-r--r-- | internal/api/server_test.go | 69 |
2 files changed, 118 insertions, 0 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index 8415b28..608cdd4 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -60,6 +60,8 @@ func (s *Server) routes() { s.mux.HandleFunc("GET /api/tasks", s.handleListTasks) s.mux.HandleFunc("GET /api/tasks/{id}", s.handleGetTask) s.mux.HandleFunc("POST /api/tasks/{id}/run", s.handleRunTask) + s.mux.HandleFunc("POST /api/tasks/{id}/accept", s.handleAcceptTask) + s.mux.HandleFunc("POST /api/tasks/{id}/reject", s.handleRejectTask) s.mux.HandleFunc("GET /api/tasks/{id}/subtasks", s.handleListSubtasks) s.mux.HandleFunc("GET /api/tasks/{id}/executions", s.handleListExecutions) s.mux.HandleFunc("GET /api/executions/{id}", s.handleGetExecution) @@ -210,6 +212,53 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { }) } +func (s *Server) handleAcceptTask(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + t, err := s.store.GetTask(id) + if err != nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"}) + return + } + if !task.ValidTransition(t.State, task.StateCompleted) { + writeJSON(w, http.StatusConflict, map[string]string{ + "error": fmt.Sprintf("task cannot be accepted from state %s", t.State), + }) + return + } + if err := s.store.UpdateTaskState(id, task.StateCompleted); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "task accepted", "task_id": id}) +} + +func (s *Server) handleRejectTask(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + t, err := s.store.GetTask(id) + if err != nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"}) + return + } + if !task.ValidTransition(t.State, task.StatePending) { + writeJSON(w, http.StatusConflict, map[string]string{ + "error": fmt.Sprintf("task cannot be rejected from state %s", t.State), + }) + return + } + var input struct { + Comment string `json:"comment"` + } + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON: " + err.Error()}) + return + } + if err := s.store.RejectTask(id, input.Comment); err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]string{"message": "task rejected", "task_id": id}) +} + func (s *Server) handleListSubtasks(w http.ResponseWriter, r *http.Request) { parentID := r.PathValue("id") tasks, err := s.store.ListSubtasks(parentID) diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 68f3657..5094961 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -252,6 +252,75 @@ func TestRunTask_CompletedTask_Returns409(t *testing.T) { } } +func TestAcceptTask_ReadyTask_Returns200(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "accept-ready", task.StateReady) + + req := httptest.NewRequest("POST", "/api/tasks/accept-ready/accept", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) + } + + got, _ := store.GetTask("accept-ready") + if got.State != task.StateCompleted { + t.Errorf("task state: want COMPLETED, got %v", got.State) + } +} + +func TestAcceptTask_NonReadyTask_Returns409(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "accept-pending", task.StatePending) + + req := httptest.NewRequest("POST", "/api/tasks/accept-pending/accept", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusConflict { + t.Errorf("status: want 409, got %d; body: %s", w.Code, w.Body.String()) + } +} + +func TestRejectTask_ReadyTask_Returns200(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "reject-ready", task.StateReady) + + body := bytes.NewBufferString(`{"comment": "needs more detail"}`) + req := httptest.NewRequest("POST", "/api/tasks/reject-ready/reject", body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("status: want 200, got %d; body: %s", w.Code, w.Body.String()) + } + + got, _ := store.GetTask("reject-ready") + if got.State != task.StatePending { + t.Errorf("task state: want PENDING, got %v", got.State) + } + if got.RejectionComment != "needs more detail" { + t.Errorf("rejection_comment: want 'needs more detail', got %q", got.RejectionComment) + } +} + +func TestRejectTask_NonReadyTask_Returns409(t *testing.T) { + srv, store := testServer(t) + createTaskWithState(t, store, "reject-pending", task.StatePending) + + body := bytes.NewBufferString(`{"comment": "comment"}`) + req := httptest.NewRequest("POST", "/api/tasks/reject-pending/reject", body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusConflict { + t.Errorf("status: want 409, got %d; body: %s", w.Code, w.Body.String()) + } +} + func TestCORS_Headers(t *testing.T) { srv, _ := testServer(t) |
