CheddahBot/tests/test_clickup.py

343 lines
12 KiB
Python

"""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()