import asyncio import httpx import pytest from src import gitea_proxy, main @pytest.mark.anyio async def test_notification_reply_returns_the_confirmed_comment_for_exact_once_append(monkeypatch): def handler(request): if request.method == "GET": return httpx.Response(200, json={ "repository": {"full_name": "stackchain/api"}, "subject": { "type": "Issue", "url": "https://forge.example/api/v1/repos/stackchain/api/issues/7", }, }) return httpx.Response(201, json={ "id": 91, "user": {"login": "timmy"}, "body": "Please retry the worker.", "created_at": "2026-08-07T19:00:00Z", "html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-91", }) monkeypatch.setattr(gitea_proxy, "GITEA_URL", "https://forge.example") gitea_proxy.start_client(transport=httpx.MockTransport(handler)) try: result = await gitea_proxy.reply_to_notification(42, "Please retry the worker.") finally: await gitea_proxy.stop_client() assert result == { "id": 91, "author": "timmy", "body": "Please retry the worker.", "created_at": "2026-08-07T19:00:00Z", "url": "https://forge.example/stackchain/api/issues/7#issuecomment-91", } @pytest.mark.anyio async def test_notification_reply_api_validates_and_posts_without_marking_read(monkeypatch): calls = [] async def reply(thread_id, body): calls.append((thread_id, body)) return { "id": 91, "url": "https://forge.example/stackchain/api/issues/7#issuecomment-91", } async def must_not_mark_read(_thread_id): raise AssertionError("replying must not mark the update read") monkeypatch.setattr(main.gitea_proxy, "reply_to_notification", reply) monkeypatch.setattr(main, "mark_notification_read", must_not_mark_read) transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/notifications/42/reply", json={"body": " Please retry the worker. "}, ) empty = await client.post( "/api/v1/notifications/42/reply", json={"body": " "} ) oversized = await client.post( "/api/v1/notifications/42/reply", json={"body": "x" * 10_001} ) assert response.status_code == 201 assert response.json() == { "id": 91, "url": "https://forge.example/stackchain/api/issues/7#issuecomment-91", } assert response.headers["cache-control"] == "no-store" assert empty.status_code == 422 assert oversized.status_code == 422 assert calls == [(42, "Please retry the worker.")] @pytest.mark.anyio @pytest.mark.parametrize("failure", ["timeout", "upstream"]) async def test_notification_reply_failure_is_sanitized_retryable_and_no_store( monkeypatch, failure ): async def reply(_thread_id, _body): if failure == "timeout": await asyncio.sleep(0.05) raise httpx.HTTPError("token=secret upstream exploded") monkeypatch.setattr(main.gitea_proxy, "reply_to_notification", reply) monkeypatch.setattr(main, "NOTIFICATION_MUTATION_TIMEOUT_SECONDS", 0.01) transport = httpx.ASGITransport(app=main.app, raise_app_exceptions=False) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/v1/notifications/42/reply", json={"body": "Retry it"} ) assert response.status_code == 503 assert response.json() == { "error": "The reply could not be posted. Your draft is safe; please retry." } assert response.headers["retry-after"] == "1" assert response.headers["cache-control"] == "no-store" assert "secret" not in response.text @pytest.mark.anyio async def test_notification_reply_cannot_be_invoked_by_a_foreign_browser_origin(): transport = httpx.ASGITransport(app=main.app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: response = await client.options( "/api/v1/notifications/42/reply", headers={ "Origin": "https://evil.example", "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "content-type", }, ) assert response.status_code != 200 assert "access-control-allow-origin" not in response.headers