From 79f6cecab4d3729375f4a39cbf134ff484be8723 Mon Sep 17 00:00:00 2001 From: timmy Date: Tue, 18 Aug 2026 08:58:35 +0000 Subject: [PATCH] fix: keep Gitea links on configured forge (Closes #1072) --- README.md | 5 ++ src/gitea_proxy.py | 51 +++++++--------- tests/test_gitea_notifications.py | 26 ++++----- tests/test_gitea_work_search.py | 14 ++--- tests/test_global_search.py | 80 ++++++++++++++++++-------- tests/test_issue_api.py | 76 ++++++++++++------------ tests/test_issue_attachments.py | 8 +-- tests/test_review_api.py | 4 +- tests/test_update_reply_attachments.py | 12 ++-- 9 files changed, 153 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 7347f05..eb0b7db 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,11 @@ export STACKCHAIN_PUSH_DB='/var/lib/stackchain-dashboard/push-subscriptions.sqli uvicorn src.main:app --host 127.0.0.1 --port 8000 ``` +`GITEA_URL` is also the trust boundary for every Gitea resource link returned +to the browser. Configure the externally reachable scheme, host, and subpath +(for example `https://forge.example.com/git`); cross-origin, downgraded, and +same-host links outside that subpath are discarded. + For a systemd deployment, keep those values in a root-readable environment file (`chmod 600`), reference it with `EnvironmentFile=`, and keep secrets out of the unit command line and repository. Liveness alone does not prove that operators can use the diff --git a/src/gitea_proxy.py b/src/gitea_proxy.py index 973b82d..f82c356 100644 --- a/src/gitea_proxy.py +++ b/src/gitea_proxy.py @@ -445,7 +445,7 @@ def _normalize_global_search_item(item: Any, kind: str) -> dict | None: return None repository = item.get("repository") repository = repository if isinstance(repository, dict) else {} - url = _safe_web_url(item.get("html_url")) + url = _safe_gitea_web_url(item.get("html_url")) if not ( isinstance(item.get("number"), int) and isinstance(item.get("title"), str) @@ -614,7 +614,7 @@ async def work_preview(repository: str, kind: str, number: int) -> dict: if isinstance(label, dict) and isinstance(label.get("name"), str) ], "assignees": assignee_names, - "url": _safe_web_url(issue.get("html_url")), + "url": _safe_gitea_web_url(issue.get("html_url")), "claimable": actual_kind == "issue" and state == "open" and not assignee_names, "reopenable": actual_kind == "issue" and state == "closed", "assigned_to_me": bool(login and login in assignee_names), @@ -650,7 +650,7 @@ def _normalize_available_issue(item: Any) -> dict | None: "assignees": [], "updated_at": item.get("updated_at", "") if isinstance(item.get("updated_at"), str) else "", - "url": _safe_web_url(item.get("html_url")), + "url": _safe_gitea_web_url(item.get("html_url")), } @@ -756,13 +756,6 @@ async def issues() -> WorkItems: ) -def _safe_web_url(value: Any) -> str: - if not isinstance(value, str): - return "" - parsed = urlsplit(value) - return value if parsed.scheme in {"http", "https"} and parsed.netloc else "" - - def _safe_gitea_web_url(value: Any) -> str: if not isinstance(value, str) or not value.strip(): return "" @@ -819,8 +812,8 @@ def _normalize_notifications(threads: Any) -> list[dict]: subject = thread.get("subject") repository = repository if isinstance(repository, dict) else {} subject = subject if isinstance(subject, dict) else {} - subject_url = _safe_web_url(subject.get("html_url")) - latest_url = _safe_web_url(subject.get("latest_comment_html_url")) + subject_url = _safe_gitea_web_url(subject.get("html_url")) + latest_url = _safe_gitea_web_url(subject.get("latest_comment_html_url")) number_text = ( urlsplit(subject_url).path.rstrip("/").rsplit("/", 1)[-1] if subject_url @@ -1053,8 +1046,8 @@ async def notification_detail(thread_id: int) -> dict: comment = comment if isinstance(comment, dict) else {} user_value = comment.get("user") user = user_value if isinstance(user_value, dict) else {} - subject_url = _safe_web_url(subject.get("html_url")) - latest_url = _safe_web_url(comment.get("html_url")) or _safe_web_url( + subject_url = _safe_gitea_web_url(subject.get("html_url")) + latest_url = _safe_gitea_web_url(comment.get("html_url")) or _safe_gitea_web_url( subject.get("latest_comment_html_url") ) assignee_values = subject_detail.get("assignees") @@ -1236,7 +1229,7 @@ async def upload_notification_attachment( if not isinstance(attachment, dict): raise ValueError("Gitea attachment response was not an object") name = attachment.get("name") - url = _safe_web_url(attachment.get("browser_download_url")) + url = _safe_gitea_web_url(attachment.get("browser_download_url")) size = attachment.get("size") if not isinstance(name, str) or not name or not url or not isinstance(size, int): raise ValueError("Gitea did not confirm the attachment") @@ -1275,7 +1268,7 @@ def _normalize_issue_comment(comment: dict) -> dict: "created_at": comment.get("created_at", "") if isinstance(comment.get("created_at"), str) else "", - "url": _safe_web_url(comment.get("html_url")), + "url": _safe_gitea_web_url(comment.get("html_url")), } @@ -1357,7 +1350,7 @@ async def upload_assigned_issue_attachment( if not isinstance(attachment, dict): raise ValueError("Gitea attachment response was not an object") name = attachment.get("name") - url = _safe_web_url(attachment.get("browser_download_url")) + url = _safe_gitea_web_url(attachment.get("browser_download_url")) size = attachment.get("size") if not isinstance(name, str) or not name or not url or not isinstance(size, int): raise ValueError("Gitea did not confirm the attachment") @@ -1383,7 +1376,7 @@ async def upload_preview_attachment( if not isinstance(attachment, dict): raise ValueError("Gitea attachment response was not an object") name = attachment.get("name") - url = _safe_web_url(attachment.get("browser_download_url")) + url = _safe_gitea_web_url(attachment.get("browser_download_url")) size = attachment.get("size") if not isinstance(name, str) or not name or not url or not isinstance(size, int): raise ValueError("Gitea did not confirm the attachment") @@ -1410,7 +1403,7 @@ async def upload_assigned_pull_attachment( if not isinstance(attachment, dict): raise ValueError("Gitea attachment response was not an object") name = attachment.get("name") - url = _safe_web_url(attachment.get("browser_download_url")) + url = _safe_gitea_web_url(attachment.get("browser_download_url")) size = attachment.get("size") if not isinstance(name, str) or not name or not url or not isinstance(size, int): raise ValueError("Gitea did not confirm the attachment") @@ -1586,7 +1579,7 @@ async def create_issue( "updated_at": issue.get("updated_at", "") if isinstance(issue.get("updated_at"), str) else "", - "url": _safe_web_url(issue.get("html_url")), + "url": _safe_gitea_web_url(issue.get("html_url")), } @@ -1640,7 +1633,7 @@ async def claim_available_issue(repository: str, number: int) -> dict: "assignees": logins, "updated_at": confirmed.get("updated_at", "") if isinstance(confirmed.get("updated_at"), str) else "", - "url": _safe_web_url(confirmed.get("html_url")), + "url": _safe_gitea_web_url(confirmed.get("html_url")), } @@ -1700,7 +1693,7 @@ async def reopen_issue(repository: str, number: int) -> dict: "assignees": logins, "updated_at": confirmed.get("updated_at", "") if isinstance(confirmed.get("updated_at"), str) else "", - "url": _safe_web_url(confirmed.get("html_url")), + "url": _safe_gitea_web_url(confirmed.get("html_url")), } @@ -2256,7 +2249,7 @@ async def issue_detail(repository: str, number: int) -> dict: and isinstance(issue["milestone"].get("title"), str) else None ), - "url": _safe_web_url(issue.get("html_url")), + "url": _safe_gitea_web_url(issue.get("html_url")), "labels": [ label["name"] for label in labels if isinstance(label, dict) and isinstance(label.get("name"), str) @@ -2297,7 +2290,7 @@ async def issue_dependencies(repository: str, number: int, limit: int = 20) -> l "number": dependency_number, "title": item.get("title", "") if isinstance(item.get("title"), str) else "", "state": "open", - "url": _safe_web_url(item.get("html_url")), + "url": _safe_gitea_web_url(item.get("html_url")), }) return dependencies @@ -2425,7 +2418,7 @@ async def _update_issue_content( "body": body, "state": issue.get("state", "open"), "updated_at": issue.get("updated_at", ""), - "url": _safe_web_url(issue.get("html_url")), + "url": _safe_gitea_web_url(issue.get("html_url")), } raise IssueEditConflictError("issue changed upstream") @@ -2450,7 +2443,7 @@ async def _update_issue_content( "body": body, "state": confirmed.get("state", "open"), "updated_at": confirmed.get("updated_at", ""), - "url": _safe_web_url(confirmed.get("html_url")), + "url": _safe_gitea_web_url(confirmed.get("html_url")), } @@ -2518,7 +2511,7 @@ async def resolve_work_route( "number": number, "title": target.get("title", "") if isinstance(target.get("title"), str) else "", "state": state, - "url": _safe_web_url(target.get("html_url")), + "url": _safe_gitea_web_url(target.get("html_url")), **({"is_review": True, "work_reasons": ["review_requested"]} if kind == "review" else {}), **({ "is_filed": True, @@ -2623,7 +2616,7 @@ async def pull_completion_detail(repository: str, number: int) -> dict: "number": number, "title": pull.get("title") if isinstance(pull.get("title"), str) else "", "body": pull.get("body") if isinstance(pull.get("body"), str) else "", - "url": _safe_web_url(pull.get("html_url")), + "url": _safe_gitea_web_url(pull.get("html_url")), "author": user.get("login") if isinstance(user.get("login"), str) else "", "head_sha": sha, "state": pull.get("state") if isinstance(pull.get("state"), str) else "", @@ -2865,7 +2858,7 @@ async def submit_pull_review( return { "id": review.get("id"), "state": review.get("state") or "COMMENT", - "url": _safe_web_url(review.get("html_url")), + "url": _safe_gitea_web_url(review.get("html_url")), } diff --git a/tests/test_gitea_notifications.py b/tests/test_gitea_notifications.py index 5f06716..a122a0c 100644 --- a/tests/test_gitea_notifications.py +++ b/tests/test_gitea_notifications.py @@ -20,7 +20,7 @@ async def test_notification_page_preserves_upstream_total_without_loading_other_ "subject": { "title": "Retry failed deploy", "type": "Issue", - "html_url": "https://forge.example/stackchain/api/issues/7", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7", }, }], ) @@ -170,8 +170,8 @@ async def test_unread_notifications_are_normalized_for_mobile_handoff(): "title": "Retry failed deploy", "type": "Issue", "state": "open", - "html_url": "https://forge.example/stackchain/api/issues/7", - "latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7", + "latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", }, }, {"id": 43, "repository": None, "subject": None}, @@ -193,8 +193,8 @@ async def test_unread_notifications_are_normalized_for_mobile_handoff(): "title": "Retry failed deploy", "subject_type": "Issue", "state": "open", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", - "subject_url": "https://forge.example/stackchain/api/issues/7", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", + "subject_url": "http://127.0.0.1:3000/stackchain/api/issues/7", }, { "id": 43, @@ -245,8 +245,8 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re "state": "open", "url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/7", "latest_comment_url": "http://127.0.0.1:3000/api/v1/repos/stackchain/api/issues/comments/9", - "html_url": "https://forge.example/stackchain/api/issues/7", - "latest_comment_html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7", + "latest_comment_html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", }, }) if request.url.path.endswith("/issues/7"): @@ -262,7 +262,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re "body": "Logs point to the worker timeout.", "created_at": "2026-08-06T12:30:00Z", "user": {"login": "alexander"}, - "html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", }) if request.url.path.endswith("/issues/7/comments"): return httpx.Response(200, json=[{ @@ -270,7 +270,7 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re "body": "Logs point to the worker timeout.", "created_at": "2026-08-06T12:30:00Z", "user": {"login": "alexander"}, - "html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", }], headers={"X-Total-Count": "1"}) return httpx.Response(404) @@ -292,13 +292,13 @@ async def test_notification_detail_loads_subject_and_latest_comment_for_inbox_re "title": "Retry failed deploy", "subject_type": "Issue", "state": "open", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", "subject_body": "Deploy fails after **three** retries.", "latest_comment": { "author": "alexander", "body": "Logs point to the worker timeout.", "created_at": "2026-08-06T12:30:00Z", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-9", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-9", }, "issue": {"number": 7, "assignees": [], "claimable": True}, "acknowledge_supported": True, @@ -362,7 +362,7 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin "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", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91", }) gitea_proxy.start_client(transport=httpx.MockTransport(upstream)) @@ -388,7 +388,7 @@ async def test_reply_to_notification_posts_to_its_issue_conversation(subject_kin "author": "timmy", "body": "Please retry the worker.", "created_at": "2026-08-07T19:00:00Z", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-91", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-91", } diff --git a/tests/test_gitea_work_search.py b/tests/test_gitea_work_search.py index 4a22759..484235f 100644 --- a/tests/test_gitea_work_search.py +++ b/tests/test_gitea_work_search.py @@ -47,7 +47,7 @@ async def test_work_page_preserves_total_and_reason_without_loading_other_pages( "title": "Older review", "state": "open", "repository": {"full_name": "stackchain/api"}, - "html_url": "https://forge.example/stackchain/api/pulls/51", + "html_url": "http://127.0.0.1:3000/stackchain/api/pulls/51", }], ) @@ -76,7 +76,7 @@ async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_dist "id": 878, "number": 878, "title": "Completed delegation", "state": "closed", "updated_at": "2026-08-15T12:00:00Z", "repository": {"full_name": "stackchain/dashboard"}, - "html_url": "https://forge.example/stackchain/dashboard/issues/878", + "html_url": "http://127.0.0.1:3000/stackchain/dashboard/issues/878", }]) gitea_proxy.start_client(transport=httpx.MockTransport(upstream)) @@ -94,7 +94,7 @@ async def test_filed_work_page_loads_open_and_closed_authored_issues_with_a_dist "id": 878, "number": 878, "title": "Completed delegation", "state": "closed", "updated_at": "2026-08-15T12:00:00Z", "repository": {"full_name": "stackchain/dashboard"}, - "html_url": "https://forge.example/stackchain/dashboard/issues/878", + "html_url": "http://127.0.0.1:3000/stackchain/dashboard/issues/878", "work_reasons": ["created_by_me"], }], } @@ -106,7 +106,7 @@ async def test_issue_context_merges_self_assigned_and_filed_streams_without_dupl del page, limit common = { "id": 7, "number": 7, "title": "My own filing", "state": "open", - "repository": {"full_name": "stackchain/api"}, "html_url": "https://forge.example/issues/7", + "repository": {"full_name": "stackchain/api"}, "html_url": "http://127.0.0.1:3000/issues/7", } if stream == "issue": return {"items": [{**common, "assignees": [{"login": "timmy"}]}], "page": 1, "total": 1, "has_more": False} @@ -816,7 +816,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work( "repository": {"full_name": "stackchain/mobile"}, "updated_at": "2026-08-06T12:00:00Z", "due_date": "2026-08-09T23:59:59Z", - "html_url": "https://forge.example/stackchain/mobile/issues/7", + "html_url": "http://127.0.0.1:3000/stackchain/mobile/issues/7", }] async def pulls(): @@ -831,7 +831,7 @@ async def test_context_preserves_repository_and_update_time_for_cross_repo_work( "work_reasons": ["assigned_to_me", "review_requested"], "repository": {"full_name": "stackchain/api"}, "updated_at": "2026-08-06T11:00:00Z", - "html_url": "https://forge.example/stackchain/api/pulls/7", + "html_url": "http://127.0.0.1:3000/stackchain/api/pulls/7", }] monkeypatch.setattr(main, "current_user", user) @@ -864,7 +864,7 @@ async def test_pull_review_detail_combines_pr_files_status_and_reviews(monkeypat return { "title": "Review API", "body": "Please check the retry flow.", - "html_url": "https://forge.example/stackchain/api/pulls/7", + "html_url": "http://127.0.0.1:3000/stackchain/api/pulls/7", "user": {"login": "alex"}, "head": {"sha": "abc123"}, } diff --git a/tests/test_global_search.py b/tests/test_global_search.py index 4af5e90..ffb4105 100644 --- a/tests/test_global_search.py +++ b/tests/test_global_search.py @@ -131,7 +131,7 @@ async def test_global_search_specific_type_uses_one_full_width_scoped_stream(): "title": "Review mobile search", "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": "https://forge.example/stackchain/web/pulls/9", + "html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9", }]) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -161,7 +161,7 @@ async def test_global_search_endpoint_returns_bounded_normalized_results(monkeyp "number": 42, "title": "Repair mobile queue", "state": "open", - "url": "https://forge.example/stackchain/api/issues/42", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42", }], "partial": False, } @@ -180,7 +180,7 @@ async def test_global_search_endpoint_returns_bounded_normalized_results(monkeyp "number": 42, "title": "Repair mobile queue", "state": "open", - "url": "https://forge.example/stackchain/api/issues/42", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42", }], "partial": False} @@ -200,7 +200,7 @@ async def test_global_search_preview_returns_normalized_action_context(monkeypat "author": "alex", "labels": ["P1"], "assignees": [], - "url": "https://forge.example/stackchain/api/issues/42", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42", "claimable": True, "assigned_to_me": False, } @@ -233,7 +233,7 @@ async def test_global_search_preview_conversation_returns_a_bounded_authorized_p "author": "alexander", "body": "Current **decision**", "created_at": "2026-08-14T12:00:00Z", - "url": "https://forge.example/stackchain/api/issues/42#issuecomment-7", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42#issuecomment-7", }], "page": 3, "older_page": 2, @@ -278,7 +278,7 @@ async def test_global_search_preview_reply_is_idempotent_for_an_authorized_pull( "author": "timmy", "body": body, "created_at": "2026-08-14T12:30:00Z", - "url": "https://forge.example/stackchain/api/pulls/42#issuecomment-91", + "url": "http://127.0.0.1:3000/stackchain/api/pulls/42#issuecomment-91", } monkeypatch.setattr(main.gitea_proxy, "work_preview", preview) @@ -345,7 +345,7 @@ async def test_global_search_preview_attachment_is_idempotent_for_exact_visible_ async def upload(repository, number, filename, content_type, content): uploads.append((repository, number, filename, content_type, content)) - return {"name": filename, "url": "https://forge.example/evidence/photo.png", "size": len(content)} + return {"name": filename, "url": "http://127.0.0.1:3000/evidence/photo.png", "size": len(content)} monkeypatch.setattr(main.gitea_proxy, "work_preview", preview) monkeypatch.setattr(main.gitea_proxy, "upload_preview_attachment", upload) @@ -367,7 +367,7 @@ async def test_global_search_preview_attachment_is_idempotent_for_exact_visible_ assert first.status_code == replay.status_code == 201 assert replay.json() == first.json() - assert first.json()["markdown"] == "![photo.png]()" + assert first.json()["markdown"] == "![photo.png]()" assert previews == [("stackchain/api", "pull", 42)] assert uploads == [("stackchain/api", 42, "photo.png", "image/png", image)] @@ -408,11 +408,11 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results() return httpx.Response(200, json=[{ "id": 4, "number": 42, "title": "Repair queue", "state": "open", "repository": {"full_name": "stackchain/api"}, - "html_url": "https://forge.example/stackchain/api/issues/42", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/42", }, { "id": 4, "number": 42, "title": "Repair queue", "state": "open", "repository": {"full_name": "stackchain/api"}, - "html_url": "https://forge.example/stackchain/api/issues/42", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/42", }, { "id": 5, "number": 43, "title": "Unsafe", "state": "open", "repository": {"full_name": "stackchain/api"}, @@ -421,7 +421,7 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results() return httpx.Response(200, json=[{ "id": 8, "number": 9, "title": "Improve search", "state": "closed", "repository": {"full_name": "stackchain/web"}, - "html_url": "https://forge.example/stackchain/web/pulls/9", + "html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9", }]) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -440,11 +440,11 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results() "items": [{ "kind": "issue", "repository": "stackchain/api", "number": 42, "title": "Repair queue", "state": "open", - "url": "https://forge.example/stackchain/api/issues/42", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42", }, { "kind": "pull", "repository": "stackchain/web", "number": 9, "title": "Improve search", "state": "closed", - "url": "https://forge.example/stackchain/web/pulls/9", + "url": "http://127.0.0.1:3000/stackchain/web/pulls/9", }], "partial": False, "has_more": False, @@ -454,6 +454,38 @@ async def test_global_search_queries_issues_and_pulls_and_skips_unsafe_results() } +@pytest.mark.anyio +async def test_global_search_keeps_results_on_the_configured_forge_origin(monkeypatch): + monkeypatch.setattr(gitea_proxy, "GITEA_URL", "https://forge.example/git") + + async def handler(request): + if request.url.params["type"] == "pulls": + return httpx.Response(200, json=[]) + return httpx.Response(200, json=[{ + "id": 1, "number": 41, "title": "Foreign", "state": "open", + "repository": {"full_name": "stackchain/api"}, + "html_url": "https://attacker.example/stackchain/api/issues/41", + }, { + "id": 3, "number": 43, "title": "Downgraded", "state": "open", + "repository": {"full_name": "stackchain/api"}, + "html_url": "http://forge.example/git/stackchain/api/issues/43", + }, { + "id": 2, "number": 42, "title": "Relative", "state": "open", + "repository": {"full_name": "stackchain/api"}, + "html_url": "/git/stackchain/api/issues/42#issuecomment-7", + }]) + + gitea_proxy.start_client(transport=httpx.MockTransport(handler)) + try: + result = await gitea_proxy.global_search("queue", 10) + finally: + await gitea_proxy.stop_client() + + assert [(item["number"], item["url"]) for item in result["items"]] == [ + (42, "https://forge.example/git/stackchain/api/issues/42#issuecomment-7") + ] + + @pytest.mark.anyio async def test_global_search_returns_healthy_stream_when_other_stream_fails(): async def handler(request): @@ -464,7 +496,7 @@ async def test_global_search_returns_healthy_stream_when_other_stream_fails(): "title": "Improve search", "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": "https://forge.example/stackchain/web/pulls/9", + "html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9", }]) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -480,7 +512,7 @@ async def test_global_search_returns_healthy_stream_when_other_stream_fails(): "number": 9, "title": "Improve search", "state": "open", - "url": "https://forge.example/stackchain/web/pulls/9", + "url": "http://127.0.0.1:3000/stackchain/web/pulls/9", }], "partial": True, "has_more": True, @@ -509,7 +541,7 @@ async def test_global_search_retries_a_failed_stream_without_advancing_its_page( "title": f'{params["type"]} {number}', "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": f'https://forge.example/stackchain/web/{params["type"]}/{number}', + "html_url": f'http://127.0.0.1:3000/stackchain/web/{params["type"]}/{number}', }]) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -546,7 +578,7 @@ async def test_global_search_odd_limit_keeps_both_streams_contiguous_across_page "title": f'{params["type"]} {number}', "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": f'https://forge.example/stackchain/web/{params["type"]}/{number}', + "html_url": f'http://127.0.0.1:3000/stackchain/web/{params["type"]}/{number}', } for number in range(start, start + stream_limit)]) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -579,7 +611,7 @@ async def test_global_search_caps_combined_results_to_requested_limit(): "title": f"{kind} {number}", "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": f"https://forge.example/stackchain/web/{kind}/{number}", + "html_url": f"http://127.0.0.1:3000/stackchain/web/{kind}/{number}", } for number in (1, 2)] return httpx.Response(200, json=items) @@ -607,7 +639,7 @@ async def test_global_search_pages_each_stream_and_balances_results(): "title": f"{kind} page {page} result {number}", "state": "open", "repository": {"full_name": "stackchain/web"}, - "html_url": f"https://forge.example/stackchain/web/{kind}/{page * 10 + number}", + "html_url": f"http://127.0.0.1:3000/stackchain/web/{kind}/{page * 10 + number}", } for number in (1, 2, 3, 4)] return httpx.Response(200, json=items) @@ -650,7 +682,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i "title": "Repair queue", "body": "Keep mobile operators moving.", "state": "open", - "html_url": "https://forge.example/stackchain/api/issues/42", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/42", "user": {"login": "alex"}, "labels": [{"name": "P1"}, None], "assignees": [], @@ -672,7 +704,7 @@ async def test_work_preview_normalizes_details_and_only_allows_unassigned_open_i "author": "alex", "labels": ["P1"], "assignees": [], - "url": "https://forge.example/stackchain/api/issues/42", + "url": "http://127.0.0.1:3000/stackchain/api/issues/42", "claimable": True, "reopenable": False, "assigned_to_me": False, @@ -690,7 +722,7 @@ async def test_work_preview_derives_pull_kind_and_never_offers_issue_claim(): "number": 9, "title": "Improve search", "state": "open", - "html_url": "https://forge.example/stackchain/web/pulls/9", + "html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9", "pull_request": {"merged": False}, "assignees": [], }) @@ -722,7 +754,7 @@ async def test_work_preview_marks_authoritative_requested_pull_review_actionable "number": 9, "title": "Improve search", "state": "open", - "html_url": "https://forge.example/stackchain/web/pulls/9", + "html_url": "http://127.0.0.1:3000/stackchain/web/pulls/9", "pull_request": {"merged": False}, "assignees": [], }) @@ -746,7 +778,7 @@ async def test_work_preview_offers_reopen_only_for_closed_issues(): "number": 42, "title": "Resume work", "state": "closed", - "html_url": "https://forge.example/stackchain/api/issues/42", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/42", "assignees": [], }) diff --git a/tests/test_issue_api.py b/tests/test_issue_api.py index 2358ac4..dea8e05 100644 --- a/tests/test_issue_api.py +++ b/tests/test_issue_api.py @@ -680,7 +680,7 @@ async def test_gitea_edit_issue_confirms_already_applied_content_after_lost_resp "updated_at": "2026-08-07T10:02:00Z", "assignees": [{"login": "timmy"}], "pull_request": None, - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -699,7 +699,7 @@ async def test_gitea_edit_issue_confirms_already_applied_content_after_lost_resp assert result == { "repository": "stackchain/api", "number": 17, "title": "My draft", "body": "Draft body", "state": "open", "updated_at": "2026-08-07T10:02:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } @@ -720,7 +720,7 @@ async def test_gitea_edit_issue_revalidates_assignment_and_confirms_content(): return httpx.Response(200, json={ "number": 17, "title": "Clarified", "body": "New body", "state": "open", "updated_at": "2026-08-07T10:01:00Z", - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -740,7 +740,7 @@ async def test_gitea_edit_issue_revalidates_assignment_and_confirms_content(): assert result == { "repository": "stackchain/api", "number": 17, "title": "Clarified", "body": "New body", "state": "open", "updated_at": "2026-08-07T10:01:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } @@ -804,7 +804,7 @@ async def test_create_issue_endpoint_derives_self_assignment_and_returns_confirm "labels": ["P0"], "assignees": [assignee], "updated_at": "2026-08-07T03:00:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main.gitea_proxy, "current_user", user) @@ -1014,7 +1014,7 @@ async def test_repository_search_endpoint_returns_minimal_visible_matches(monkey "id": 151, "name": "mobile", "full_name": "stackchain/mobile", - "html_url": "https://forge.example/stackchain/mobile", + "html_url": "http://127.0.0.1:3000/stackchain/mobile", "description": "secret context that the picker does not need", "private": True, }] @@ -1042,7 +1042,7 @@ async def test_repository_page_endpoint_and_issue_creation_support_later_reposit return { "items": [{ "id": 51, "name": "later", "full_name": "stackchain/later", - "description": "", "html_url": "https://forge.example/stackchain/later", + "description": "", "html_url": "http://127.0.0.1:3000/stackchain/later", "updated_at": "2026-08-09T12:00:00Z", }], "page": 2, "total": 51, "has_more": False, @@ -1061,7 +1061,7 @@ async def test_repository_page_endpoint_and_issue_creation_support_later_reposit "id": 403, "number": 403, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-09T12:00:00Z", - "url": "https://forge.example/stackchain/later/issues/403", + "url": "http://127.0.0.1:3000/stackchain/later/issues/403", } monkeypatch.setattr(main.gitea_proxy, "repo_page", page) @@ -1087,7 +1087,7 @@ def test_context_exposes_truthful_repository_pagination(): repositories = gitea_proxy.RepositoryItems( [{ "id": 1, "name": "api", "full_name": "stackchain/api", - "description": "", "html_url": "https://forge.example/stackchain/api", + "description": "", "html_url": "http://127.0.0.1:3000/stackchain/api", "updated_at": "2026-08-09T12:00:00Z", }], {"page": 1, "total": 51, "has_more": True}, @@ -1124,7 +1124,7 @@ async def test_create_issue_atomically_validates_and_sends_release_plan(monkeypa "milestone": {"id": 9, "title": "August RC"}, "due_date": "2026-08-31T23:59:59Z", "updated_at": "2026-08-07T20:00:00Z", - "url": "https://forge.example/stackchain/api/issues/221", + "url": "http://127.0.0.1:3000/stackchain/api/issues/221", } monkeypatch.setattr(main.gitea_proxy, "current_user", user) @@ -1176,7 +1176,7 @@ async def test_create_issue_replays_one_upstream_result_for_concurrent_idempoten "id": 81, "number": 17, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-07T03:00:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main.gitea_proxy, "current_user", user) @@ -1222,7 +1222,7 @@ async def test_completed_issue_creation_replays_after_ledger_reconstruction(monk "id": 201, "number": 201, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-07T15:00:00Z", - "url": "https://forge.example/stackchain/api/issues/201", + "url": "http://127.0.0.1:3000/stackchain/api/issues/201", } database = tmp_path / "issues.sqlite3" @@ -1280,7 +1280,7 @@ async def test_create_issue_rejects_changed_payload_for_an_existing_idempotency_ "id": 81, "number": 17, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-07T03:00:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main.gitea_proxy, "current_user", user) @@ -1324,7 +1324,7 @@ async def test_create_issue_retry_recovers_result_after_the_first_request_times_ "id": 81, "number": 17, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-07T03:00:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main, "ISSUE_ACTION_TIMEOUT_SECONDS", 0.01) @@ -1367,7 +1367,7 @@ async def test_create_issue_durable_ledger_evicts_completed_entry_at_size_limit( "id": 81, "number": 17, "title": title, "state": "open", "repository": repository, "labels": [], "assignees": [assignee], "updated_at": "2026-08-07T03:00:00Z", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } ledger = IdempotencyLedger( @@ -1407,7 +1407,7 @@ async def test_gitea_create_issue_posts_self_assignment_and_normalizes_confirmat return httpx.Response(201, json={ "id": 81, "number": 17, "title": "Capture mobile work", "state": "open", "updated_at": "2026-08-07T03:00:00Z", - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", "assignees": [{"login": "timmy"}], "labels": [{"id": 3, "name": "P0"}], }) @@ -1441,7 +1441,7 @@ async def test_gitea_create_issue_omits_assignment_and_confirms_no_owner(): return httpx.Response(201, json={ "id": 83, "number": 19, "title": "Backlog capture", "state": "open", "updated_at": "2026-08-07T03:00:00Z", - "html_url": "https://forge.example/stackchain/api/issues/19", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/19", "assignees": [], "labels": [], }) @@ -1634,7 +1634,7 @@ async def test_gitea_create_issue_requires_confirmed_self_assignment(): async def handler(_request): return httpx.Response(201, json={ "id": 81, "number": 17, "title": "Capture mobile work", "state": "open", - "html_url": "https://forge.example/stackchain/api/issues/17", "assignees": [], + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", "assignees": [], }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -1657,14 +1657,14 @@ async def test_gitea_claim_available_issue_rechecks_then_confirms_authenticated_ return httpx.Response(200, json={ "id": 81, "number": 17, "title": "Available", "state": "open", "assignees": None, "pull_request": None, "labels": [], - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) if request.method == "GET" and request.url.path == "/api/v1/user": return httpx.Response(200, json={"login": "timmy"}) return httpx.Response(200, json={ "id": 81, "number": 17, "title": "Available", "state": "open", "assignees": [{"login": "timmy"}], "labels": [], - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -1693,14 +1693,14 @@ async def test_gitea_reopen_issue_rechecks_closed_state_and_confirms_self_assign return httpx.Response(200, json={ "id": 81, "number": 17, "title": "Resume work", "state": "closed", "assignees": [], "pull_request": None, "labels": [{"name": "P1"}], - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) if request.method == "GET" and request.url.path == "/api/v1/user": return httpx.Response(200, json={"login": "timmy"}) return httpx.Response(200, json={ "id": 81, "number": 17, "title": "Resume work", "state": "open", "assignees": [{"login": "timmy"}], "labels": [{"name": "P1"}], - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -1719,7 +1719,7 @@ async def test_gitea_reopen_issue_rechecks_closed_state_and_confirms_self_assign "id": 81, "number": 17, "title": "Resume work", "state": "open", "repository": "stackchain/api", "labels": ["P1"], "assignees": ["timmy"], "updated_at": "", - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } @@ -1734,7 +1734,7 @@ async def test_gitea_reopen_issue_retry_accepts_already_open_self_assigned_issue return httpx.Response(200, json={ "id": 81, "number": 17, "title": "Resume work", "state": "open", "assignees": [{"login": "timmy"}], "pull_request": None, "labels": [], - "html_url": "https://forge.example/stackchain/api/issues/17", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/17", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -1757,7 +1757,7 @@ async def test_claim_available_issue_endpoint_returns_confirmed_work_item(monkey return { "id": 81, "number": number, "title": "Available", "state": "open", "repository": repository, "labels": [], "assignees": ["timmy"], - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main.gitea_proxy, "claim_available_issue", claim) @@ -1797,7 +1797,7 @@ async def test_reopen_issue_endpoint_returns_confirmed_resumable_work_item(monke return { "id": 81, "number": number, "title": "Resume work", "state": "open", "repository": repository, "labels": ["P1"], "assignees": ["timmy"], - "url": "https://forge.example/stackchain/api/issues/17", + "url": "http://127.0.0.1:3000/stackchain/api/issues/17", } monkeypatch.setattr(main.gitea_proxy, "reopen_issue", reopen, raising=False) @@ -2564,7 +2564,7 @@ async def test_gitea_issue_comment_posts_body_and_returns_safe_identity(): "id": 82, "body": "Ready to ship", "created_at": "2026-08-07T10:10:00Z", - "html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-82", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-82", "user": {"login": "timmy"}, }, ) @@ -2584,7 +2584,7 @@ async def test_gitea_issue_comment_posts_body_and_returns_safe_identity(): "author": "timmy", "body": "Ready to ship", "created_at": "2026-08-07T10:10:00Z", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-82", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-82", } @@ -2602,7 +2602,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "number": 3, "title": "Restore signing service", "state": "open", - "html_url": "https://forge.example/stackchain/platform/issues/3", + "html_url": "http://127.0.0.1:3000/stackchain/platform/issues/3", "repository": {"full_name": "stackchain/platform"}, }, { @@ -2621,7 +2621,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "id": 81, "body": "Latest update", "created_at": "2026-08-07T10:00:00Z", - "html_url": "https://forge.example/stackchain/api/issues/7#issuecomment-81", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81", "user": {"login": "sam"}, } ], @@ -2635,7 +2635,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "body": "Full issue context", "updated_at": "2026-08-07T09:59:00Z", "milestone": {"id": 9, "title": "August RC", "description": "not exposed"}, - "html_url": "https://forge.example/stackchain/api/issues/7", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/7", "labels": [{"name": "P1"}], "assignees": [{"login": "timmy"}], }, @@ -2661,7 +2661,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "updated_at": "2026-08-07T09:59:00Z", "due_date": None, "milestone": {"id": 9, "title": "August RC"}, - "url": "https://forge.example/stackchain/api/issues/7", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7", "labels": ["P1"], "assignees": ["timmy"], "dependencies_available": True, @@ -2671,7 +2671,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "number": 3, "title": "Restore signing service", "state": "open", - "url": "https://forge.example/stackchain/platform/issues/3", + "url": "http://127.0.0.1:3000/stackchain/platform/issues/3", } ], "comments": [ @@ -2680,7 +2680,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "author": "sam", "body": "Latest update", "created_at": "2026-08-07T10:00:00Z", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-81", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81", } ], "conversation": { @@ -2690,7 +2690,7 @@ async def test_gitea_issue_detail_returns_normalized_context_and_recent_comments "author": "sam", "body": "Latest update", "created_at": "2026-08-07T10:00:00Z", - "url": "https://forge.example/stackchain/api/issues/7#issuecomment-81", + "url": "http://127.0.0.1:3000/stackchain/api/issues/7#issuecomment-81", } ], "page": 1, @@ -2984,7 +2984,7 @@ async def test_gitea_add_dependency_validates_assignment_candidate_and_confirmat return httpx.Response(200, json=[{ "number": 9, "state": "open", "title": "Restore API", "repository": {"full_name": "stackchain/api"}, - "html_url": "https://forge.example/stackchain/api/issues/9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/9", }] if mutated else []) if request.method == "POST" and path.endswith("/dependencies"): return httpx.Response(201, json={}) @@ -3003,7 +3003,7 @@ async def test_gitea_add_dependency_validates_assignment_candidate_and_confirmat assert json.loads(mutation.content) == {"owner": "stackchain", "repo": "api", "index": 9} assert result["dependencies"] == [{ "repository": "stackchain/api", "number": 9, "title": "Restore API", - "state": "open", "url": "https://forge.example/stackchain/api/issues/9", + "state": "open", "url": "http://127.0.0.1:3000/stackchain/api/issues/9", }] @@ -3013,7 +3013,7 @@ async def test_gitea_dependency_retry_returns_canonical_state_without_duplicate_ dependency = { "number": 9, "state": "open", "title": "Restore API", "repository": {"full_name": "stackchain/api"}, - "html_url": "https://forge.example/stackchain/api/issues/9", + "html_url": "http://127.0.0.1:3000/stackchain/api/issues/9", } async def handler(request): diff --git a/tests/test_issue_attachments.py b/tests/test_issue_attachments.py index 7fa2242..5eb1588 100644 --- a/tests/test_issue_attachments.py +++ b/tests/test_issue_attachments.py @@ -356,7 +356,7 @@ async def test_gitea_attachment_revalidates_assignment_and_sends_multipart(): "id": 9, "name": "checkout.png", "size": len(PNG_BYTES), - "browser_download_url": "https://forge.example/attachments/checkout.png", + "browser_download_url": "http://127.0.0.1:3000/attachments/checkout.png", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -379,7 +379,7 @@ async def test_gitea_attachment_revalidates_assignment_and_sends_multipart(): assert PNG_BYTES in upload.content assert result == { "name": "checkout.png", - "url": "https://forge.example/attachments/checkout.png", + "url": "http://127.0.0.1:3000/attachments/checkout.png", "size": len(PNG_BYTES), } @@ -514,7 +514,7 @@ async def test_gitea_pull_attachment_revalidates_assignment_before_exact_pull_up }) return httpx.Response(201, json={ "name": "pull.png", "size": len(PNG_BYTES), - "browser_download_url": "https://forge.example/attachments/pull.png", + "browser_download_url": "http://127.0.0.1:3000/attachments/pull.png", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -531,4 +531,4 @@ async def test_gitea_pull_attachment_revalidates_assignment_before_exact_pull_up ("POST", "/api/v1/repos/stackchain/web/issues/31/assets"), ] assert PNG_BYTES in requests[-1].content - assert result["url"] == "https://forge.example/attachments/pull.png" + assert result["url"] == "http://127.0.0.1:3000/attachments/pull.png" diff --git a/tests/test_review_api.py b/tests/test_review_api.py index cccfc78..5caa03b 100644 --- a/tests/test_review_api.py +++ b/tests/test_review_api.py @@ -237,7 +237,7 @@ async def test_gitea_review_submission_checks_head_then_maps_decision_upstream() json={ "id": 91, "state": "APPROVED", - "html_url": "https://forge.example/reviews/91", + "html_url": "http://127.0.0.1:3000/reviews/91", }, ) @@ -257,7 +257,7 @@ async def test_gitea_review_submission_checks_head_then_maps_decision_upstream() assert result == { "id": 91, "state": "APPROVED", - "url": "https://forge.example/reviews/91", + "url": "http://127.0.0.1:3000/reviews/91", } diff --git a/tests/test_update_reply_attachments.py b/tests/test_update_reply_attachments.py index 9b72f66..f199c79 100644 --- a/tests/test_update_reply_attachments.py +++ b/tests/test_update_reply_attachments.py @@ -40,7 +40,7 @@ async def test_notification_attachment_endpoint_resolves_exact_pull_server_side( async def upload(thread_id, filename, content_type, content): calls.append((thread_id, filename, content_type, content)) - return {"name": filename, "url": "https://forge.example/a/proof.webp", "size": len(content)} + return {"name": filename, "url": "http://127.0.0.1:3000/a/proof.webp", "size": len(content)} monkeypatch.setattr(main.gitea_proxy, "upload_notification_attachment", upload, raising=False) transport = httpx.ASGITransport(app=main.app) @@ -52,7 +52,7 @@ async def test_notification_attachment_endpoint_resolves_exact_pull_server_side( ) assert response.status_code == 201 - assert response.json()["markdown"] == "![proof.png]()" + assert response.json()["markdown"] == "![proof.png]()" assert calls == [(527, "proof.png", "image/png", PNG)] assert main.request_body_limit("POST", "/api/v1/notifications/527/attachments") == 2 * 1024 * 1024 + 64 * 1024 @@ -73,7 +73,7 @@ async def test_proxy_notification_upload_trusts_only_matching_gitea_subject_path }) return httpx.Response(201, json={ "name": "proof.png", "size": len(PNG), - "browser_download_url": "https://forge.example/a/proof.png", + "browser_download_url": "http://127.0.0.1:3000/a/proof.png", }) gitea_proxy.start_client(transport=httpx.MockTransport(handler)) @@ -86,7 +86,7 @@ async def test_proxy_notification_upload_trusts_only_matching_gitea_subject_path ("GET", "/api/v1/notifications/threads/527"), ("POST", "/api/v1/repos/stackchain/web/issues/31/assets"), ] - assert result["url"] == "https://forge.example/a/proof.png" + assert result["url"] == "http://127.0.0.1:3000/a/proof.png" @pytest.mark.anyio @@ -157,7 +157,7 @@ const store={{claimNext:async()=>item?{{...item}}:null,update:async(_id,fn)=>{{i complete:async()=>{{item=null;}},release:async()=>{{item={{...item,status:'queued'}};}},fail:async()=>{{}},countBlocked:async()=>0}}; const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity')return{{login:'timmy'}}; calls.push({{url,key:options.headers?.['Idempotency-Key'],body:options.body instanceof FormData?'multipart':options.body?JSON.parse(options.body):null}}); - if(url.endsWith('/attachments'))return{{markdown:'![phone.webp](https://forge.example/phone.webp)'}}; + if(url.endsWith('/attachments'))return{{markdown:'![phone.webp](http://127.0.0.1:3000/phone.webp)'}}; if(url.endsWith('/reply') && replyAttempts++===0){{const error=new Error('offline');error.status=503;throw error;}} return url.endsWith('/reply')?{{id:8}}:{{status:'read'}}; }}; @@ -178,5 +178,5 @@ const fetchJson=async(url,options={{}})=>{{if(url==='api/v1/background-identity' assert [call["key"] for call in output["calls"][:3]] == [ "reply-image:attachment", "reply-image:reply", "reply-image:reply" ] - assert output["calls"][2]["body"] == {"body": "![phone.webp](https://forge.example/phone.webp)"} + assert output["calls"][2]["body"] == {"body": "![phone.webp](http://127.0.0.1:3000/phone.webp)"} assert output["result"]["confirmed"] == [{"status": "read"}] -- 2.43.0