Files
MeshChatX/tests/backend/http_api_contract_helpers.py

71 lines
2.2 KiB
Python

"""Helpers for HTTP API route contract checks (meshchat.py vs frontend)."""
from __future__ import annotations
import json
import re
from pathlib import Path
_ROUTE_DECORATOR = re.compile(
r'@routes\.(get|post|patch|delete|put)\(\s*(?:\n\s*)?["\']([^"\']+)["\']',
re.MULTILINE,
)
def extract_meshchat_http_routes(meshchat_py: Path) -> list[dict[str, str]]:
text = meshchat_py.read_text(encoding="utf-8")
rows: list[dict[str, str]] = []
for m in _ROUTE_DECORATOR.finditer(text):
rows.append({"method": m.group(1).upper(), "path": m.group(2)})
rows.sort(key=lambda x: (x["path"], x["method"]))
return rows
def path_matches_aiohttp_route(route: str, path: str) -> bool:
pattern = ""
i = 0
while i < len(route):
if route[i] == "{":
j = route.find("}", i)
if j == -1:
return False
pattern += "[^/]+"
i = j + 1
else:
pattern += re.escape(route[i])
i += 1
return re.fullmatch(pattern, path) is not None
def extract_frontend_api_paths(frontend_root: Path) -> set[str]:
out: set[str] = set()
for path in list(frontend_root.rglob("*.vue")) + list(frontend_root.rglob("*.js")):
if "node_modules" in path.parts:
continue
text = path.read_text(encoding="utf-8")
for m in re.finditer(r"`(/api/v1[^`]+)`", text):
s = m.group(1).split("?")[0]
s = re.sub(r"\$\{[^}]+\}", "a", s)
out.add(s)
for m in re.finditer(r'["\'](/api/v1[^"\']+)["\']', text):
s = m.group(1).split("?")[0]
if "${" in s:
continue
out.add(s)
return out
def load_route_fixture(fixture_path: Path) -> list[dict[str, str]]:
data = json.loads(fixture_path.read_text(encoding="utf-8"))
routes = data["routes"]
routes.sort(key=lambda x: (x["path"], x["method"]))
return routes
def write_route_fixture(fixture_path: Path, routes: list[dict[str, str]]) -> None:
fixture_path.parent.mkdir(parents=True, exist_ok=True)
fixture_path.write_text(
json.dumps({"routes": routes}, indent=2) + "\n",
encoding="utf-8",
)