@aporthq/aport-agent-guardrails 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +217 -0
- package/README.md +481 -0
- package/bin/agent-guardrails +133 -0
- package/bin/aport-create-passport.sh +444 -0
- package/bin/aport-cursor-hook.sh +90 -0
- package/bin/aport-guardrail-api.sh +108 -0
- package/bin/aport-guardrail-bash.sh +394 -0
- package/bin/aport-guardrail-v2.sh +5 -0
- package/bin/aport-guardrail.sh +5 -0
- package/bin/aport-resolve-paths.sh +71 -0
- package/bin/aport-status.sh +276 -0
- package/bin/frameworks/crewai.sh +49 -0
- package/bin/frameworks/cursor.sh +95 -0
- package/bin/frameworks/langchain.sh +48 -0
- package/bin/frameworks/n8n.sh +36 -0
- package/bin/frameworks/openclaw.sh +19 -0
- package/bin/lib/allowlist.sh +18 -0
- package/bin/lib/common.sh +28 -0
- package/bin/lib/config.sh +46 -0
- package/bin/lib/constants.sh +232 -0
- package/bin/lib/detect.sh +65 -0
- package/bin/lib/error.sh +269 -0
- package/bin/lib/passport.sh +19 -0
- package/bin/lib/templates/.gitkeep +1 -0
- package/bin/lib/templates/config.yaml +6 -0
- package/bin/lib/validation.sh +206 -0
- package/bin/openclaw +660 -0
- package/docs/ADDING_A_FRAMEWORK.md +87 -0
- package/docs/AGENTS.md.example +40 -0
- package/docs/CODE_REVIEW.md +192 -0
- package/docs/DEPLOYMENT_READINESS.md +81 -0
- package/docs/FAQ_SECURITY_SCANNERS.md +373 -0
- package/docs/FRAMEWORK_ROADMAP.md +41 -0
- package/docs/HOSTED_PASSPORT_SETUP.md +362 -0
- package/docs/IMPLEMENTING_YOUR_OWN_EVALUATOR.md +433 -0
- package/docs/OPENCLAW_COMPATIBILITY.md +73 -0
- package/docs/OPENCLAW_LOCAL_INTEGRATION.md +596 -0
- package/docs/OPENCLAW_TOOLS_AND_POLICIES.md +54 -0
- package/docs/QUICKSTART.md +470 -0
- package/docs/QUICKSTART_OPENCLAW_PLUGIN.md +470 -0
- package/docs/README.md +28 -0
- package/docs/RELEASE.md +87 -0
- package/docs/REPO_LAYOUT.md +47 -0
- package/docs/SKILLS_ECOSYSTEM_ANALYSIS_FEB17.md +1260 -0
- package/docs/TOOL_POLICY_MAPPING.md +46 -0
- package/docs/UPGRADE.md +46 -0
- package/docs/VERIFICATION_METHODS.md +97 -0
- package/docs/assets/README.md +8 -0
- package/docs/assets/porter.svg +54 -0
- package/docs/development/ERROR_CODES.md +616 -0
- package/docs/frameworks/GITHUB_ISSUE_PROPOSALS.md +1105 -0
- package/docs/frameworks/crewai.md +114 -0
- package/docs/frameworks/cursor.md +159 -0
- package/docs/frameworks/langchain.md +72 -0
- package/docs/frameworks/n8n.md +40 -0
- package/docs/frameworks/openclaw.md +40 -0
- package/docs/launch/ADD_APORT_AWESOME_LISTS_INSTRUCTIONS.md +146 -0
- package/docs/launch/ANNOUNCEMENT_GUIDE.md +266 -0
- package/docs/launch/AWESOME_REPOS.md +53 -0
- package/docs/launch/CURSOR_VSCODE_HOOKS_RESEARCH.md +77 -0
- package/docs/launch/DEMO_TERMINAL_OUTPUT.txt +48 -0
- package/docs/launch/DRY_AND_PLAN_CHECKLIST.md +47 -0
- package/docs/launch/EVIDENCE_README.md +61 -0
- package/docs/launch/EVIDENCE_TERMINAL_CAPTURE.txt +10 -0
- package/docs/launch/FRAMEWORK_SUPPORT_PLAN.md +1640 -0
- package/docs/launch/LAUNCH_READINESS_CHECKLIST.md +237 -0
- package/docs/launch/LAUNCH_STRATEGY_SUMMARY.md +464 -0
- package/docs/launch/OPENCLAW_FEEDBACK_AND_FIXES.md +85 -0
- package/docs/launch/POST_1_VALENTINE_IMPROVED.md +233 -0
- package/docs/launch/POST_2_GUARDRAIL_IMPROVED.md +369 -0
- package/docs/launch/PRE_LAUNCH_FIXES.md +766 -0
- package/docs/launch/QUICK_LAUNCH_CHECKLIST.md +400 -0
- package/docs/launch/READINESS_SUMMARY.md +262 -0
- package/docs/launch/README.md +68 -0
- package/docs/launch/USER_STORIES.md +327 -0
- package/docs/launch/scripts/add-aport-awesome-pr.sh +69 -0
- package/docs/operations/MONITORING.md +588 -0
- package/docs/reviews/2026-02-18-staff-review.md +268 -0
- package/extensions/openclaw-aport/README.md +415 -0
- package/extensions/openclaw-aport/index.js +625 -0
- package/extensions/openclaw-aport/openclaw-aport.js +7 -0
- package/extensions/openclaw-aport/openclaw.plugin.json +46 -0
- package/extensions/openclaw-aport/package.json +36 -0
- package/extensions/openclaw-aport/test.js +307 -0
- package/external/aport-policies/README.md +363 -0
- package/external/aport-policies/agent.session.create.v1/README.md +345 -0
- package/external/aport-policies/agent.session.create.v1/policy.json +162 -0
- package/external/aport-policies/agent.tool.register.v1/README.md +361 -0
- package/external/aport-policies/agent.tool.register.v1/policy.json +172 -0
- package/external/aport-policies/code.release.publish.v1/README.md +51 -0
- package/external/aport-policies/code.release.publish.v1/policy.json +121 -0
- package/external/aport-policies/code.repository.merge.v1/README.md +287 -0
- package/external/aport-policies/code.repository.merge.v1/express.example.js +332 -0
- package/external/aport-policies/code.repository.merge.v1/fastapi.example.py +370 -0
- package/external/aport-policies/code.repository.merge.v1/policy.json +162 -0
- package/external/aport-policies/data.export.create.v1/README.md +226 -0
- package/external/aport-policies/data.export.create.v1/express.example.js +172 -0
- package/external/aport-policies/data.export.create.v1/fastapi.example.py +165 -0
- package/external/aport-policies/data.export.create.v1/policy.json +133 -0
- package/external/aport-policies/data.report.ingest.v1/README.md +134 -0
- package/external/aport-policies/data.report.ingest.v1/express.example.js +105 -0
- package/external/aport-policies/data.report.ingest.v1/minimal-example.js +68 -0
- package/external/aport-policies/data.report.ingest.v1/policy.json +174 -0
- package/external/aport-policies/finance.crypto.trade.v1/README.md +146 -0
- package/external/aport-policies/finance.crypto.trade.v1/express.example.js +109 -0
- package/external/aport-policies/finance.crypto.trade.v1/minimal-example.js +65 -0
- package/external/aport-policies/finance.crypto.trade.v1/policy.json +176 -0
- package/external/aport-policies/finance.payment.charge.v1/README.md +326 -0
- package/external/aport-policies/finance.payment.charge.v1/express.example.js +250 -0
- package/external/aport-policies/finance.payment.charge.v1/fastapi.example.py +227 -0
- package/external/aport-policies/finance.payment.charge.v1/minimal-example.js +64 -0
- package/external/aport-policies/finance.payment.charge.v1/policy.json +224 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/passport.instance.json +42 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/passport.template.json +40 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/payments-charge-policy.test.js +817 -0
- package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py +486 -0
- package/external/aport-policies/finance.payment.payout.v1/README.md +78 -0
- package/external/aport-policies/finance.payment.payout.v1/policy.json +181 -0
- package/external/aport-policies/finance.payment.refund.v1/README.md +275 -0
- package/external/aport-policies/finance.payment.refund.v1/express.example.js +167 -0
- package/external/aport-policies/finance.payment.refund.v1/fastapi.example.py +136 -0
- package/external/aport-policies/finance.payment.refund.v1/minimal-example.js +183 -0
- package/external/aport-policies/finance.payment.refund.v1/policy.json +216 -0
- package/external/aport-policies/finance.payment.refund.v1/tests/refunds-policy.test.js +924 -0
- package/external/aport-policies/finance.payment.refund.v1/tests/test_refunds_policy.py +778 -0
- package/external/aport-policies/finance.transaction.execute.v1/README.md +309 -0
- package/external/aport-policies/finance.transaction.execute.v1/express.example.js +261 -0
- package/external/aport-policies/finance.transaction.execute.v1/fastapi.example.py +231 -0
- package/external/aport-policies/finance.transaction.execute.v1/minimal-example.js +78 -0
- package/external/aport-policies/finance.transaction.execute.v1/policy.json +189 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/passport.instance.json +42 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/passport.template.json +42 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/test_transactions_policy.py +214 -0
- package/external/aport-policies/finance.transaction.execute.v1/tests/transactions-policy.test.js +306 -0
- package/external/aport-policies/governance.data.access.v1/README.md +292 -0
- package/external/aport-policies/governance.data.access.v1/express.example.js +321 -0
- package/external/aport-policies/governance.data.access.v1/fastapi.example.py +279 -0
- package/external/aport-policies/governance.data.access.v1/minimal-example.js +65 -0
- package/external/aport-policies/governance.data.access.v1/policy.json +208 -0
- package/external/aport-policies/governance.data.access.v1/tests/contexts.jsonl +12 -0
- package/external/aport-policies/governance.data.access.v1/tests/data-access-policy.test.js +308 -0
- package/external/aport-policies/governance.data.access.v1/tests/expected.jsonl +12 -0
- package/external/aport-policies/governance.data.access.v1/tests/passport.instance.json +56 -0
- package/external/aport-policies/governance.data.access.v1/tests/passport.template.json +56 -0
- package/external/aport-policies/governance.data.access.v1/tests/test_data_access_policy.py +214 -0
- package/external/aport-policies/legal.contract.review.v1/README.md +109 -0
- package/external/aport-policies/legal.contract.review.v1/policy.json +378 -0
- package/external/aport-policies/legal.contract.review.v1/tests/legal-contract-review-policy.test.js +609 -0
- package/external/aport-policies/legal.contract.review.v1/tests/passport.template.json +49 -0
- package/external/aport-policies/mcp.tool.execute.v1/README.md +301 -0
- package/external/aport-policies/mcp.tool.execute.v1/policy.json +141 -0
- package/external/aport-policies/messaging.message.send.v1/README.md +230 -0
- package/external/aport-policies/messaging.message.send.v1/express.example.js +183 -0
- package/external/aport-policies/messaging.message.send.v1/fastapi.example.py +193 -0
- package/external/aport-policies/messaging.message.send.v1/policy.json +144 -0
- package/external/aport-policies/policy-template.json +107 -0
- package/external/aport-policies/system.command.execute.v1/README.md +275 -0
- package/external/aport-policies/system.command.execute.v1/policy.json +146 -0
- package/external/aport-spec/CONTRIBUTING.md +273 -0
- package/external/aport-spec/LICENSE +21 -0
- package/external/aport-spec/README.md +168 -0
- package/external/aport-spec/conformance/README.md +294 -0
- package/external/aport-spec/conformance/cases/data.export.v1/contexts/allow_users.json +6 -0
- package/external/aport-spec/conformance/cases/data.export.v1/contexts/deny_pii.json +6 -0
- package/external/aport-spec/conformance/cases/data.export.v1/expected/allow_users.decision.json +19 -0
- package/external/aport-spec/conformance/cases/data.export.v1/expected/deny_pii.decision.json +19 -0
- package/external/aport-spec/conformance/cases/data.export.v1/passports/template.json +29 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/allow_50usd.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_150usd.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/contexts/deny_currency.json +9 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/allow_50usd.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_150usd.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/expected/deny_currency.decision.json +19 -0
- package/external/aport-spec/conformance/cases/payments.refunds.v1/passports/template.json +42 -0
- package/external/aport-spec/conformance/package.json +44 -0
- package/external/aport-spec/conformance/pnpm-lock.yaml +642 -0
- package/external/aport-spec/conformance/src/cases.ts +371 -0
- package/external/aport-spec/conformance/src/ed25519.ts +167 -0
- package/external/aport-spec/conformance/src/jcs.ts +85 -0
- package/external/aport-spec/conformance/src/runner.ts +533 -0
- package/external/aport-spec/conformance/src/validators.ts +185 -0
- package/external/aport-spec/conformance/test-runner.js +315 -0
- package/external/aport-spec/conformance/tsconfig.json +21 -0
- package/external/aport-spec/error-schema.json +192 -0
- package/external/aport-spec/index.json +12 -0
- package/external/aport-spec/integrations/clawmoat/README.md +12 -0
- package/external/aport-spec/integrations/shield/README.md +245 -0
- package/external/aport-spec/integrations/shield/adapters/index.js +116 -0
- package/external/aport-spec/integrations/shield/adapters/system-command-execute.js +133 -0
- package/external/aport-spec/integrations/shield/test/README.md +58 -0
- package/external/aport-spec/integrations/shield/test/shield.md +40 -0
- package/external/aport-spec/integrations/shield/test/test-shield-to-verify.js +274 -0
- package/external/aport-spec/metrics-schema.json +504 -0
- package/external/aport-spec/oap/CHANGELOG.md +54 -0
- package/external/aport-spec/oap/VERSION.md +40 -0
- package/external/aport-spec/oap/capability-registry.md +229 -0
- package/external/aport-spec/oap/conformance.md +257 -0
- package/external/aport-spec/oap/decision-schema.json +114 -0
- package/external/aport-spec/oap/examples/context.refund.usd.50.json +9 -0
- package/external/aport-spec/oap/examples/decision.allow.sample.json +20 -0
- package/external/aport-spec/oap/examples/decision.deny.sample.json +23 -0
- package/external/aport-spec/oap/examples/passport.instance.v1.json +50 -0
- package/external/aport-spec/oap/examples/passport.template.v1.json +71 -0
- package/external/aport-spec/oap/oap-spec.md +426 -0
- package/external/aport-spec/oap/passport-schema.json +396 -0
- package/external/aport-spec/oap/security.md +213 -0
- package/external/aport-spec/oap/vc/context-oap-v1.jsonld +137 -0
- package/external/aport-spec/oap/vc/examples/oap-decision-vc.json +37 -0
- package/external/aport-spec/oap/vc/examples/oap-passport-vc.json +68 -0
- package/external/aport-spec/oap/vc/tools/INTEGRATION.md +375 -0
- package/external/aport-spec/oap/vc/tools/README.md +278 -0
- package/external/aport-spec/oap/vc/tools/examples/decision-to-vc.js +66 -0
- package/external/aport-spec/oap/vc/tools/examples/passport-to-vc.js +83 -0
- package/external/aport-spec/oap/vc/tools/examples/vc-to-decision.js +77 -0
- package/external/aport-spec/oap/vc/tools/examples/vc-to-passport.js +94 -0
- package/external/aport-spec/oap/vc/tools/package.json +38 -0
- package/external/aport-spec/oap/vc/tools/pnpm-lock.yaml +472 -0
- package/external/aport-spec/oap/vc/tools/src/cli.ts +226 -0
- package/external/aport-spec/oap/vc/tools/src/crypto-utils.ts +427 -0
- package/external/aport-spec/oap/vc/tools/src/index.ts +653 -0
- package/external/aport-spec/oap/vc/tools/src/test.ts +148 -0
- package/external/aport-spec/oap/vc/tools/src/vp.ts +382 -0
- package/external/aport-spec/oap/vc/tools/test-simple.js +214 -0
- package/external/aport-spec/oap/vc/tools/tsconfig.json +19 -0
- package/external/aport-spec/oap/vc/vc-mapping.md +443 -0
- package/external/aport-spec/passport-schema.json +586 -0
- package/external/aport-spec/rate-limiting.md +136 -0
- package/external/aport-spec/transport-profile.md +325 -0
- package/external/aport-spec/webhook-spec.md +314 -0
- package/package.json +70 -0
- package/skills/aport-agent-guardrail/SKILL.md +314 -0
- package/src/evaluator.js +252 -0
- package/src/server/index.js +72 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for finance.payment.refund.v1 policy
|
|
3
|
+
Tests all enforcement rules, edge cases, and fraud prevention
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import asyncio
|
|
8
|
+
from unittest.mock import AsyncMock, patch, MagicMock
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
# Mock the policy verification
|
|
12
|
+
class MockPolicyResponse:
|
|
13
|
+
def __init__(self, allow=True, reasons=None, decision_id=None, remaining_daily_cap=None):
|
|
14
|
+
self.allow = allow
|
|
15
|
+
self.reasons = reasons or []
|
|
16
|
+
self.decision_id = decision_id
|
|
17
|
+
self.remaining_daily_cap = remaining_daily_cap or {}
|
|
18
|
+
self.passport = {
|
|
19
|
+
'evaluation': {
|
|
20
|
+
'decision_id': decision_id,
|
|
21
|
+
'remaining_daily_cap': remaining_daily_cap or {}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def mock_policy_verification(response):
|
|
26
|
+
"""Mock the policy verification endpoint"""
|
|
27
|
+
async def mock_verify(*args, **kwargs):
|
|
28
|
+
return response
|
|
29
|
+
return mock_verify
|
|
30
|
+
|
|
31
|
+
class TestRefundsV1Policy:
|
|
32
|
+
"""Test suite for finance.payment.refund.v1 policy functionality"""
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def valid_context(self):
|
|
36
|
+
"""Valid refund context for testing"""
|
|
37
|
+
return {
|
|
38
|
+
"order_id": "ORD-12345",
|
|
39
|
+
"customer_id": "CUST-67890",
|
|
40
|
+
"amount_minor": 5000,
|
|
41
|
+
"currency": "USD",
|
|
42
|
+
"region": "US",
|
|
43
|
+
"reason_code": "customer_request",
|
|
44
|
+
"idempotency_key": "idempotency_key_123",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def mock_client(self):
|
|
49
|
+
"""Mock AgentPassportClient"""
|
|
50
|
+
client = MagicMock()
|
|
51
|
+
client.verify_agent_passport = AsyncMock()
|
|
52
|
+
return client
|
|
53
|
+
|
|
54
|
+
def test_required_fields_validation_success(self, valid_context):
|
|
55
|
+
"""Test that refund with all required fields is allowed"""
|
|
56
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
57
|
+
|
|
58
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
59
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
60
|
+
allow=True,
|
|
61
|
+
decision_id="dec_123",
|
|
62
|
+
remaining_daily_cap={"USD": 25000}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", valid_context)
|
|
66
|
+
|
|
67
|
+
assert result["allowed"] is True
|
|
68
|
+
assert "policy_result" in result
|
|
69
|
+
|
|
70
|
+
def test_required_fields_validation_failure(self):
|
|
71
|
+
"""Test that refund missing required fields is denied"""
|
|
72
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
73
|
+
|
|
74
|
+
invalid_context = {
|
|
75
|
+
"order_id": "ORD-12345",
|
|
76
|
+
# Missing customer_id, amount_minor, currency, region, reason_code, idempotency_key
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
80
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
81
|
+
allow=False,
|
|
82
|
+
reasons=[
|
|
83
|
+
{"code": "missing_required_field", "message": "customer_id is required"},
|
|
84
|
+
{"code": "missing_required_field", "message": "amount_minor is required"},
|
|
85
|
+
{"code": "missing_required_field", "message": "currency is required"},
|
|
86
|
+
{"code": "missing_required_field", "message": "region is required"},
|
|
87
|
+
{"code": "missing_required_field", "message": "reason_code is required"},
|
|
88
|
+
{"code": "missing_required_field", "message": "idempotency_key is required"},
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", invalid_context)
|
|
93
|
+
|
|
94
|
+
assert result["allowed"] is False
|
|
95
|
+
assert len(result["violations"]) == 6
|
|
96
|
+
|
|
97
|
+
def test_currency_support(self):
|
|
98
|
+
"""Test support for multiple currencies"""
|
|
99
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
100
|
+
|
|
101
|
+
currencies = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"]
|
|
102
|
+
|
|
103
|
+
for currency in currencies:
|
|
104
|
+
context = {
|
|
105
|
+
"order_id": "ORD-12345",
|
|
106
|
+
"customer_id": "CUST-67890",
|
|
107
|
+
"amount_minor": 1000,
|
|
108
|
+
"currency": currency,
|
|
109
|
+
"region": "US",
|
|
110
|
+
"reason_code": "customer_request",
|
|
111
|
+
"idempotency_key": f"idempotency_{currency}",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
115
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
116
|
+
allow=True,
|
|
117
|
+
decision_id=f"dec_{currency}",
|
|
118
|
+
remaining_daily_cap={currency: 25000}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
122
|
+
|
|
123
|
+
assert result["allowed"] is True
|
|
124
|
+
|
|
125
|
+
def test_unsupported_currency(self):
|
|
126
|
+
"""Test that unsupported currency is denied"""
|
|
127
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
128
|
+
|
|
129
|
+
context = {
|
|
130
|
+
"order_id": "ORD-12345",
|
|
131
|
+
"customer_id": "CUST-67890",
|
|
132
|
+
"amount_minor": 1000,
|
|
133
|
+
"currency": "XYZ", # Unsupported currency
|
|
134
|
+
"region": "US",
|
|
135
|
+
"reason_code": "customer_request",
|
|
136
|
+
"idempotency_key": "idempotency_xyz",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
140
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
141
|
+
allow=False,
|
|
142
|
+
reasons=[
|
|
143
|
+
{"code": "currency_not_supported", "message": "Currency XYZ is not supported"}
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
148
|
+
|
|
149
|
+
assert result["allowed"] is False
|
|
150
|
+
assert result["violations"][0]["code"] == "currency_not_supported"
|
|
151
|
+
|
|
152
|
+
def test_amount_precision_validation(self):
|
|
153
|
+
"""Test amount precision validation for different currencies"""
|
|
154
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
155
|
+
|
|
156
|
+
test_cases = [
|
|
157
|
+
{"currency": "USD", "amount": 1000, "valid": True}, # $10.00 - valid
|
|
158
|
+
{"currency": "USD", "amount": 1001, "valid": False}, # $10.01 - invalid precision
|
|
159
|
+
{"currency": "JPY", "amount": 1000, "valid": True}, # ¥1000 - valid
|
|
160
|
+
{"currency": "JPY", "amount": 1001, "valid": True}, # ¥1001 - valid (no decimals)
|
|
161
|
+
{"currency": "EUR", "amount": 1000, "valid": True}, # €10.00 - valid
|
|
162
|
+
{"currency": "EUR", "amount": 1001, "valid": False}, # €10.01 - invalid precision
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
for test_case in test_cases:
|
|
166
|
+
context = {
|
|
167
|
+
"order_id": "ORD-12345",
|
|
168
|
+
"customer_id": "CUST-67890",
|
|
169
|
+
"amount_minor": test_case["amount"],
|
|
170
|
+
"currency": test_case["currency"],
|
|
171
|
+
"region": "US",
|
|
172
|
+
"reason_code": "customer_request",
|
|
173
|
+
"idempotency_key": f"idempotency_{test_case['currency']}_{test_case['amount']}",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
177
|
+
if test_case["valid"]:
|
|
178
|
+
mock_verify.return_value = MockPolicyResponse(allow=True)
|
|
179
|
+
else:
|
|
180
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
181
|
+
allow=False,
|
|
182
|
+
reasons=[
|
|
183
|
+
{"code": "invalid_amount", "message": f"Amount {test_case['amount']} has invalid precision for currency {test_case['currency']}"}
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
188
|
+
|
|
189
|
+
assert result["allowed"] == test_case["valid"]
|
|
190
|
+
|
|
191
|
+
def test_amount_bounds_validation(self):
|
|
192
|
+
"""Test amount bounds validation"""
|
|
193
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
194
|
+
|
|
195
|
+
test_cases = [
|
|
196
|
+
{"amount": 0, "valid": False, "reason": "Amount must be positive"},
|
|
197
|
+
{"amount": -100, "valid": False, "reason": "Amount must be positive"},
|
|
198
|
+
{"amount": 1, "valid": True, "reason": "Minimum amount"},
|
|
199
|
+
{"amount": 1000000000, "valid": False, "reason": "Amount exceeds maximum"},
|
|
200
|
+
{"amount": 999999999, "valid": True, "reason": "Maximum valid amount"},
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
for test_case in test_cases:
|
|
204
|
+
context = {
|
|
205
|
+
"order_id": "ORD-12345",
|
|
206
|
+
"customer_id": "CUST-67890",
|
|
207
|
+
"amount_minor": test_case["amount"],
|
|
208
|
+
"currency": "USD",
|
|
209
|
+
"region": "US",
|
|
210
|
+
"reason_code": "customer_request",
|
|
211
|
+
"idempotency_key": f"idempotency_{test_case['amount']}",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
215
|
+
if test_case["valid"]:
|
|
216
|
+
mock_verify.return_value = MockPolicyResponse(allow=True)
|
|
217
|
+
else:
|
|
218
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
219
|
+
allow=False,
|
|
220
|
+
reasons=[
|
|
221
|
+
{"code": "invalid_amount", "message": test_case["reason"]}
|
|
222
|
+
]
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
226
|
+
|
|
227
|
+
assert result["allowed"] == test_case["valid"]
|
|
228
|
+
|
|
229
|
+
def test_assurance_level_requirements_l2(self):
|
|
230
|
+
"""Test L2 requirement for amounts <= $100"""
|
|
231
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
232
|
+
|
|
233
|
+
context = {
|
|
234
|
+
"order_id": "ORD-12345",
|
|
235
|
+
"customer_id": "CUST-67890",
|
|
236
|
+
"amount_minor": 10000, # $100
|
|
237
|
+
"currency": "USD",
|
|
238
|
+
"region": "US",
|
|
239
|
+
"reason_code": "customer_request",
|
|
240
|
+
"idempotency_key": "idempotency_l2",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
244
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
245
|
+
allow=True,
|
|
246
|
+
decision_id="dec_l2_pass",
|
|
247
|
+
remaining_daily_cap={"USD": 25000}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
251
|
+
|
|
252
|
+
assert result["allowed"] is True
|
|
253
|
+
|
|
254
|
+
def test_assurance_level_requirements_l3(self):
|
|
255
|
+
"""Test L3 requirement for amounts $100-$500"""
|
|
256
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
257
|
+
|
|
258
|
+
context = {
|
|
259
|
+
"order_id": "ORD-12345",
|
|
260
|
+
"customer_id": "CUST-67890",
|
|
261
|
+
"amount_minor": 25000, # $250
|
|
262
|
+
"currency": "USD",
|
|
263
|
+
"region": "US",
|
|
264
|
+
"reason_code": "customer_request",
|
|
265
|
+
"idempotency_key": "idempotency_l3",
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
269
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
270
|
+
allow=False,
|
|
271
|
+
reasons=[
|
|
272
|
+
{"code": "assurance_too_low", "message": "Refund amount 25000 USD requires L3 assurance level, but agent has L2"}
|
|
273
|
+
]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
277
|
+
|
|
278
|
+
assert result["allowed"] is False
|
|
279
|
+
assert result["violations"][0]["code"] == "assurance_too_low"
|
|
280
|
+
|
|
281
|
+
def test_assurance_level_requirements_deny_over_500(self):
|
|
282
|
+
"""Test that amounts > $500 are denied in v1"""
|
|
283
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
284
|
+
|
|
285
|
+
context = {
|
|
286
|
+
"order_id": "ORD-12345",
|
|
287
|
+
"customer_id": "CUST-67890",
|
|
288
|
+
"amount_minor": 60000, # $600
|
|
289
|
+
"currency": "USD",
|
|
290
|
+
"region": "US",
|
|
291
|
+
"reason_code": "customer_request",
|
|
292
|
+
"idempotency_key": "idempotency_deny",
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
296
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
297
|
+
allow=False,
|
|
298
|
+
reasons=[
|
|
299
|
+
{"code": "assurance_too_low", "message": "Refund amount 60000 USD requires L4 assurance level, but agent has L3"}
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
304
|
+
|
|
305
|
+
assert result["allowed"] is False
|
|
306
|
+
|
|
307
|
+
def test_idempotency_protection_first_request(self):
|
|
308
|
+
"""Test that first request with idempotency key is allowed"""
|
|
309
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
310
|
+
|
|
311
|
+
context = {
|
|
312
|
+
"order_id": "ORD-12345",
|
|
313
|
+
"customer_id": "CUST-67890",
|
|
314
|
+
"amount_minor": 5000,
|
|
315
|
+
"currency": "USD",
|
|
316
|
+
"region": "US",
|
|
317
|
+
"reason_code": "customer_request",
|
|
318
|
+
"idempotency_key": "unique_key_123",
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
322
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
323
|
+
allow=True,
|
|
324
|
+
decision_id="dec_first",
|
|
325
|
+
remaining_daily_cap={"USD": 25000}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
329
|
+
|
|
330
|
+
assert result["allowed"] is True
|
|
331
|
+
|
|
332
|
+
def test_idempotency_protection_duplicate(self):
|
|
333
|
+
"""Test that duplicate idempotency key is denied"""
|
|
334
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
335
|
+
|
|
336
|
+
context = {
|
|
337
|
+
"order_id": "ORD-12345",
|
|
338
|
+
"customer_id": "CUST-67890",
|
|
339
|
+
"amount_minor": 5000,
|
|
340
|
+
"currency": "USD",
|
|
341
|
+
"region": "US",
|
|
342
|
+
"reason_code": "customer_request",
|
|
343
|
+
"idempotency_key": "duplicate_key_123",
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
347
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
348
|
+
allow=False,
|
|
349
|
+
reasons=[
|
|
350
|
+
{"code": "idempotency_replay", "message": "Duplicate idempotency key detected. Previous decision: dec_duplicate"}
|
|
351
|
+
]
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
355
|
+
|
|
356
|
+
assert result["allowed"] is False
|
|
357
|
+
assert result["violations"][0]["code"] == "idempotency_replay"
|
|
358
|
+
|
|
359
|
+
def test_daily_cap_enforcement_within_limit(self):
|
|
360
|
+
"""Test that refund within daily cap is allowed"""
|
|
361
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
362
|
+
|
|
363
|
+
context = {
|
|
364
|
+
"order_id": "ORD-12345",
|
|
365
|
+
"customer_id": "CUST-67890",
|
|
366
|
+
"amount_minor": 5000,
|
|
367
|
+
"currency": "USD",
|
|
368
|
+
"region": "US",
|
|
369
|
+
"reason_code": "customer_request",
|
|
370
|
+
"idempotency_key": "daily_cap_test",
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
374
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
375
|
+
allow=True,
|
|
376
|
+
decision_id="dec_daily_cap",
|
|
377
|
+
remaining_daily_cap={"USD": 20000}
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
381
|
+
|
|
382
|
+
assert result["allowed"] is True
|
|
383
|
+
assert result["policy_result"].passport["evaluation"]["remaining_daily_cap"]["USD"] == 20000
|
|
384
|
+
|
|
385
|
+
def test_daily_cap_enforcement_exceeded(self):
|
|
386
|
+
"""Test that refund exceeding daily cap is denied"""
|
|
387
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
388
|
+
|
|
389
|
+
context = {
|
|
390
|
+
"order_id": "ORD-12345",
|
|
391
|
+
"customer_id": "CUST-67890",
|
|
392
|
+
"amount_minor": 5000,
|
|
393
|
+
"currency": "USD",
|
|
394
|
+
"region": "US",
|
|
395
|
+
"reason_code": "customer_request",
|
|
396
|
+
"idempotency_key": "daily_cap_exceeded",
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
400
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
401
|
+
allow=False,
|
|
402
|
+
reasons=[
|
|
403
|
+
{"code": "daily_cap_exceeded", "message": "Daily cap 25000 USD exceeded for USD; current 23000 + 5000 > 25000"}
|
|
404
|
+
],
|
|
405
|
+
remaining_daily_cap={"USD": 2000}
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
409
|
+
|
|
410
|
+
assert result["allowed"] is False
|
|
411
|
+
assert result["violations"][0]["code"] == "daily_cap_exceeded"
|
|
412
|
+
|
|
413
|
+
def test_cross_currency_protection(self):
|
|
414
|
+
"""Test that cross-currency refunds are denied"""
|
|
415
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
416
|
+
|
|
417
|
+
context = {
|
|
418
|
+
"order_id": "ORD-12345",
|
|
419
|
+
"customer_id": "CUST-67890",
|
|
420
|
+
"amount_minor": 5000,
|
|
421
|
+
"currency": "USD",
|
|
422
|
+
"order_currency": "EUR", # Different from refund currency
|
|
423
|
+
"region": "US",
|
|
424
|
+
"reason_code": "customer_request",
|
|
425
|
+
"idempotency_key": "cross_currency_test",
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
429
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
430
|
+
allow=False,
|
|
431
|
+
reasons=[
|
|
432
|
+
{"code": "cross_currency_denied", "message": "Cross-currency refunds are not supported in v1"}
|
|
433
|
+
]
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
437
|
+
|
|
438
|
+
assert result["allowed"] is False
|
|
439
|
+
assert result["violations"][0]["code"] == "cross_currency_denied"
|
|
440
|
+
|
|
441
|
+
def test_order_balance_validation_within_balance(self):
|
|
442
|
+
"""Test that refund within order balance is allowed"""
|
|
443
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
444
|
+
|
|
445
|
+
context = {
|
|
446
|
+
"order_id": "ORD-12345",
|
|
447
|
+
"customer_id": "CUST-67890",
|
|
448
|
+
"amount_minor": 5000,
|
|
449
|
+
"currency": "USD",
|
|
450
|
+
"order_total_minor": 10000,
|
|
451
|
+
"already_refunded_minor": 2000,
|
|
452
|
+
"region": "US",
|
|
453
|
+
"reason_code": "customer_request",
|
|
454
|
+
"idempotency_key": "balance_valid",
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
458
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
459
|
+
allow=True,
|
|
460
|
+
decision_id="dec_balance_valid",
|
|
461
|
+
remaining_daily_cap={"USD": 25000}
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
465
|
+
|
|
466
|
+
assert result["allowed"] is True
|
|
467
|
+
|
|
468
|
+
def test_order_balance_validation_exceeded(self):
|
|
469
|
+
"""Test that refund exceeding order balance is denied"""
|
|
470
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
471
|
+
|
|
472
|
+
context = {
|
|
473
|
+
"order_id": "ORD-12345",
|
|
474
|
+
"customer_id": "CUST-67890",
|
|
475
|
+
"amount_minor": 5000,
|
|
476
|
+
"currency": "USD",
|
|
477
|
+
"order_total_minor": 10000,
|
|
478
|
+
"already_refunded_minor": 8000, # Only 2000 remaining
|
|
479
|
+
"region": "US",
|
|
480
|
+
"reason_code": "customer_request",
|
|
481
|
+
"idempotency_key": "balance_exceeded",
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
485
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
486
|
+
allow=False,
|
|
487
|
+
reasons=[
|
|
488
|
+
{"code": "order_balance_exceeded", "message": "Refund amount 5000 exceeds remaining order balance 2000"}
|
|
489
|
+
]
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
493
|
+
|
|
494
|
+
assert result["allowed"] is False
|
|
495
|
+
assert result["violations"][0]["code"] == "order_balance_exceeded"
|
|
496
|
+
|
|
497
|
+
def test_region_validation_allowed(self):
|
|
498
|
+
"""Test that refund in allowed region is permitted"""
|
|
499
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
500
|
+
|
|
501
|
+
context = {
|
|
502
|
+
"order_id": "ORD-12345",
|
|
503
|
+
"customer_id": "CUST-67890",
|
|
504
|
+
"amount_minor": 5000,
|
|
505
|
+
"currency": "USD",
|
|
506
|
+
"region": "US",
|
|
507
|
+
"reason_code": "customer_request",
|
|
508
|
+
"idempotency_key": "region_valid",
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
512
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
513
|
+
allow=True,
|
|
514
|
+
decision_id="dec_region_valid",
|
|
515
|
+
remaining_daily_cap={"USD": 25000}
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
519
|
+
|
|
520
|
+
assert result["allowed"] is True
|
|
521
|
+
|
|
522
|
+
def test_region_validation_denied(self):
|
|
523
|
+
"""Test that refund in disallowed region is denied"""
|
|
524
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
525
|
+
|
|
526
|
+
context = {
|
|
527
|
+
"order_id": "ORD-12345",
|
|
528
|
+
"customer_id": "CUST-67890",
|
|
529
|
+
"amount_minor": 5000,
|
|
530
|
+
"currency": "USD",
|
|
531
|
+
"region": "RESTRICTED",
|
|
532
|
+
"reason_code": "customer_request",
|
|
533
|
+
"idempotency_key": "region_invalid",
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
537
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
538
|
+
allow=False,
|
|
539
|
+
reasons=[
|
|
540
|
+
{"code": "region_not_allowed", "message": "Region RESTRICTED is not allowed for this agent"}
|
|
541
|
+
]
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
545
|
+
|
|
546
|
+
assert result["allowed"] is False
|
|
547
|
+
assert result["violations"][0]["code"] == "region_not_allowed"
|
|
548
|
+
|
|
549
|
+
def test_reason_code_validation_valid(self):
|
|
550
|
+
"""Test that valid reason codes are allowed"""
|
|
551
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
552
|
+
|
|
553
|
+
valid_reason_codes = ["customer_request", "defective", "not_as_described", "duplicate", "fraud"]
|
|
554
|
+
|
|
555
|
+
for reason_code in valid_reason_codes:
|
|
556
|
+
context = {
|
|
557
|
+
"order_id": "ORD-12345",
|
|
558
|
+
"customer_id": "CUST-67890",
|
|
559
|
+
"amount_minor": 5000,
|
|
560
|
+
"currency": "USD",
|
|
561
|
+
"region": "US",
|
|
562
|
+
"reason_code": reason_code,
|
|
563
|
+
"idempotency_key": f"reason_{reason_code}",
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
567
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
568
|
+
allow=True,
|
|
569
|
+
decision_id=f"dec_{reason_code}",
|
|
570
|
+
remaining_daily_cap={"USD": 25000}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
574
|
+
|
|
575
|
+
assert result["allowed"] is True
|
|
576
|
+
|
|
577
|
+
def test_reason_code_validation_invalid(self):
|
|
578
|
+
"""Test that invalid reason codes are denied"""
|
|
579
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
580
|
+
|
|
581
|
+
context = {
|
|
582
|
+
"order_id": "ORD-12345",
|
|
583
|
+
"customer_id": "CUST-67890",
|
|
584
|
+
"amount_minor": 5000,
|
|
585
|
+
"currency": "USD",
|
|
586
|
+
"region": "US",
|
|
587
|
+
"reason_code": "invalid_reason",
|
|
588
|
+
"idempotency_key": "invalid_reason_test",
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
592
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
593
|
+
allow=False,
|
|
594
|
+
reasons=[
|
|
595
|
+
{"code": "reason_code_invalid", "message": "Reason code invalid_reason is not supported"}
|
|
596
|
+
]
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
600
|
+
|
|
601
|
+
assert result["allowed"] is False
|
|
602
|
+
assert result["violations"][0]["code"] == "reason_code_invalid"
|
|
603
|
+
|
|
604
|
+
def test_error_handling_policy_verification_failure(self):
|
|
605
|
+
"""Test handling of policy verification failures"""
|
|
606
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
607
|
+
|
|
608
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
609
|
+
mock_verify.return_value = None # Simulate verification failure
|
|
610
|
+
|
|
611
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", {})
|
|
612
|
+
|
|
613
|
+
assert result["allowed"] is False
|
|
614
|
+
assert result["reason"] == "policy_verification_failed"
|
|
615
|
+
|
|
616
|
+
def test_error_handling_network_error(self):
|
|
617
|
+
"""Test handling of network errors"""
|
|
618
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
619
|
+
|
|
620
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
621
|
+
mock_verify.side_effect = Exception("Network error")
|
|
622
|
+
|
|
623
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", {})
|
|
624
|
+
|
|
625
|
+
assert result["allowed"] is False
|
|
626
|
+
assert result["reason"] == "policy_check_error"
|
|
627
|
+
|
|
628
|
+
def test_edge_cases_extreme_amounts(self):
|
|
629
|
+
"""Test prevention of extremely large amounts"""
|
|
630
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
631
|
+
|
|
632
|
+
context = {
|
|
633
|
+
"order_id": "ORD-12345",
|
|
634
|
+
"customer_id": "CUST-67890",
|
|
635
|
+
"amount_minor": 2**63 - 1, # Maximum safe integer
|
|
636
|
+
"currency": "USD",
|
|
637
|
+
"region": "US",
|
|
638
|
+
"reason_code": "customer_request",
|
|
639
|
+
"idempotency_key": "extreme_amount",
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
643
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
644
|
+
allow=False,
|
|
645
|
+
reasons=[
|
|
646
|
+
{"code": "invalid_amount", "message": "Amount exceeds maximum allowed amount"}
|
|
647
|
+
]
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
651
|
+
|
|
652
|
+
assert result["allowed"] is False
|
|
653
|
+
|
|
654
|
+
def test_edge_cases_negative_amounts(self):
|
|
655
|
+
"""Test prevention of negative amounts"""
|
|
656
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
657
|
+
|
|
658
|
+
context = {
|
|
659
|
+
"order_id": "ORD-12345",
|
|
660
|
+
"customer_id": "CUST-67890",
|
|
661
|
+
"amount_minor": -1000,
|
|
662
|
+
"currency": "USD",
|
|
663
|
+
"region": "US",
|
|
664
|
+
"reason_code": "customer_request",
|
|
665
|
+
"idempotency_key": "negative_amount",
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
669
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
670
|
+
allow=False,
|
|
671
|
+
reasons=[
|
|
672
|
+
{"code": "invalid_amount", "message": "Amount must be positive"}
|
|
673
|
+
]
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
677
|
+
|
|
678
|
+
assert result["allowed"] is False
|
|
679
|
+
|
|
680
|
+
def test_edge_cases_zero_amounts(self):
|
|
681
|
+
"""Test prevention of zero amounts"""
|
|
682
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
683
|
+
|
|
684
|
+
context = {
|
|
685
|
+
"order_id": "ORD-12345",
|
|
686
|
+
"customer_id": "CUST-67890",
|
|
687
|
+
"amount_minor": 0,
|
|
688
|
+
"currency": "USD",
|
|
689
|
+
"region": "US",
|
|
690
|
+
"reason_code": "customer_request",
|
|
691
|
+
"idempotency_key": "zero_amount",
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
695
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
696
|
+
allow=False,
|
|
697
|
+
reasons=[
|
|
698
|
+
{"code": "invalid_amount", "message": "Amount must be positive"}
|
|
699
|
+
]
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
703
|
+
|
|
704
|
+
assert result["allowed"] is False
|
|
705
|
+
|
|
706
|
+
def test_idempotency_key_format_validation(self):
|
|
707
|
+
"""Test validation of idempotency key format"""
|
|
708
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_sync
|
|
709
|
+
|
|
710
|
+
invalid_keys = ["", "a", "a" * 100, "invalid@key", "key with spaces"]
|
|
711
|
+
|
|
712
|
+
for key in invalid_keys:
|
|
713
|
+
context = {
|
|
714
|
+
"order_id": "ORD-12345",
|
|
715
|
+
"customer_id": "CUST-67890",
|
|
716
|
+
"amount_minor": 5000,
|
|
717
|
+
"currency": "USD",
|
|
718
|
+
"region": "US",
|
|
719
|
+
"reason_code": "customer_request",
|
|
720
|
+
"idempotency_key": key,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
724
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
725
|
+
allow=False,
|
|
726
|
+
reasons=[
|
|
727
|
+
{"code": "invalid_idempotency_key", "message": "Invalid idempotency key format"}
|
|
728
|
+
]
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
result = check_policy_sync("agent_123", "finance.payment.refund.v1", context)
|
|
732
|
+
|
|
733
|
+
assert result["allowed"] is False
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class TestRefundsV1PolicyIntegration:
|
|
737
|
+
"""Integration tests for finance.payment.refund.v1 policy"""
|
|
738
|
+
|
|
739
|
+
def test_fastapi_middleware_integration(self):
|
|
740
|
+
"""Test FastAPI middleware integration"""
|
|
741
|
+
from aporthq_middleware_fastapi.middleware_v2 import require_refunds_policy
|
|
742
|
+
|
|
743
|
+
# Test that the function exists and has the right signature
|
|
744
|
+
assert callable(require_refunds_policy)
|
|
745
|
+
|
|
746
|
+
dependency = require_refunds_policy("agent_123", True, True)
|
|
747
|
+
assert callable(dependency)
|
|
748
|
+
|
|
749
|
+
@pytest.mark.asyncio
|
|
750
|
+
async def test_async_policy_compliance(self):
|
|
751
|
+
"""Test async policy compliance checking"""
|
|
752
|
+
from aporthq_sdk_python.policy_enforcement import check_policy_compliance
|
|
753
|
+
|
|
754
|
+
context = {
|
|
755
|
+
"order_id": "ORD-12345",
|
|
756
|
+
"customer_id": "CUST-67890",
|
|
757
|
+
"amount_minor": 5000,
|
|
758
|
+
"currency": "USD",
|
|
759
|
+
"region": "US",
|
|
760
|
+
"reason_code": "customer_request",
|
|
761
|
+
"idempotency_key": "async_test",
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
with patch('aporthq_sdk_python.policy_enforcement.verify_policy_compliance') as mock_verify:
|
|
765
|
+
mock_verify.return_value = MockPolicyResponse(
|
|
766
|
+
allow=True,
|
|
767
|
+
decision_id="dec_async",
|
|
768
|
+
remaining_daily_cap={"USD": 25000}
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
result = await check_policy_compliance("agent_123", "finance.payment.refund.v1", context)
|
|
772
|
+
|
|
773
|
+
assert result["allowed"] is True
|
|
774
|
+
assert "policy_result" in result
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
if __name__ == "__main__":
|
|
778
|
+
pytest.main([__file__])
|