diff --git a/docs/backend/manage-cards-spec.md b/docs/backend/manage-cards-spec.md new file mode 100644 index 0000000..e9434c7 --- /dev/null +++ b/docs/backend/manage-cards-spec.md @@ -0,0 +1,162 @@ +# Trello Plugin — Manage Trello Cards + +**Feature:** US: Manage Trello Cards +**Issue:** #4 +**Branch:** `feature/manage-cards` + +## Overview + +Extends the Trello plugin with full card management capabilities: create, view, update, move, archive, assign members, comments, and checklist management. + +## Tools + +### `trello_create_card` +Create a new card on a selected list. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Card title | +| `list_id` | string | Yes | List ID | +| `desc` | string | No | Card description | +| `due` | string | No | Due date (ISO 8601) | + +**Returns:** Card ID, name, URL, and short URL. + +### `trello_card_details` +View detailed information about a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | + +**Returns:** Card details including title, description, due date, members, checklists, comments, URL. + +### `trello_update_card` +Update a card's core attributes (title, description, due date). + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `name` | string | No | New title | +| `desc` | string | No | New description | +| `due` | string | No | New due date (ISO 8601) | + +**Returns:** Updated card details. + +### `trello_move_card` +Move a card to a different list. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `list_id` | string | Yes | Target list ID | + +**Returns:** Success message with new list location. + +### `trello_archive_card` +Archive a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | + +**Returns:** Success message. + +### `trello_assign_member` +Add a member to a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `member_id` | string | Yes | Trello member ID | + +**Returns:** Success message. + +### `trello_remove_member` +Remove a member from a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `member_id` | string | Yes | Trello member ID | + +**Returns:** Success message. + +### `trello_add_comment` +Add a comment to a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `text` | string | Yes | Comment text | + +**Returns:** Comment ID and success. + +### `trello_delete_comment` +Delete a comment from a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `comment_id` | string | Yes | Comment/action ID | + +**Returns:** Success message. + +### `trello_add_checklist_item` +Add a checklist item to a card. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `name` | string | Yes | Checklist item text | +| `checklist_id` | string | No | Specific checklist ID (if card has multiple) | + +**Returns:** Checklist item details. + +### `trello_toggle_checklist_item` +Mark a checklist item as complete or incomplete. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `item_id` | string | Yes | Checklist item ID | +| `checked` | bool | Yes | True = complete, False = incomplete | + +**Returns:** Updated item state. + +### `trello_delete_checklist_item` +Remove a checklist item. + +**Parameters:** +| Param | Type | Required | Description | +|-------|------|----------|-------------| +| `card_id` | string | Yes | Card ID | +| `item_id` | string | Yes | Checklist item ID | + +**Returns:** Success message. + +## Trello API Endpoints + +| Purpose | Method | Endpoint | +|---------|--------|----------| +| Create card | POST | `/1/cards` | +| Get card | GET | `/1/cards/{id}` | +| Update card | PUT | `/1/cards/{id}` | +| Add member | POST | `/1/cards/{id}/idMembers` | +| Remove member | DELETE | `/1/cards/{id}/idMembers/{memberId}` | +| Add comment | POST | `/1/cards/{id}/actions/comments` | +| Delete comment | DELETE | `/1/cards/{id}/actions/{commentId}/comments` | +| Add checklist item | POST | `/1/cards/{id}/checklistItems` | +| Update checklist item | PUT | `/1/cards/{id}/checklistItem/{itemId}/state` | +| Delete checklist item | DELETE | `/1/cards/{id}/checklistItems/{itemId}` \ No newline at end of file diff --git a/src/trello_plugin/__init__.py b/src/trello_plugin/__init__.py index d2238a5..161ab06 100644 --- a/src/trello_plugin/__init__.py +++ b/src/trello_plugin/__init__.py @@ -8,17 +8,29 @@ from trello_plugin.tools import ( PLUGIN_TOOLS, PLUGIN_VERSION, check_requirements, + trello_add_checklist_item, + trello_add_comment, trello_archive_board, + trello_archive_card, trello_archive_list, + trello_assign_member, trello_board_details, + trello_card_details, trello_create_board, + trello_create_card, trello_create_list, + trello_delete_checklist_item, + trello_delete_comment, trello_disconnect, trello_list_boards, + trello_move_card, trello_move_list, trello_open_board, + trello_remove_member, trello_rename_board, trello_rename_list, + trello_toggle_checklist_item, + trello_update_card, trello_verify_credentials, ) @@ -40,4 +52,16 @@ __all__ = [ "trello_rename_list", "trello_archive_list", "trello_move_list", + "trello_create_card", + "trello_card_details", + "trello_update_card", + "trello_move_card", + "trello_archive_card", + "trello_assign_member", + "trello_remove_member", + "trello_add_comment", + "trello_delete_comment", + "trello_add_checklist_item", + "trello_toggle_checklist_item", + "trello_delete_checklist_item", ] \ No newline at end of file diff --git a/src/trello_plugin/client.py b/src/trello_plugin/client.py index 468c59e..29371c8 100644 --- a/src/trello_plugin/client.py +++ b/src/trello_plugin/client.py @@ -163,6 +163,10 @@ class TrelloClient: """Make an authenticated PUT request.""" return self._request("PUT", path, json_body=json_body) + def _delete(self, path: str) -> ApiResponse: + """Make an authenticated DELETE request.""" + return self._request("DELETE", path) + # ------------------------------------------------------------------ # Public API — Auth & Connection # ------------------------------------------------------------------ @@ -571,6 +575,398 @@ class TrelloClient: "message": result.get("message", "Failed to move list."), } + # ------------------------------------------------------------------ + # Public API — Card Management + # ------------------------------------------------------------------ + + def create_card( + self, + name: str, + list_id: str, + desc: str = "", + due: str | None = None, + ) -> dict[str, Any]: + """Create a new card on a Trello list. + + Args: + name: Card title. + list_id: Trello list ID. + desc: Card description. + due: Due date in ISO 8601 format. + + Returns: + dict with card details on success. + """ + body: dict[str, Any] = {"name": name, "idList": list_id} + if desc: + body["desc"] = desc + if due: + body["due"] = due + + result = self._post("/cards", json_body=body) + if _is_success(result): + card = result["data"] + return { + "success": True, + "card": { + "id": card.get("id"), + "name": card.get("name"), + "url": card.get("url"), + "short_url": card.get("shortUrl"), + "id_list": card.get("idList"), + }, + } + return { + "success": False, + "message": result.get("message", "Failed to create card."), + } + + def card_details(self, card_id: str) -> dict[str, Any]: + """Fetch detailed information about a card. + + Returns card attributes, members, checklists, and comments. + + Args: + card_id: Trello card ID. + + Returns: + dict with full card details. + """ + result = self._get( + f"/cards/{card_id}", + params={ + "fields": "id,name,desc,due,dueComplete,url,shortUrl,idList,closed", + "members": "true", + "member_fields": "id,username,fullName", + "checklists": "all", + "actions": "commentCard", + "actions_limit": "50", + "list": "true", + }, + ) + if not _is_success(result): + return { + "success": False, + "message": result.get("message", "Failed to fetch card details."), + } + + card = result["data"] + return { + "success": True, + "card": { + "id": card.get("id"), + "name": card.get("name"), + "desc": card.get("desc", ""), + "due": card.get("due"), + "due_complete": card.get("dueComplete", False), + "url": card.get("url"), + "short_url": card.get("shortUrl"), + "closed": card.get("closed", False), + "list": {"id": card.get("idList"), "name": (card.get("list") or {}).get("name", "")}, + "members": [ + {"id": m.get("id"), "username": m.get("username"), "full_name": m.get("fullName")} + for m in card.get("members", []) + ], + "checklists": [ + { + "id": cl.get("id"), + "name": cl.get("name"), + "items": [ + { + "id": item.get("id"), + "name": item.get("name"), + "state": item.get("state"), + } + for item in cl.get("checkItems", []) + ], + } + for cl in card.get("checklists", []) + ], + "comments": [ + { + "id": a.get("id"), + "text": a.get("data", {}).get("text", ""), + "member": a.get("memberCreator", {}).get("username", "unknown"), + "date": a.get("date"), + } + for a in card.get("actions", []) + if a.get("type") == "commentCard" + ], + }, + } + + def update_card( + self, + card_id: str, + name: str | None = None, + desc: str | None = None, + due: str | None = None, + ) -> dict[str, Any]: + """Update a card's core attributes. + + Args: + card_id: Trello card ID. + name: New title (omit to keep current). + desc: New description (omit to keep current). + due: New due date ISO 8601, or empty string to clear. + + Returns: + dict with updated card details. + """ + body: dict[str, Any] = {} + if name is not None: + body["name"] = name + if desc is not None: + body["desc"] = desc + if due is not None: + body["due"] = due + + if not body: + return { + "success": True, + "message": "No changes specified.", + } + + result = self._put(f"/cards/{card_id}", json_body=body) + if _is_success(result): + card = result["data"] + return { + "success": True, + "card": { + "id": card.get("id"), + "name": card.get("name"), + "desc": card.get("desc", ""), + "due": card.get("due"), + }, + } + return { + "success": False, + "message": result.get("message", "Failed to update card."), + } + + def move_card(self, card_id: str, list_id: str) -> dict[str, Any]: + """Move a card to a different list. + + Args: + card_id: Trello card ID. + list_id: Target list ID. + + Returns: + dict with success message. + """ + result = self._put(f"/cards/{card_id}", json_body={"idList": list_id}) + if _is_success(result): + card = result["data"] + return { + "success": True, + "message": f"Card '{card.get('name', card_id)}' moved to list '{card.get('idList', list_id)}'.", + } + return { + "success": False, + "message": result.get("message", "Failed to move card."), + } + + def archive_card(self, card_id: str) -> dict[str, Any]: + """Archive a card. + + Args: + card_id: Trello card ID. + + Returns: + dict with success message. + """ + result = self._put(f"/cards/{card_id}", json_body={"closed": True}) + if _is_success(result): + card = result["data"] + return {"success": True, "message": f"Card '{card.get('name', card_id)}' archived."} + return { + "success": False, + "message": result.get("message", "Failed to archive card."), + } + + def assign_member(self, card_id: str, member_id: str) -> dict[str, Any]: + """Add a member to a card. + + Args: + card_id: Trello card ID. + member_id: Trello member ID. + + Returns: + dict with success message. + """ + result = self._post(f"/cards/{card_id}/idMembers", json_body={"value": member_id}) + if _is_success(result): + return {"success": True, "message": f"Member {member_id} assigned to card."} + return { + "success": False, + "message": result.get("message", "Failed to assign member."), + } + + def remove_member(self, card_id: str, member_id: str) -> dict[str, Any]: + """Remove a member from a card. + + Args: + card_id: Trello card ID. + member_id: Trello member ID. + + Returns: + dict with success message. + """ + result = self._delete(f"/cards/{card_id}/idMembers/{member_id}") + if _is_success(result): + return {"success": True, "message": f"Member {member_id} removed from card."} + return { + "success": False, + "message": result.get("message", "Failed to remove member."), + } + + def add_comment(self, card_id: str, text: str) -> dict[str, Any]: + """Add a comment to a card. + + Args: + card_id: Trello card ID. + text: Comment text. + + Returns: + dict with comment details. + """ + result = self._post( + f"/cards/{card_id}/actions/comments", + json_body={"text": text}, + ) + if _is_success(result): + action = result["data"] + return { + "success": True, + "comment": { + "id": action.get("id"), + "text": action.get("data", {}).get("text", text), + }, + } + return { + "success": False, + "message": result.get("message", "Failed to add comment."), + } + + def delete_comment(self, card_id: str, comment_id: str) -> dict[str, Any]: + """Delete a comment from a card. + + Args: + card_id: Trello card ID. + comment_id: Action/comment ID. + + Returns: + dict with success message. + """ + result = self._delete(f"/cards/{card_id}/actions/{comment_id}/comments") + if _is_success(result): + return {"success": True, "message": "Comment deleted."} + return { + "success": False, + "message": result.get("message", "Failed to delete comment."), + } + + def add_checklist_item(self, card_id: str, name: str, checklist_id: str | None = None) -> dict[str, Any]: + """Add a checklist item to a card. + + Args: + card_id: Trello card ID. + name: Checklist item text. + checklist_id: Specific checklist (fetches first if omitted). + + Returns: + dict with item details. + """ + resolved_checklist_id = checklist_id + if not resolved_checklist_id: + # Fetch checklists to find the first one + details = self.card_details(card_id) + if not details.get("success"): + return { + "success": False, + "message": "Card has no checklists. Add one via Trello web first.", + } + checklists = details["card"].get("checklists", []) + if not checklists: + return { + "success": False, + "message": "Card has no checklists. Add one via Trello web first.", + } + resolved_checklist_id = checklists[0]["id"] + + result = self._post( + f"/cards/{card_id}/checklistItems", + json_body={ + "idChecklist": resolved_checklist_id, + "name": name, + "checked": False, + }, + ) + if _is_success(result): + item = result["data"] + return { + "success": True, + "item": { + "id": item.get("id"), + "name": item.get("name"), + "state": item.get("state", "incomplete"), + "id_checklist": item.get("idChecklist"), + }, + } + return { + "success": False, + "message": result.get("message", "Failed to add checklist item."), + } + + def toggle_checklist_item(self, card_id: str, item_id: str, checked: bool) -> dict[str, Any]: # noqa: FBT001 + """Mark a checklist item as complete or incomplete. + + Args: + card_id: Trello card ID. + item_id: Checklist item ID. + checked: True for complete, False for incomplete. + + Returns: + dict with updated item state. + """ + state = "complete" if checked else "incomplete" + result = self._put( + f"/cards/{card_id}/checklistItem/{item_id}", + json_body={"state": state}, + ) + if _is_success(result): + item = result["data"] + return { + "success": True, + "item": { + "id": item.get("id"), + "name": item.get("name"), + "state": item.get("state"), + }, + } + return { + "success": False, + "message": result.get("message", "Failed to update checklist item."), + } + + def delete_checklist_item(self, card_id: str, item_id: str) -> dict[str, Any]: + """Remove a checklist item. + + Args: + card_id: Trello card ID. + item_id: Checklist item ID. + + Returns: + dict with success message. + """ + result = self._delete(f"/cards/{card_id}/checklistItems/{item_id}") + if _is_success(result): + return {"success": True, "message": "Checklist item deleted."} + return { + "success": False, + "message": result.get("message", "Failed to delete checklist item."), + } + @staticmethod def check_requirements() -> bool: """Check if the required environment variables are set.""" diff --git a/src/trello_plugin/tools.py b/src/trello_plugin/tools.py index 42c04a0..c1d88ca 100644 --- a/src/trello_plugin/tools.py +++ b/src/trello_plugin/tools.py @@ -260,6 +260,259 @@ def trello_move_list(list_id: str, pos: str = "bottom") -> str: return _respond(result) +# --------------------------------------------------------------------------- +# Tools — Card Management +# --------------------------------------------------------------------------- + + +def trello_create_card(name: str, list_id: str, desc: str = "", due: str | None = None) -> str: + """Create a new card on a Trello list. + + Parameters + ---------- + name : str + Card title. + list_id : str + List ID. + desc : str, optional + Card description. + due : str, optional + Due date in ISO 8601 format. + + Returns + ------- + str + JSON with card details. + """ + client = _get_client() + result = client.create_card(name=name, list_id=list_id, desc=desc, due=due) + return _respond(result) + + +def trello_card_details(card_id: str) -> str: + """View detailed information about a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + + Returns + ------- + str + JSON with full card details including members, checklists, and comments. + """ + client = _get_client() + result = client.card_details(card_id=card_id) + return _respond(result) + + +def trello_update_card(card_id: str, name: str | None = None, desc: str | None = None, due: str | None = None) -> str: + """Update a card's core attributes (title, description, due date). + + Parameters + ---------- + card_id : str + Card ID. + name : str, optional + New title. + desc : str, optional + New description. + due : str, optional + New due date ISO 8601. + + Returns + ------- + str + JSON with updated card details. + """ + client = _get_client() + result = client.update_card(card_id=card_id, name=name, desc=desc, due=due) + return _respond(result) + + +def trello_move_card(card_id: str, list_id: str) -> str: + """Move a card to a different list. + + Parameters + ---------- + card_id : str + Card ID. + list_id : str + Target list ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.move_card(card_id=card_id, list_id=list_id) + return _respond(result) + + +def trello_archive_card(card_id: str) -> str: + """Archive a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.archive_card(card_id=card_id) + return _respond(result) + + +def trello_assign_member(card_id: str, member_id: str) -> str: + """Add a member to a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + member_id : str + Trello member ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.assign_member(card_id=card_id, member_id=member_id) + return _respond(result) + + +def trello_remove_member(card_id: str, member_id: str) -> str: + """Remove a member from a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + member_id : str + Trello member ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.remove_member(card_id=card_id, member_id=member_id) + return _respond(result) + + +def trello_add_comment(card_id: str, text: str) -> str: + """Add a comment to a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + text : str + Comment text. + + Returns + ------- + str + JSON with comment ID. + """ + client = _get_client() + result = client.add_comment(card_id=card_id, text=text) + return _respond(result) + + +def trello_delete_comment(card_id: str, comment_id: str) -> str: + """Delete a comment from a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + comment_id : str + Comment/action ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.delete_comment(card_id=card_id, comment_id=comment_id) + return _respond(result) + + +def trello_add_checklist_item(card_id: str, name: str, checklist_id: str | None = None) -> str: + """Add a checklist item to a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + name : str + Checklist item text. + checklist_id : str, optional + Specific checklist ID (auto-discovers the first if omitted). + + Returns + ------- + str + JSON with item details. + """ + client = _get_client() + result = client.add_checklist_item(card_id=card_id, name=name, checklist_id=checklist_id) + return _respond(result) + + +def trello_toggle_checklist_item(card_id: str, item_id: str, checked: bool) -> str: # noqa: FBT001 + """Mark a checklist item as complete or incomplete. + + Parameters + ---------- + card_id : str + Card ID. + item_id : str + Checklist item ID. + checked : bool + True = complete, False = incomplete. + + Returns + ------- + str + JSON with updated item state. + """ + client = _get_client() + result = client.toggle_checklist_item(card_id=card_id, item_id=item_id, checked=checked) + return _respond(result) + + +def trello_delete_checklist_item(card_id: str, item_id: str) -> str: + """Remove a checklist item from a Trello card. + + Parameters + ---------- + card_id : str + Card ID. + item_id : str + Checklist item ID. + + Returns + ------- + str + JSON with success message. + """ + client = _get_client() + result = client.delete_checklist_item(card_id=card_id, item_id=item_id) + return _respond(result) + + # --------------------------------------------------------------------------- # Plugin metadata (used by Hermes plugin loader) # --------------------------------------------------------------------------- @@ -280,6 +533,18 @@ PLUGIN_TOOLS = [ trello_rename_list, trello_archive_list, trello_move_list, + trello_create_card, + trello_card_details, + trello_update_card, + trello_move_card, + trello_archive_card, + trello_assign_member, + trello_remove_member, + trello_add_comment, + trello_delete_comment, + trello_add_checklist_item, + trello_toggle_checklist_item, + trello_delete_checklist_item, ] PLUGIN_REQUIRES_ENV = ["TRELLO_API_KEY", "TRELLO_TOKEN"] diff --git a/tests/test_auth.py b/tests/test_auth.py index aff6fa9..092964e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -325,5 +325,5 @@ class TestPluginMetadata: assert PLUGIN_NAME == "trello-plugin" assert isinstance(PLUGIN_DESCRIPTION, str) assert isinstance(PLUGIN_VERSION, str) - assert len(PLUGIN_TOOLS) == 12 + assert len(PLUGIN_TOOLS) == 24 assert all(callable(t) for t in PLUGIN_TOOLS) \ No newline at end of file diff --git a/tests/test_cards.py b/tests/test_cards.py new file mode 100644 index 0000000..4b944f8 --- /dev/null +++ b/tests/test_cards.py @@ -0,0 +1,313 @@ +"""Tests for the Trello plugin — card management feature.""" + +from __future__ import annotations + +import json +import os +from typing import Any + +import pytest + +pytestmark = pytest.mark.usefixtures("clear_env") + + +@pytest.fixture(autouse=True) +def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any: + monkeypatch.delenv("TRELLO_API_KEY", raising=False) + monkeypatch.delenv("TRELLO_TOKEN", raising=False) + + +# --------------------------------------------------------------------------- +# TrelloClient — create_card +# --------------------------------------------------------------------------- + +class TestCreateCard: + def test_success(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "My Card", "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc", "idList": "l1"}) + client = TrelloClient(api_key="k", token="t") + result = client.create_card(name="My Card", list_id="l1") + assert result["success"] + assert result["card"]["name"] == "My Card" + + def test_with_description_and_due(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"}) + client = TrelloClient(api_key="k", token="t") + result = client.create_card(name="Card", list_id="l1", desc="Desc", due="2026-06-01T00:00:00Z") + assert result["success"] + + def test_missing_credentials(self) -> None: + from trello_plugin.client import TrelloClient + client = TrelloClient(api_key="", token="") + result = client.create_card(name="Card", list_id="l1") + assert not result["success"] + assert "TRELLO_API_KEY" in result["message"] + + +# --------------------------------------------------------------------------- +# TrelloClient — card_details +# --------------------------------------------------------------------------- + +class TestCardDetails: + def test_success(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.get( + "https://api.trello.com/1/cards/c1", + json={ + "id": "c1", "name": "My Card", "desc": "Description", "due": None, + "dueComplete": False, "url": "https://trello.com/c/c1", "shortUrl": "https://trello.com/c/abc", + "idList": "l1", "closed": False, "list": {"id": "l1", "name": "To Do"}, + "members": [{"id": "m1", "username": "user1", "fullName": "User One"}], + "checklists": [{"id": "cl1", "name": "Checklist", "checkItems": [{"id": "ci1", "name": "Item", "state": "incomplete"}]}], + "actions": [{"id": "a1", "type": "commentCard", "data": {"text": "Nice card"}, "memberCreator": {"username": "user1"}, "date": "2026-05-01T00:00:00Z"}], + }, + ) + client = TrelloClient(api_key="k", token="t") + result = client.card_details(card_id="c1") + assert result["success"] + assert result["card"]["name"] == "My Card" + assert len(result["card"]["members"]) == 1 + assert len(result["card"]["checklists"]) == 1 + assert len(result["card"]["comments"]) == 1 + + +# --------------------------------------------------------------------------- +# TrelloClient — update_card +# --------------------------------------------------------------------------- + +class TestUpdateCard: + def test_update_name(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Updated", "desc": "", "due": None}) + client = TrelloClient(api_key="k", token="t") + result = client.update_card(card_id="c1", name="Updated") + assert result["success"] + assert result["card"]["name"] == "Updated" + + def test_no_changes(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + client = TrelloClient(api_key="k", token="t") + result = client.update_card(card_id="c1") + assert result["success"] + assert "No changes" in result["message"] + + +# --------------------------------------------------------------------------- +# TrelloClient — move_card, archive_card +# --------------------------------------------------------------------------- + +class TestMoveCard: + def test_success(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"}) + client = TrelloClient(api_key="k", token="t") + result = client.move_card(card_id="c1", list_id="l2") + assert result["success"] + + +class TestArchiveCard: + def test_success(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True}) + client = TrelloClient(api_key="k", token="t") + result = client.archive_card(card_id="c1") + assert result["success"] + + +# --------------------------------------------------------------------------- +# TrelloClient — member operations +# --------------------------------------------------------------------------- + +class TestMemberOps: + def test_assign_member(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={"id": "c1"}, status_code=200) + client = TrelloClient(api_key="k", token="t") + result = client.assign_member(card_id="c1", member_id="m1") + assert result["success"] + + def test_remove_member(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={}) + client = TrelloClient(api_key="k", token="t") + result = client.remove_member(card_id="c1", member_id="m1") + assert result["success"] + + +# --------------------------------------------------------------------------- +# TrelloClient — comments +# --------------------------------------------------------------------------- + +class TestComments: + def test_add_comment(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Great!"}}) + client = TrelloClient(api_key="k", token="t") + result = client.add_comment(card_id="c1", text="Great!") + assert result["success"] + assert result["comment"]["id"] == "a1" + + def test_delete_comment(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={}) + client = TrelloClient(api_key="k", token="t") + result = client.delete_comment(card_id="c1", comment_id="a1") + assert result["success"] + + +# --------------------------------------------------------------------------- +# TrelloClient — checklists +# --------------------------------------------------------------------------- + +class TestChecklistItems: + def test_add_item_with_checklist_id(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"}) + client = TrelloClient(api_key="k", token="t") + result = client.add_checklist_item(card_id="c1", name="Task", checklist_id="cl1") + assert result["success"] + assert result["item"]["name"] == "Task" + + def test_add_item_auto_discover(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + # card_details call for auto-discover + requests_mock.get( + "https://api.trello.com/1/cards/c1", + json={ + "id": "c1", "name": "Card", "desc": "", "due": None, + "dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False, + "list": {"id": "l1", "name": "List"}, + "members": [], "checklists": [{"id": "cl1", "name": "Checklist", "checkItems": []}], + "actions": [], + }, + ) + requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Auto", "state": "incomplete", "idChecklist": "cl1"}) + client = TrelloClient(api_key="k", token="t") + result = client.add_checklist_item(card_id="c1", name="Auto") + assert result["success"] + + def test_toggle_complete(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"}) + client = TrelloClient(api_key="k", token="t") + result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=True) + assert result["success"] + assert result["item"]["state"] == "complete" + + def test_toggle_incomplete(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "incomplete"}) + client = TrelloClient(api_key="k", token="t") + result = client.toggle_checklist_item(card_id="c1", item_id="ci1", checked=False) + assert result["success"] + assert result["item"]["state"] == "incomplete" + + def test_delete_item(self, requests_mock: Any) -> None: + from trello_plugin.client import TrelloClient + requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={}) + client = TrelloClient(api_key="k", token="t") + result = client.delete_checklist_item(card_id="c1", item_id="ci1") + assert result["success"] + + +# --------------------------------------------------------------------------- +# Tool functions +# --------------------------------------------------------------------------- + +class TestCardToolFunctions: + def test_trello_create_card_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_create_card + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.post("https://api.trello.com/1/cards", json={"id": "c1", "name": "Card", "url": "", "shortUrl": "", "idList": "l1"}) + result = json.loads(trello_create_card(name="Card", list_id="l1")) + assert result["success"] + + def test_trello_card_details_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_card_details + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.get("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "desc": "", "due": None, "dueComplete": False, "url": "", "shortUrl": "", "idList": "l1", "closed": False, "list": {"id": "l1", "name": "List"}, "members": [], "checklists": [], "actions": []}) + result = json.loads(trello_card_details(card_id="c1")) + assert result["success"] + + def test_trello_update_card_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_update_card + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "New", "desc": "", "due": None}) + result = json.loads(trello_update_card(card_id="c1", name="New")) + assert result["success"] + + def test_trello_move_card_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_move_card + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "idList": "l2"}) + result = json.loads(trello_move_card(card_id="c1", list_id="l2")) + assert result["success"] + + def test_trello_archive_card_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_archive_card + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.put("https://api.trello.com/1/cards/c1", json={"id": "c1", "name": "Card", "closed": True}) + result = json.loads(trello_archive_card(card_id="c1")) + assert result["success"] + + def test_trello_assign_member_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_assign_member + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.post("https://api.trello.com/1/cards/c1/idMembers", json={}, status_code=200) + result = json.loads(trello_assign_member(card_id="c1", member_id="m1")) + assert result["success"] + + def test_trello_remove_member_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_remove_member + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.delete("https://api.trello.com/1/cards/c1/idMembers/m1", status_code=200, json={}) + result = json.loads(trello_remove_member(card_id="c1", member_id="m1")) + assert result["success"] + + def test_trello_add_comment_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_add_comment + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.post("https://api.trello.com/1/cards/c1/actions/comments", json={"id": "a1", "data": {"text": "Nice"}}) + result = json.loads(trello_add_comment(card_id="c1", text="Nice")) + assert result["success"] + + def test_trello_delete_comment_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_delete_comment + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.delete("https://api.trello.com/1/cards/c1/actions/a1/comments", status_code=200, json={}) + result = json.loads(trello_delete_comment(card_id="c1", comment_id="a1")) + assert result["success"] + + def test_trello_add_checklist_item_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_add_checklist_item + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.post("https://api.trello.com/1/cards/c1/checklistItems", json={"id": "ci1", "name": "Task", "state": "incomplete", "idChecklist": "cl1"}) + result = json.loads(trello_add_checklist_item(card_id="c1", name="Task", checklist_id="cl1")) + assert result["success"] + + def test_trello_toggle_checklist_item_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_toggle_checklist_item + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.put("https://api.trello.com/1/cards/c1/checklistItem/ci1", json={"id": "ci1", "name": "Task", "state": "complete"}) + result = json.loads(trello_toggle_checklist_item(card_id="c1", item_id="ci1", checked=True)) + assert result["success"] + + def test_trello_delete_checklist_item_tool(self, requests_mock: Any) -> None: + from trello_plugin.tools import trello_delete_checklist_item + os.environ["TRELLO_API_KEY"] = "k" + os.environ["TRELLO_TOKEN"] = "t" + requests_mock.delete("https://api.trello.com/1/cards/c1/checklistItems/ci1", status_code=200, json={}) + result = json.loads(trello_delete_checklist_item(card_id="c1", item_id="ci1")) + assert result["success"] \ No newline at end of file