"""sub2api 网关接入单测:管理 API 客户端 + 幂等开通 + provider 覆盖。 不跑真实模型、不联网:HTTP 经 request 可调用对象注入假实现; 开通走内存 SQLite;host/sandbox 侧只断言命令与 env 构造。 """ import os import sqlite3 import sys import unittest from pathlib import Path try: from cloud import sub2api from cloud.sub2api import Sub2apiAdmin, ensure_user_gateway_key except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud import sub2api from cloud.sub2api import Sub2apiAdmin, ensure_user_gateway_key def _conn(): conn = sqlite3.connect(":memory:") conn.row_factory = sqlite3.Row return conn class FakeApi: """按 (method, path) 录调用、按表回包的假 sub2api。""" def __init__(self): self.calls = [] self.admin_token = "adm-tok" self.user_token = "usr-tok" self.groups = [{"id": 2, "name": "codex-system"}] self.next_user_id = 10 self.next_key_id = 20 self.fail_refresh_once = False self.balance_calls = [] self.fail_balance = False def __call__(self, method, url, body=None, token=None): path = url.split("28084", 1)[-1] if "28084" in url else url self.calls.append((method, path, body, token)) if path == "/api/v1/auth/login" and body.get("email") == "adm@x": return 200, {"data": {"access_token": self.admin_token, "refresh_token": "adm-ref", "expires_in": 3600}} if path == "/api/v1/auth/login": return 200, {"data": {"access_token": self.user_token, "refresh_token": "usr-ref", "expires_in": 3600}} if path == "/api/v1/auth/refresh": return 200, {"data": {"access_token": "new-tok", "refresh_token": "new-ref", "expires_in": 3600}} if path.startswith("/api/v1/admin/users/") \ and path.endswith("/balance") and method == "POST": self.balance_calls.append(body) if self.fail_balance: return 400, {"message": "bad balance"} return 200, {"data": {"balance": body.get("balance")}} if path == "/api/v1/admin/users" and method == "POST": self.next_user_id += 1 return 200, {"data": {"id": self.next_user_id}} if path == "/api/v1/admin/groups" and method == "GET": return 200, {"data": self.groups} if path == "/api/v1/keys" and method == "POST": self.next_key_id += 1 return 200, {"data": {"id": self.next_key_id, "key": "sk-probe-%d" % self.next_key_id}} if path == "/v1/models" and method == "GET": return 200, {"data": [{"id": "gpt-5"}, {"id": "claude-x"}]} return 404, {"message": "not found"} class TestSub2apiAdmin(unittest.TestCase): def setUp(self): self.api = FakeApi() self.admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw", request=self.api) def test_create_user_and_group_lookup(self): uid = self.admin.create_user("u@x", "pw", "u1") self.assertEqual(uid, 11) self.assertEqual(self.admin.resolve_group_id(), 2) def test_401_refreshes_and_retries(self): # 第一次管理调用先 401:注入一个先 401 后放行的 request。 calls = {"n": 0} real = self.api def flaky(method, url, body=None, token=None): calls["n"] += 1 if calls["n"] == 2: return 401, {"message": "expired"} return real(method, url, body, token) admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw", request=flaky) uid = admin.create_user("u@x", "pw", "u1") self.assertEqual(uid, 11) paths = [c[1] for c in self.api.calls] self.assertIn("/api/v1/auth/refresh", paths) class TestEnsureGatewayKey(unittest.TestCase): def setUp(self): self.api = FakeApi() self.admin = Sub2apiAdmin("http://x:28084", "adm@x", "pw", request=self.api) self.env = {"SUB2API_BASE_URL": "http://x:28084", "SUB2API_GROUP_NAME": "codex-system"} self.old = {k: os.environ.get(k) for k in self.env} os.environ.update(self.env) def tearDown(self): for k, v in self.old.items(): if v is None: os.environ.pop(k, None) else: os.environ[k] = v def test_provision_then_idempotent(self): conn = _conn() key = ensure_user_gateway_key(conn, "user-abc", admin=self.admin, request=self.api) self.assertTrue(key.startswith("sk-probe-")) calls_before = len(self.api.calls) key2 = ensure_user_gateway_key(conn, "user-abc", admin=self.admin, request=self.api) self.assertEqual(key, key2) # 第二次是纯读库,不再调管理 API。 self.assertEqual(len(self.api.calls), calls_before) row = conn.execute( "SELECT sub2api_user_id, refresh_token FROM user_gateway_keys" " WHERE user_id = ?", ("user-abc",)).fetchone() self.assertEqual(row[0], 11) self.assertEqual(row[1], "usr-ref") def test_list_models(self): models = sub2api.list_gateway_models("http://x:28084", "sk-1", request=self.api) self.assertEqual(models, ["gpt-5", "claude-x"]) def test_provision_grants_opening_balance(self): conn = _conn() key = ensure_user_gateway_key(conn, "user-bal", admin=self.admin, request=self.api) self.assertTrue(key.startswith("sk-probe-")) self.assertEqual(len(self.api.balance_calls), 1) self.assertEqual(self.api.balance_calls[0]["balance"], 10.0) self.assertEqual(self.api.balance_calls[0]["operation"], "set") def test_provision_balance_failure_cleans_up(self): self.api.fail_balance = True conn = _conn() with self.assertRaises(sub2api.Sub2apiError): ensure_user_gateway_key(conn, "user-fail", admin=self.admin, request=self.api) paths = [(m, p) for (m, p, _b, _t) in self.api.calls] self.assertIn(("DELETE", "/api/v1/keys/21"), paths) self.assertIn(("DELETE", "/api/v1/admin/users/11"), paths) row = conn.execute( "SELECT COUNT(*) FROM user_gateway_keys" " WHERE user_id = ?", ("user-fail",)).fetchone() self.assertEqual(row[0], 0) def test_provision_zero_opening_balance_skips_grant(self): old = os.environ.get("CLOUD_SUB2API_OPENING_BALANCE_USD") os.environ["CLOUD_SUB2API_OPENING_BALANCE_USD"] = "0" try: conn = _conn() ensure_user_gateway_key(conn, "user-zero", admin=self.admin, request=self.api) finally: if old is None: os.environ.pop("CLOUD_SUB2API_OPENING_BALANCE_USD", None) else: os.environ["CLOUD_SUB2API_OPENING_BALANCE_USD"] = old self.assertEqual(self.api.balance_calls, []) class TestProviderCommand(unittest.TestCase): def test_server_command_sub2api(self): sys.path.insert(0, str(Path(__file__).resolve().parents[2])) import host old = os.environ.get("SUB2API_BASE_URL") os.environ["SUB2API_BASE_URL"] = "http://172.17.0.1:28084" try: cmd = host._server_command("/bin/codex", provider="sub2api") finally: if old is None: os.environ.pop("SUB2API_BASE_URL", None) else: os.environ["SUB2API_BASE_URL"] = old self.assertIn("app-server", cmd[-1]) joined = " ".join(cmd) self.assertIn('model_provider="sub2api"', joined) self.assertIn("http://172.17.0.1:28084/v1", joined) self.assertIn('env_key="SUB2API_API_KEY"', joined) def test_server_command_sub2api_needs_base_url(self): sys.path.insert(0, str(Path(__file__).resolve().parents[2])) import host old = os.environ.pop("SUB2API_BASE_URL", None) try: with self.assertRaises(RuntimeError): host._server_command("/bin/codex", provider="sub2api") finally: if old is not None: os.environ["SUB2API_BASE_URL"] = old def test_server_command_unknown_provider(self): sys.path.insert(0, str(Path(__file__).resolve().parents[2])) import host with self.assertRaises(ValueError): host._server_command("/bin/codex", provider="nope") class TestSandboxProviderFlag(unittest.TestCase): def test_base_command_flag(self): from cloud.sandbox import LocalExecutor, DockerExecutor local = LocalExecutor("/tmp/host.py") cmd = local._base_command(model_provider="sub2api") self.assertIn("--model-provider", cmd) self.assertIn("sub2api", cmd) docker = DockerExecutor("/tmp/host.py") inner = docker._inner_host_command("dispatch", model_provider="sub2api") self.assertIn("--model-provider", inner) def test_run_local_env_merge_and_loud_fail(self): from cloud.sandbox import LocalExecutor seen = {} def runner(cmd, env=None): seen["env"] = env return object() local = LocalExecutor("/tmp/host.py", runner=runner) local._run_local(["x"], {"SUB2API_API_KEY": "sk-1"}) self.assertEqual(seen["env"]["SUB2API_API_KEY"], "sk-1") def cmd_only(cmd): return object() local2 = LocalExecutor("/tmp/host.py", runner=cmd_only) with self.assertRaises(RuntimeError): local2._run_local(["x"], {"SUB2API_API_KEY": "sk-1"}) class TestDockerSecretEnv(unittest.TestCase): def _docker(self, runner): from cloud.sandbox import DockerExecutor ex = DockerExecutor("/tmp/host.py", runner=runner, env_key=None) ex._check_docker = lambda: None return ex def test_secret_name_only_in_argv(self): from cloud.sandbox import DockerExecutor seen = {} def runner(cmd, timeout=None, env=None): seen["cmd"] = cmd seen["env"] = env return object() ex = self._docker(runner) task = {"id": "abc", "prompt": "hi"} cmd = ex.build_run_command( task, ["python3", "/app/host.py", "status"], extra_env={"SUB2API_API_KEY": "sk-secret", "HTTP_PROXY": "http://x:8888"}) self.assertIn("SUB2API_API_KEY", cmd) self.assertNotIn("SUB2API_API_KEY=sk-secret", cmd) self.assertIn("HTTP_PROXY=http://x:8888", cmd) env = ex._docker_cli_env({"SUB2API_API_KEY": "sk-secret"}) self.assertEqual(env["SUB2API_API_KEY"], "sk-secret") self.assertIsNone(ex._docker_cli_env({"HTTP_PROXY": "http://x:1"})) self.assertIsNone(ex._docker_cli_env(None)) def test_call_runner_env_loud_fail(self): from cloud.sandbox import DockerExecutor def cmd_only(cmd, timeout=None): return object() ex = self._docker(cmd_only) with self.assertRaises(RuntimeError): ex._call_runner(["docker"], env={"SUB2API_API_KEY": "sk-1"}) if __name__ == "__main__": unittest.main()