"""Tests for the ClickUp REST API client and task parsing.""" from __future__ import annotations import httpx import respx from cheddahbot.clickup import BASE_URL, ClickUpClient, ClickUpTask # ── ClickUpTask.from_api ── class TestClickUpTaskParsing: def test_parses_basic_fields(self, sample_clickup_task_data): task = ClickUpTask.from_api(sample_clickup_task_data) assert task.id == "abc123" assert task.name == "Write PR for Acme Corp" assert task.status == "to do" assert task.description == "Draft a press release about the new product launch." assert task.url == "https://app.clickup.com/t/abc123" assert task.list_id == "list_1" assert task.list_name == "Content Tasks" def test_resolves_dropdown_by_orderindex(self, sample_clickup_task_data): task = ClickUpTask.from_api(sample_clickup_task_data) assert task.task_type == "Press Release" assert task.custom_fields["Task Type"] == "Press Release" def test_preserves_text_custom_fields(self, sample_clickup_task_data): task = ClickUpTask.from_api(sample_clickup_task_data) assert task.custom_fields["Company"] == "Acme Corp" def test_handles_null_dropdown_value(self, sample_clickup_task_data): task = ClickUpTask.from_api(sample_clickup_task_data) assert task.custom_fields["Priority Level"] is None def test_no_custom_fields(self, sample_clickup_task_no_type): task = ClickUpTask.from_api(sample_clickup_task_no_type) assert task.task_type == "" assert task.custom_fields == {} def test_custom_task_type_field_name(self, sample_clickup_task_data): """When task_type_field_name doesn't match, task_type should be empty.""" task = ClickUpTask.from_api(sample_clickup_task_data, task_type_field_name="Work Type") assert task.task_type == "" # But custom fields still parsed assert task.custom_fields["Task Type"] == "Press Release" def test_status_normalized_to_lowercase(self): data = { "id": "x", "name": "Test", "status": {"status": "In Progress", "type": "active"}, "custom_fields": [], } task = ClickUpTask.from_api(data) assert task.status == "in progress" def test_missing_description_defaults_empty(self): data = { "id": "x", "name": "Test", "status": {"status": "open", "type": "open"}, "custom_fields": [], } task = ClickUpTask.from_api(data) assert task.description == "" def test_dropdown_resolved_by_id_fallback(self): """When orderindex doesn't match, fall back to id matching.""" data = { "id": "x", "name": "Test", "status": {"status": "open", "type": "open"}, "custom_fields": [ { "id": "cf_1", "name": "Task Type", "type": "drop_down", "value": "opt_2", # string id, not int orderindex "type_config": { "options": [ {"id": "opt_1", "name": "Press Release", "orderindex": 0}, {"id": "opt_2", "name": "Link Building", "orderindex": 1}, ] }, } ], } task = ClickUpTask.from_api(data) assert task.custom_fields["Task Type"] == "Link Building" assert task.task_type == "Link Building" # ── ClickUpClient ── class TestClickUpClient: @respx.mock def test_get_tasks(self): respx.get(f"{BASE_URL}/list/list_1/task").mock( return_value=httpx.Response( 200, json={ "tasks": [ { "id": "t1", "name": "Task One", "status": {"status": "to do", "type": "open"}, "custom_fields": [], }, { "id": "t2", "name": "Task Two", "status": {"status": "to do", "type": "open"}, "custom_fields": [], }, ] }, ) ) client = ClickUpClient(api_token="pk_test_123") tasks = client.get_tasks("list_1", statuses=["to do"]) assert len(tasks) == 2 assert tasks[0].id == "t1" assert tasks[1].name == "Task Two" client.close() @respx.mock def test_get_tasks_from_space(self): # Mock folders endpoint respx.get(f"{BASE_URL}/space/space_1/folder").mock( return_value=httpx.Response( 200, json={ "folders": [ { "id": "f1", "lists": [{"id": "list_a"}, {"id": "list_b"}], } ] }, ) ) # Mock folderless lists respx.get(f"{BASE_URL}/space/space_1/list").mock( return_value=httpx.Response(200, json={"lists": [{"id": "list_c"}]}) ) # Mock task responses for each list for list_id in ("list_a", "list_b", "list_c"): respx.get(f"{BASE_URL}/list/{list_id}/task").mock( return_value=httpx.Response( 200, json={ "tasks": [ { "id": f"t_{list_id}", "name": f"Task from {list_id}", "status": {"status": "to do", "type": "open"}, "custom_fields": [], } ] }, ) ) client = ClickUpClient(api_token="pk_test_123") tasks = client.get_tasks_from_space("space_1", statuses=["to do"]) assert len(tasks) == 3 task_ids = {t.id for t in tasks} assert task_ids == {"t_list_a", "t_list_b", "t_list_c"} client.close() @respx.mock def test_update_task_status(self): respx.put(f"{BASE_URL}/task/t1").mock(return_value=httpx.Response(200, json={})) client = ClickUpClient(api_token="pk_test_123") result = client.update_task_status("t1", "in progress") assert result is True request = respx.calls.last.request assert b'"status"' in request.content assert b'"in progress"' in request.content client.close() @respx.mock def test_update_task_status_failure(self): respx.put(f"{BASE_URL}/task/t1").mock( return_value=httpx.Response(403, json={"err": "forbidden"}) ) client = ClickUpClient(api_token="pk_test_123") result = client.update_task_status("t1", "done") assert result is False client.close() @respx.mock def test_add_comment(self): respx.post(f"{BASE_URL}/task/t1/comment").mock(return_value=httpx.Response(200, json={})) client = ClickUpClient(api_token="pk_test_123") result = client.add_comment("t1", "CheddahBot completed this task.") assert result is True request = respx.calls.last.request assert b"CheddahBot completed" in request.content client.close() @respx.mock def test_add_comment_failure(self): respx.post(f"{BASE_URL}/task/t1/comment").mock( return_value=httpx.Response(500, json={"err": "server error"}) ) client = ClickUpClient(api_token="pk_test_123") result = client.add_comment("t1", "Hello") assert result is False client.close() @respx.mock def test_get_custom_fields(self): respx.get(f"{BASE_URL}/list/list_1/field").mock( return_value=httpx.Response( 200, json={ "fields": [ {"id": "cf_1", "name": "Task Type", "type": "drop_down"}, {"id": "cf_2", "name": "Company", "type": "short_text"}, ] }, ) ) client = ClickUpClient(api_token="pk_test_123") fields = client.get_custom_fields("list_1") assert len(fields) == 2 assert fields[0]["name"] == "Task Type" client.close() @respx.mock def test_upload_attachment_success(self, tmp_path): docx_file = tmp_path / "report.docx" docx_file.write_bytes(b"fake docx content") respx.post(f"{BASE_URL}/task/t1/attachment").mock(return_value=httpx.Response(200, json={})) client = ClickUpClient(api_token="pk_test_123") result = client.upload_attachment("t1", docx_file) assert result is True request = respx.calls.last.request assert b"report.docx" in request.content # Multipart upload should NOT have application/json content-type assert "multipart/form-data" in request.headers.get("content-type", "") client.close() @respx.mock def test_upload_attachment_http_failure(self, tmp_path): docx_file = tmp_path / "report.docx" docx_file.write_bytes(b"fake docx content") respx.post(f"{BASE_URL}/task/t1/attachment").mock( return_value=httpx.Response(403, json={"err": "forbidden"}) ) client = ClickUpClient(api_token="pk_test_123") result = client.upload_attachment("t1", docx_file) assert result is False client.close() def test_upload_attachment_file_not_found(self): client = ClickUpClient(api_token="pk_test_123") result = client.upload_attachment("t1", "/nonexistent/file.docx") assert result is False client.close() @respx.mock def test_auth_header_sent(self): respx.get(f"{BASE_URL}/list/list_1/task").mock( return_value=httpx.Response(200, json={"tasks": []}) ) client = ClickUpClient(api_token="pk_secret_token") client.get_tasks("list_1") request = respx.calls.last.request assert request.headers["authorization"] == "pk_secret_token" client.close() @respx.mock def test_get_tasks_from_space_handles_folder_error(self): """If folders endpoint fails, still fetch folderless lists.""" respx.get(f"{BASE_URL}/space/space_1/folder").mock( return_value=httpx.Response(403, json={"err": "forbidden"}) ) respx.get(f"{BASE_URL}/space/space_1/list").mock( return_value=httpx.Response(200, json={"lists": [{"id": "list_x"}]}) ) respx.get(f"{BASE_URL}/list/list_x/task").mock( return_value=httpx.Response( 200, json={ "tasks": [ { "id": "t_x", "name": "Task X", "status": {"status": "to do", "type": "open"}, "custom_fields": [], } ] }, ) ) client = ClickUpClient(api_token="pk_test") tasks = client.get_tasks_from_space("space_1") assert len(tasks) == 1 assert tasks[0].id == "t_x" client.close()