""" 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"}