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

237 lines
8.3 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.
"""webhooks.py 单测:签名校验、事件解析、命令派发、用户映射、通知门禁。
只用标准库,不联网、不连真实 GitHub/Slack、不跑真实模型回合。
签名测试用的 secret 均为单测内临时值,不是生产密钥。
"""
import hashlib
import hmac
import os
import sys
import tempfile
import time
import unittest
# 本文件位于 cloud/tests/,被测模块位于 cloud/;cloud/ 目前不是包
# (协调员统一创建 __init__.py),故把 cloud/ 加到 sys.path 后按
# 顶层模块导入 webhooks。
sys.path.insert(
0, os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
)
import webhooks # noqa: E402
from webhooks import ( # noqa: E402
ALLOWED_NOTIFICATION_EVENTS,
NotificationService,
SlackUserMap,
parse_github_pr_comment,
parse_slack_command,
verify_github_signature,
verify_slack_signature,
)
def _github_signature(secret, payload):
return "sha256=" + hmac.new(
secret.encode("utf-8"), payload, hashlib.sha256
).hexdigest()
def _slack_signature(secret, timestamp, body):
basestring = f"v0:{timestamp}:{body}"
return "v0=" + hmac.new(
secret.encode("utf-8"), basestring.encode("utf-8"), hashlib.sha256
).hexdigest()
class TestGithubSignature(unittest.TestCase):
def test_correct_signature_passes(self):
secret = "test-github-secret"
payload = b'{"action":"created"}'
header = _github_signature(secret, payload)
self.assertTrue(verify_github_signature(secret, payload, header))
def test_wrong_signature_rejected(self):
secret = "test-github-secret"
payload = b'{"action":"created"}'
header = _github_signature("wrong-secret", payload)
self.assertFalse(verify_github_signature(secret, payload, header))
def test_tampered_payload_rejected(self):
secret = "test-github-secret"
header = _github_signature(secret, b'{"action":"created"}')
self.assertFalse(
verify_github_signature(secret, b'{"action":"deleted"}', header)
)
def test_missing_header_rejected(self):
self.assertFalse(
verify_github_signature("test-github-secret", b"{}", None)
)
self.assertFalse(verify_github_signature("test-github-secret", b"{}", ""))
def test_missing_secret_rejected(self):
payload = b"{}"
self.assertFalse(verify_github_signature("", payload, "sha256=abc"))
self.assertFalse(verify_github_signature(None, payload, "sha256=abc"))
class TestParseGithubPrComment(unittest.TestCase):
def _payload(self, body, action="created"):
return {
"action": action,
"repository": {"full_name": "owner/repo"},
"issue": {"number": 42},
"comment": {"body": body, "user": {"login": "alice"}},
}
def test_fix_review_comment_parsed(self):
result = parse_github_pr_comment(
self._payload("请帮忙 /fix-review 看一下")
)
self.assertEqual(
result,
{
"type": "fix_review",
"repo": "owner/repo",
"pr_number": 42,
"comment": "请帮忙 /fix-review 看一下",
"author": "alice",
},
)
def test_comment_without_trigger_returns_none(self):
self.assertIsNone(parse_github_pr_comment(self._payload("好看,LGTM")))
def test_non_created_action_returns_none(self):
self.assertIsNone(
parse_github_pr_comment(
self._payload("有 /fix-review 但是已编辑", action="edited")
)
)
class TestSlackSignature(unittest.TestCase):
def test_correct_signature_passes(self):
secret = "test-slack-secret"
timestamp = str(int(time.time()))
body = "command=/codex&text=status"
header = _slack_signature(secret, timestamp, body)
self.assertTrue(
verify_slack_signature(secret, timestamp, body, header)
)
def test_wrong_signature_rejected(self):
secret = "test-slack-secret"
timestamp = str(int(time.time()))
body = "command=/codex&text=status"
header = _slack_signature("wrong-secret", timestamp, body)
self.assertFalse(
verify_slack_signature(secret, timestamp, body, header)
)
def test_expired_timestamp_rejected(self):
# 超过 300 秒:用正确密钥签一个 301 秒前的时间戳,仍应拒绝。
secret = "test-slack-secret"
timestamp = str(int(time.time()) - 301)
body = "command=/codex&text=status"
header = _slack_signature(secret, timestamp, body)
self.assertFalse(
verify_slack_signature(secret, timestamp, body, header)
)
def test_missing_params_rejected(self):
timestamp = str(int(time.time()))
self.assertFalse(
verify_slack_signature("", timestamp, "body", "v0=abc")
)
self.assertFalse(
verify_slack_signature("test-slack-secret", timestamp, "body", None)
)
class TestParseSlackCommand(unittest.TestCase):
def test_dispatch_prompt(self):
self.assertEqual(
parse_slack_command("/codex 做一个加法函数"),
{"command": "codex", "prompt": "做一个加法函数"},
)
def test_dispatch_status(self):
self.assertEqual(
parse_slack_command("/codex status my-task"),
{"command": "status", "task": "my-task"},
)
def test_empty_or_non_codex_returns_none_command(self):
self.assertEqual(
parse_slack_command(""), {"command": None, "prompt": ""}
)
self.assertEqual(
parse_slack_command("hello"),
{"command": None, "prompt": "hello"},
)
self.assertEqual(
parse_slack_command("/other do something"),
{"command": None, "prompt": "/other do something"},
)
class TestSlackUserMap(unittest.TestCase):
def test_map_and_lookup(self):
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "slack_users.db")
user_map = SlackUserMap(db_path)
try:
self.assertIsNone(user_map.lookup("U123"))
user_map.map("U123", "user-1")
self.assertEqual(user_map.lookup("U123"), "user-1")
# 覆盖映射
user_map.map("U123", "user-2")
self.assertEqual(user_map.lookup("U123"), "user-2")
# 未映射的 id 仍返回 None
self.assertIsNone(user_map.lookup("U999"))
finally:
user_map.close()
class TestNotificationService(unittest.TestCase):
def test_allowed_event_sent_and_senders_called(self):
calls = {"webhook": [], "slack": []}
service = NotificationService(
webhook_sender=lambda payload: calls["webhook"].append(payload),
slack_sender=lambda payload: calls["slack"].append(payload),
)
result = service.notify("completed", "task-1", "proj-1", "done")
self.assertEqual(result, {"sent": True, "channels": ["webhook", "slack"]})
self.assertEqual(len(calls["webhook"]), 1)
self.assertEqual(len(calls["slack"]), 1)
self.assertEqual(calls["webhook"][0]["event_type"], "completed")
self.assertEqual(calls["webhook"][0]["task_id"], "task-1")
def test_disallowed_event_not_sent_and_senders_not_called(self):
calls = []
service = NotificationService(
webhook_sender=lambda payload: calls.append(("webhook", payload)),
slack_sender=lambda payload: calls.append(("slack", payload)),
)
result = service.notify("running", "task-1", "proj-1")
self.assertEqual(result, {"sent": False, "reason": "event not allowed"})
self.assertEqual(calls, [])
def test_no_senders_does_not_error(self):
service = NotificationService()
result = service.notify("completed", "task-1", "proj-1")
self.assertEqual(result, {"sent": True, "channels": []})
def test_allowed_events_set(self):
self.assertEqual(
ALLOWED_NOTIFICATION_EVENTS,
{"completed", "failed", "goal-unmet", "merge-failed",
"review-requested"},
)
if __name__ == "__main__":
unittest.main()