238 lines
6.7 KiB
Markdown
238 lines
6.7 KiB
Markdown
# LINE / Telegram 驗證整合煙霧測試模式
|
||
|
||
> 這份文件給未來要補或修改 smoke / integration test 的人看。
|
||
>
|
||
> 目標不是「再寫一批單元測試」,而是驗證:**真事件路徑接起來後,整條驗證 / 授權 / handoff 水管沒有漏水。**
|
||
|
||
相關背景:
|
||
- `references/line-telegram-verification-lifecycle.md`
|
||
- `references/line-telegram-test-map.md`
|
||
- `references/line-telegram-must-run-commands.md`
|
||
|
||
---
|
||
|
||
## 什麼情況該寫 smoke test
|
||
|
||
當你改到下面任一類時,不能只靠 helper / route unit tests:
|
||
|
||
- LINE / Telegram adapter intake gate
|
||
- onboarding / verification state machine
|
||
- verified email handoff guard
|
||
- quota fallback / email fallback
|
||
- admin unbind / pairing revoke
|
||
- runner authz 與 verified binding 的交界
|
||
|
||
一句話判斷:
|
||
|
||
> 如果這個改動需要同時假設「adapter 有正確 dispatch」和「store / authz 也會照預期接住」,就該補 smoke test。
|
||
|
||
---
|
||
|
||
## Smoke test 的最小骨架
|
||
|
||
一支合格的 LINE / Telegram 驗證 smoke test,至少要穿過這些步驟:
|
||
|
||
1. **真平台事件進 adapter**
|
||
2. **adapter 真的 dispatch 到 `handle_message(...)`**
|
||
3. **`GatewayUserStore.process_inbound_message(...)` 產生 onboarding / verification decision**
|
||
4. **`verify_token(...)` 後 verified binding 生效**
|
||
5. **live auth / handoff 路徑真的吃到 verified binding**
|
||
6. **`unbind_identity(...)` 後 verified / pairing / authz / handoff 失效**
|
||
7. **同一來源再次進來時,可重新 onboarding**
|
||
|
||
如果只測到第 3 步或第 4 步,通常還不算 smoke,只算比較胖的 unit test。
|
||
|
||
---
|
||
|
||
## 目前已存在的參考實作
|
||
|
||
檔案:
|
||
- `tests/gateway/test_line_telegram_verification_smoke.py`
|
||
|
||
關鍵測試:
|
||
- `test_line_dm_verification_smoke`
|
||
- `test_telegram_dm_verification_smoke`
|
||
|
||
這兩顆目前已覆蓋:
|
||
|
||
### LINE
|
||
- DM event 經 `LineAdapter._dispatch_event(...)` 進來
|
||
- 進 `GatewayUserStore.process_inbound_message(...)`
|
||
- `verify_token(...)` 後 `get_verified_email_for_source(...)` 生效
|
||
- `_send_email_quota_fallback(...)` 真的寄到 live 綁定 email
|
||
- `unbind_identity(...)` 後 fallback 失效
|
||
- 同來源再次進來可重新要求驗證
|
||
|
||
### Telegram
|
||
- text update 經 `TelegramAdapter._handle_text_message(...)` 進來
|
||
- text batch flush 後真的 dispatch
|
||
- `verify_token(...)` 後 verified source 生效
|
||
- `PairingStore()._approve_user(...)` 後 runner authz 生效
|
||
- `unbind_identity(...)` 後 runner authz 失效
|
||
- 同來源再次進來可重新要求驗證
|
||
|
||
---
|
||
|
||
## 兩個最容易踩的坑
|
||
|
||
### 1. Telegram text handler 不是同步直送
|
||
|
||
**症狀:**
|
||
- 你呼叫 `_handle_text_message(...)`
|
||
- 但 `decisions` / `handle_message` 沒收到任何東西
|
||
- 看起來像 Telegram 流程壞掉
|
||
|
||
**真正原因:**
|
||
- Telegram text path 先進 `_enqueue_text_event(...)`
|
||
- 真正 dispatch 發生在 `_flush_text_batch(...)`
|
||
- 如果測試沒等 flush,會誤判成流程沒進去
|
||
|
||
**建議模式:**
|
||
|
||
```python
|
||
async def _run_telegram_text(adapter, update):
|
||
await adapter._handle_text_message(update, SimpleNamespace())
|
||
pending = list(adapter._pending_text_batch_tasks.values())
|
||
if pending:
|
||
await asyncio.gather(*pending)
|
||
```
|
||
|
||
**原則:**
|
||
不要只呼叫 `_handle_text_message()` 就假設 event 已送達。
|
||
|
||
---
|
||
|
||
### 2. LINE / Telegram 第二次重送 onboarding 可能撞到 resend cooldown
|
||
|
||
**症狀:**
|
||
- 你在 unbind 後立刻重送同一個 email
|
||
- 預期拿到 `send_verification_email`
|
||
- 實際卻拿到 `handled`
|
||
|
||
**真正原因:**
|
||
- `verification_requests.last_sent_at` 還在 cooldown 視窗內
|
||
- 你撞到 resend / rate-limit 保護,不是狀態機壞掉
|
||
|
||
**建議模式:**
|
||
在本地 smoke test 裡,把 `last_sent_at` 人工往前調,讓你驗的是「重新 onboarding 能否成立」,不是節流是否生效。
|
||
|
||
```sql
|
||
UPDATE verification_requests
|
||
SET last_sent_at = datetime('now', '-11 minutes')
|
||
WHERE platform = ? AND external_user_id = ?
|
||
```
|
||
|
||
**原則:**
|
||
當測試目標是驗證狀態轉換,允許把節流窗移開;但要在註解或文件說明這是為了避開 rate-limit 噪音。
|
||
|
||
---
|
||
|
||
## 建議測試設計原則
|
||
|
||
### A. 盡量走真 adapter 入口
|
||
|
||
優先選:
|
||
- LINE:`_dispatch_event(...)`
|
||
- Telegram:`_handle_text_message(...)`
|
||
|
||
比起直接手刻 `MessageEvent` 然後呼叫 `handle_message(...)`,這樣更能抓到:
|
||
- source 解析錯誤
|
||
- DM / group / room 分流錯誤
|
||
- prefilter / batching / postback 之類的邊界問題
|
||
|
||
### B. mock 要薄,不要整條路都 fake 掉
|
||
|
||
可以 mock:
|
||
- LINE client 的 reply / push / loading
|
||
- email send function
|
||
- bot network calls
|
||
|
||
不要 mock 到:
|
||
- `GatewayUserStore.process_inbound_message(...)`
|
||
- `verify_token(...)`
|
||
- `unbind_identity(...)`
|
||
- runner `_is_user_authorized(...)`
|
||
|
||
否則你只是把 smoke test 再拆回單元測試 cosplay。
|
||
|
||
### C. 要驗「生效」與「失效」兩面
|
||
|
||
不夠只驗:
|
||
- verify 後可用
|
||
|
||
還要驗:
|
||
- unbind 後失效
|
||
- pairing revoke 後失效
|
||
- fallback / handoff 不再寄到舊綁定 email
|
||
|
||
### D. 最後一輪最好重跑 onboarding
|
||
|
||
真正容易壞的常常不是第一次 onboarding,而是:
|
||
- verify 過一次
|
||
- unbind
|
||
- 再次 onboarding
|
||
|
||
所以 smoke test 最後建議再讓同一來源進來一次,確認沒有殘留狀態把它卡死。
|
||
|
||
---
|
||
|
||
## 建議命名方式
|
||
|
||
推薦:
|
||
- `test_line_dm_verification_smoke`
|
||
- `test_telegram_dm_verification_smoke`
|
||
- `test_<platform>_<path>_smoke`
|
||
|
||
不推薦:
|
||
- `test_integration_1`
|
||
- `test_full_flow`
|
||
- `test_regression`
|
||
|
||
原則是:
|
||
**檔名 / test name 一眼要看得出是哪個平台、哪條路、是不是 smoke。**
|
||
|
||
---
|
||
|
||
## 什麼時候要擴充既有 smoke,而不是另開新檔
|
||
|
||
優先擴充 `tests/gateway/test_line_telegram_verification_smoke.py`,如果你的改動仍屬於:
|
||
- DM onboarding / verification
|
||
- verified binding / handoff
|
||
- unbind / pairing revoke
|
||
- authz 存活與失效
|
||
|
||
只有在測試目標已明顯換成另一條主線時,再另開檔,例如:
|
||
- forum / topic / group 專用 state machine
|
||
- webhook retry / duplicate event idempotency
|
||
- home-channel / control-channel 特化行為
|
||
|
||
---
|
||
|
||
## Smoke test 完成後至少再跑什麼
|
||
|
||
最少建議:
|
||
|
||
```bash
|
||
pytest -q \
|
||
tests/gateway/test_line_telegram_verification_smoke.py \
|
||
tests/hermes_cli/test_web_verification_admin.py \
|
||
tests/gateway/test_line_plugin.py \
|
||
tests/gateway/test_telegram_auth_check.py \
|
||
tests/gateway/test_verified_email_handoff_guard_helpers.py
|
||
```
|
||
|
||
如果改到 pairing / allowlist bypass,再加:
|
||
|
||
```bash
|
||
pytest -q \
|
||
tests/gateway/test_pairing.py \
|
||
tests/gateway/test_pairing_allowlist_bypass.py
|
||
```
|
||
|
||
---
|
||
|
||
## 一句話總結
|
||
|
||
> Smoke test 的任務不是證明某個 helper 很乖,
|
||
> 而是證明 **LINE / Telegram 真事件進來後,adapter、store、authz、handoff、unbind 這幾層還能一起活著回家。**
|