Files
awoooi/apps/api/tests/test_telegram_button_consistency.py
Your Name d3a4fb4d15 feat(t0): Task A 按鈕一致性測試 + Task C Gitea→Telegram 通知收尾
Task A — Telegram 按鈕鬼魂鐵律測試(補測 production telegram_gateway.py)
- test_telegram_button_consistency.py 新增 14 測試
  - send_info_notification 兩鍵 [📋 詳情][📊 歷史]
  - _send_approval_card_to_group reply_markup
  - callback_data 對齊 INFO_ACTIONS 白名單
  - parse_callback_data + handler 完整性

Task C — Gitea CI/CD → Telegram 告警轉發
- GiteaPullRequest.merged 欄位(HasMerged bool json:"merged")
- _send_gitea_notification helper:Redis SET NX EX 600s 去重
- handle_pull_request: closed+merged → PR Merged Telegram 卡片
- handle_workflow_run: status=failure → 部署/構建失敗卡片
- 不加按鈕(feedback_no_ghost_buttons.md 合規)
- test_gitea_webhook.py +247 行新測試

驗收: K8s GITEA_WEBHOOK_SECRET 64 bytes 
      Gitea hook #4 events: pull_request + push + workflow_run 
      端點 HMAC 401 驗簽 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:17:17 +08:00

