- github_app:纯标准库 RS256 签 App JWT(PKCS#1/PKCS#8), 换 installation token(现换现用、不缓存),按仓库名查安装 记录,任一步失败回落 None - pr:app_token 路径 push 走 credential helper(token 只进 子进程环境),建 PR 走 REST API(读 default_branch 当 base); 不传 token 时 gh 路径不变 - server:/pr 端点按项目 repo_url 解析仓库名、现换 token, 响应加 auth_via=app|pat,换失败回落 PAT 不报错 - 测试:JWT/换 token/解析 9 个、pr App 路径 6 个、端点级 App 与 PAT 回落 2 个
249 lines
8.8 KiB
Python
249 lines
8.8 KiB
Python
"""POST /tasks/{id}/pr 的 App-token 接线单测(端点级):
|
||
|
||
App 路径:项目 repo_url 解析出仓库名、库里有该仓库的 App
|
||
安装记录 -> 现换 installation token(fake 换 token 客户端 +
|
||
测试专用 pem,JWT 纯标准库签发、不联网)-> push 走
|
||
credential helper、建 PR 走 GitHub API(fake),响应里
|
||
auth_via=app。
|
||
PAT 回落:没有安装记录 -> 走原来的 gh 路径,auth_via=pat。
|
||
|
||
git/GitHub API 一律 fake,不跑真实命令;token 值绝不出现
|
||
在 git 命令记录里。
|
||
"""
|
||
|
||
import http.client
|
||
import json
|
||
import os
|
||
import sqlite3
|
||
import sys
|
||
import tempfile
|
||
import threading
|
||
import unittest
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
|
||
try:
|
||
from cloud.auth import AuthStore
|
||
from cloud.db import connect as _db_connect
|
||
from cloud.github_app import (
|
||
record_installation_repos,
|
||
save_app_config,
|
||
)
|
||
from cloud.server import create_server
|
||
from cloud.tests.test_app_token import TEST_KEY_PKCS1
|
||
except ImportError:
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||
from cloud.auth import AuthStore
|
||
from cloud.db import connect as _db_connect
|
||
from cloud.github_app import (
|
||
record_installation_repos,
|
||
save_app_config,
|
||
)
|
||
from cloud.server import create_server
|
||
from cloud.tests.test_app_token import TEST_KEY_PKCS1
|
||
|
||
|
||
def _result(returncode=0, stdout="", stderr=""):
|
||
return SimpleNamespace(returncode=returncode, stdout=stdout,
|
||
stderr=stderr)
|
||
|
||
|
||
class _Git:
|
||
"""假 git:remote/branch/add/commit/push,记录命令。"""
|
||
|
||
def __init__(self, remote, branch):
|
||
self.calls = []
|
||
self.remote = remote
|
||
self.branch = branch
|
||
|
||
def __call__(self, cmd, cwd):
|
||
self.calls.append(list(cmd))
|
||
if "remote" in cmd:
|
||
return _result(0, self.remote + "\n")
|
||
if "branch" in cmd:
|
||
return _result(0, self.branch + "\n")
|
||
if "add" in cmd:
|
||
return _result(0, "")
|
||
if "commit" in cmd:
|
||
return _result(1, "",
|
||
"nothing to commit, working tree clean")
|
||
if "push" in cmd:
|
||
return _result(0, "")
|
||
return _result(0, "")
|
||
|
||
|
||
class _Gh:
|
||
"""假 gh:auth 通过、pr create 回链接。"""
|
||
|
||
def __init__(self, pr_url):
|
||
self.calls = []
|
||
self.pr_url = pr_url
|
||
|
||
def __call__(self, cmd, cwd):
|
||
self.calls.append(list(cmd))
|
||
if cmd[:2] == ["gh", "auth"]:
|
||
return _result(0, "", "")
|
||
if cmd[:2] == ["gh", "pr"]:
|
||
return _result(0, self.pr_url + "\n")
|
||
return _result(0, "")
|
||
|
||
|
||
class _Api:
|
||
"""假 GitHub API:GET 仓库回 default_branch,POST pulls 回 PR。"""
|
||
|
||
def __init__(self):
|
||
self.calls = []
|
||
|
||
def __call__(self, method, url, token, payload):
|
||
self.calls.append((method, url, token, payload))
|
||
if method == "GET":
|
||
return 200, {"default_branch": "main"}
|
||
if method == "POST":
|
||
return 201, {"html_url": "https://github.com/octo/a/pull/77",
|
||
"number": 77}
|
||
raise AssertionError("unexpected API call: %s %s" % (method, url))
|
||
|
||
|
||
def _token_client(method, url, headers, body):
|
||
"""假换 token:JWT 已在前面签好(只验格式),直接回 token。"""
|
||
assert method == "POST", method
|
||
assert "/access_tokens" in url, url
|
||
assert headers["Authorization"].startswith("Bearer "), headers
|
||
return 201, {"token": "srv-app-token",
|
||
"expires_at": "2030-01-01T00:00:00Z",
|
||
"permissions": {"contents": "write"},
|
||
"repository_selection": "selected"}
|
||
|
||
|
||
class _Case(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.db_path = os.path.join(self.tmp.name, "control.db")
|
||
self.auth = AuthStore(self.db_path)
|
||
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||
self.port = self.server.server_address[1]
|
||
self.thread = threading.Thread(
|
||
target=self.server.serve_forever, daemon=True)
|
||
self.thread.start()
|
||
self.worktrees = os.path.join(self.tmp.name, "worktrees")
|
||
os.makedirs(self.worktrees, exist_ok=True)
|
||
self.server.host_worktrees_dir = self.worktrees
|
||
self.git = None
|
||
self.api = _Api()
|
||
self.server.pr_github_api = self.api
|
||
self.server.pr_app_token_client = _token_client
|
||
|
||
def tearDown(self):
|
||
self.server.shutdown()
|
||
self.server.server_close()
|
||
self.thread.join(timeout=5)
|
||
try:
|
||
self.auth.conn.close()
|
||
except Exception:
|
||
pass
|
||
self.tmp.cleanup()
|
||
|
||
def _req(self, method, path, body=None, key=None):
|
||
conn = http.client.HTTPConnection(
|
||
"127.0.0.1", self.port, timeout=10)
|
||
hdrs = {}
|
||
payload = None
|
||
if body is not None:
|
||
payload = json.dumps(body).encode("utf-8")
|
||
hdrs["Content-Type"] = "application/json"
|
||
if key is not None:
|
||
hdrs["X-API-Key"] = key
|
||
try:
|
||
conn.request(method, path, body=payload, headers=hdrs)
|
||
resp = conn.getresponse()
|
||
data = resp.read()
|
||
finally:
|
||
conn.close()
|
||
try:
|
||
parsed = json.loads(data.decode("utf-8")) if data else None
|
||
except ValueError:
|
||
parsed = None
|
||
return resp.status, parsed
|
||
|
||
def _project(self, name, repo_url):
|
||
status, project = self._req(
|
||
"POST", "/projects",
|
||
{"name": name, "repo_url": repo_url, "auto_pr": 1},
|
||
self.owner_key)
|
||
self.assertEqual(status, 201, project)
|
||
return project["id"]
|
||
|
||
def _task(self, task_id, project_id):
|
||
conn = sqlite3.connect(self.db_path)
|
||
try:
|
||
conn.execute(
|
||
"INSERT INTO tasks(id, project_id, prompt, status,"
|
||
" depends_on, priority, created_by, created_at,"
|
||
" updated_at, last_heartbeat) VALUES(?, ?, ?, 'merged',"
|
||
" '[]', 0, 'system', 0, 0, NULL)",
|
||
(task_id, project_id, "do it"))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
os.makedirs(os.path.join(self.worktrees, task_id), exist_ok=True)
|
||
|
||
def test_app_path_via_endpoint(self):
|
||
project_id = self._project(
|
||
"octo/a", "https://github.com/octo/a")
|
||
conn = _db_connect(self.db_path)
|
||
try:
|
||
save_app_config(conn, {
|
||
"app_id": 5476837, "slug": "fake-app",
|
||
"client_id": "Iv1.x", "webhook_secret": "fake-secret",
|
||
"pem": TEST_KEY_PKCS1,
|
||
"html_url": "https://github.com/apps/fake-app"})
|
||
record_installation_repos(conn, 175380082, "octo",
|
||
["octo/a"])
|
||
finally:
|
||
conn.close()
|
||
self.git = _Git("https://github.com/octo/a.git", "host/t-app-1")
|
||
self.server.pr_git_runner = self.git
|
||
self._task("t-app-1", project_id)
|
||
status, body = self._req(
|
||
"POST", "/tasks/t-app-1/pr", key=self.owner_key)
|
||
self.assertEqual(status, 200, body)
|
||
self.assertEqual(body["pr_url"],
|
||
"https://github.com/octo/a/pull/77")
|
||
self.assertEqual(body["pr_number"], 77)
|
||
self.assertEqual(body["auth_via"], "app")
|
||
pushes = [c for c in self.git.calls if "push" in c]
|
||
self.assertEqual(len(pushes), 1)
|
||
self.assertTrue(any(a.startswith("credential.helper=")
|
||
for a in pushes[0]))
|
||
self.assertNotIn("srv-app-token",
|
||
" ".join(" ".join(c) for c in self.git.calls))
|
||
posts = [c for c in self.api.calls if c[0] == "POST"]
|
||
self.assertEqual(len(posts), 1)
|
||
self.assertEqual(posts[0][2], "srv-app-token")
|
||
|
||
def test_pat_fallback_via_endpoint(self):
|
||
project_id = self._project(
|
||
"octo/b", "https://github.com/octo/b")
|
||
self.git = _Git("https://github.com/octo/b.git", "host/t-pat-1")
|
||
self.server.pr_git_runner = self.git
|
||
gh = _Gh("https://github.com/octo/b/pull/88")
|
||
self.server.pr_gh_runner = gh
|
||
self.server.pr_gh_which = "/usr/bin/gh"
|
||
self._task("t-pat-1", project_id)
|
||
status, body = self._req(
|
||
"POST", "/tasks/t-pat-1/pr", key=self.owner_key)
|
||
self.assertEqual(status, 200, body)
|
||
self.assertEqual(body["pr_url"],
|
||
"https://github.com/octo/b/pull/88")
|
||
self.assertEqual(body["auth_via"], "pat")
|
||
pushes = [c for c in self.git.calls if "push" in c]
|
||
self.assertEqual(len(pushes), 1)
|
||
self.assertFalse(any(a.startswith("credential.helper=")
|
||
for a in pushes[0]))
|
||
self.assertEqual(self.api.calls, [])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|