package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/service" ) func TestWorkHandler_Fail(t *testing.T) { mockQueue := newMockWorkQueue() workService := service.NewWorkService(mockQueue, service.WorkServiceConfig{}) handler := NewWorkHandler(workService) // Pre-populate a running task mockQueue.tasks["task-1"] = &domain.WorkTask{ ID: "task-1", ProjectID: "test-project", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusRunning, WorkerID: "worker-1", MaxRetries: 3, CreatedAt: time.Now(), } router := chi.NewRouter() handler.Mount(router) tests := []struct { name string taskID string body FailWorkRequest wantStatus int }{ { name: "valid_fail", taskID: "task-1", body: FailWorkRequest{Error: "Build failed: npm error"}, wantStatus: http.StatusOK, }, { name: "missing_error", taskID: "task-1", body: FailWorkRequest{}, wantStatus: http.StatusBadRequest, }, { name: "task_not_found", taskID: "nonexistent", body: FailWorkRequest{Error: "Failed"}, wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { body, _ := json.Marshal(tt.body) req := httptest.NewRequest(http.MethodPost, "/work/"+tt.taskID+"/fail", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("got status %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String()) } }) } } func TestWorkHandler_Cancel(t *testing.T) { mockQueue := newMockWorkQueue() workService := service.NewWorkService(mockQueue, service.WorkServiceConfig{}) handler := NewWorkHandler(workService) // Pre-populate tasks mockQueue.tasks["pending-task"] = &domain.WorkTask{ ID: "pending-task", ProjectID: "test-project", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusPending, CreatedAt: time.Now(), } mockQueue.tasks["running-task"] = &domain.WorkTask{ ID: "running-task", ProjectID: "test-project", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusRunning, CreatedAt: time.Now(), } router := chi.NewRouter() handler.Mount(router) tests := []struct { name string taskID string wantStatus int }{ { name: "cancel_pending_task", taskID: "pending-task", wantStatus: http.StatusOK, }, { name: "cancel_running_task_fails", taskID: "running-task", wantStatus: http.StatusNotFound, // Can only cancel pending tasks }, { name: "task_not_found", taskID: "nonexistent", wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/work/"+tt.taskID+"/cancel", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("got status %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String()) } }) } } func TestWorkHandler_GetTask(t *testing.T) { mockQueue := newMockWorkQueue() workService := service.NewWorkService(mockQueue, service.WorkServiceConfig{}) handler := NewWorkHandler(workService) // Pre-populate a task mockQueue.tasks["task-1"] = &domain.WorkTask{ ID: "task-1", ProjectID: "test-project", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusRunning, Spec: map[string]any{ "prompt": "Build it", }, CreatedAt: time.Now(), } router := chi.NewRouter() handler.Mount(router) tests := []struct { name string taskID string wantStatus int }{ { name: "get_existing_task", taskID: "task-1", wantStatus: http.StatusOK, }, { name: "task_not_found", taskID: "nonexistent", wantStatus: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/"+tt.taskID, nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != tt.wantStatus { t.Errorf("got status %d, want %d; body: %s", rec.Code, tt.wantStatus, rec.Body.String()) } }) } } func TestWorkHandler_ListByProject(t *testing.T) { mockQueue := newMockWorkQueue() workService := service.NewWorkService(mockQueue, service.WorkServiceConfig{}) handler := NewWorkHandler(workService) // Pre-populate tasks mockQueue.tasks["task-1"] = &domain.WorkTask{ ID: "task-1", ProjectID: "project-a", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusPending, CreatedAt: time.Now(), } mockQueue.tasks["task-2"] = &domain.WorkTask{ ID: "task-2", ProjectID: "project-a", Type: domain.WorkTaskTypeTest, Status: domain.WorkTaskStatusCompleted, CreatedAt: time.Now(), } mockQueue.tasks["task-3"] = &domain.WorkTask{ ID: "task-3", ProjectID: "project-b", Type: domain.WorkTaskTypeDeploy, Status: domain.WorkTaskStatusRunning, CreatedAt: time.Now(), } router := chi.NewRouter() handler.Mount(router) t.Run("list_all_for_project", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal: %v", err) } data := resp["data"].(map[string]any) total := int(data["total"].(float64)) if total != 2 { t.Errorf("got %d tasks, want 2", total) } // Verify pagination metadata is present if _, ok := data["limit"]; !ok { t.Error("expected limit in response") } if _, ok := data["offset"]; !ok { t.Error("expected offset in response") } }) t.Run("list_with_status_filter", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a?status=pending", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d", rec.Code, http.StatusOK) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal: %v", err) } data := resp["data"].(map[string]any) total := int(data["total"].(float64)) if total != 1 { t.Errorf("got %d tasks, want 1", total) } }) t.Run("list_with_pagination", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a?limit=1&offset=0", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d", rec.Code, http.StatusOK) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal: %v", err) } data := resp["data"].(map[string]any) // Total should reflect all matching tasks total := int(data["total"].(float64)) if total != 2 { t.Errorf("got total=%d, want 2", total) } // But tasks returned should be limited tasks := data["tasks"].([]any) if len(tasks) != 1 { t.Errorf("got %d tasks returned, want 1", len(tasks)) } // Verify limit/offset are reflected if int(data["limit"].(float64)) != 1 { t.Errorf("got limit=%v, want 1", data["limit"]) } if int(data["offset"].(float64)) != 0 { t.Errorf("got offset=%v, want 0", data["offset"]) } }) t.Run("invalid_limit", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a?limit=invalid", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("invalid_offset", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a?offset=invalid", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want %d", rec.Code, http.StatusBadRequest) } }) t.Run("invalid_status_filter", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/work/projects/project-a?status=invalid", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want %d", rec.Code, http.StatusBadRequest) } }) } func TestWorkHandler_Stats(t *testing.T) { mockQueue := newMockWorkQueue() workService := service.NewWorkService(mockQueue, service.WorkServiceConfig{}) handler := NewWorkHandler(workService) // Pre-populate tasks with various statuses mockQueue.tasks["task-1"] = &domain.WorkTask{ID: "task-1", Status: domain.WorkTaskStatusPending} mockQueue.tasks["task-2"] = &domain.WorkTask{ID: "task-2", Status: domain.WorkTaskStatusPending} mockQueue.tasks["task-3"] = &domain.WorkTask{ID: "task-3", Status: domain.WorkTaskStatusRunning} mockQueue.tasks["task-4"] = &domain.WorkTask{ID: "task-4", Status: domain.WorkTaskStatusCompleted} mockQueue.tasks["task-5"] = &domain.WorkTask{ID: "task-5", Status: domain.WorkTaskStatusFailed} router := chi.NewRouter() handler.Mount(router) req := httptest.NewRequest(http.MethodGet, "/work/stats", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal: %v", err) } data := resp["data"].(map[string]any) if int(data["pending"].(float64)) != 2 { t.Errorf("got pending=%v, want 2", data["pending"]) } if int(data["running"].(float64)) != 1 { t.Errorf("got running=%v, want 1", data["running"]) } if int(data["completed"].(float64)) != 1 { t.Errorf("got completed=%v, want 1", data["completed"]) } if int(data["failed"].(float64)) != 1 { t.Errorf("got failed=%v, want 1", data["failed"]) } }