198 lines
8.0 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.
"""
Telegram 按鈕一致性測試
======================
驗證 send_info_notification 與 _send_approval_card_to_group 的 reply_markup 鏈路
測試策略 (遵循 feedback_no_mock_testing.md):
- 直接讀取 source code 驗證實作結構(不發送實際 Telegram 訊息)
- 鬼魂按鈕鐵律feedback_no_ghost_buttons.md:
每個按鈕必須同時具備 callback_data 格式/handler/MCP能力
建立: 2026-04-25 (台北時區)
建立者: ogt + Claude Sonnet 4.6
"""
import re
def _read_gateway() -> str:
with open("src/services/telegram_gateway.py", encoding="utf-8") as f:
return f.read()
def _read_security_interceptor() -> str:
with open("src/services/security_interceptor.py", encoding="utf-8") as f:
return f.read()
# =============================================================================
# Test: send_info_notification 按鈕結構
# =============================================================================
class TestSendInfoNotificationKeyboard:
"""驗證 TYPE-1 純資訊通知現在附帶 read-only 按鈕"""
def test_send_info_notification_has_reply_markup(self):
"""send_info_notification payload 包含 reply_markup"""
source = _read_gateway()
# 取出 send_info_notification 函式體(從定義到下一個 async def
match = re.search(
r"async def send_info_notification.*?(?=\n async def |\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 send_info_notification 函式"
fn_body = match.group(0)
assert '"reply_markup"' in fn_body, (
"send_info_notification 必須在 payload 中包含 reply_markup"
)
def test_send_info_notification_detail_button_correct_format(self):
"""detail 按鈕使用 2-part info 格式 detail:{incident_id}"""
source = _read_gateway()
match = re.search(
r"async def send_info_notification.*?(?=\n async def |\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 send_info_notification 函式"
fn_body = match.group(0)
assert 'f"detail:{incident_id}"' in fn_body, (
"detail 按鈕 callback_data 必須使用 detail:{incident_id} 格式ADR-050 2-part info"
)
def test_send_info_notification_history_button_correct_format(self):
"""history 按鈕使用 2-part info 格式 history:{incident_id}"""
source = _read_gateway()
match = re.search(
r"async def send_info_notification.*?(?=\n async def |\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 send_info_notification 函式"
fn_body = match.group(0)
assert 'f"history:{incident_id}"' in fn_body, (
"history 按鈕 callback_data 必須使用 history:{incident_id} 格式ADR-050 2-part info"
)
def test_send_info_notification_no_nonce_in_buttons(self):
"""send_info_notification 按鈕不含 nonce純查類無副作用"""
source = _read_gateway()
match = re.search(
r"async def send_info_notification.*?(?=\n async def |\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 send_info_notification 函式"
fn_body = match.group(0)
assert "generate_callback_nonce" not in fn_body, (
"send_info_notification 的按鈕不應包含 nonceTYPE-1 為純資訊,不應有寫類操作)"
)
# =============================================================================
# Test: _send_approval_card_to_group 按鈕結構
# =============================================================================
class TestSendApprovalCardToGroupKeyboard:
"""驗證群組卡片現在附帶 read-only 按鈕"""
def test_group_card_calls_send_to_group_with_reply_markup(self):
"""_send_approval_card_to_group 呼叫 send_to_group 時傳遞 reply_markup"""
source = _read_gateway()
match = re.search(
r"async def _send_approval_card_to_group.*?(?=\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 _send_approval_card_to_group 函式"
fn_body = match.group(0)
assert "reply_markup" in fn_body, (
"_send_approval_card_to_group 必須傳 reply_markup 給 send_to_group"
)
def test_group_card_detail_button_correct_format(self):
"""群組卡片 detail 按鈕使用 2-part info 格式"""
source = _read_gateway()
match = re.search(
r"async def _send_approval_card_to_group.*?(?=\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 _send_approval_card_to_group 函式"
fn_body = match.group(0)
assert 'f"detail:{incident_id}"' in fn_body, (
"群組卡片 detail 按鈕必須使用 2-part info 格式"
)
def test_group_card_no_nonce_buttons(self):
"""群組卡片絕不包含 nonceADR-075 斷點 C防洩漏"""
source = _read_gateway()
match = re.search(
r"async def _send_approval_card_to_group.*?(?=\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 _send_approval_card_to_group 函式"
fn_body = match.group(0)
assert "generate_callback_nonce" not in fn_body, (
"_send_approval_card_to_group 群組訊息絕不含 nonceADR-075 斷點 C 安全設計)"
)
def test_group_card_keyboard_conditional_on_incident_id(self):
"""群組按鍵僅在有 incident_id 時才建立(防 empty callback_data"""
source = _read_gateway()
match = re.search(
r"async def _send_approval_card_to_group.*?(?=\n # ====)",
source,
re.DOTALL,
)
assert match, "找不到 _send_approval_card_to_group 函式"
fn_body = match.group(0)
# 確認有 if incident_id 守衛
assert "if incident_id" in fn_body, (
"群組按鍵必須有 if incident_id 守衛,避免 callback_data 空字串"
)
# =============================================================================
# Test: callback handler 完整性(鬼魂按鈕鐵律)
# =============================================================================
class TestCallbackHandlerCompleteness:
"""驗證 detail/history 按鈕有對應的 handler鬼魂按鈕鐵律三條件之二"""
def test_detail_action_in_info_actions_whitelist(self):
"""detail action 在 INFO_ACTIONS 白名單中"""
source = _read_security_interceptor()
assert '"detail"' in source, "detail 必須在 INFO_ACTIONS 白名單"
# 確認是在 INFO_ACTIONS 集合定義中
assert "INFO_ACTIONS" in source
def test_history_action_in_info_actions_whitelist(self):
"""history action 在 INFO_ACTIONS 白名單中"""
source = _read_security_interceptor()
assert '"history"' in source, "history 必須在 INFO_ACTIONS 白名單"
def test_detail_handler_exists_in_handle_callback(self):
"""handle_callback 有 detail handler 分支"""
source = _read_gateway()
assert 'action == "detail"' in source, (
"handle_callback 必須有 detail handler 分支"
)
def test_history_handler_exists_in_handle_callback(self):
"""handle_callback 有 history handler 分支"""
source = _read_gateway()
assert 'action == "history"' in source, (
"handle_callback 必須有 history handler 分支"
)
def test_send_incident_detail_method_exists(self):
"""_send_incident_detail 方法存在handler 的實際執行體)"""
assert "_send_incident_detail" in _read_gateway()
def test_send_incident_history_method_exists(self):
"""_send_incident_history 方法存在handler 的實際執行體)"""
assert "_send_incident_history" in _read_gateway()