Files
codex-app-server/cloud/tests/test_oauth.py
T
host f39ed3654e ci3: lint/mypy 第二阶段清理 + 覆盖率 76% -> 78%(门槛不动)
基底:#251 头 174b631(堆叠于 #242 feat-ci-expand 之上),
基线 origin/main aebc6238;本轨不改 #251 已定语义、不动
workflow 门槛、不删检查、不加 skip。

lint(ruff E,F@120):109 -> 10
- F401 43 -> 0:删未用导入;try/except 双绑定的名字出现
  “打地鼠”(删 except 分支后 try 分支又被标出),两分支
  均已核对正文无引用后清掉;MCP 三模块的 Error 名不删,
  改走 importlib 属性绑定(见下),两路径绑定都保留。
- E702 40 -> 0:全在 host.py main() 的 argparse 段,纯分行,
  tokenize 按分号 token 拆(含 2377 行多行语句只断首个分号);
  一次 5 空格缩进失误已回滚重做并以 ast + 全量 host 测试验证。
- E501 13 -> 0:host.py 5 项随 E702 分行连带清掉;mcp_server
  3 项把描述拆成相邻字面量(AST 比对 24 条工具描述与原值
  完全一致,注意 ruff 按东亚宽度计长,CJK 算 2);test_host
  5 项断开 git commit 参数表。
- E402 8 -> 10:生产侧 2 项清掉(db.py _re、github_app
  _base64 移到顶部,之前无引用、无重名);剩余 10 项全是
  tests/ 的 sys.path 前置 import(6 项旧 + 4 个新测试文件
  沿用同一既定模式),移上去会破坏单文件直跑,有意保留。
- F841 4 -> 0:只去绑定、不删调用(run_task、DB 入队、
  tarfile 上下文、git 提交),各有注释与原测试/新测试覆盖;
  host dispatch_from_branch 新增真实 git 仓库回归测试。
- F811 1 -> 0:server 本地 _row_get 与 peer_messages.row_get
  逐行相同,删本地定义,全部调用点解析到同一 import;
  新增 test_server_row_get(同一对象 + 三路行为)。

mypy(生产范围):24 -> 0,零新增 type: ignore
- no-redef 9 -> 0:MCP 三模块 try/except 两次 from-import
  改 importlib.import_module + 属性绑定(cloud 优先、flat
  copy 回落、内层 ImportError 同样回落,语义与原来一致;
  flat-copy 子进程测试证实回落后 __module__ 为顶层名);
  server _row_get 随 F811 消除。
- arg-type 10 -> 0:harness2_tasks shared 改 TypedDict
  逐键精确类型;CodexProbeAdapter 补 no-op validate_tree
  (_adapter_validate 本就跳过缺失方法,行为一致)。
- var-annotated 3 -> 0:sub2api _SHARED_ADMIN/_login_slots、
  verify-multiuser RESULTS 加精确注解。
- assignment/misc 2 -> 0:marker_tools ZoneInfo 先声明
  Any 再 try import(草稿先验 mypy 通过)。

bandit:总数 286 不变,High 仍 4 项,全在 host.py,本轨不改
(属第 3 项白名单轨范围),只做 triage:
- 1319 run_verify:无插值,整串验测命令,管道/&& 须 shell
  语义(新增 test_host_run_verify 断言管道与 && 真实生效)。
- 1497 notify:模板来自算子配置,三处插值全部 shlex.quote,
  证据为既有 test_notify_failure_quotes_shell_metacharacters
  (; touch 与 $(touch ) 不执行)。
- 2099/2103 goal verify:无插值,任务配置验测串,同 run_verify。
- 无新增 High;pip-audit 0 漏洞。

覆盖率(同口径三套件 coverage 计量):75.96% -> 77.87%
(22392 行/缺 5385 -> 22437 行/缺 4963,显示 76% -> 78%)
- client.py 0% -> 90%(pipe 假子进程 + main 分支)
- run_parallel.py 0% -> 97%、migrate 包装 0% -> 95%
- playwright_mcp 37% -> 62%、imagegen_mcp 47% -> 79%、
  websearch_mcp 51% -> 86%、deploy.py 56% -> 77%
- verify-multiuser 0% -> 27%(仅 check/req helper;main()
  是主机侧真实多用户实弹验证,按门禁不建生产任务、
  不以 mock 冒充,仍为缺口)
- host.py 55% -> 57%(文件 2020 -> 2060 行系 E702 分行
  的计量口径变化,非代码膨胀)
- 未到 85%:门槛保持 75% 不动,不删低覆盖模块出统计口径;
  缺口主力仍是 server.py / worker.py / host.py 大文件,
  下一阶段须针对性深测,非本轨快赢可及。

验证:三套件 1576 -> 1629 项全过(tests 127、cloud/tests
1449 skipped=4、deploy/tests 53);ruff E9 通过;mypy 0;
bandit High 4(同上);pip-audit 0 漏洞;试跑产物已删,
提交后工作树干净。
2026-11-03 02:48:54 +00:00

275 lines
11 KiB
Python

