feat(forge): fondation multi-forges — connexions chiffrées, registre, client GitHub (P0/P1)
- Tables forge_connections/users/workspaces (+ membres, projets, états, audit) ; migration des anciennes bases avec portées (workspace_id, connection_id) par défaut (1, 1). - Jetons chiffrés Fernet (KANBANTE_MASTER_KEY ou clé générée en base), jamais réaffichés. - ForgeRegistry : connexion héritée créée depuis l'environnement, espace Principal, instantanés préfixés par connexion. - GitHubClient complet (Bearer, pagination Link, étiquettes par nom, PATCH archived, branches via git/refs). - Paramètres surchargeables en base (résolveur config, P3).
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""GitHubClient (0.24.0, P1): same protocol as GiteaClient,
|
||||
GitHub wire shape underneath. Driven through MockTransport."""
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.github_client import GitHubClient
|
||||
|
||||
|
||||
def make_client(handler) -> GitHubClient:
|
||||
return GitHubClient(
|
||||
base_url="https://api.github.com",
|
||||
token="ghp_test",
|
||||
cache_ttl=0,
|
||||
transport=httpx.MockTransport(handler),
|
||||
)
|
||||
|
||||
|
||||
async def test_auth_header_is_bearer():
|
||||
seen = {}
|
||||
|
||||
def handler(request):
|
||||
seen["auth"] = request.headers.get("authorization")
|
||||
seen["accept"] = request.headers.get("accept")
|
||||
return httpx.Response(200, json={"login": "octocat"})
|
||||
|
||||
client = make_client(handler)
|
||||
user = await client.get_user()
|
||||
assert user["login"] == "octocat"
|
||||
assert seen["auth"] == "Bearer ghp_test"
|
||||
assert "application/vnd.github+json" in seen["accept"]
|
||||
|
||||
|
||||
async def test_pagination_follows_link_headers():
|
||||
def handler(request):
|
||||
if "page=2" in str(request.url):
|
||||
return httpx.Response(200, json=[{"full_name": "a/deux"}])
|
||||
return httpx.Response(
|
||||
200,
|
||||
json=[{"full_name": "a/un"}],
|
||||
headers={
|
||||
"Link": '<https://api.github.com/user/repos?page=2>; rel="next"'
|
||||
},
|
||||
)
|
||||
|
||||
client = make_client(handler)
|
||||
repos = await client._fetch_search_repos()
|
||||
assert [r["full_name"] for r in repos] == ["a/un", "a/deux"]
|
||||
|
||||
|
||||
async def test_labels_are_addressed_by_name():
|
||||
seen = {}
|
||||
|
||||
def handler(request):
|
||||
if request.method == "GET" and request.url.path.endswith("/labels"):
|
||||
return httpx.Response(
|
||||
200, json=[{"id": 991, "name": "kanban:todo", "color": "3b82f6"}]
|
||||
)
|
||||
if request.method == "POST":
|
||||
seen["body"] = request.content.decode()
|
||||
return httpx.Response(200, json=[])
|
||||
raise AssertionError(request.url)
|
||||
|
||||
client = make_client(handler)
|
||||
label = await client.ensure_label("a", "b", "kanban:todo", "3b82f6")
|
||||
assert label["id"] == "kanban:todo" # name, not GitHub's numeric id
|
||||
await client.add_issue_label("a", "b", 7, label["id"])
|
||||
assert '"kanban:todo"' in seen["body"]
|
||||
|
||||
|
||||
async def test_search_issues_normalizes_repository():
|
||||
def handler(request):
|
||||
assert "is%3Aissue" in str(request.url) or "is:issue" in str(request.url)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"items": [
|
||||
{
|
||||
"number": 3,
|
||||
"title": "T",
|
||||
"repository_url": "https://api.github.com/repos/a/b",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
client = make_client(handler)
|
||||
issues = await client.search_issues(q="mot", state="open")
|
||||
assert issues[0]["repository"]["full_name"] == "a/b"
|
||||
|
||||
|
||||
async def test_set_archived_uses_patch():
|
||||
seen = {}
|
||||
|
||||
def handler(request):
|
||||
seen["method"] = request.method
|
||||
seen["body"] = request.content.decode()
|
||||
return httpx.Response(200, json={"archived": True})
|
||||
|
||||
client = make_client(handler)
|
||||
await client.set_archived("a", "b", True)
|
||||
assert seen["method"] == "PATCH"
|
||||
assert '"archived": true' in seen["body"] or '"archived":true' in seen["body"]
|
||||
|
||||
|
||||
async def test_starred_roundtrip():
|
||||
seen = {}
|
||||
|
||||
def handler(request):
|
||||
seen["method"] = request.method
|
||||
seen["path"] = request.url.path
|
||||
return httpx.Response(204)
|
||||
|
||||
client = make_client(handler)
|
||||
await client.set_starred("a", "b", True)
|
||||
assert seen["method"] == "PUT"
|
||||
assert seen["path"] == "/user/starred/a/b"
|
||||
Reference in New Issue
Block a user