Files
codex-app-server/cloud/tests/test_server_pr_app_token.py
T
host 25d9b3d938 App-token:推分支建 PR 改走 GitHub App installation token
- 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 个
2026-10-26 16:47:55 +08:00

249 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()