Files
trello-mcp/tests/test_trello.py
T
maxerton b4cabbf146 trello-tools: плагин и маркетплейс gp-tools для Claude Code
Сервер запускается через uv run --script, ключи запрашиваются
при установке плагина (userConfig) вместо .env.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 14:14:29 +00:00

267 lines
9.4 KiB
Python

import sys
from pathlib import Path
import httpx
import pytest
import respx
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import trello # noqa: E402
@pytest.mark.parametrize(
"url, expected",
[
("https://trello.com/c/abc123/45-название-карточки", "abc123"),
("https://trello.com/c/XyZ987", "XyZ987"),
],
)
def test_parse_short_link_ok(url, expected):
assert trello.parse_short_link(url) == expected
def test_parse_short_link_invalid():
with pytest.raises(ValueError):
trello.parse_short_link("https://example.com/not-a-trello-link")
@pytest.mark.asyncio
async def test_get_card_builds_request(monkeypatch):
monkeypatch.setenv("TRELLO_API_KEY", "key123")
monkeypatch.setenv("TRELLO_TOKEN", "tok456")
with respx.mock:
route = respx.get("https://api.trello.com/1/cards/abc123").mock(
return_value=httpx.Response(
200,
json={
"id": "5f0000000000000000000001",
"name": "Карточка",
"desc": "Описание",
"shortUrl": "https://trello.com/c/abc123",
"idBoard": "board1",
"idList": "list1",
"labels": [
{"id": "l1", "name": "Баг", "color": "red"},
{"id": "l2", "name": "", "color": "green"},
],
"attachments": [
{
"id": "a1",
"name": "screen.png",
"url": "https://trello.com/1/cards/c/attachments/a1/download/screen.png",
"mimeType": "image/png",
"bytes": 1024,
"isUpload": True,
"date": "2026-01-01T00:00:00.000Z",
},
{
"id": "a2",
"name": "Связанная",
"url": "https://trello.com/c/zzz999/7-linked",
"mimeType": "",
"bytes": None,
"isUpload": False,
"date": "2026-01-01T00:00:00.000Z",
},
],
},
)
)
actions_route = respx.get(
"https://api.trello.com/1/cards/abc123/actions"
).mock(
return_value=httpx.Response(
200,
json=[
{
"data": {"text": "второй комментарий"},
"date": "2026-01-02T00:00:00.000Z",
"memberCreator": {"fullName": "Аня"},
},
{
"data": {"text": "первый комментарий"},
"date": "2026-01-01T00:00:00.000Z",
"memberCreator": {"fullName": "Максим"},
},
],
)
)
result = await trello.get_card("https://trello.com/c/abc123/45-title")
assert route.called
assert actions_route.called
request = route.calls.last.request
assert request.url.params["key"] == "key123"
assert request.url.params["token"] == "tok456"
assert request.url.params["attachments"] == "true"
assert result.pop("labels") == [
{"name": "Баг", "color": "red"},
{"name": "", "color": "green"},
]
attachments = result.pop("attachments")
assert [(a["id"], a["kind"]) for a in attachments] == [("a1", "image"), ("a2", "card")]
assert attachments[0]["mime"] == "image/png" and attachments[0]["size"] == 1024
assert result == {
"id": "5f0000000000000000000001",
"name": "Карточка",
"desc": "Описание",
"url": "https://trello.com/c/abc123",
"board_id": "board1",
"list_id": "list1",
"comments": [
{"author": "Максим", "text": "первый комментарий", "date": "2026-01-01T00:00:00.000Z"},
{"author": "Аня", "text": "второй комментарий", "date": "2026-01-02T00:00:00.000Z"},
],
}
@pytest.mark.asyncio
async def test_get_comments_builds_request(monkeypatch):
monkeypatch.setenv("TRELLO_API_KEY", "key123")
monkeypatch.setenv("TRELLO_TOKEN", "tok456")
with respx.mock:
route = respx.get("https://api.trello.com/1/cards/card1/actions").mock(
return_value=httpx.Response(
200,
json=[
{
"data": {"text": "коммент без автора"},
"date": "2026-01-01T00:00:00.000Z",
"memberCreator": None,
}
],
)
)
result = await trello.get_comments("card1")
assert route.called
request = route.calls.last.request
assert request.url.params["filter"] == "commentCard"
assert result == [
{"author": "?", "text": "коммент без автора", "date": "2026-01-01T00:00:00.000Z"}
]
@pytest.mark.asyncio
async def test_add_comment_builds_request(monkeypatch):
monkeypatch.setenv("TRELLO_API_KEY", "key123")
monkeypatch.setenv("TRELLO_TOKEN", "tok456")
with respx.mock:
route = respx.post(
"https://api.trello.com/1/cards/card1/actions/comments"
).mock(return_value=httpx.Response(200, json={"id": "comment1"}))
result = await trello.add_comment("card1", "QA: https://qa.example/docs/xyz")
assert route.called
request = route.calls.last.request
assert request.url.params["text"] == "QA: https://qa.example/docs/xyz"
assert result == {"id": "comment1"}
def test_get_card_requires_credentials(monkeypatch):
monkeypatch.delenv("TRELLO_API_KEY", raising=False)
monkeypatch.delenv("TRELLO_TOKEN", raising=False)
with pytest.raises(trello.TrelloConfigError):
trello._credentials()
@pytest.mark.parametrize(
"attachment, kind",
[
({"url": "https://trello.com/c/abc/1-x", "isUpload": False}, "card"),
({"url": "https://trello.com/1/.../a.png", "isUpload": True, "mimeType": "image/jpeg"}, "image"),
({"url": "https://trello.com/1/.../a.pdf", "isUpload": True, "mimeType": "application/pdf"}, "file"),
({"url": "https://figma.com/file/xyz", "isUpload": False}, "link"),
],
)
def test_attachment_kind(attachment, kind):
assert trello._attachment_kind(attachment) == kind
ATTACHMENT_META_URL = "https://api.trello.com/1/cards/card1/attachments/a1"
DOWNLOAD_URL = "https://trello.com/1/cards/card1/attachments/a1/download/лог ошибки.txt"
@pytest.fixture
def trello_creds(monkeypatch, tmp_path):
monkeypatch.setenv("TRELLO_API_KEY", "key123")
monkeypatch.setenv("TRELLO_TOKEN", "tok456")
monkeypatch.setenv("TRELLO_ATTACHMENTS_DIR", str(tmp_path))
return tmp_path
@pytest.mark.asyncio
async def test_get_attachment_downloads_upload_with_oauth(trello_creds):
meta = {
"id": "a1",
"name": "лог ошибки.txt",
"url": DOWNLOAD_URL,
"mimeType": "text/plain",
"bytes": 5,
"isUpload": True,
}
with respx.mock:
respx.get(ATTACHMENT_META_URL).mock(return_value=httpx.Response(200, json=meta))
download = respx.get(DOWNLOAD_URL).mock(return_value=httpx.Response(200, content=b"oops!"))
result = await trello.get_attachment("card1", "a1")
assert download.calls.last.request.headers["Authorization"] == (
'OAuth oauth_consumer_key="key123", oauth_token="tok456"'
)
assert result["data"] == b"oops!"
assert Path(result["path"]).read_bytes() == b"oops!"
assert Path(result["path"]).parent == trello_creds / "card1"
@pytest.mark.asyncio
async def test_get_attachment_link_not_downloaded(trello_creds):
meta = {"id": "a1", "name": "Макет", "url": "https://figma.com/file/x", "isUpload": False}
with respx.mock:
respx.get(ATTACHMENT_META_URL).mock(return_value=httpx.Response(200, json=meta))
result = await trello.get_attachment("card1", "a1")
assert result["kind"] == "link"
assert "data" not in result and "path" not in result
@pytest.mark.asyncio
async def test_get_attachment_refuses_non_trello_upload_host(trello_creds):
meta = {
"id": "a1",
"name": "x.pdf",
"url": "https://evil.example.com/x.pdf",
"mimeType": "application/pdf",
"bytes": 5,
"isUpload": True,
}
with respx.mock:
respx.get(ATTACHMENT_META_URL).mock(return_value=httpx.Response(200, json=meta))
with pytest.raises(ValueError, match="не на Trello"):
await trello.get_attachment("card1", "a1")
@pytest.mark.asyncio
async def test_get_attachment_too_big(trello_creds):
meta = {
"id": "a1",
"name": "video.mp4",
"url": DOWNLOAD_URL,
"mimeType": "video/mp4",
"bytes": trello.MAX_ATTACHMENT_BYTES + 1,
"isUpload": True,
}
with respx.mock:
respx.get(ATTACHMENT_META_URL).mock(return_value=httpx.Response(200, json=meta))
download = respx.get(DOWNLOAD_URL)
with pytest.raises(ValueError, match="больше"):
await trello.get_attachment("card1", "a1")
assert not download.called