"""oauth 脚手架单测:state、授权 URL、回调、换 token 失败。
全部外部调用均为注入的 fake http 客户端,本文件不向真实
GitHub / Google 发任何请求;出现的 client_id / secret 哨兵值
均为测试用的假值(形如 test- / fake- 前缀),不是真实凭证。
"""
import os
import sys
import tempfile
import time
import unittest
from pathlib import Path
from unittest import mock
from urllib.parse import parse_qs, urlparse
try:
from cloud.auth import AuthStore
from cloud.oauth import (
OAuthError,
OAuthStateStore,
build_authorization_url,
exchange_code_for_token,
handle_oauth_callback,
map_external_identity_to_local_user,
start_oauth_login,
)
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.oauth import (
OAuthError,
OAuthStateStore,
build_authorization_url,
exchange_code_for_token,
handle_oauth_callback,
map_external_identity_to_local_user,
start_oauth_login,
)
PROVIDER = "github"
CLIENT_ID = "test-client-id-not-real"
REDIRECT_URI = "https://example.invalid/oauth/callback"
FAKE_SECRET_SENTINEL = "fake-secret-sentinel-not-real"
def _fake_token_client(token_result=None, error=None):
"""返回 fake http_client:记录调用,不联网。"""
calls = []
def fake(token_url, payload):
calls.append((token_url, dict(payload)))
if error is not None:
raise RuntimeError(error)
return dict(token_result or {"access_token": "fake-token-not-real"})
fake.calls = calls
return fake
class TestOAuthState(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "oauth.db")
self.store = OAuthStateStore(self.db_path)
def tearDown(self):
self.tmp.cleanup()
def test_state_create_then_validate_ok(self):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
self.assertTrue(state)
record = self.store.validate_state(state, PROVIDER, REDIRECT_URI)
self.assertIsNotNone(record)
self.assertEqual(record["provider"], PROVIDER)
self.assertEqual(record["redirect_uri"], REDIRECT_URI)
# 一次性:第二次校验必然失败(防重放)
self.assertIsNone(self.store.validate_state(state, PROVIDER))
def test_state_expired_rejected(self):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
expired_now = time.time() + 601 # 默认 TTL 600 秒
self.assertIsNone(self.store.validate_state(
state, PROVIDER, now=expired_now))
# 过期行已被删除,不会残留
row = self.store.conn.execute(
"SELECT state FROM oauth_states WHERE state = ?", (state,)
).fetchone()
self.assertIsNone(row)
def test_state_mismatch_rejected(self):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
# 不存在的 state
self.assertIsNone(self.store.validate_state(
"no-such-state", PROVIDER))
# provider 不匹配:拒绝,且不消耗合法 state
self.assertIsNone(self.store.validate_state(state, "google"))
# redirect_uri 不匹配:同样拒绝且不消耗
self.assertIsNone(self.store.validate_state(
state, PROVIDER, "https://example.invalid/other"))
# 合法组合仍可通过(前两次不匹配没有烧掉它)
self.assertIsNotNone(self.store.validate_state(
state, PROVIDER, REDIRECT_URI))
class TestAuthorizationUrl(unittest.TestCase):
def test_url_contains_params_and_no_secret(self):
state = "test-state-abc"
url = build_authorization_url(
PROVIDER, CLIENT_ID, REDIRECT_URI,
scope="read:user", state=state)
parsed = urlparse(url)
self.assertEqual(parsed.netloc, "github.com")
params = {k: v[0] for k, v in parse_qs(parsed.query).items()}
self.assertEqual(params["client_id"], CLIENT_ID)
self.assertEqual(params["redirect_uri"], REDIRECT_URI)
self.assertEqual(params["scope"], "read:user")
self.assertEqual(params["state"], state)
self.assertNotIn("secret", url.lower())
self.assertNotIn(FAKE_SECRET_SENTINEL, url)
def test_url_default_scope_and_google(self):
url = build_authorization_url(
"github", CLIENT_ID, REDIRECT_URI, state="s1")
self.assertIn("read%3Auser", url) # 默认 scope read:user
url = build_authorization_url(
"google", CLIENT_ID, REDIRECT_URI, state="s2")
params = {k: v[0] for k, v in parse_qs(urlparse(url).query).items()}
self.assertEqual(params["scope"], "openid email profile")
self.assertEqual(params["response_type"], "code")
self.assertNotIn("secret", url.lower())
def test_start_login_stores_state(self):
tmp = tempfile.TemporaryDirectory()
try:
store = OAuthStateStore(os.path.join(tmp.name, "s.db"))
state, url = start_oauth_login(
store, PROVIDER, CLIENT_ID, REDIRECT_URI)
self.assertIn("state=" + state, url)
row = store.conn.execute(
"SELECT provider FROM oauth_states WHERE state = ?",
(state,),
).fetchone()
self.assertEqual(row[0], PROVIDER)
finally:
tmp.cleanup()
def test_unknown_provider_rejected(self):
with self.assertRaises(OAuthError):
build_authorization_url(
"not-a-provider", CLIENT_ID, REDIRECT_URI)
class TestOAuthCallback(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "oauth.db")
self.store = OAuthStateStore(self.db_path)
# 同库上的 AuthStore:用于断言“失败不建用户/不记审计”。
self.auth = AuthStore(self.db_path)
def tearDown(self):
self.tmp.cleanup()
def _login(self, fake=None, **kwargs):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
return handle_oauth_callback(
self.store, PROVIDER, "test-code-not-real", state,
CLIENT_ID, REDIRECT_URI,
http_client=fake or _fake_token_client(
{"access_token": "fake-token-not-real",
"id": 12345, "login": "fake-user"}),
**kwargs)
def _count(self, table):
return self.store.conn.execute(
"SELECT COUNT(*) FROM %s" % table).fetchone()[0]
def test_callback_success_returns_identity(self):
identity = self._login()
self.assertEqual(identity["provider"], PROVIDER)
self.assertEqual(identity["external_id"], "12345")
self.assertEqual(identity["login"], "fake-user")
# access_token 不返回、不入库:回调只交身份
self.assertNotIn("access_token", identity)
def test_callback_maps_identity_to_local_user(self):
identity = self._login()
user_id, created = map_external_identity_to_local_user(
self.auth, PROVIDER, identity)
self.assertTrue(created)
user_id2, created2 = map_external_identity_to_local_user(
self.auth, PROVIDER, identity)
self.assertEqual(user_id, user_id2)
self.assertFalse(created2)
def test_callback_state_expired_rejected(self):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
with self.assertRaises(OAuthError) as ctx:
handle_oauth_callback(
self.store, PROVIDER, "code", state, CLIENT_ID, REDIRECT_URI,
http_client=_fake_token_client(),
now=time.time() + 601)
self.assertEqual(ctx.exception.reason, "state_expired")
def test_callback_state_mismatch_rejected(self):
state = self.store.create_state(PROVIDER, REDIRECT_URI)
with self.assertRaises(OAuthError) as ctx:
handle_oauth_callback(
self.store, "google", "code", state, CLIENT_ID, REDIRECT_URI,
http_client=_fake_token_client())
self.assertEqual(ctx.exception.reason, "state_mismatch")
with self.assertRaises(OAuthError) as ctx:
handle_oauth_callback(
self.store, PROVIDER, "code", "missing-state",
CLIENT_ID, REDIRECT_URI,
http_client=_fake_token_client())
self.assertEqual(ctx.exception.reason, "state_not_found")
def test_token_exchange_failure_creates_no_session_or_user(self):
fake = _fake_token_client(error="provider refused code")
state = self.store.create_state(PROVIDER, REDIRECT_URI)
with self.assertRaises(OAuthError) as ctx:
handle_oauth_callback(
self.store, PROVIDER, "bad-code", state,
CLIENT_ID, REDIRECT_URI, http_client=fake)
self.assertEqual(ctx.exception.reason, "token_exchange_failed")
# 换 token 失败:无用户、无审计(本模块本来就不建会话,
# 这里断言失败路径也没有任何副作用写入同库的 auth 表)。
self.assertEqual(self._count("users"), 0)
self.assertEqual(self._count("audit_log"), 0)
def test_token_error_dict_creates_nothing(self):
fake = _fake_token_client(
{"error": "bad_verification_code"})
state = self.store.create_state(PROVIDER, REDIRECT_URI)
with self.assertRaises(OAuthError):
handle_oauth_callback(
self.store, PROVIDER, "bad-code", state,
CLIENT_ID, REDIRECT_URI, http_client=fake)
self.assertEqual(self._count("users"), 0)
def test_real_path_without_secret_refuses_before_network(self):
# http_client=None 且环境变量无 secret:必须在联网前拒绝。
# 用 patch 清空两个 secret 环境变量,并断言 urlopen 未被调用。
with mock.patch.dict(os.environ, {}, clear=False):
os.environ.pop("GITHUB_OAUTH_CLIENT_SECRET", None)
os.environ.pop("GOOGLE_OAUTH_CLIENT_SECRET", None)
with mock.patch("cloud.oauth._urllib_post") as mocked:
with self.assertRaises(OAuthError) as ctx:
exchange_code_for_token(
PROVIDER, "code", CLIENT_ID, REDIRECT_URI,
http_client=None)
self.assertEqual(
ctx.exception.reason, "client_secret_missing")
mocked.assert_not_called()
def test_userinfo_client_used_when_token_has_no_identity(self):
def userinfo(access_token):
self.assertEqual(access_token, "fake-token-not-real")
return {"id": 999, "login": "via-userinfo"}
state = self.store.create_state(PROVIDER, REDIRECT_URI)
identity = handle_oauth_callback(
self.store, PROVIDER, "code", state, CLIENT_ID, REDIRECT_URI,
http_client=_fake_token_client(
{"access_token": "fake-token-not-real"}),
userinfo_client=userinfo)
self.assertEqual(identity["external_id"], "999")
self.assertEqual(identity["login"], "via-userinfo")
if __name__ == "__main__":
unittest.main()