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
329 lines
12 KiB
Python
329 lines
12 KiB
Python
"""Tests for the Trello plugin — auth & connection feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from collections.abc import Generator
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from pytest import MonkeyPatch
|
|
import requests
|
|
|
|
# Tell pytest we plan to set env vars in tests
|
|
pytestmark = pytest.mark.usefixtures("clear_env")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def clear_env(monkeypatch: MonkeyPatch) -> Generator[None, None, None]:
|
|
"""Remove Trello env vars before each test so state is predictable."""
|
|
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
|
|
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
|
|
yield
|
|
|
|
|
|
def _make_session(mocker: Any) -> requests.Session:
|
|
"""Build a requests.Session with a mocked adapter."""
|
|
session = requests.Session()
|
|
adapter = mocker.get_adapter()
|
|
session.mount("https://", adapter)
|
|
return session
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TrelloClient — verify_credentials
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestVerifyCredentials:
|
|
"""Tests for TrelloClient.verify_credentials()."""
|
|
|
|
def test_success(self, requests_mock: Any) -> None:
|
|
"""Happy path: valid creds return member info."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
json={"id": "member123", "username": "testuser", "fullName": "Test User"},
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is True
|
|
assert "testuser" in result["message"]
|
|
assert result["member"]["id"] == "member123"
|
|
assert result["member"]["username"] == "testuser"
|
|
|
|
def test_missing_credentials(self) -> None:
|
|
"""Both API key and token must be set."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
client = TrelloClient(api_key="", token="")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "TRELLO_API_KEY" in result["message"]
|
|
|
|
def test_auth_failure_401(self, requests_mock: Any) -> None:
|
|
"""401 response surfaces as auth failure."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
status_code=401,
|
|
)
|
|
client = TrelloClient(api_key="bad_key", token="bad_tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "401" in result["message"]
|
|
|
|
def test_forbidden_403(self, requests_mock: Any) -> None:
|
|
"""403 surfaces as access denied."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
status_code=403,
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "403" in result["message"]
|
|
|
|
def test_rate_limit_429(self, requests_mock: Any) -> None:
|
|
"""429 surfaces as rate limit error."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
status_code=429,
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "rate limit" in result["message"].lower()
|
|
|
|
def test_network_error(self, requests_mock: Any) -> None:
|
|
"""Connection errors surface as network failure."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
exc=requests.exceptions.ConnectionError("connection refused"),
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "connect" in result["message"].lower()
|
|
|
|
def test_timeout(self, requests_mock: Any) -> None:
|
|
"""Timeout surfaces gracefully."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
exc=requests.exceptions.Timeout("timed out"),
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.verify_credentials()
|
|
|
|
assert result["success"] is False
|
|
assert "timed out" in result["message"].lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TrelloClient — list_boards
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestListBoards:
|
|
"""Tests for TrelloClient.list_boards()."""
|
|
|
|
def test_success(self, requests_mock: Any) -> None:
|
|
"""Happy path: returns a list of boards."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
mock_boards = [
|
|
{"id": "b1", "name": "Board One", "url": "https://trello.com/b/b1", "closed": False, "starred": False},
|
|
{"id": "b2", "name": "Board Two", "url": "https://trello.com/b/b2", "closed": True, "starred": False},
|
|
]
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me/boards",
|
|
json=mock_boards,
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.list_boards()
|
|
|
|
assert result["success"] is True
|
|
assert len(result["boards"]) == 2
|
|
assert result["boards"][0]["name"] == "Board One"
|
|
assert result["boards"][1]["closed"] is True
|
|
|
|
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.list_boards()
|
|
|
|
assert result["success"] is False
|
|
assert "TRELLO_API_KEY" in result["message"]
|
|
|
|
def test_empty_boards(self, requests_mock: Any) -> None:
|
|
"""No boards returns an empty list."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me/boards",
|
|
json=[],
|
|
)
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.list_boards()
|
|
|
|
assert result["success"] is True
|
|
assert result["boards"] == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TrelloClient — disconnect
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestDisconnect:
|
|
"""Tests for TrelloClient.disconnect()."""
|
|
|
|
def test_disconnect_clears_credentials(self) -> None:
|
|
"""Disconnect clears the in-memory credentials."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
client = TrelloClient(api_key="key", token="tok")
|
|
result = client.disconnect()
|
|
|
|
assert result["success"] is True
|
|
assert "cleared" in result["message"].lower()
|
|
|
|
# Subsequent calls should fail
|
|
verify_result = client.verify_credentials()
|
|
assert verify_result["success"] is False
|
|
assert "TRELLO_API_KEY" in verify_result["message"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# check_requirements
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCheckRequirements:
|
|
"""Tests for TrelloClient.check_requirements()."""
|
|
|
|
def test_returns_false_when_missing(self) -> None:
|
|
"""Returns False when env vars are not set."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
assert TrelloClient.check_requirements() is False
|
|
|
|
def test_returns_true_when_set(self) -> None:
|
|
"""Returns True when both env vars are set."""
|
|
from trello_plugin.client import TrelloClient
|
|
|
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
|
assert TrelloClient.check_requirements() is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool functions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestToolFunctions:
|
|
"""Tests for the Hermes tool wrapper functions."""
|
|
|
|
def test_trello_verify_credentials_tool(self, requests_mock: Any) -> None:
|
|
"""Tool returns valid JSON string on success."""
|
|
from trello_plugin.tools import trello_verify_credentials
|
|
|
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
|
|
)
|
|
result = json.loads(trello_verify_credentials())
|
|
assert result["success"] is True
|
|
assert result["member"]["username"] == "bot"
|
|
|
|
def test_trello_list_boards_tool(self, requests_mock: Any) -> None:
|
|
"""Tool returns valid JSON string on success."""
|
|
from trello_plugin.tools import trello_list_boards
|
|
|
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me/boards",
|
|
json=[{"id": "b1", "name": "Test", "url": "https://trello.com/b/b1", "closed": False, "starred": False}],
|
|
)
|
|
result = json.loads(trello_list_boards())
|
|
assert result["success"] is True
|
|
assert len(result["boards"]) == 1
|
|
|
|
def test_trello_disconnect_tool(self, requests_mock: Any) -> None:
|
|
"""Tool clears credentials and returns success."""
|
|
from trello_plugin.tools import trello_disconnect, trello_verify_credentials
|
|
|
|
os.environ["TRELLO_API_KEY"] = "test-key"
|
|
os.environ["TRELLO_TOKEN"] = "test-token"
|
|
|
|
# First verify works
|
|
requests_mock.get(
|
|
"https://api.trello.com/1/members/me",
|
|
json={"id": "m1", "username": "bot", "fullName": "Bot User"},
|
|
)
|
|
verify = json.loads(trello_verify_credentials())
|
|
assert verify["success"] is True
|
|
|
|
# Then disconnect
|
|
disc = json.loads(trello_disconnect())
|
|
assert disc["success"] is True
|
|
|
|
# With env vars still set, verify should reconnect successfully
|
|
# (disconnect clears the in-memory client; env vars persist)
|
|
verify2 = json.loads(trello_verify_credentials())
|
|
assert verify2["success"] is True
|
|
assert verify2["member"]["username"] == "bot"
|
|
|
|
def test_check_requirements_tool(self, requests_mock: Any) -> None:
|
|
"""check_requirements returns False when env not set."""
|
|
from trello_plugin.tools import check_requirements
|
|
|
|
assert check_requirements() is False
|
|
|
|
os.environ["TRELLO_API_KEY"] = "k"
|
|
os.environ["TRELLO_TOKEN"] = "t"
|
|
assert check_requirements() is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Plugin metadata
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestPluginMetadata:
|
|
"""Tests for the plugin metadata constants."""
|
|
|
|
def test_plugin_constants(self) -> None:
|
|
from trello_plugin.tools import (
|
|
PLUGIN_DESCRIPTION,
|
|
PLUGIN_NAME,
|
|
PLUGIN_TOOLS,
|
|
PLUGIN_VERSION,
|
|
)
|
|
|
|
assert PLUGIN_NAME == "trello-plugin"
|
|
assert isinstance(PLUGIN_DESCRIPTION, str)
|
|
assert isinstance(PLUGIN_VERSION, str)
|
|
assert len(PLUGIN_TOOLS) == 8
|
|
assert all(callable(t) for t in PLUGIN_TOOLS) |