271 lines
9.5 KiB
Python
271 lines
9.5 KiB
Python
import asyncio
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from src import gitea_proxy, main
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_issue_detail_endpoint_returns_assigned_issue_with_no_store(monkeypatch):
|
|
async def assigned(repository, number):
|
|
return (repository, number) == ("stackchain/api", 7)
|
|
|
|
async def detail(repository, number):
|
|
return {"repository": repository, "number": number, "title": "Fix mobile flow"}
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned, raising=False)
|
|
monkeypatch.setattr(main.gitea_proxy, "issue_detail", detail)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/repos/stackchain/api/issues/7/detail")
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert response.json() == {
|
|
"repository": "stackchain/api", "number": 7, "title": "Fix mobile flow"
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_issue_detail_deadline_is_retryable_sanitized_and_cancels_work(monkeypatch):
|
|
cancelled = asyncio.Event()
|
|
|
|
async def assigned(repository, number):
|
|
try:
|
|
await asyncio.Event().wait()
|
|
finally:
|
|
cancelled.set()
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
|
monkeypatch.setattr(main, "ISSUE_ACTION_TIMEOUT_SECONDS", 0.01)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.get("/api/v1/repos/stackchain/api/issues/7/detail")
|
|
|
|
assert response.status_code == 503
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert response.headers["retry-after"] == "1"
|
|
assert response.json() == {"error": "Loading the issue timed out. Please retry."}
|
|
assert cancelled.is_set()
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize(
|
|
("method", "path", "json_body"),
|
|
[
|
|
("GET", "/api/v1/repos/stackchain/api/issues/7/detail", None),
|
|
("POST", "/api/v1/repos/stackchain/api/issues/7/comments", {"body": "Hello"}),
|
|
("PATCH", "/api/v1/repos/stackchain/api/issues/7/close", None),
|
|
],
|
|
)
|
|
async def test_issue_actions_reject_items_not_assigned_to_service_user(
|
|
monkeypatch, method, path, json_body
|
|
):
|
|
async def assigned(repository, number):
|
|
return False
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.request(method, path, json=json_body)
|
|
|
|
assert response.status_code == 404
|
|
assert response.headers["cache-control"] == "no-store"
|
|
|
|
|
|
@pytest.mark.anyio
|
|
@pytest.mark.parametrize("body", [" ", "x" * 10_001])
|
|
async def test_issue_comment_rejects_blank_or_oversized_body_before_upstream(
|
|
monkeypatch, body
|
|
):
|
|
called = False
|
|
|
|
async def assigned(repository, number):
|
|
nonlocal called
|
|
called = True
|
|
return True
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.post(
|
|
"/api/v1/repos/stackchain/api/issues/7/comments", json={"body": body}
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert called is False
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_issue_comment_endpoint_posts_only_to_assigned_issue(monkeypatch):
|
|
calls = []
|
|
|
|
async def assigned(repository, number):
|
|
return True
|
|
|
|
async def comment(repository, number, body):
|
|
calls.append((repository, number, body))
|
|
return {"id": 82, "author": "timmy", "body": body}
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
|
monkeypatch.setattr(main.gitea_proxy, "comment_on_issue", comment)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.post(
|
|
"/api/v1/repos/stackchain/api/issues/7/comments",
|
|
json={"body": " Ready to ship "},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert response.json() == {"id": 82, "author": "timmy", "body": "Ready to ship"}
|
|
assert calls == [("stackchain/api", 7, "Ready to ship")]
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_issue_close_endpoint_mutates_assigned_issue_only_after_confirmation(monkeypatch):
|
|
calls = []
|
|
|
|
async def assigned(repository, number):
|
|
return True
|
|
|
|
async def close(repository, number):
|
|
calls.append((repository, number))
|
|
return {"number": number, "state": "closed", "closed_at": "now"}
|
|
|
|
monkeypatch.setattr(main.gitea_proxy, "is_assigned_issue", assigned)
|
|
monkeypatch.setattr(main.gitea_proxy, "close_issue", close)
|
|
transport = httpx.ASGITransport(app=main.app)
|
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
|
response = await client.patch("/api/v1/repos/stackchain/api/issues/7/close")
|
|
|
|
assert response.status_code == 200
|
|
assert response.headers["cache-control"] == "no-store"
|
|
assert response.json()["state"] == "closed"
|
|
assert calls == [("stackchain/api", 7)]
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_gitea_close_issue_patches_state_and_confirms_closed_response():
|
|
requests = []
|
|
|
|
async def handler(request):
|
|
requests.append(request)
|
|
return httpx.Response(
|
|
200,
|
|
json={"number": 7, "state": "closed", "closed_at": "2026-08-07T10:20:00Z"},
|
|
)
|
|
|
|
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
|
try:
|
|
result = await gitea_proxy.close_issue("stackchain/api", 7)
|
|
finally:
|
|
await gitea_proxy.stop_client()
|
|
|
|
assert len(requests) == 1
|
|
assert requests[0].method == "PATCH"
|
|
assert requests[0].url.path == "/api/v1/repos/stackchain/api/issues/7"
|
|
assert requests[0].content == b'{"state":"closed"}'
|
|
assert result == {"number": 7, "state": "closed", "closed_at": "2026-08-07T10:20:00Z"}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_gitea_issue_comment_posts_body_and_returns_safe_identity():
|
|
requests = []
|
|
|
|
async def handler(request):
|
|
requests.append(request)
|
|
return httpx.Response(
|
|
201,
|
|
json={
|
|
"id": 82,
|
|
"body": "Ready to ship",
|
|
"created_at": "2026-08-07T10:10:00Z",
|
|
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-82",
|
|
"user": {"login": "timmy"},
|
|
},
|
|
)
|
|
|
|
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
|
try:
|
|
result = await gitea_proxy.comment_on_issue("stackchain/api", 7, "Ready to ship")
|
|
finally:
|
|
await gitea_proxy.stop_client()
|
|
|
|
assert len(requests) == 1
|
|
assert requests[0].method == "POST"
|
|
assert requests[0].url.path == "/api/v1/repos/stackchain/api/issues/7/comments"
|
|
assert requests[0].content == b'{"body":"Ready to ship"}'
|
|
assert result == {
|
|
"id": 82,
|
|
"author": "timmy",
|
|
"body": "Ready to ship",
|
|
"created_at": "2026-08-07T10:10:00Z",
|
|
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-82",
|
|
}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments():
|
|
requests = []
|
|
|
|
async def handler(request):
|
|
requests.append((request.method, request.url.path, request.url.query.decode()))
|
|
if request.url.path.endswith("/comments"):
|
|
return httpx.Response(
|
|
200,
|
|
json=[
|
|
{
|
|
"id": 81,
|
|
"body": "Latest update",
|
|
"created_at": "2026-08-07T10:00:00Z",
|
|
"html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-81",
|
|
"user": {"login": "sam"},
|
|
}
|
|
],
|
|
)
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"number": 7,
|
|
"title": "Fix mobile flow",
|
|
"state": "open",
|
|
"body": "Full issue context",
|
|
"html_url": "https://forge.example/stackchain/api/issues/7",
|
|
"labels": [{"name": "P1"}],
|
|
"assignees": [{"login": "timmy"}],
|
|
},
|
|
)
|
|
|
|
gitea_proxy.start_client(transport=httpx.MockTransport(handler))
|
|
try:
|
|
result = await gitea_proxy.issue_detail("stackchain/api", 7)
|
|
finally:
|
|
await gitea_proxy.stop_client()
|
|
|
|
assert requests == [
|
|
("GET", "/api/v1/repos/stackchain/api/issues/7", ""),
|
|
("GET", "/api/v1/repos/stackchain/api/issues/7/comments", "limit=20&page=1"),
|
|
]
|
|
assert result == {
|
|
"repository": "stackchain/api",
|
|
"number": 7,
|
|
"title": "Fix mobile flow",
|
|
"state": "open",
|
|
"body": "Full issue context",
|
|
"url": "https://forge.example/stackchain/api/issues/7",
|
|
"labels": ["P1"],
|
|
"assignees": ["timmy"],
|
|
"comments": [
|
|
{
|
|
"id": 81,
|
|
"author": "sam",
|
|
"body": "Latest update",
|
|
"created_at": "2026-08-07T10:00:00Z",
|
|
"url": "https://forge.example/stackchain/api/issues/7#issuecomment-81",
|
|
}
|
|
],
|
|
}
|