Files
Marko (Hermes Implementer) f4bff86b70 feat: implement Trello board management
Add 5 board management tools extending the Trello client:
- trello_create_board — create a new board
- trello_rename_board — rename an existing board
- trello_archive_board — close/archive a board
- trello_open_board — re-open a closed board
- trello_board_details — view board with lists and members

Includes board name-to-ID resolution, 16 new tests, and spec doc.

Issue: #2
2026-05-26 22:28:49 +00:00

406 lines
14 KiB
Python

"""Tests for the Trello plugin — board management feature."""
from __future__ import annotations
import json
import os
from typing import Any
import pytest
import requests
pytestmark = pytest.mark.usefixtures("clear_env")
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def clear_env(monkeypatch: pytest.MonkeyPatch) -> Any:
"""Remove Trello env vars before each test so state is predictable."""
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
# ---------------------------------------------------------------------------
# TrelloClient — create_board
# ---------------------------------------------------------------------------
class TestCreateBoard:
"""Tests for TrelloClient.create_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Happy path: creates a board and returns details."""
from trello_plugin.client import TrelloClient
requests_mock.post(
"https://api.trello.com/1/boards",
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
status_code=200,
)
client = TrelloClient(api_key="key", token="tok")
result = client.create_board(name="New Board")
assert result["success"] is True
assert result["board"]["name"] == "New Board"
assert result["board"]["id"] == "b1"
def test_missing_credentials(self) -> None:
"""Missing creds returns error before any network call."""
from trello_plugin.client import TrelloClient
client = TrelloClient(api_key="", token="")
result = client.create_board(name="Board")
assert result["success"] is False
assert "TRELLO_API_KEY" in result["message"]
# ---------------------------------------------------------------------------
# TrelloClient — rename_board
# ---------------------------------------------------------------------------
class TestRenameBoard:
"""Tests for TrelloClient.rename_board()."""
def test_success_by_id(self, requests_mock: Any) -> None:
"""Renaming a board by ID works."""
from trello_plugin.client import TrelloClient
# Resolve board
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Old Name", "url": "https://trello.com/b/b1"},
)
# Rename
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "New Name", "url": "https://trello.com/b/b1"},
)
client = TrelloClient(api_key="key", token="tok")
result = client.rename_board(board_id="b1", name="New Name")
assert result["success"] is True
assert result["board"]["name"] == "New Name"
def test_board_not_found(self, requests_mock: Any) -> None:
"""Non-existent board returns a clear error."""
from trello_plugin.client import TrelloClient
# Board lookup fails
requests_mock.get(
"https://api.trello.com/1/boards/nonexistent",
status_code=404,
)
# List boards returns empty
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.rename_board(board_id="nonexistent", name="New")
assert result["success"] is False
assert "not found" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — archive_board
# ---------------------------------------------------------------------------
class TestArchiveBoard:
"""Tests for TrelloClient.archive_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Archiving a board returns success message."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Archive
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "closed": True},
)
client = TrelloClient(api_key="key", token="tok")
result = client.archive_board(board_id="b1")
assert result["success"] is True
assert "archived" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — open_board
# ---------------------------------------------------------------------------
class TestOpenBoard:
"""Tests for TrelloClient.open_board()."""
def test_success(self, requests_mock: Any) -> None:
"""Opening a closed board returns success message."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Open
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "closed": False},
)
client = TrelloClient(api_key="key", token="tok")
result = client.open_board(board_id="b1")
assert result["success"] is True
assert "opened" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — board_details
# ---------------------------------------------------------------------------
class TestBoardDetails:
"""Tests for TrelloClient.board_details()."""
def test_success(self, requests_mock: Any) -> None:
"""Board details return lists and members."""
from trello_plugin.client import TrelloClient
# Resolve
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
# Details fetch
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={
"id": "b1",
"name": "My Board",
"url": "https://trello.com/b/b1",
"desc": "A test board",
"closed": False,
"starred": False,
"lists": [
{"id": "l1", "name": "To Do"},
{"id": "l2", "name": "Done"},
],
"members": [
{"id": "m1", "username": "user1", "fullName": "User One"},
],
},
)
client = TrelloClient(api_key="key", token="tok")
result = client.board_details(board_id="b1")
assert result["success"] is True
assert len(result["board"]["lists"]) == 2
assert len(result["board"]["members"]) == 1
assert result["board"]["lists"][0]["name"] == "To Do"
def test_not_found(self, requests_mock: Any) -> None:
"""Non-existent board returns error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/nonexistent",
status_code=404,
)
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[],
)
client = TrelloClient(api_key="key", token="tok")
result = client.board_details(board_id="nonexistent")
assert result["success"] is False
assert "not found" in result["message"].lower()
# ---------------------------------------------------------------------------
# TrelloClient — _resolve_board_id
# ---------------------------------------------------------------------------
class TestResolveBoardId:
"""Tests for TrelloClient._resolve_board_id()."""
def test_resolves_by_id(self, requests_mock: Any) -> None:
"""Resolves a valid ID directly."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1"},
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("b1")
assert result["id"] == "b1"
def test_resolves_by_name(self, requests_mock: Any) -> None:
"""Resolves a board name to its ID."""
from trello_plugin.client import TrelloClient
# ID lookup fails
requests_mock.get(
"https://api.trello.com/1/boards/My%20Board",
status_code=404,
)
# List boards for name matching
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[
{"id": "b1", "name": "My Board", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
{"id": "b2", "name": "Other Board", "url": "https://trello.com/b/b2", "closed": False, "starred": False},
],
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("My Board")
assert result["id"] == "b1"
def test_duplicate_name_error(self, requests_mock: Any) -> None:
"""Multiple boards with same name returns error."""
from trello_plugin.client import TrelloClient
requests_mock.get(
"https://api.trello.com/1/boards/Duplicate",
status_code=404,
)
requests_mock.get(
"https://api.trello.com/1/members/me/boards",
json=[
{"id": "b1", "name": "Duplicate", "url": "", "closed": False, "starred": False},
{"id": "b2", "name": "Duplicate", "url": "", "closed": False, "starred": False},
],
)
client = TrelloClient(api_key="key", token="tok")
result = client._resolve_board_id("Duplicate")
assert "success" in result and result["success"] is False
assert "multiple" in result["message"].lower()
# ---------------------------------------------------------------------------
# Tool functions
# ---------------------------------------------------------------------------
class TestBoardToolFunctions:
"""Tests for the Hermes board management tool wrappers."""
def test_trello_create_board_tool(self, requests_mock: Any) -> None:
"""Create board tool returns valid JSON."""
from trello_plugin.tools import trello_create_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.post(
"https://api.trello.com/1/boards",
json={"id": "b1", "name": "New Board", "url": "https://trello.com/b/b1"},
)
result = json.loads(trello_create_board(name="New Board"))
assert result["success"] is True
assert result["board"]["name"] == "New Board"
def test_trello_rename_board_tool(self, requests_mock: Any) -> None:
"""Rename board tool returns valid JSON."""
from trello_plugin.tools import trello_rename_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Old", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Renamed", "url": ""},
)
result = json.loads(trello_rename_board(board_id="b1", name="Renamed"))
assert result["success"] is True
assert result["board"]["name"] == "Renamed"
def test_trello_archive_board_tool(self, requests_mock: Any) -> None:
"""Archive board tool returns valid JSON."""
from trello_plugin.tools import trello_archive_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "closed": True},
)
result = json.loads(trello_archive_board(board_id="b1"))
assert result["success"] is True
assert "archived" in result["message"].lower()
def test_trello_open_board_tool(self, requests_mock: Any) -> None:
"""Open board tool returns valid JSON."""
from trello_plugin.tools import trello_open_board
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "url": ""},
)
requests_mock.put(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test Board", "closed": False},
)
result = json.loads(trello_open_board(board_id="b1"))
assert result["success"] is True
assert "opened" in result["message"].lower()
def test_trello_board_details_tool(self, requests_mock: Any) -> None:
"""Board details tool returns valid JSON."""
from trello_plugin.tools import trello_board_details
os.environ["TRELLO_API_KEY"] = "key"
os.environ["TRELLO_TOKEN"] = "tok"
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={"id": "b1", "name": "Test", "url": ""},
)
requests_mock.get(
"https://api.trello.com/1/boards/b1",
json={
"id": "b1",
"name": "Test",
"url": "",
"desc": "",
"closed": False,
"starred": False,
"lists": [{"id": "l1", "name": "To Do"}],
"members": [],
},
)
result = json.loads(trello_board_details(board_id="b1"))
assert result["success"] is True
assert len(result["board"]["lists"]) == 1