split: handler mixins for oauth/identity/project/automation/fullauto (94 methods) on integration base 02835f6
- CloudControlPlaneHandler mixes in OAuthMixin, IdentityMemoryMixin, ProjectMixin, AutomationMixin, FullAutoMixin; shared helpers move to cloud/handler_support.py with server.py re-exports (old import paths keep working, incl. _validate_env_branch for tests). - Method bodies moved verbatim; routers, base plumbing, task/lifecycle, webhooks stay in server.py. No >100-line method body split. - test_handler_split_conformance.py (6 tests) pins resolution, layer bans and method accounting (238 total, 144 + 94, no duplicates). - test_p1_security_server.py: monkeypatch targets follow the defining module (handler_project MAX_PROJECTS_PER_USER, handler_identity relay helpers); assertions unchanged. (cherry picked from commit 92165a2b097126470e4827327d96d592a253adf1)
This commit is contained in:
1 parent
c900a0f78d
commit
935b530f84
9 files changed
+3439
-3119
No files matched your search
@@ -0,0 +1,162 @@
|
||||
"""Tests for the v2 handler mixin split (integration base 02835f6).
|
||||
|
||||
Pins the split contract so a future violation fails CI:
|
||||
- the 94 moved methods still resolve on CloudControlPlaneHandler and
|
||||
come from their mixin module (behaviour unchanged, only the defining
|
||||
module moved);
|
||||
- mixin/support modules never import cloud.server (no cycle), never
|
||||
call sqlite3.connect and never import psycopg (narrow AST scan, same
|
||||
as #238 arch_conformance.scan_imports);
|
||||
- server.py re-exports the moved shared helpers as identical objects,
|
||||
so `from cloud.server import _task_to_dict` keeps working;
|
||||
- handler method accounting: 238 total, 144 left in server.py itself,
|
||||
94 in mixins, no name defined twice.
|
||||
|
||||
Only the standard library; no real model, no network, no secrets.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
||||
from cloud.arch_conformance import scan_imports # noqa: E402
|
||||
from cloud.handler_support import ( # noqa: E402
|
||||
_apply_default_quota, _default_quota_usd_micros, _task_to_dict)
|
||||
from cloud.server import CloudControlPlaneHandler # noqa: E402
|
||||
from cloud import server as _server_module # noqa: E402
|
||||
|
||||
MOVED = {
|
||||
"cloud.handler_oauth": [
|
||||
"_oauth_provider", "_oauth_login", "_oauth_clients",
|
||||
"_oauth_finish", "_oauth_callback", "_oauth_error_page",
|
||||
"_oauth_success_page", "_oauth_bind_success_page",
|
||||
"_oauth_callback_get", "_oauth_bind_route",
|
||||
"_oauth_bind_initiate", "_oauth_bindings_get",
|
||||
"_oauth_bindings_delete", "_github_app_public_base",
|
||||
"_github_app_error_page", "_github_app_setup_state",
|
||||
"_github_app_setup_page", "_github_app_callback",
|
||||
"_github_app_status"],
|
||||
"cloud.handler_identity": [
|
||||
"_memory_create", "_memory_list", "_memory_supersede",
|
||||
"_identity_store", "_identity_close", "_identity_profile_get",
|
||||
"_identity_profile_upsert", "_identity_user_memories_list",
|
||||
"_identity_user_memory_create", "_identity_user_memory_supersede",
|
||||
"_identity_user_memory_delete", "_identity_context",
|
||||
"_auto_memory_store", "_auto_memory_close",
|
||||
"_auto_memory_settings_get", "_auto_memory_settings_upsert",
|
||||
"_auto_memory_log", "_auto_memory_run", "_task_auto_memory_flags",
|
||||
"_project_identity_memory_create",
|
||||
"_project_identity_memories_list",
|
||||
"_project_identity_memory_supersede",
|
||||
"_project_identity_memory_delete",
|
||||
"_kb_index", "_kb_search", "_project_cost", "_task_cost",
|
||||
"_relay_models", "_models_overview"],
|
||||
"cloud.handler_project": [
|
||||
"_fetch_environment", "_create_environment", "_list_environments",
|
||||
"_get_environment", "_archive_environment", "_create_project",
|
||||
"_update_project", "_github_repos_list", "_list_projects",
|
||||
"_can_manage_members", "_set_member_row", "_project_members_list",
|
||||
"_resolve_invite_target", "_project_member_invite",
|
||||
"_project_member_patch", "_project_member_delete",
|
||||
"_remove_member_row", "_project_exists", "_check_project",
|
||||
"_project_push_subscription_delete"],
|
||||
"cloud.handler_automation": [
|
||||
"_push_create_endpoint", "_push_dispatch_endpoint",
|
||||
"_automation_create", "_automation_list",
|
||||
"_todos_list", "_todos_create", "_todos_get", "_todos_update",
|
||||
"_todos_delete", "_automation_cancel", "_automation_get",
|
||||
"_automation_bind_get", "_automation_bind", "_automation_unbind",
|
||||
"_automation_heartbeat", "_automation_update", "_triage_inbox"],
|
||||
"cloud.handler_fullauto": [
|
||||
"_full_auto_status", "_full_auto_config_plan",
|
||||
"_full_auto_validate_action", "_full_auto_merge_gate",
|
||||
"_full_auto_ci_fix", "_full_auto_release_plan",
|
||||
"_full_auto_rollback_plan", "_full_auto_audit_list",
|
||||
"_full_auto_execute"],
|
||||
}
|
||||
|
||||
NEW_MODULES = ["cloud/handler_support.py", "cloud/handler_oauth.py",
|
||||
"cloud/handler_identity.py", "cloud/handler_project.py",
|
||||
"cloud/handler_automation.py", "cloud/handler_fullauto.py"]
|
||||
|
||||
|
||||
def _own_methods(cls):
|
||||
return {name for name, value in vars(cls).items()
|
||||
if callable(value)}
|
||||
|
||||
|
||||
class TestMovedMethodsResolve(unittest.TestCase):
|
||||
def test_all_moved_methods_resolve_from_handler(self):
|
||||
for module, names in MOVED.items():
|
||||
for name in names:
|
||||
func = getattr(CloudControlPlaneHandler, name, None)
|
||||
self.assertIsNotNone(
|
||||
func, "%s missing on handler" % name)
|
||||
self.assertEqual(
|
||||
func.__module__, module,
|
||||
"%s should be defined in %s" % (name, module))
|
||||
|
||||
def test_moved_count_is_94(self):
|
||||
total = sum(len(names) for names in MOVED.values())
|
||||
self.assertEqual(total, 94)
|
||||
|
||||
|
||||
class TestNoCyclesOrDrivers(unittest.TestCase):
|
||||
def test_new_modules_pass_layer_scan(self):
|
||||
repo_root = os.path.dirname(os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(__file__))))
|
||||
for rel in NEW_MODULES:
|
||||
with open(os.path.join(repo_root, rel),
|
||||
encoding="utf-8") as f:
|
||||
scanned = scan_imports(f.read())
|
||||
self.assertNotIn("server", scanned["cloud"],
|
||||
"%s must not import cloud.server" % rel)
|
||||
self.assertFalse(scanned["driver_connect"],
|
||||
"%s must not call sqlite3.connect" % rel)
|
||||
self.assertFalse(scanned["driver_import"],
|
||||
"%s must not import psycopg" % rel)
|
||||
|
||||
|
||||
class TestReexports(unittest.TestCase):
|
||||
def test_shared_helpers_are_identical_objects(self):
|
||||
self.assertIs(_server_module._task_to_dict, _task_to_dict)
|
||||
self.assertIs(_server_module._apply_default_quota,
|
||||
_apply_default_quota)
|
||||
self.assertIs(_server_module._default_quota_usd_micros,
|
||||
_default_quota_usd_micros)
|
||||
|
||||
def test_base_plumbing_stays_in_server(self):
|
||||
own = _own_methods(CloudControlPlaneHandler)
|
||||
for name in ["_send_json", "_send_html", "_read_body",
|
||||
"_read_json", "_db", "_authenticate", "do_GET",
|
||||
"do_POST", "_dispatch", "_dispatch_project",
|
||||
"_dispatch_task_route"]:
|
||||
self.assertIn(name, own, "%s must stay in server.py" % name)
|
||||
|
||||
|
||||
class TestMethodAccounting(unittest.TestCase):
|
||||
def test_total_238_no_duplicates(self):
|
||||
seen = {}
|
||||
dups = []
|
||||
classes = [CloudControlPlaneHandler]
|
||||
for base in CloudControlPlaneHandler.__mro__:
|
||||
if base.__module__.startswith("cloud.handler_"):
|
||||
classes.append(base)
|
||||
total = 0
|
||||
for cls in classes:
|
||||
for name in _own_methods(cls):
|
||||
total += 1
|
||||
if name in seen:
|
||||
dups.append((seen[name], cls.__name__, name))
|
||||
seen[name] = cls.__name__
|
||||
self.assertEqual(dups, [])
|
||||
self.assertEqual(total, 238)
|
||||
self.assertEqual(len(_own_methods(CloudControlPlaneHandler)),
|
||||
238 - 94)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -24,6 +24,11 @@ try:
|
||||
from cloud.push import create_subscription
|
||||
from cloud.server import create_server
|
||||
from cloud import server as server_mod
|
||||
# 拆分后补丁目标跟随方法定义模块:项目上限在 handler_project,
|
||||
# relay 模型查询在 handler_identity(方法查本模块全局变量,
|
||||
# 补丁 cloud.server 已不再生效,见 code-to10-v2-split 摘要)。
|
||||
from cloud import handler_identity as handler_identity_mod
|
||||
from cloud import handler_project as handler_project_mod
|
||||
from cloud.sub2api import Sub2apiError
|
||||
from cloud.sub2api import ensure_gateway_keys_table
|
||||
from cloud.sub2api import get_user_gateway_key
|
||||
@@ -36,6 +41,8 @@ except ImportError:
|
||||
from cloud.push import create_subscription
|
||||
from cloud.server import create_server
|
||||
from cloud import server as server_mod
|
||||
from cloud import handler_identity as handler_identity_mod
|
||||
from cloud import handler_project as handler_project_mod
|
||||
from cloud.sub2api import Sub2apiError
|
||||
from cloud.sub2api import ensure_gateway_keys_table
|
||||
from cloud.sub2api import get_user_gateway_key
|
||||
@@ -265,7 +272,7 @@ class ServerP1TestCase(unittest.TestCase):
|
||||
os.environ.pop("CLOUD_GATEWAY", None)
|
||||
os.environ.pop("CLOUD_BACKUP_ROOT", None)
|
||||
os.environ.pop("CLOUD_BACKUP_DIR", None)
|
||||
self._old_max_projects = server_mod.MAX_PROJECTS_PER_USER
|
||||
self._old_max_projects = handler_project_mod.MAX_PROJECTS_PER_USER
|
||||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||||
self.port = self.server.server_address[1]
|
||||
self.thread = threading.Thread(
|
||||
@@ -287,7 +294,7 @@ class ServerP1TestCase(unittest.TestCase):
|
||||
except Exception:
|
||||
pass
|
||||
self.tmp.cleanup()
|
||||
server_mod.MAX_PROJECTS_PER_USER = self._old_max_projects
|
||||
handler_project_mod.MAX_PROJECTS_PER_USER = self._old_max_projects
|
||||
if self._old_admins is None:
|
||||
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
|
||||
else:
|
||||
@@ -304,11 +311,13 @@ class ServerP1TestCase(unittest.TestCase):
|
||||
os.environ.pop("CLOUD_BACKUP_DIR", None)
|
||||
else:
|
||||
os.environ["CLOUD_BACKUP_DIR"] = self._old_backup_dir
|
||||
# 恢复猴子补丁过的 server 模块函数(relay/rotate 相关测试用)。
|
||||
for name in ("_validate_sub2api_key", "_list_sub2api_models",
|
||||
"_rotate_sub2api_key"):
|
||||
# 恢复猴子补丁过的模块函数(relay 在 handler_identity,
|
||||
# rotate 仍在 server;补丁目标跟随方法定义模块)。
|
||||
for mod, name in ((handler_identity_mod, "_validate_sub2api_key"),
|
||||
(handler_identity_mod, "_list_sub2api_models"),
|
||||
(server_mod, "_rotate_sub2api_key")):
|
||||
try:
|
||||
mock.patch.object(server_mod, name).stop()
|
||||
mock.patch.object(mod, name).stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -437,7 +446,7 @@ class ServerP1TestCase(unittest.TestCase):
|
||||
{"name": "ok", "id": "y" * 65}, self.owner_key)
|
||||
self.assertEqual(s, 400, body)
|
||||
self.assertIn("64", body["error"])
|
||||
server_mod.MAX_PROJECTS_PER_USER = 1
|
||||
handler_project_mod.MAX_PROJECTS_PER_USER = 1
|
||||
# setUp 已建 1 个 owner 项目,限额 1 时再建即超限。
|
||||
s, body = _req(self.port, "POST", "/projects",
|
||||
{"name": "second"}, self.owner_key)
|
||||
@@ -488,27 +497,27 @@ class ServerP1TestCase(unittest.TestCase):
|
||||
# -- sub2api 懒开通 / 轮换端点 --------------------------------------
|
||||
def test_relay_models_lazy_provision_not_just_pending(self):
|
||||
os.environ["CLOUD_GATEWAY"] = "sub2api"
|
||||
orig_validate = server_mod._validate_sub2api_key
|
||||
orig_list = server_mod._list_sub2api_models
|
||||
orig_validate = handler_identity_mod._validate_sub2api_key
|
||||
orig_list = handler_identity_mod._list_sub2api_models
|
||||
try:
|
||||
server_mod._validate_sub2api_key = (
|
||||
handler_identity_mod._validate_sub2api_key = (
|
||||
lambda conn, uid: ("provisioned", "fake-key", 7, 9))
|
||||
server_mod._list_sub2api_models = (
|
||||
handler_identity_mod._list_sub2api_models = (
|
||||
lambda origin, key: ["model-a"])
|
||||
s, body = _req(self.port, "GET", "/relay/models",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(s, 200, body)
|
||||
self.assertEqual(body["models"], ["model-a"])
|
||||
self.assertEqual(body["provisioning"], "provisioned")
|
||||
server_mod._validate_sub2api_key = (
|
||||
handler_identity_mod._validate_sub2api_key = (
|
||||
lambda conn, uid: (_ for _ in ()).throw(
|
||||
Sub2apiError("网关暂不可用")))
|
||||
s, body = _req(self.port, "GET", "/relay/models",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(s, 502, body)
|
||||
finally:
|
||||
server_mod._validate_sub2api_key = orig_validate
|
||||
server_mod._list_sub2api_models = orig_list
|
||||
handler_identity_mod._validate_sub2api_key = orig_validate
|
||||
handler_identity_mod._list_sub2api_models = orig_list
|
||||
|
||||
def test_gateway_rotate_endpoint(self):
|
||||
s, body = _req(self.port, "POST", "/users/me/gateway/rotate",
|
||||
|
||||
Reference in new issue
Block a user