Files
ITWorx_MCP_Hub-Public/connectors/gitea/fake.py
T

678 lines
30 KiB
Python

"""Deterministic in-memory Gitea HTTP transport for contract tests."""
from __future__ import annotations
import base64
import json
from typing import Any
from urllib.parse import parse_qs, unquote, urlparse
import httpx
from .managed_validation import (
MANAGED_VALIDATION_CONTEXT,
MANAGED_VALIDATION_PATH,
MANAGED_VALIDATION_WORKFLOW,
)
class FakeGiteaServer:
"""A tiny bounded Gitea API fixture with observable requests and no network."""
def __init__(
self,
*,
unsupported_search: bool = False,
deny_user_repositories: bool = False,
deny_protected_workflow_writes: bool = False,
redirect_to: str | None = None,
gitea_version: str = "1.26.4",
deny_merge_transition: bool = False,
) -> None:
self.unsupported_search = unsupported_search
self.deny_user_repositories = deny_user_repositories
self.deny_protected_workflow_writes = deny_protected_workflow_writes
self.redirect_to = redirect_to
self.gitea_version = gitea_version
self.deny_merge_transition = deny_merge_transition
self.recorded_methods: list[str] = []
self.recorded_authorizations: list[str] = []
self.recorded_paths: list[str] = []
self.recorded_bodies: list[dict[str, Any]] = []
self._repos = {
"acme/hub": {
"owner": {"login": "acme"},
"name": "hub",
"full_name": "acme/hub",
"description": "ITWorx MCP Hub",
"private": True,
"default_branch": "main",
"updated_at": "2026-08-01T10:00:00Z",
"token": "fixture-token-must-not-leak",
}
}
self._branches: list[dict[str, Any]] = [
{"name": "main", "commit": {"id": "a" * 40}, "protected": True},
{"name": "release", "commit": {"id": "b" * 40}, "protected": False},
{"name": "feature", "commit": {"id": "c" * 40}, "protected": False},
]
self._branch_protections: dict[str, list[dict[str, Any]]] = {
"acme/hub": [
{
"rule_name": "main",
"branch_name": "main",
"priority": 1,
"enable_push": False,
"enable_push_whitelist": False,
"push_whitelist_usernames": [],
"push_whitelist_teams": [],
"push_whitelist_deploy_keys": False,
"enable_force_push": False,
"enable_force_push_allowlist": False,
"force_push_allowlist_usernames": [],
"force_push_allowlist_teams": [],
"force_push_allowlist_deploy_keys": False,
"enable_merge_whitelist": False,
"merge_whitelist_usernames": [],
"merge_whitelist_teams": [],
"enable_bypass_allowlist": False,
"bypass_allowlist_usernames": [],
"bypass_allowlist_teams": [],
"enable_status_check": True,
"status_check_contexts": [MANAGED_VALIDATION_CONTEXT],
"required_approvals": 0,
"enable_approvals_whitelist": False,
"approvals_whitelist_username": [],
"approvals_whitelist_teams": [],
"block_on_rejected_reviews": True,
"block_on_official_review_requests": False,
"block_on_outdated_branch": True,
"dismiss_stale_approvals": True,
"ignore_stale_approvals": False,
"require_signed_commits": False,
"protected_file_patterns": "",
"unprotected_file_patterns": "",
"block_admin_merge_override": True,
}
]
}
self._managed_workflows: dict[str, str] = {"acme/hub": MANAGED_VALIDATION_WORKFLOW}
self._commits = [
{
"sha": "a" * 40,
"commit": {"message": "Add bounded connector", "author": {"name": "Ada"}},
},
{"sha": "b" * 40, "commit": {"message": "Document the API", "author": {"name": "Lin"}}},
{"sha": "c" * 40, "commit": {"message": "Add tests", "author": {"name": "Kai"}}},
]
self._compare_commits = [self._commits[0]]
self._reverse_compare_commits: list[dict[str, Any]] = []
self._releases = [
{"tag_name": "v1.0.0", "name": "First release", "draft": False, "prerelease": False}
]
self._issues = [
{
"number": 1,
"title": "Bounded read API",
"state": "open",
"html_url": "https://gitea.example/acme/hub/issues/1",
}
]
self._pulls: list[dict[str, Any]] = [
{
"number": 7,
"title": "Add governed Codex workflows",
"state": "open",
"draft": False,
"mergeable": True,
"html_url": "https://gitea.example/acme/hub/pulls/7",
"head": {"ref": "codex/workflows", "sha": "e" * 40},
"base": {"ref": "main", "sha": "a" * 40},
"updated_at": "2026-08-21T20:00:00Z",
}
]
self._pull_files = [
{
"filename": "services/runtime/mcp_server.py",
"status": "modified",
"additions": 42,
"deletions": 2,
}
]
self._statuses = [
{
"status": "success",
"context": MANAGED_VALIDATION_CONTEXT,
"description": "All checks passed",
"target_url": "https://gitea.example/acme/hub/actions/runs/42",
"created_at": "2026-08-21T20:05:00Z",
}
]
self._file = "# ITWorx MCP Hub\n\nThis is a safe fixture.\n"
self._reviews: list[dict[str, Any]] = []
self._review_comments = [
{"id": 12, "path": "README.md", "new_position": 1, "body": "Clarify this."}
]
self._action_run = {
"id": 42,
"name": "ChatGPT validation",
"status": "completed",
"conclusion": "success",
"event": "pull_request",
"head_branch": "codex/workflows",
"head_sha": "e" * 40,
"workflow_id": "managed-validation.yml",
"html_url": "https://gitea.example/acme/hub/actions/runs/42",
}
self._action_job = {
"id": 43,
"name": "full",
"status": "completed",
"conclusion": "success",
}
self._action_job_logs = "all checks passed\n"
def transport(self) -> httpx.MockTransport:
return httpx.MockTransport(self._handle)
def _handle(self, request: httpx.Request) -> httpx.Response:
self.recorded_methods.append(request.method)
self.recorded_authorizations.append(request.headers.get("authorization", ""))
if self.redirect_to is not None:
return httpx.Response(302, headers={"location": self.redirect_to}, request=request)
parsed = urlparse(str(request.url))
self.recorded_paths.append(parsed.path)
if not parsed.path.startswith("/api/v1/"):
return self._json(404, {"message": "API prefix missing"}, request)
path = parsed.path.removeprefix("/api/v1/")
query = parse_qs(parsed.query)
body: dict[str, Any] = {}
if request.content:
decoded = json.loads(request.content.decode("utf-8"))
body = decoded if isinstance(decoded, dict) else {}
self.recorded_bodies.append(body)
if path == "version" and request.method == "GET":
return self._json(200, {"version": self.gitea_version}, request)
if path == "repos/search":
if self.unsupported_search:
return self._json(501, {"message": "unsupported"}, request)
if "q" not in query:
repositories = list(self._repos.values())
if query.get("private") == ["true"]:
repositories = [item for item in repositories if item.get("private") is True]
return self._json(200, {"ok": True, "data": repositories}, request)
return self._json(
200,
{
"ok": True,
"data": [{"repository": {"full_name": "acme/hub"}, "path": "README.md"}],
},
request,
)
if path == "user" and request.method == "GET":
return self._json(200, {"login": "acme"}, request)
if path == "user/repos" and request.method == "GET":
if self.deny_user_repositories:
return self._json(403, {"message": "denied"}, request)
return self._paged(list(self._repos.values()), query, request)
if path == "user/repos" and request.method == "POST":
name = str(body.get("name", ""))
qualified_name = f"acme/{name}"
if qualified_name in self._repos:
return self._json(409, {"message": "repository already exists"}, request)
auto_init = bool(body.get("auto_init", True))
repository = {
"owner": {"login": "acme"},
"name": name,
"full_name": qualified_name,
"description": str(body.get("description", "")),
"private": bool(body.get("private", True)),
"empty": not auto_init,
"default_branch": str(body.get("default_branch", "main")) if auto_init else "",
"html_url": f"https://gitea.example/{qualified_name}",
"updated_at": "2026-08-26T15:00:00Z",
}
self._repos[qualified_name] = repository
return self._json(201, repository, request)
if path == "repos/issues/search" and request.method == "GET":
state = str(query.get("state", ["open"])[0])
issues = [
{
**item,
"repository": {
"full_name": "acme/hub",
"name": "hub",
"owner": {"login": "acme"},
},
"updated_at": "2026-08-01T10:00:00Z",
"labels": [],
}
for item in self._issues
if state == "all" or item.get("state") == state
]
return self._paged(issues, query, request)
if path.startswith("repos/"):
path = path.removeprefix("repos/")
parts = path.split("/")
qualified_name = "/".join(parts[:2])
if len(parts) >= 2 and qualified_name not in self._repos:
return self._json(404, {"message": "not found"}, request)
if len(parts) == 2:
if request.method == "DELETE":
self._repos.pop(qualified_name, None)
self._branch_protections.pop(qualified_name, None)
self._managed_workflows.pop(qualified_name, None)
return httpx.Response(204, request=request)
if request.method != "GET":
return self._json(405, {"message": "method not allowed"}, request)
return self._json(200, self._repos[qualified_name], request, leaked_header=True)
if parts[2] == "branch_protections":
protections = self._branch_protections.setdefault(qualified_name, [])
if len(parts) == 3:
if request.method == "POST":
created = dict(body)
created.setdefault("rule_name", str(body.get("branch_name", "")))
created.setdefault("branch_name", str(body.get("rule_name", "")))
protections.append(created)
branch_name = str(created.get("rule_name") or created.get("branch_name"))
for branch in self._branches:
if branch.get("name") == branch_name:
branch["protected"] = True
return self._json(201, created, request)
return self._json(200, protections, request)
rule_name = unquote(parts[3])
protection = next(
(
item
for item in protections
if item.get("rule_name") == rule_name or item.get("branch_name") == rule_name
),
None,
)
if protection is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PATCH":
protection.update(body)
return self._json(200, protection, request)
if request.method == "DELETE":
protections.remove(protection)
return httpx.Response(204, request=request)
return self._json(200, protection, request)
if parts[2] == "branches":
if request.method == "POST":
created = {
"name": body.get("new_branch_name", ""),
"commit": {"id": "f" * 40},
"protected": False,
}
self._branches.append(created)
return self._json(201, created, request)
if len(parts) == 4:
branch_name = unquote(parts[3])
matched_branch: dict[str, object] | None = None
for item in self._branches:
if item.get("name") == branch_name:
matched_branch = item
break
if matched_branch is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "DELETE":
self._branches.remove(matched_branch)
return httpx.Response(204, request=request)
return self._json(200, matched_branch, request)
return self._paged(self._branches, query, request)
if parts[2] == "commits" and len(parts) == 3:
return self._paged(self._commits, query, request)
if parts[2:4] == ["git", "commits"] and len(parts) == 5:
return self._json(
200,
{
**self._commits[0],
"sha": unquote(parts[4]),
"verification": {"verified": True, "reason": "gpg"},
"stats": {"additions": 42, "deletions": 2, "total": 44},
"files": [{"filename": "README.md", "status": "modified"}],
"html_url": "https://gitea.example/acme/hub/commit/example",
},
request,
)
if parts[2] == "compare" and len(parts) == 4:
compare_range = unquote(parts[3])
reverse_ranges: set[str] = set()
for pull in self._pulls:
head = pull.get("head")
base = pull.get("base")
if isinstance(head, dict) and isinstance(base, dict):
reverse_ranges.add(f"{head.get('sha', '')}...{base.get('sha', '')}")
compare_commits = (
self._reverse_compare_commits
if compare_range in reverse_ranges
else self._compare_commits
)
return self._json(
200,
{
"total_commits": len(compare_commits),
"commits": compare_commits,
"files": [{"filename": "README.md", "status": "modified"}],
},
request,
)
if parts[2] == "releases":
return self._paged(self._releases, query, request)
if parts[2] == "issues":
if request.method == "POST" and len(parts) == 3:
created_issue = {
"number": len(self._issues) + 1,
"title": body.get("title", ""),
"state": "open",
"html_url": f"https://gitea.example/acme/hub/issues/{len(self._issues) + 1}",
}
self._issues.append(created_issue)
return self._json(201, created_issue, request)
if len(parts) == 4:
issue_number = int(parts[3])
issue = next(
(item for item in self._issues if item.get("number") == issue_number), None
)
if issue is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PATCH":
issue.update(
{
key: value
for key, value in body.items()
if key in {"title", "body", "state"}
}
)
return self._json(200, issue, request)
if request.method == "POST" and len(parts) == 5 and parts[4] == "comments":
return self._json(
201,
{
"id": 99,
"body": body.get("body", ""),
"html_url": f"https://gitea.example/acme/hub/issues/{parts[3]}#issuecomment-99",
},
request,
)
return self._paged(self._issues, query, request)
if parts[2] == "pulls" and len(parts) == 3:
if request.method == "POST":
created_pull = {
"number": len(self._pulls) + 8,
"title": body.get("title", ""),
"state": "open",
"draft": False,
"mergeable": True,
"html_url": "https://gitea.example/acme/hub/pulls/8",
"head": {"ref": body.get("head", ""), "sha": "f" * 40},
"base": {"ref": body.get("base", ""), "sha": "a" * 40},
"updated_at": "2026-08-24T20:00:00Z",
}
self._pulls.append(created_pull)
return self._json(201, created_pull, request)
return self._paged(self._pulls, query, request)
if parts[2] == "pulls" and len(parts) == 4 and parts[3] == "7":
if request.method == "PATCH":
self._pulls[0].update(
{key: value for key, value in body.items() if key in {"title", "body", "state"}}
)
if body.get("state") == "open":
self._action_run.update(
event="pull_request",
status="queued",
conclusion="",
head_sha=self._pulls[0]["head"]["sha"],
)
return self._json(200, self._pulls[0], request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "update"]:
if query.get("style") not in [["merge"], ["rebase"]]:
return self._json(422, {"message": "invalid style"}, request)
head = self._pulls[0].get("head")
base = self._pulls[0].get("base")
if not isinstance(head, dict) or not isinstance(base, dict):
return self._json(500, {"message": "invalid fake pull request"}, request)
head["sha"] = "d" * 40
base["sha"] = "a" * 40
return self._json(200, {}, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "merge"]:
if self.deny_merge_transition:
return self._json(405, {"message": "merge blocked by repository policy"}, request)
strategy_key = "Do" if self.gitea_version.startswith(("1.14.", "1.25.")) else "do"
if body.get(strategy_key) not in {
"merge",
"rebase",
"rebase-merge",
"squash",
"fast-forward-only",
}:
return self._json(405, {"message": "unsupported merge strategy"}, request)
self._pulls[0].update({"state": "closed", "merged": True, "merge_commit_sha": "6" * 40})
for branch in self._branches:
if branch.get("name") == "main":
branch["commit"]["id"] = "6" * 40
return self._json(200, {}, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "files"]:
return self._paged(self._pull_files, query, request)
if parts[2] == "pulls" and len(parts) == 5 and parts[3:] == ["7", "reviews"]:
return self._paged(self._reviews, query, request)
if (
parts[2] == "pulls"
and len(parts) == 7
and parts[3:5] == ["7", "reviews"]
and parts[6] == "comments"
):
return self._json(200, self._review_comments, request)
if parts[2] == "commits" and len(parts) == 5 and parts[4] == "statuses":
return self._paged(self._statuses, query, request)
if parts[2:4] == ["git", "trees"] and len(parts) == 5:
if self.unsupported_search:
return self._json(501, {"message": "unsupported"}, request)
return self._json(
200,
{
"sha": "a" * 40,
"tree": [
{
"path": "README.md",
"type": "blob",
"sha": "d" * 40,
"size": len(self._file.encode()),
}
],
"truncated": False,
},
request,
)
if parts[2:4] == ["git", "blobs"] and len(parts) == 5:
return self._json(
200,
{
"sha": parts[4],
"encoding": "base64",
"content": base64.b64encode(self._file.encode()).decode(),
"size": len(self._file.encode()),
},
request,
)
if parts[2:4] == ["actions", "workflows"] and len(parts) == 4:
workflows = []
if qualified_name in self._managed_workflows:
workflows.append(
{
"id": "managed-validation.yml",
"name": "Managed validation",
"path": MANAGED_VALIDATION_PATH,
}
)
return self._json(
200,
{"workflows": workflows},
request,
)
if parts[2:4] == ["actions", "workflows"] and len(parts) == 6 and parts[5] == "dispatches":
raw_inputs = body.get("inputs")
inputs: dict[str, Any] = raw_inputs if isinstance(raw_inputs, dict) else {}
profile = str(inputs.get("profile", "full"))
ref = str(body.get("ref", ""))
dispatch_branch: dict[str, Any] | None = next(
(item for item in self._branches if item.get("name") == ref), None
)
head_sha = str(
(dispatch_branch or {}).get("commit", {}).get("id", self._action_run["head_sha"])
)
self._action_run.update(
{
"event": "workflow_dispatch",
"head_branch": ref,
"head_sha": head_sha,
"workflow_id": unquote(parts[4]),
"display_title": f"Managed validation / {profile}",
}
)
self._action_job["name"] = "full"
return self._json(200, self._action_run, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 4:
return self._json(200, {"workflow_runs": [self._action_run]}, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 6 and parts[5] == "rerun":
self._action_run.update(status="queued", conclusion="")
return httpx.Response(204, request=request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 5:
return self._json(200, self._action_run, request)
if parts[2:4] == ["actions", "runs"] and len(parts) == 6 and parts[5] == "jobs":
return self._json(200, {"jobs": [self._action_job]}, request)
if parts[2:4] == ["actions", "jobs"] and len(parts) == 6 and parts[5] == "logs":
return httpx.Response(
200,
headers={"content-type": "text/plain"},
content=self._action_job_logs.encode(),
request=request,
)
if parts[2] == "contents" and len(parts) == 3:
return self._json(
200,
[
{
"name": "README.md",
"path": "README.md",
"type": "file",
"size": len(self._file.encode()),
"sha": "d" * 40,
}
],
request,
)
if (
parts[2] == "contents"
and len(parts) >= 4
and "/".join(parts[3:]) == MANAGED_VALIDATION_PATH
):
existing = self._managed_workflows.get(qualified_name)
protection = next(iter(self._branch_protections.get(qualified_name, [])), None)
temporary_push_allowed = bool(
protection
and protection.get("enable_push") is True
and protection.get("enable_push_whitelist") is True
and "acme" in protection.get("push_whitelist_usernames", [])
)
if (
request.method in {"POST", "PUT"}
and self.deny_protected_workflow_writes
and protection is not None
and not temporary_push_allowed
):
return self._json(403, {"message": "protected branch"}, request)
if request.method == "POST":
if existing is not None:
return self._json(422, {"message": "already exists"}, request)
self._managed_workflows[qualified_name] = base64.b64decode(
str(body.get("content", ""))
).decode()
return self._json(
201,
{"content": {"path": MANAGED_VALIDATION_PATH, "sha": "9" * 40}},
request,
)
if existing is None:
return self._json(404, {"message": "not found"}, request)
if request.method == "PUT":
self._managed_workflows[qualified_name] = base64.b64decode(
str(body.get("content", ""))
).decode()
return self._json(
200,
{"content": {"path": MANAGED_VALIDATION_PATH, "sha": "9" * 40}},
request,
)
if request.method == "DELETE":
self._managed_workflows.pop(qualified_name, None)
return self._json(200, {"commit": {"sha": "7" * 40}}, request)
return self._json(
200,
{
"name": "managed-validation.yml",
"path": MANAGED_VALIDATION_PATH,
"sha": "d" * 40,
"content": base64.b64encode(existing.encode()).decode(),
"encoding": "base64",
},
request,
)
if parts[2] == "contents" and len(parts) >= 4 and "/".join(parts[3:]) == "README.md":
if request.method == "DELETE":
return self._json(200, {"commit": {"sha": "7" * 40}}, request)
if request.method in {"POST", "PUT"}:
if request.method == "POST" and self._repos[qualified_name].get("empty"):
branch_name = str(body.get("branch", "main"))
self._repos[qualified_name]["empty"] = False
self._repos[qualified_name]["default_branch"] = branch_name
if not any(item.get("name") == branch_name for item in self._branches):
self._branches.append(
{
"name": branch_name,
"commit": {"id": "8" * 40},
"protected": False,
}
)
return self._json(
201 if request.method == "POST" else 200,
{
"content": {"path": "README.md", "sha": "9" * 40},
"commit": {"sha": "8" * 40},
},
request,
)
return self._json(
200,
{
"name": "README.md",
"path": "README.md",
"sha": "d" * 40,
"content": base64.b64encode(self._file.encode()).decode(),
"encoding": "base64",
"token": "fixture-token-must-not-leak",
},
request,
)
return self._json(404, {"message": "not found"}, request)
def _paged(
self, items: list[dict[str, Any]], query: dict[str, list[str]], request: httpx.Request
) -> httpx.Response:
page = max(int(query.get("page", ["1"])[0]), 1)
limit = max(int(query.get("limit", ["100"])[0]), 1)
start = (page - 1) * limit
return self._json(200, items[start : start + limit], request)
@staticmethod
def _json(
status: int, payload: object, request: httpx.Request, *, leaked_header: bool = False
) -> httpx.Response:
headers = {"content-type": "application/json"}
if leaked_header:
headers["x-fixture-token"] = "fixture-token-must-not-leak"
return httpx.Response(
status, headers=headers, content=json.dumps(payload).encode(), request=request
)