Merge pull request 'Keep dashboard Gitea links on the configured forge origin' (#1073) from timmy/1072-configured-forge-link-boundary into main
All checks were successful
CI / lint (push) Successful in 2m41s
CI / build-release (push) Successful in 6s
CI / browser-journey (push) Successful in 2m9s
CI / release-candidate (push) Successful in 7s

This commit is contained in:
rockachopa 2026-08-18 09:04:35 +00:00
commit fb7bfe0a5d
9 changed files with 153 additions and 123 deletions

View File

@ -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

View File

@ -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")),
}

View File

@ -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",
}

View File

@ -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"},
}

View File

@ -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](<https://forge.example/evidence/photo.png>)"
assert first.json()["markdown"] == "![photo.png](<http://127.0.0.1:3000/evidence/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": [],
})

View File

@ -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):

View File

@ -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"

View File

@ -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",
}

View File

@ -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](<https://forge.example/a/proof.webp>)"
assert response.json()["markdown"] == "![proof.png](<http://127.0.0.1:3000/a/proof.webp>)"
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"}]