"""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)