package handlers import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "task-dashboard/internal/models" ) func TestHandleWidgetBucketsCreate_CreatesBucket(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} body := `{"name":"Gutters","cycle_days":30,"pick_n":2}` req := httptest.NewRequest("POST", "/api/widget/buckets", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleWidgetBucketsCreate(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } var bucket models.MaintenanceBucket if err := json.NewDecoder(w.Body).Decode(&bucket); err != nil { t.Fatal(err) } if bucket.Name != "Gutters" || bucket.CycleDays != 30 || bucket.PickN != 2 { t.Errorf("bucket = %+v", bucket) } } func TestHandleWidgetBucketsCreate_InvalidPickN_Returns400(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} body := `{"name":"Gutters","cycle_days":30,"pick_n":0}` req := httptest.NewRequest("POST", "/api/widget/buckets", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleWidgetBucketsCreate(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } } func TestHandleWidgetBucketsGet_ReturnsBuckets(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} if _, err := h.store.CreateBucket("Gutters", 30, 2); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/api/widget/buckets", nil) w := httptest.NewRecorder() h.HandleWidgetBucketsGet(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } var buckets []models.MaintenanceBucket if err := json.NewDecoder(w.Body).Decode(&buckets); err != nil { t.Fatal(err) } if len(buckets) != 1 || buckets[0].Name != "Gutters" { t.Errorf("buckets = %+v", buckets) } } func TestHandleWidgetBucketItemsAdd_AssignsTask(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } task := models.Task{ID: "task-1", Content: "Clean gutters", Priority: 1} if err := h.store.CreateNativeTask(task); err != nil { t.Fatal(err) } body := `{"task_id":"task-1"}` req := withURLParam(httptest.NewRequest("POST", "/api/widget/buckets/"+bucket.ID+"/items", strings.NewReader(body)), "id", bucket.ID) w := httptest.NewRecorder() h.HandleWidgetBucketItemsAdd(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } updated, err := h.store.GetNativeTaskByID("task-1") if err != nil { t.Fatal(err) } if updated.BucketID != bucket.ID || updated.BucketState != "dormant" { t.Errorf("task = %+v", updated) } } func TestHandleWidgetBucketItemsAdd_UnknownTask_Returns404(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } body := `{"task_id":"nope"}` req := withURLParam(httptest.NewRequest("POST", "/api/widget/buckets/"+bucket.ID+"/items", strings.NewReader(body)), "id", bucket.ID) w := httptest.NewRecorder() h.HandleWidgetBucketItemsAdd(w, req) if w.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", w.Code) } } func TestHandleWidgetTaskDefer_ReturnsToDormant(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} // pick_n=1 with a spare reserve item in the pool: activate one, then // defer it -- the reserve is what backfill should pick, so the // just-deferred item (now the pool's only dormant item at the moment // selectBucketCycle would otherwise look) isn't immediately re-picked. bucket, err := h.store.CreateBucket("Gutters", 30, 1) if err != nil { t.Fatal(err) } if err := h.store.CreateNativeTask(models.Task{ID: "task-1", Content: "task-1", Priority: 1}); err != nil { t.Fatal(err) } if err := h.store.AddBucketItem(bucket.ID, "task-1"); err != nil { t.Fatal(err) } // Activate task-1 via a real cycle run so it's a legit active bucket item. if _, err := h.store.RunBucketCycles(time.Now()); err != nil { t.Fatal(err) } // Add the reserve item AFTER the cycle runs, so it's still dormant when task-1 is deferred. if err := h.store.CreateNativeTask(models.Task{ID: "reserve", Content: "reserve", Priority: 1}); err != nil { t.Fatal(err) } if err := h.store.AddBucketItem(bucket.ID, "reserve"); err != nil { t.Fatal(err) } body := `{"id":"task-1"}` req := httptest.NewRequest("POST", "/api/widget/task/defer", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleWidgetTaskDefer(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } updated, err := h.store.GetNativeTaskByID("task-1") if err != nil { t.Fatal(err) } if updated.BucketState != "dormant" { t.Errorf("BucketState = %q, want dormant", updated.BucketState) } } func TestHandleWidgetTaskDefer_NotABucketItem_Returns404(t *testing.T) { db, cleanup := setupTestDB(t) defer cleanup() h := &Handler{store: db} task := models.Task{ID: "task-1", Content: "Plain task", Priority: 1} if err := h.store.CreateNativeTask(task); err != nil { t.Fatal(err) } body := `{"id":"task-1"}` req := httptest.NewRequest("POST", "/api/widget/task/defer", strings.NewReader(body)) w := httptest.NewRecorder() h.HandleWidgetTaskDefer(w, req) if w.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", w.Code) } }