package handlers import ( "net/http" "net/http/httptest" "strings" "testing" "task-dashboard/internal/models" ) func TestHandleBucketsCreate_Web_CreatesBucket(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() req := httptest.NewRequest("POST", "/settings/buckets", strings.NewReader("name=Gutters&cycle_days=30&pick_n=2")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() h.HandleBucketsCreate(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } buckets, err := h.store.GetBuckets() if err != nil { t.Fatal(err) } if len(buckets) != 1 || buckets[0].Name != "Gutters" { t.Errorf("buckets = %+v", buckets) } } func TestHandleBucketsCreate_Web_InvalidPickN_Returns400(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() req := httptest.NewRequest("POST", "/settings/buckets", strings.NewReader("name=Gutters&cycle_days=30&pick_n=0")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() h.HandleBucketsCreate(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } } func TestHandleBucketItemAdd_Web_CreatesAndAssignsTask(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } req := withURLParam(httptest.NewRequest("POST", "/settings/buckets/"+bucket.ID+"/items", strings.NewReader("title=Clean+gutters")), "id", bucket.ID) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() h.HandleBucketItemAdd(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } items, err := h.store.GetBucketItems(bucket.ID) if err != nil { t.Fatal(err) } if len(items) != 1 || items[0].Content != "Clean gutters" || items[0].BucketState != "dormant" { t.Errorf("items = %+v", items) } } func TestHandleBucketItemAdd_Web_EmptyTitle_Returns400(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } req := withURLParam(httptest.NewRequest("POST", "/settings/buckets/"+bucket.ID+"/items", strings.NewReader("title=")), "id", bucket.ID) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() h.HandleBucketItemAdd(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", w.Code) } } func TestHandleBucketItemRemove_Web_RemovesMembership(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } if err := h.store.CreateNativeTask(models.Task{ID: "task-1", Content: "Clean gutters", Priority: 1}); err != nil { t.Fatal(err) } if err := h.store.AddBucketItem(bucket.ID, "task-1"); err != nil { t.Fatal(err) } req := httptest.NewRequest("POST", "/settings/buckets/items/remove", strings.NewReader("task_id=task-1")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() h.HandleBucketItemRemove(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } task, err := h.store.GetNativeTaskByID("task-1") if err != nil { t.Fatal(err) } if task.BucketID != "" { t.Errorf("task.BucketID = %q, want empty", task.BucketID) } } func TestHandleBucketDelete_Web_RemovesBucket(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() bucket, err := h.store.CreateBucket("Gutters", 30, 2) if err != nil { t.Fatal(err) } req := withURLParam(httptest.NewRequest("DELETE", "/settings/buckets/"+bucket.ID, nil), "id", bucket.ID) w := httptest.NewRecorder() h.HandleBucketDelete(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } buckets, err := h.store.GetBuckets() if err != nil { t.Fatal(err) } if len(buckets) != 0 { t.Errorf("buckets = %+v, want none", buckets) } } func TestHandleTabTasks_IncludesBucketsProjectsLabels(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() if _, err := h.store.CreateBucket("Gutters", 30, 1); err != nil { t.Fatal(err) } if _, err := h.store.CreateProject("Home", "#3B82F6"); err != nil { t.Fatal(err) } if err := h.store.SetLabelColor("chore", "#F59E0B"); err != nil { t.Fatal(err) } req := httptest.NewRequest("GET", "/tabs/tasks", nil) w := httptest.NewRecorder() h.HandleTabTasks(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } mock := h.renderer.(*MockRenderer) lastCall := mock.Calls[len(mock.Calls)-1] data, ok := lastCall.Data.(struct { Atoms []models.Atom FutureAtoms []models.Atom Boards []models.Board Chains []models.ChainSummary Buckets []models.BucketSummary Projects []models.Project Labels []models.LabelColor Today string }) if !ok { t.Fatalf("unexpected data type %T", lastCall.Data) } if len(data.Buckets) != 1 || data.Buckets[0].Bucket.Name != "Gutters" { t.Errorf("Buckets = %+v", data.Buckets) } if len(data.Projects) != 1 || data.Projects[0].Name != "Home" { t.Errorf("Projects = %+v", data.Projects) } if len(data.Labels) != 1 || data.Labels[0].Name != "chore" { t.Errorf("Labels = %+v", data.Labels) } }