feat: implement Trello authentication & connection plugin
Add TrelloClient for Trello REST API interaction with env-var-based credential management (TRELLO_API_KEY, TRELLO_TOKEN). Implements: - trello_verify_credentials — verify API key/token against Trello API - trello_list_boards — list accessible Trello boards - trello_disconnect — clear in-memory credentials Includes full test suite (18 tests), spec document, and plugin metadata. Issue: #1
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
"""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
|
||||
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() -> Generator[None, None, None]:
|
||||
"""Remove Trello env vars before each test so state is predictable."""
|
||||
# Use monkeypatch via request directly — fallback to os.environ
|
||||
saved_key = os.environ.pop("TRELLO_API_KEY", None)
|
||||
saved_token = os.environ.pop("TRELLO_TOKEN", None)
|
||||
yield
|
||||
if saved_key is not None:
|
||||
os.environ["TRELLO_API_KEY"] = saved_key
|
||||
if saved_token is not None:
|
||||
os.environ["TRELLO_TOKEN"] = saved_token
|
||||
|
||||
|
||||
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) == 3
|
||||
assert all(callable(t) for t in PLUGIN_TOOLS)
|
||||
Reference in New Issue
Block a user