from __future__ import annotations from dataclasses import dataclass from typing import Any import httpx @dataclass(frozen=True) class ReleaseTestConfig: base_url: str admin_username: str admin_password: str timeout_seconds: float tenant_a_code: str tenant_a_name: str tenant_b_code: str tenant_b_name: str module_name: str module_path: str class ReleaseApiClient: def __init__(self, config: ReleaseTestConfig, token: str | None = None) -> None: self.config = config self.token = token self._client = httpx.Client(base_url=config.base_url, timeout=config.timeout_seconds) def close(self) -> None: self._client.close() def with_token(self, token: str) -> "ReleaseApiClient": return ReleaseApiClient(self.config, token=token) def request(self, method: str, path: str, *, expected_status: int | None = 200, **kwargs: Any) -> httpx.Response: headers = dict(kwargs.pop("headers", {}) or {}) if self.token: headers["Authorization"] = f"Bearer {self.token}" response = self._client.request(method, path, headers=headers, **kwargs) if expected_status is not None: assert response.status_code == expected_status, self._format_error(response) return response def get(self, path: str, **kwargs: Any) -> httpx.Response: return self.request("GET", path, **kwargs) def post(self, path: str, **kwargs: Any) -> httpx.Response: return self.request("POST", path, **kwargs) def put(self, path: str, **kwargs: Any) -> httpx.Response: return self.request("PUT", path, **kwargs) def patch(self, path: str, **kwargs: Any) -> httpx.Response: return self.request("PATCH", path, **kwargs) def delete(self, path: str, **kwargs: Any) -> httpx.Response: return self.request("DELETE", path, **kwargs) def login_password(self, username: str, password: str) -> dict[str, Any]: response = self.post( "/api/auth/login", json={"username": username, "password": password}, expected_status=200, ) payload = response.json() assert payload.get("success") is True, payload data = payload.get("data") or {} assert data.get("access_token"), payload return data def login_oauth( self, *, sub: str, username: str, nickname: str, ou_id: str, ou_name: str, area: str | None = None, ) -> dict[str, Any]: response = self.post( "/api/auth/login", json={ "userInfo": { "sub": sub, "username": username, "nickname": nickname, "email": f"{username}@pytest.local", "phone_number": "13800000000", "ou_id": ou_id, "ou_name": ou_name, "is_leader": False, }, "area": area, "expiresIn": 3600, }, expected_status=200, ) payload = response.json() assert payload.get("success") is True, payload data = payload.get("data") or {} assert data.get("access_token"), payload return data @staticmethod def json_data(response: httpx.Response) -> Any: payload = response.json() if isinstance(payload, dict) and "data" in payload: return payload["data"] return payload @staticmethod def _format_error(response: httpx.Response) -> str: try: body = response.json() except Exception: body = response.text return f"{response.request.method} {response.request.url} -> {response.status_code}: {body}" def flatten_route_paths(routes: list[dict[str, Any]]) -> set[str]: paths: set[str] = set() def collect(items: list[dict[str, Any]]) -> None: for item in items: route_path = str(item.get("route_path") or "") if route_path: paths.add(route_path) children = item.get("children") or [] if isinstance(children, list): collect(children) collect(routes) return paths