test(api): P1-3/P1-4 ApprovalRequestCreate + Telegram 測試
P1-3: ApprovalRequestCreate 欄位對齊測試 (13 tests) - 必填欄位驗證 (action, description, requested_by) - BlastRadius Model 驗證 - SignOz/Sentry/GitHub Webhook 格式驗證 - Pydantic v2 額外欄位行為驗證 P1-4: Telegram 整合驗證測試 (19 tests) - SignOzMetricsBlock 格式化 - TelegramMessage 結構 - 風險等級 Emoji 映射 - Webhook → Telegram 訊息流程 遵循: feedback_no_mock_testing.md (禁止 Mock) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
303
apps/api/tests/test_approval_field_alignment.py
Normal file
303
apps/api/tests/test_approval_field_alignment.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
P1-3: ApprovalRequestCreate 欄位對齊測試
|
||||
=========================================
|
||||
驗證 SignOz + GitHub Webhook 的 ApprovalRequestCreate 符合 ApprovalRequestBase schema
|
||||
|
||||
測試策略 (遵循 feedback_no_mock_testing.md):
|
||||
- 直接測試 Pydantic Model 驗證
|
||||
- 使用真實 Schema 驗證欄位
|
||||
- 不使用 Mock
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-29 (台北時區)
|
||||
建立者: Claude Code (P1-3 Unit Test)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.models.approval import (
|
||||
ApprovalRequestCreate,
|
||||
ApprovalRequestBase,
|
||||
BlastRadius,
|
||||
DataImpact,
|
||||
DryRunCheck,
|
||||
RiskLevel,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: ApprovalRequestCreate Schema 驗證
|
||||
# =============================================================================
|
||||
|
||||
class TestApprovalRequestCreateSchema:
|
||||
"""測試 ApprovalRequestCreate 必填欄位"""
|
||||
|
||||
def test_required_fields_present(self):
|
||||
"""✅ 必填欄位: action, description, risk_level, requested_by"""
|
||||
request = ApprovalRequestCreate(
|
||||
action="Test Action",
|
||||
description="Test Description",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test-webhook",
|
||||
)
|
||||
assert request.action == "Test Action"
|
||||
assert request.description == "Test Description"
|
||||
assert request.risk_level == RiskLevel.MEDIUM
|
||||
assert request.requested_by == "test-webhook"
|
||||
|
||||
def test_missing_action_raises_error(self):
|
||||
"""❌ 缺少 action 應拋出 ValidationError"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ApprovalRequestCreate(
|
||||
description="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test",
|
||||
)
|
||||
assert "action" in str(exc_info.value)
|
||||
|
||||
def test_missing_description_raises_error(self):
|
||||
"""❌ 缺少 description 應拋出 ValidationError"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ApprovalRequestCreate(
|
||||
action="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test",
|
||||
)
|
||||
assert "description" in str(exc_info.value)
|
||||
|
||||
def test_missing_requested_by_raises_error(self):
|
||||
"""❌ 缺少 requested_by 應拋出 ValidationError"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ApprovalRequestCreate(
|
||||
action="Test",
|
||||
description="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
)
|
||||
assert "requested_by" in str(exc_info.value)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: BlastRadius Model 驗證
|
||||
# =============================================================================
|
||||
|
||||
class TestBlastRadiusSchema:
|
||||
"""測試 BlastRadius 是 Pydantic Model,不是 Enum"""
|
||||
|
||||
def test_blast_radius_is_model(self):
|
||||
"""✅ BlastRadius 是 BaseModel,可接受參數"""
|
||||
br = BlastRadius(
|
||||
affected_pods=1,
|
||||
estimated_downtime="0",
|
||||
related_services=["api"],
|
||||
data_impact=DataImpact.READ_ONLY,
|
||||
)
|
||||
assert br.affected_pods == 1
|
||||
assert br.estimated_downtime == "0"
|
||||
assert br.related_services == ["api"]
|
||||
assert br.data_impact == DataImpact.READ_ONLY
|
||||
|
||||
def test_blast_radius_default_values(self):
|
||||
"""✅ BlastRadius 有預設值"""
|
||||
br = BlastRadius()
|
||||
assert br.affected_pods == 0
|
||||
assert br.estimated_downtime == "0"
|
||||
assert br.related_services == []
|
||||
assert br.data_impact == DataImpact.NONE
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: SignOz Webhook ApprovalRequestCreate 格式
|
||||
# =============================================================================
|
||||
|
||||
class TestSignOzWebhookApproval:
|
||||
"""測試 SignOz Webhook 的 ApprovalRequestCreate 格式"""
|
||||
|
||||
def test_signoz_approval_format(self):
|
||||
"""✅ SignOz Webhook 應使用正確欄位格式"""
|
||||
# 模擬 signoz_webhook.py 的建立方式
|
||||
alert_name = "HighErrorRate"
|
||||
service_name = "api-service"
|
||||
description = "Error rate > 5%"
|
||||
|
||||
request = ApprovalRequestCreate(
|
||||
action=f"SignOz Alert: {alert_name}",
|
||||
description=description,
|
||||
risk_level=RiskLevel.HIGH,
|
||||
blast_radius=BlastRadius(
|
||||
affected_pods=1,
|
||||
estimated_downtime="0",
|
||||
related_services=[service_name],
|
||||
data_impact=DataImpact.READ_ONLY,
|
||||
),
|
||||
dry_run_checks=[],
|
||||
requested_by="signoz-webhook",
|
||||
metadata={
|
||||
"source": "signoz",
|
||||
"alert_name": alert_name,
|
||||
"labels": {"service": service_name},
|
||||
},
|
||||
)
|
||||
|
||||
assert request.action == "SignOz Alert: HighErrorRate"
|
||||
assert request.description == description
|
||||
assert request.requested_by == "signoz-webhook"
|
||||
assert request.blast_radius.related_services == [service_name]
|
||||
assert request.metadata["source"] == "signoz"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: GitHub Webhook ApprovalRequestCreate 格式
|
||||
# =============================================================================
|
||||
|
||||
class TestGitHubWebhookApproval:
|
||||
"""測試 GitHub Webhook 的 ApprovalRequestCreate 格式"""
|
||||
|
||||
def test_github_ci_failure_approval_format(self):
|
||||
"""✅ GitHub CI Failure 應使用正確欄位格式"""
|
||||
repo = "wooo-ai/awoooi"
|
||||
root_cause = "Build failed due to missing dependency"
|
||||
suggestion = "npm install missing-package"
|
||||
|
||||
request = ApprovalRequestCreate(
|
||||
action=f"CI Failure Repair: {repo}",
|
||||
description=f"Root Cause: {root_cause}\nSuggestion: {suggestion}",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
blast_radius=BlastRadius(
|
||||
affected_pods=1,
|
||||
estimated_downtime="~5min",
|
||||
related_services=[repo],
|
||||
data_impact=DataImpact.NONE,
|
||||
),
|
||||
dry_run_checks=[],
|
||||
requested_by="github-webhook",
|
||||
metadata={
|
||||
"source": "github",
|
||||
"alert_type": "ci_failure_repair",
|
||||
"target_resource": repo,
|
||||
"namespace": "github-actions",
|
||||
},
|
||||
)
|
||||
|
||||
assert request.action == f"CI Failure Repair: {repo}"
|
||||
assert "Root Cause:" in request.description
|
||||
assert request.requested_by == "github-webhook"
|
||||
assert request.metadata["source"] == "github"
|
||||
assert request.metadata["alert_type"] == "ci_failure_repair"
|
||||
|
||||
def test_github_code_review_approval_format(self):
|
||||
"""✅ GitHub Code Review 應使用正確欄位格式"""
|
||||
repo = "wooo-ai/awoooi"
|
||||
target = "src/main.py"
|
||||
|
||||
request = ApprovalRequestCreate(
|
||||
action=f"Code Review Security: {repo}",
|
||||
description=f"Root Cause: Code review found security concerns in {target}",
|
||||
risk_level=RiskLevel.HIGH,
|
||||
blast_radius=BlastRadius(
|
||||
affected_pods=1,
|
||||
estimated_downtime="0",
|
||||
related_services=[repo],
|
||||
data_impact=DataImpact.READ_ONLY,
|
||||
),
|
||||
dry_run_checks=[],
|
||||
requested_by="github-webhook",
|
||||
metadata={
|
||||
"source": "github",
|
||||
"alert_type": "code_review_security",
|
||||
"target": target,
|
||||
},
|
||||
)
|
||||
|
||||
assert request.action == f"Code Review Security: {repo}"
|
||||
assert request.requested_by == "github-webhook"
|
||||
assert request.metadata["alert_type"] == "code_review_security"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Sentry Webhook ApprovalRequestCreate 格式 (參照用)
|
||||
# =============================================================================
|
||||
|
||||
class TestSentryWebhookApproval:
|
||||
"""測試 Sentry Webhook 的 ApprovalRequestCreate 格式 (已正確)"""
|
||||
|
||||
def test_sentry_approval_format(self):
|
||||
"""✅ Sentry Webhook 格式正確 (參照用)"""
|
||||
level = "error"
|
||||
culprit = "api.views.handler"
|
||||
project = "awoooi-api"
|
||||
|
||||
request = ApprovalRequestCreate(
|
||||
action=f"Sentry {level.upper()} Alert: {culprit}",
|
||||
description="TypeError: Cannot read property 'x' of undefined",
|
||||
risk_level=RiskLevel.HIGH,
|
||||
blast_radius=BlastRadius(
|
||||
affected_pods=1,
|
||||
estimated_downtime="0",
|
||||
related_services=[project],
|
||||
data_impact=DataImpact.READ_ONLY,
|
||||
),
|
||||
dry_run_checks=[],
|
||||
requested_by="sentry-webhook",
|
||||
metadata={
|
||||
"source": "sentry",
|
||||
"issue_id": "12345",
|
||||
},
|
||||
)
|
||||
|
||||
assert request.action == "Sentry ERROR Alert: api.views.handler"
|
||||
assert request.requested_by == "sentry-webhook"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: 錯誤欄位驗證 (Pydantic v2 預設忽略額外欄位)
|
||||
# =============================================================================
|
||||
|
||||
class TestInvalidFieldsBehavior:
|
||||
"""
|
||||
測試舊格式 (錯誤欄位) 行為
|
||||
|
||||
注意: Pydantic v2 預設 extra='ignore',額外欄位不會拋出錯誤
|
||||
但是缺少必填欄位仍會拋出 ValidationError
|
||||
"""
|
||||
|
||||
def test_action_type_without_action_raises_error(self):
|
||||
"""❌ 只有 action_type 而沒有 action 會失敗 (缺少必填欄位)"""
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
ApprovalRequestCreate(
|
||||
action_type="SignOz Alert", # 錯誤: 這不是有效欄位
|
||||
description="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test",
|
||||
# 缺少 action (必填)
|
||||
)
|
||||
assert "action" in str(exc_info.value)
|
||||
|
||||
def test_extra_fields_are_ignored(self):
|
||||
"""✅ 額外欄位會被忽略 (Pydantic v2 預設行為)"""
|
||||
# 這不會拋出錯誤,額外欄位會被忽略
|
||||
request = ApprovalRequestCreate(
|
||||
action="Test",
|
||||
description="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test",
|
||||
target_resource="api-service", # 額外欄位,會被忽略
|
||||
context={"key": "value"}, # 額外欄位,會被忽略
|
||||
)
|
||||
# 驗證 request 物件沒有這些屬性
|
||||
assert not hasattr(request, "target_resource")
|
||||
assert not hasattr(request, "context")
|
||||
# 但必填欄位存在
|
||||
assert request.action == "Test"
|
||||
assert request.requested_by == "test"
|
||||
|
||||
def test_metadata_is_valid_field(self):
|
||||
"""✅ metadata 是有效欄位 (context 應改用 metadata)"""
|
||||
request = ApprovalRequestCreate(
|
||||
action="Test",
|
||||
description="Test",
|
||||
risk_level=RiskLevel.MEDIUM,
|
||||
requested_by="test",
|
||||
metadata={"key": "value"}, # 正確欄位
|
||||
)
|
||||
assert request.metadata == {"key": "value"}
|
||||
Reference in New Issue
Block a user