b4cabbf146
Сервер запускается через uv run --script, ключи запрашиваются при установке плагина (userConfig) вместо .env. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
9.4 KiB
Python
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
|