package http import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/nomadcode/nomadcode-core/internal/storage" "github.com/nomadcode/nomadcode-core/internal/workitem" "github.com/nomadcode/nomadcode-core/internal/workitempipeline" ) type fakeWorkItemTaskCreator struct { task storage.Task err error calledWith *workitempipeline.CreateTaskInput } func (f *fakeWorkItemTaskCreator) CreateTaskFromWorkItem(_ context.Context, input workitempipeline.CreateTaskInput) (storage.Task, error) { f.calledWith = &input return f.task, f.err } func TestPlaneTaskRequestBuildsProviderNeutralRef(t *testing.T) { req := createPlaneTaskRequest{ WorkspaceSlug: " acme ", ProjectID: " proj-1 ", WorkItemID: " work-1 ", ExternalURL: " https://plane.example/work-1 ", } ref, err := req.workItemRef() if err != nil { t.Fatalf("workItemRef returned error: %v", err) } if ref.Provider != workitem.ProviderID("plane") { t.Errorf("provider: got %q", ref.Provider) } if ref.Tenant != "acme" { t.Errorf("tenant: got %q", ref.Tenant) } if ref.Project != "proj-1" { t.Errorf("project: got %q", ref.Project) } if ref.ID != "work-1" { t.Errorf("id: got %q", ref.ID) } if ref.URL != "https://plane.example/work-1" { t.Errorf("url: got %q", ref.URL) } } func TestCreatePlaneTaskDelegatesToWorkItemPipeline(t *testing.T) { creator := &fakeWorkItemTaskCreator{ task: storage.Task{ID: "task-123", Status: "pending"}, } h := &Handler{planeWorkItemTasks: creator} body := `{"workspace_slug":"acme","project_id":"proj-1","work_item_id":"work-1","state_id":"state-1","comment":"my note"}` req := httptest.NewRequest(http.MethodPost, "/api/integrations/plane/tasks", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() h.CreatePlaneTask(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected %d, got %d: %s", http.StatusCreated, rec.Code, rec.Body.String()) } if creator.calledWith == nil { t.Fatal("CreateTaskFromWorkItem was not called") } if creator.calledWith.Ref.Provider != "plane" { t.Errorf("ref.provider: got %q", creator.calledWith.Ref.Provider) } if creator.calledWith.Ref.Tenant != "acme" { t.Errorf("ref.tenant: got %q", creator.calledWith.Ref.Tenant) } if creator.calledWith.Ref.Project != "proj-1" { t.Errorf("ref.project: got %q", creator.calledWith.Ref.Project) } if creator.calledWith.Ref.ID != "work-1" { t.Errorf("ref.id: got %q", creator.calledWith.Ref.ID) } if creator.calledWith.StateID != "state-1" { t.Errorf("state_id: got %q", creator.calledWith.StateID) } if creator.calledWith.Comment != "my note" { t.Errorf("comment: got %q", creator.calledWith.Comment) } var resp map[string]string if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if resp["id"] != "task-123" || resp["status"] != "pending" { t.Errorf("response: %v", resp) } } func TestCreatePlaneTaskReturns503WhenPipelineNotConfigured(t *testing.T) { h := &Handler{} req := httptest.NewRequest(http.MethodPost, "/api/integrations/plane/tasks", strings.NewReader(`{}`)) rec := httptest.NewRecorder() h.CreatePlaneTask(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("expected 503, got %d", rec.Code) } var resp map[string]string if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if resp["error"] != "plane client is not configured" { t.Errorf("error message: got %q", resp["error"]) } }