package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "git.jnss.me/joakim/opal/internal/engine" ) func TestMain(m *testing.M) { testDir := "/tmp/opal-handler-test" engine.SetConfigDirOverride(testDir) engine.SetDataDirOverride(testDir) if err := os.MkdirAll(testDir, 0755); err != nil { panic(err) } if err := engine.InitDB(); err != nil { panic(err) } defer engine.CloseDB() code := m.Run() os.RemoveAll(testDir) os.Exit(code) } func TestParseTask_DescriptionOnly(t *testing.T) { body := `{"input": "buy groceries"}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data, ok := resp["data"].(map[string]interface{}) if !ok { t.Fatal("expected data in response") } task, ok := data["task"].(map[string]interface{}) if !ok { t.Fatal("expected task in data") } if task["Description"] != "buy groceries" { t.Errorf("expected description 'buy groceries', got %v", task["Description"]) } } func TestParseTask_WithModifiers(t *testing.T) { body := `{"input": "review PR priority:H project:backend +code"}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data := resp["data"].(map[string]interface{}) task := data["task"].(map[string]interface{}) if task["Description"] != "review PR" { t.Errorf("expected description 'review PR', got %v", task["Description"]) } if task["Project"] != "backend" { t.Errorf("expected project 'backend', got %v", task["Project"]) } } func TestParseTask_WithRecurrence(t *testing.T) { body := `{"input": "team meeting due:2026-03-01 recur:1w +meetings"}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusCreated { t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data := resp["data"].(map[string]interface{}) task := data["task"].(map[string]interface{}) // The returned task should be the first instance (pending, with ParentUUID) if task["ParentUUID"] == nil { t.Error("expected ParentUUID to be set for recurring instance") } } func TestParseTask_EmptyInput(t *testing.T) { body := `{"input": ""}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestParseTask_OnlyModifiers(t *testing.T) { body := `{"input": "+tag priority:H"}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestParseTask_InvalidModifier(t *testing.T) { body := `{"input": "some task due:notadate"}` req := httptest.NewRequest(http.MethodPost, "/tasks/parse", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() ParseTask(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestListTasks_WithReport(t *testing.T) { // Create a task first so the report has something to return _, err := engine.CreateTask("report test task") if err != nil { t.Fatalf("failed to create test task: %v", err) } req := httptest.NewRequest(http.MethodGet, "/tasks?report=list", nil) w := httptest.NewRecorder() ListTasks(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data := resp["data"].(map[string]interface{}) if data["report"] != "list" { t.Errorf("expected report name 'list', got %v", data["report"]) } if data["count"] == nil { t.Error("expected count in response") } if data["tasks"] == nil { t.Error("expected tasks in response") } } func TestListTasks_UnknownReport(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/tasks?report=nonexistent", nil) w := httptest.NewRecorder() ListTasks(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestListTasks_NoReport(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/tasks", nil) w := httptest.NewRecorder() ListTasks(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } var resp map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to decode response: %v", err) } // Without report param, data should be the tasks array directly (not wrapped) if resp["success"] != true { t.Error("expected success to be true") } }