From dc87c51f9492910df2616555b048138947f2834e Mon Sep 17 00:00:00 2001 From: Rory O'Keeffe Date: Tue, 2 Jun 2026 20:35:14 +0100 Subject: [PATCH 1/3] fix: allow hidden collaborator emails --- tests/test_api_projects.py | 32 ++++++++++++++++++++++++++++++++ todoist_api_python/models.py | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/test_api_projects.py b/tests/test_api_projects.py index 212d430..034f5d2 100644 --- a/tests/test_api_projects.py +++ b/tests/test_api_projects.py @@ -415,3 +415,35 @@ async def test_get_collaborators( assert len(respx_mock.calls) == count + 1 assert collaborators == default_collaborators_list[i] count += 1 + + +@pytest.mark.asyncio +async def test_get_collaborators_accepts_hidden_email( + todoist_api: TodoistAPI, + todoist_api_async: TodoistAPIAsync, + respx_mock: respx.MockRouter, +) -> None: + project_id = "6X7rM8997g3RQmvh" + endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/collaborators" + response: PaginatedResults = { + "results": [{"id": "123", "name": "Alice D.", "email": None}], + "next_cursor": None, + } + + mock_route( + respx_mock, + method="GET", + url=endpoint, + request_headers=api_headers(), + response_json=response, + response_status=200, + ) + + collaborators = next(todoist_api.get_collaborators(project_id)) + + assert collaborators == [Collaborator(id="123", name="Alice D.", email=None)] + + collaborators_async_iter = await todoist_api_async.get_collaborators(project_id) + + async for collaborators in collaborators_async_iter: + assert collaborators == [Collaborator(id="123", name="Alice D.", email=None)] diff --git a/todoist_api_python/models.py b/todoist_api_python/models.py index 5185a3c..55364f7 100644 --- a/todoist_api_python/models.py +++ b/todoist_api_python/models.py @@ -142,7 +142,7 @@ class _(JSONPyWizard.Meta): # noqa:N801 v1 = True id: str - email: str + email: str | None name: str From cfa7d509627df162435a60b386d7e956e93356f8 Mon Sep 17 00:00:00 2001 From: Rory O'Keeffe Date: Thu, 2 Jul 2026 14:23:42 +0100 Subject: [PATCH 2/3] test: Cover hidden collaborator email via shared fixture. Per review, the nullable-email case lives in the default collaborator fixtures instead of a standalone test, so the existing pagination test exercises it on both sync and async paths. Co-Authored-By: Claude Fable 5 --- tests/data/test_defaults.py | 2 ++ tests/test_api_projects.py | 32 -------------------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/tests/data/test_defaults.py b/tests/data/test_defaults.py index 8d14794..42090b6 100644 --- a/tests/data/test_defaults.py +++ b/tests/data/test_defaults.py @@ -164,6 +164,8 @@ class PaginatedItems(TypedDict): DEFAULT_COLLABORATOR_RESPONSE_3 = dict(DEFAULT_COLLABORATOR_RESPONSE) DEFAULT_COLLABORATOR_RESPONSE_3["id"] = "6X7rjKtP98vG84rK" +# Workspaces can hide collaborator emails, returning null. +DEFAULT_COLLABORATOR_RESPONSE_3["email"] = None DEFAULT_COLLABORATORS_RESPONSE: list[PaginatedResults] = [ { diff --git a/tests/test_api_projects.py b/tests/test_api_projects.py index 034f5d2..212d430 100644 --- a/tests/test_api_projects.py +++ b/tests/test_api_projects.py @@ -415,35 +415,3 @@ async def test_get_collaborators( assert len(respx_mock.calls) == count + 1 assert collaborators == default_collaborators_list[i] count += 1 - - -@pytest.mark.asyncio -async def test_get_collaborators_accepts_hidden_email( - todoist_api: TodoistAPI, - todoist_api_async: TodoistAPIAsync, - respx_mock: respx.MockRouter, -) -> None: - project_id = "6X7rM8997g3RQmvh" - endpoint = f"{DEFAULT_API_URL}/projects/{project_id}/collaborators" - response: PaginatedResults = { - "results": [{"id": "123", "name": "Alice D.", "email": None}], - "next_cursor": None, - } - - mock_route( - respx_mock, - method="GET", - url=endpoint, - request_headers=api_headers(), - response_json=response, - response_status=200, - ) - - collaborators = next(todoist_api.get_collaborators(project_id)) - - assert collaborators == [Collaborator(id="123", name="Alice D.", email=None)] - - collaborators_async_iter = await todoist_api_async.get_collaborators(project_id) - - async for collaborators in collaborators_async_iter: - assert collaborators == [Collaborator(id="123", name="Alice D.", email=None)] From 8d1b9dd37f3104727dd9af4ad3f840e78d2cc108 Mon Sep 17 00:00:00 2001 From: Rory O'Keeffe Date: Thu, 2 Jul 2026 14:27:41 +0100 Subject: [PATCH 3/3] docs: Correct fixture comment to forward-compat framing. The backend currently sends the hidden@todoist.com placeholder for hidden collaborator emails; null is a possible future contract (Doist/Issues#20418), not today's behavior. Co-Authored-By: Claude Fable 5 --- tests/data/test_defaults.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/data/test_defaults.py b/tests/data/test_defaults.py index 42090b6..1ecbf19 100644 --- a/tests/data/test_defaults.py +++ b/tests/data/test_defaults.py @@ -164,7 +164,8 @@ class PaginatedItems(TypedDict): DEFAULT_COLLABORATOR_RESPONSE_3 = dict(DEFAULT_COLLABORATOR_RESPONSE) DEFAULT_COLLABORATOR_RESPONSE_3["id"] = "6X7rjKtP98vG84rK" -# Workspaces can hide collaborator emails, returning null. +# Forward-compat with Doist/Issues#20418: hidden collaborator emails are +# currently sent as "hidden@todoist.com", but may become null. DEFAULT_COLLABORATOR_RESPONSE_3["email"] = None DEFAULT_COLLABORATORS_RESPONSE: list[PaginatedResults] = [