- 自動修復 import 排序、unused imports - 手動修復 raise from、isinstance union、unused variable - scripts/ 暫時保留 (非 CI 阻擋) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
301 lines
11 KiB
Python
301 lines
11 KiB
Python
"""
|
||
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,
|
||
BlastRadius,
|
||
DataImpact,
|
||
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"}
|