Files
codex-app-server/cloud/tests/test_server_oauth_push.py
T

178 lines
6.7 KiB
Python

"""OAuth 与推送的 HTTP 端点测试。
不联网、不向真实 GitHub/Google/推送服务发请求;OAuth 回调成功
路径由模块级单测覆盖,这里只测 HTTP 层的失败路径与订阅分发。
"""
import http.client
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.oauth import OAuthStateStore
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.oauth import OAuthStateStore
from cloud.server import create_server
class OAuthPushEndpointsTestCase(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.outsider_id, self.outsider_key = self.auth.create_user("outsider")
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()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
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
# -- OAuth -----------------------------------------------------------
def test_oauth_login_returns_url_without_secret(self):
status, body = self._req(
"GET",
"/oauth/github/login?client_id=test-client"
"&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcb")
self.assertEqual(status, 200)
self.assertIn("github.com/login/oauth/authorize",
body["authorization_url"])
self.assertIn("test-client", body["authorization_url"])
self.assertNotIn("secret", body["authorization_url"])
self.assertTrue(body["state"])
def test_oauth_login_unknown_provider_400(self):
status, _ = self._req(
"GET",
"/oauth/nope/login?client_id=x"
"&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcb")
self.assertEqual(status, 400)
def test_oauth_callback_bad_state_400_no_session(self):
status, body = self._req(
"POST", "/oauth/github/callback",
{"code": "fake-code", "state": "no-such-state",
"client_id": "test-client",
"redirect_uri": "http://127.0.0.1/cb"})
self.assertEqual(status, 400)
self.assertIn("state", body["error"])
def test_oauth_callback_good_state_no_secret_400(self):
store = OAuthStateStore(self.db_path)
try:
state = store.create_state(
"github", "http://127.0.0.1/cb")
finally:
store.conn.close()
# 确保环境里没有真实 secret(本测试不依赖它)。
old = os.environ.pop("GITHUB_OAUTH_CLIENT_SECRET", None)
try:
status, body = self._req(
"POST", "/oauth/github/callback",
{"code": "fake-code", "state": state,
"client_id": "test-client",
"redirect_uri": "http://127.0.0.1/cb"})
finally:
if old is not None:
os.environ["GITHUB_OAUTH_CLIENT_SECRET"] = old
self.assertEqual(status, 400)
self.assertIn("client_secret", body["error"])
# -- 推送 -------------------------------------------------------------
def test_push_subscribe_list_dispatch_and_delete(self):
pid = self.project["id"]
status, sub = self._req(
"POST", "/projects/%s/push/subscriptions" % pid,
{"channel": "webhook", "target": "https://example.invalid/hook",
"preference": "instant"},
self.owner_key)
self.assertEqual(status, 201, sub)
status, body = self._req(
"GET", "/projects/%s/push/subscriptions" % pid,
key=self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(len(body["subscriptions"]), 1)
# HTTP 层无注入 sender:分发应报“无 sender”,不静默成功。
status, body = self._req(
"POST", "/admin/push/dispatch",
{"event_type": "failed", "project_id": pid,
"payload": {"task_id": "t1"}},
self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(len(body["errors"]), 1)
self.assertIn("no sender", body["errors"][0]["reason"])
# outsider 不能往别人项目里订阅/分发。
status, _ = self._req(
"POST", "/projects/%s/push/subscriptions" % pid,
{"channel": "webhook", "target": "https://example.invalid/x"},
self.outsider_key)
self.assertEqual(status, 403)
# 删除:DELETE 方法须可用,且删后列表为空。
conn = http.client.HTTPConnection("127.0.0.1", self.port, timeout=10)
try:
conn.request("DELETE",
"/projects/%s/push/subscriptions/%s" % (pid, sub["id"]),
headers={"X-API-Key": self.owner_key})
resp = conn.getresponse()
resp.read()
self.assertEqual(resp.status, 200)
finally:
conn.close()
status, body = self._req(
"GET", "/projects/%s/push/subscriptions" % pid,
key=self.owner_key)
self.assertEqual(body["subscriptions"], [])
def test_push_dispatch_requires_auth(self):
status, _ = self._req(
"POST", "/admin/push/dispatch",
{"event_type": "completed", "project_id": self.project["id"]})
self.assertEqual(status, 401)
if __name__ == "__main__":
unittest.main()