PMFlow/backend/tests/test_dev_sync_api.py

204 lines
6.8 KiB
Python

from __future__ import annotations
import importlib.util
import json
import os
from pathlib import Path
import sys
from types import SimpleNamespace
import types
import pytest
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://tester:tester@localhost:5432/pmflow_test")
from app.core.database import get_db
from app.dependencies.auth import get_current_user
def _load_dev_sync_module():
package_name = "app.api"
module_name = "app.api.dev_sync"
module_path = Path(__file__).resolve().parents[1] / "app" / "api" / "dev_sync.py"
if package_name not in sys.modules:
package = types.ModuleType(package_name)
package.__path__ = [str(module_path.parent)] # type: ignore[attr-defined]
sys.modules[package_name] = package
if module_name in sys.modules:
return sys.modules[module_name]
spec = importlib.util.spec_from_file_location(module_name, module_path)
if spec is None or spec.loader is None:
raise RuntimeError("无法加载 dev_sync 模块")
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
return module
dev_sync = _load_dev_sync_module()
def _build_app(*, role: str = "OWNER") -> FastAPI:
app = FastAPI()
app.include_router(dev_sync.router)
async def _fake_get_db():
yield object()
app.dependency_overrides[get_db] = _fake_get_db
app.dependency_overrides[get_current_user] = lambda: SimpleNamespace(
id=7,
tenant_id=1,
username="owner",
role=role,
)
return app
@pytest.mark.asyncio
async def test_github_install_url_route_returns_install_center_payload(monkeypatch) -> None:
class FakeService:
def __init__(self, db, tenant_id: int):
_ = db
self.tenant_id = tenant_id
async def get_github_app_install_center(self):
return {
"configured": True,
"install_url": "https://github.com/apps/pmflow/installations/new",
"callback_url": "https://pmflow.local/github/callback",
"webhook_signature_configured": True,
"app_slug": "pmflow",
}
monkeypatch.setattr(dev_sync, "AIProjectControlService", FakeService)
app = _build_app()
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.get("/integrations/github/app/install-url")
assert response.status_code == 200
payload = response.json()
assert payload["code"] == 0
assert payload["data"]["configured"] is True
assert payload["data"]["install_url"] == "https://github.com/apps/pmflow/installations/new"
@pytest.mark.asyncio
async def test_github_webhook_route_passes_raw_body_to_service(monkeypatch) -> None:
captured: dict[str, object] = {}
class FakeService:
def __init__(self, db, tenant_id: int):
_ = db
captured["tenant_id"] = tenant_id
async def ingest_github_webhook(self, **kwargs):
captured.update(kwargs)
return {
"delivery_id": kwargs["delivery_id"],
"matched_links": 1,
}
monkeypatch.setattr(dev_sync, "AIProjectControlService", FakeService)
raw_body = json.dumps(
{"tenant_id": 9, "event_type": "pull_request", "repository": {"full_name": "pmflow/control-plane"}},
separators=(",", ":"),
).encode("utf-8")
app = _build_app()
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.post(
"/integrations/github/webhook",
content=raw_body,
headers={
"Content-Type": "application/json",
"X-GitHub-Event": "pull_request",
"X-GitHub-Delivery": "delivery-1",
"X-Hub-Signature-256": "sha256=test",
},
)
assert response.status_code == 200
assert captured["tenant_id"] == 9
assert captured["event_type"] == "pull_request"
assert captured["delivery_id"] == "delivery-1"
assert captured["signature_256"] == "sha256=test"
assert captured["raw_body"] == raw_body
@pytest.mark.asyncio
async def test_github_installations_sync_route_returns_installations(monkeypatch) -> None:
class FakeService:
def __init__(self, db, tenant_id: int):
_ = db, tenant_id
async def sync_github_installations(self):
return {
"installations": [
{
"id": "row-1",
"installation_id": "101",
"account_login": "pmflow",
}
],
"repository_count": 3,
}
monkeypatch.setattr(dev_sync, "AIProjectControlService", FakeService)
app = _build_app()
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.post("/integrations/github/app/installations/sync")
assert response.status_code == 200
payload = response.json()["data"]
assert payload["repository_count"] == 3
assert payload["installations"][0]["account_login"] == "pmflow"
@pytest.mark.asyncio
async def test_github_repo_bind_route_surfaces_installation_repo_errors(monkeypatch) -> None:
class FakeService:
def __init__(self, db, tenant_id: int):
_ = db, tenant_id
async def bind_github_repository(self, *, project_id: int, github_repository_id: int):
if github_repository_id == 99:
raise ValueError("当前仓库不在 GitHub App 已授权仓库列表中")
return {
"id": "binding-1",
"project_id": str(project_id),
"provider": "github",
"full_name": "pmflow/control-plane",
"status": "active",
}
async def _allow_project(*args, **kwargs):
return None
monkeypatch.setattr(dev_sync, "AIProjectControlService", FakeService)
monkeypatch.setattr(dev_sync, "_ensure_project_member_or_manager", _allow_project)
app = _build_app()
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
ok_response = await client.post(
"/integrations/github/repos/bind",
json={"project_id": 17, "github_repository_id": 1},
)
bad_response = await client.post(
"/integrations/github/repos/bind",
json={"project_id": 17, "github_repository_id": 99},
)
assert ok_response.status_code == 200
assert ok_response.json()["data"]["full_name"] == "pmflow/control-plane"
assert bad_response.status_code == 400
assert "已授权仓库列表" in bad_response.json()["detail"]