@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
package/external/aport-policies/finance.payment.charge.v1/tests/test_payments_charge_policy.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for finance.payment.charge.v1 policy
|
|
3
|
+
Tests all enforcement rules, edge cases, and OAP compliance
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import asyncio
|
|
8
|
+
from unittest.mock import AsyncMock, patch
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
|
|
11
|
+
# Mock the policy verification endpoint
|
|
12
|
+
class MockResponse:
|
|
13
|
+
def __init__(self, json_data, status_code=200):
|
|
14
|
+
self.json_data = json_data
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
self.ok = status_code < 400
|
|
17
|
+
|
|
18
|
+
async def json(self):
|
|
19
|
+
return self.json_data
|
|
20
|
+
|
|
21
|
+
def mock_policy_verification(response_data):
|
|
22
|
+
"""Mock successful policy verification"""
|
|
23
|
+
async def mock_fetch(*args, **kwargs):
|
|
24
|
+
return MockResponse(response_data)
|
|
25
|
+
return mock_fetch
|
|
26
|
+
|
|
27
|
+
def mock_policy_verification_error(status=500):
|
|
28
|
+
"""Mock policy verification error"""
|
|
29
|
+
async def mock_fetch(*args, **kwargs):
|
|
30
|
+
return MockResponse({}, status)
|
|
31
|
+
return mock_fetch
|
|
32
|
+
|
|
33
|
+
class TestPaymentsChargeV1Policy:
|
|
34
|
+
"""Test suite for finance.payment.charge.v1 policy"""
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def valid_context(self):
|
|
38
|
+
return {
|
|
39
|
+
"amount": 1299,
|
|
40
|
+
"currency": "USD",
|
|
41
|
+
"merchant_id": "merch_abc",
|
|
42
|
+
"region": "US",
|
|
43
|
+
"shipping_country": "US",
|
|
44
|
+
"items": [{"sku": "SKU-1", "qty": 1, "category": "electronics"}],
|
|
45
|
+
"idempotency_key": "charge-ord-1001",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
def valid_passport(self):
|
|
50
|
+
return {
|
|
51
|
+
"passport_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
52
|
+
"kind": "instance",
|
|
53
|
+
"spec_version": "oap/1.0",
|
|
54
|
+
"owner_id": "org_demo_co",
|
|
55
|
+
"owner_type": "org",
|
|
56
|
+
"assurance_level": "L2",
|
|
57
|
+
"status": "active",
|
|
58
|
+
"capabilities": [
|
|
59
|
+
{
|
|
60
|
+
"id": "payments.charge",
|
|
61
|
+
"params": {"max_amount": 20000, "currency": "USD"}
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"limits": {
|
|
65
|
+
"payments.charge": {
|
|
66
|
+
"currency_limits": {
|
|
67
|
+
"USD": {"max_per_tx": 20000, "daily_cap": 100000},
|
|
68
|
+
"EUR": {"max_per_tx": 18000, "daily_cap": 90000}
|
|
69
|
+
},
|
|
70
|
+
"allowed_countries": ["US", "CA", "DE", "FR"],
|
|
71
|
+
"blocked_categories": ["weapons", "illicit"],
|
|
72
|
+
"allowed_merchant_ids": ["merch_abc", "merch_xyz"],
|
|
73
|
+
"max_items_per_tx": 5,
|
|
74
|
+
"require_assurance_at_least": "L2",
|
|
75
|
+
"idempotency_required": True
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"regions": ["US", "CA", "EU"],
|
|
79
|
+
"created_at": "2025-01-30T00:00:00Z",
|
|
80
|
+
"updated_at": "2025-01-30T00:00:00Z",
|
|
81
|
+
"version": "1.0.0"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_oap_decision_structure(self, valid_context, valid_passport):
|
|
86
|
+
"""Test that policy returns OAP-compliant decision structure"""
|
|
87
|
+
expected_decision = {
|
|
88
|
+
"decision_id": "550e8400-e29b-41d4-a716-446655440002",
|
|
89
|
+
"policy_id": "finance.payment.charge.v1",
|
|
90
|
+
"agent_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
91
|
+
"owner_id": "org_demo_co",
|
|
92
|
+
"assurance_level": "L2",
|
|
93
|
+
"allow": True,
|
|
94
|
+
"reasons": [
|
|
95
|
+
{
|
|
96
|
+
"code": "oap.allowed",
|
|
97
|
+
"message": "Transaction within limits and policy requirements"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"created_at": "2025-01-30T10:30:00Z",
|
|
101
|
+
"expires_in": 3600,
|
|
102
|
+
"passport_digest": "sha256:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef",
|
|
103
|
+
"signature": "ed25519:abcd1234efgh5678ijkl9012mnop3456qrst7890uvwx1234yzab5678cdef==",
|
|
104
|
+
"kid": "oap:registry:key-2025-01"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
108
|
+
# This would be the actual API call in a real test
|
|
109
|
+
response = await mock_policy_verification(expected_decision)()
|
|
110
|
+
decision = await response.json()
|
|
111
|
+
|
|
112
|
+
assert decision["allow"] is True
|
|
113
|
+
assert "decision_id" in decision
|
|
114
|
+
assert "policy_id" in decision
|
|
115
|
+
assert "agent_id" in decision
|
|
116
|
+
assert "owner_id" in decision
|
|
117
|
+
assert "assurance_level" in decision
|
|
118
|
+
assert "reasons" in decision
|
|
119
|
+
assert "created_at" in decision
|
|
120
|
+
assert "expires_in" in decision
|
|
121
|
+
assert "passport_digest" in decision
|
|
122
|
+
assert "signature" in decision
|
|
123
|
+
assert "kid" in decision
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_required_context_validation(self, valid_context, valid_passport):
|
|
127
|
+
"""Test validation of required context fields"""
|
|
128
|
+
# Test valid context
|
|
129
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
130
|
+
response = await mock_policy_verification({"allow": True})()
|
|
131
|
+
result = await response.json()
|
|
132
|
+
assert result["allow"] is True
|
|
133
|
+
|
|
134
|
+
# Test missing required fields
|
|
135
|
+
invalid_context = {
|
|
136
|
+
"amount": 1299,
|
|
137
|
+
"currency": "USD",
|
|
138
|
+
# Missing merchant_id, region, items, idempotency_key
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(400)):
|
|
142
|
+
response = await mock_policy_verification_error(400)()
|
|
143
|
+
assert not response.ok
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_currency_validation(self, valid_passport):
|
|
147
|
+
"""Test currency support validation"""
|
|
148
|
+
# Test supported currency
|
|
149
|
+
valid_context = {
|
|
150
|
+
"amount": 1000,
|
|
151
|
+
"currency": "EUR",
|
|
152
|
+
"merchant_id": "merch_abc",
|
|
153
|
+
"region": "EU",
|
|
154
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
155
|
+
"idempotency_key": "charge-ord-1002"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
159
|
+
response = await mock_policy_verification({"allow": True})()
|
|
160
|
+
result = await response.json()
|
|
161
|
+
assert result["allow"] is True
|
|
162
|
+
|
|
163
|
+
# Test unsupported currency
|
|
164
|
+
invalid_context = {
|
|
165
|
+
"amount": 1000,
|
|
166
|
+
"currency": "GBP", # Not supported
|
|
167
|
+
"merchant_id": "merch_abc",
|
|
168
|
+
"region": "US",
|
|
169
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
170
|
+
"idempotency_key": "charge-ord-1003"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
expected_decision = {
|
|
174
|
+
"allow": False,
|
|
175
|
+
"reasons": [{"code": "oap.currency_unsupported", "message": "Currency GBP is not supported"}]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
179
|
+
response = await mock_policy_verification(expected_decision)()
|
|
180
|
+
result = await response.json()
|
|
181
|
+
assert result["allow"] is False
|
|
182
|
+
assert result["reasons"][0]["code"] == "oap.currency_unsupported"
|
|
183
|
+
|
|
184
|
+
@pytest.mark.asyncio
|
|
185
|
+
async def test_amount_limits(self, valid_passport):
|
|
186
|
+
"""Test amount limit validation"""
|
|
187
|
+
# Test amount within limit
|
|
188
|
+
valid_context = {
|
|
189
|
+
"amount": 15000, # Within 20000 limit
|
|
190
|
+
"currency": "USD",
|
|
191
|
+
"merchant_id": "merch_abc",
|
|
192
|
+
"region": "US",
|
|
193
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
194
|
+
"idempotency_key": "charge-ord-1004"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
198
|
+
response = await mock_policy_verification({"allow": True})()
|
|
199
|
+
result = await response.json()
|
|
200
|
+
assert result["allow"] is True
|
|
201
|
+
|
|
202
|
+
# Test amount exceeding limit
|
|
203
|
+
invalid_context = {
|
|
204
|
+
"amount": 25000, # Exceeds 20000 limit
|
|
205
|
+
"currency": "USD",
|
|
206
|
+
"merchant_id": "merch_abc",
|
|
207
|
+
"region": "US",
|
|
208
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
209
|
+
"idempotency_key": "charge-ord-1005"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
expected_decision = {
|
|
213
|
+
"allow": False,
|
|
214
|
+
"reasons": [{"code": "oap.limit_exceeded", "message": "Amount exceeds per-transaction limit"}]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
218
|
+
response = await mock_policy_verification(expected_decision)()
|
|
219
|
+
result = await response.json()
|
|
220
|
+
assert result["allow"] is False
|
|
221
|
+
assert result["reasons"][0]["code"] == "oap.limit_exceeded"
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_item_count_limits(self, valid_passport):
|
|
225
|
+
"""Test item count limit validation"""
|
|
226
|
+
# Test items within limit
|
|
227
|
+
valid_context = {
|
|
228
|
+
"amount": 5000,
|
|
229
|
+
"currency": "USD",
|
|
230
|
+
"merchant_id": "merch_abc",
|
|
231
|
+
"region": "US",
|
|
232
|
+
"items": [
|
|
233
|
+
{"sku": "SKU-1", "qty": 1},
|
|
234
|
+
{"sku": "SKU-2", "qty": 1},
|
|
235
|
+
{"sku": "SKU-3", "qty": 1}
|
|
236
|
+
], # 3 items, within 5 limit
|
|
237
|
+
"idempotency_key": "charge-ord-1006"
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
241
|
+
response = await mock_policy_verification({"allow": True})()
|
|
242
|
+
result = await response.json()
|
|
243
|
+
assert result["allow"] is True
|
|
244
|
+
|
|
245
|
+
# Test items exceeding limit
|
|
246
|
+
invalid_context = {
|
|
247
|
+
"amount": 5000,
|
|
248
|
+
"currency": "USD",
|
|
249
|
+
"merchant_id": "merch_abc",
|
|
250
|
+
"region": "US",
|
|
251
|
+
"items": [
|
|
252
|
+
{"sku": "A", "qty": 1},
|
|
253
|
+
{"sku": "B", "qty": 1},
|
|
254
|
+
{"sku": "C", "qty": 1},
|
|
255
|
+
{"sku": "D", "qty": 1},
|
|
256
|
+
{"sku": "E", "qty": 1},
|
|
257
|
+
{"sku": "F", "qty": 1}
|
|
258
|
+
], # 6 items, exceeds 5 limit
|
|
259
|
+
"idempotency_key": "charge-ord-1007"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
expected_decision = {
|
|
263
|
+
"allow": False,
|
|
264
|
+
"reasons": [{"code": "oap.limit_exceeded", "message": "Item count exceeds maximum allowed"}]
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
268
|
+
response = await mock_policy_verification(expected_decision)()
|
|
269
|
+
result = await response.json()
|
|
270
|
+
assert result["allow"] is False
|
|
271
|
+
assert result["reasons"][0]["code"] == "oap.limit_exceeded"
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_merchant_validation(self, valid_passport):
|
|
275
|
+
"""Test merchant allowlist validation"""
|
|
276
|
+
# Test allowed merchant
|
|
277
|
+
valid_context = {
|
|
278
|
+
"amount": 5000,
|
|
279
|
+
"currency": "USD",
|
|
280
|
+
"merchant_id": "merch_abc", # In allowlist
|
|
281
|
+
"region": "US",
|
|
282
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
283
|
+
"idempotency_key": "charge-ord-1008"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
287
|
+
response = await mock_policy_verification({"allow": True})()
|
|
288
|
+
result = await response.json()
|
|
289
|
+
assert result["allow"] is True
|
|
290
|
+
|
|
291
|
+
# Test forbidden merchant
|
|
292
|
+
invalid_context = {
|
|
293
|
+
"amount": 5000,
|
|
294
|
+
"currency": "USD",
|
|
295
|
+
"merchant_id": "merch_bad", # Not in allowlist
|
|
296
|
+
"region": "US",
|
|
297
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
298
|
+
"idempotency_key": "charge-ord-1009"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
expected_decision = {
|
|
302
|
+
"allow": False,
|
|
303
|
+
"reasons": [{"code": "oap.merchant_forbidden", "message": "Merchant not in allowlist"}]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
307
|
+
response = await mock_policy_verification(expected_decision)()
|
|
308
|
+
result = await response.json()
|
|
309
|
+
assert result["allow"] is False
|
|
310
|
+
assert result["reasons"][0]["code"] == "oap.merchant_forbidden"
|
|
311
|
+
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_country_validation(self, valid_passport):
|
|
314
|
+
"""Test country allowlist validation"""
|
|
315
|
+
# Test allowed country
|
|
316
|
+
valid_context = {
|
|
317
|
+
"amount": 5000,
|
|
318
|
+
"currency": "USD",
|
|
319
|
+
"merchant_id": "merch_abc",
|
|
320
|
+
"region": "US",
|
|
321
|
+
"shipping_country": "US", # In allowlist
|
|
322
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
323
|
+
"idempotency_key": "charge-ord-1010"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
327
|
+
response = await mock_policy_verification({"allow": True})()
|
|
328
|
+
result = await response.json()
|
|
329
|
+
assert result["allow"] is True
|
|
330
|
+
|
|
331
|
+
# Test blocked country
|
|
332
|
+
invalid_context = {
|
|
333
|
+
"amount": 5000,
|
|
334
|
+
"currency": "USD",
|
|
335
|
+
"merchant_id": "merch_abc",
|
|
336
|
+
"region": "US",
|
|
337
|
+
"shipping_country": "BR", # Not in allowlist
|
|
338
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
339
|
+
"idempotency_key": "charge-ord-1011"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
expected_decision = {
|
|
343
|
+
"allow": False,
|
|
344
|
+
"reasons": [{"code": "oap.region_blocked", "message": "Shipping country not allowed"}]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
348
|
+
response = await mock_policy_verification(expected_decision)()
|
|
349
|
+
result = await response.json()
|
|
350
|
+
assert result["allow"] is False
|
|
351
|
+
assert result["reasons"][0]["code"] == "oap.region_blocked"
|
|
352
|
+
|
|
353
|
+
@pytest.mark.asyncio
|
|
354
|
+
async def test_category_blocking(self, valid_passport):
|
|
355
|
+
"""Test category blocklist validation"""
|
|
356
|
+
# Test allowed category
|
|
357
|
+
valid_context = {
|
|
358
|
+
"amount": 5000,
|
|
359
|
+
"currency": "USD",
|
|
360
|
+
"merchant_id": "merch_abc",
|
|
361
|
+
"region": "US",
|
|
362
|
+
"items": [{"sku": "SKU-1", "qty": 1, "category": "electronics"}], # Not blocked
|
|
363
|
+
"idempotency_key": "charge-ord-1012"
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
367
|
+
response = await mock_policy_verification({"allow": True})()
|
|
368
|
+
result = await response.json()
|
|
369
|
+
assert result["allow"] is True
|
|
370
|
+
|
|
371
|
+
# Test blocked category
|
|
372
|
+
invalid_context = {
|
|
373
|
+
"amount": 5000,
|
|
374
|
+
"currency": "USD",
|
|
375
|
+
"merchant_id": "merch_abc",
|
|
376
|
+
"region": "US",
|
|
377
|
+
"items": [{"sku": "SKU-1", "qty": 1, "category": "weapons"}], # Blocked category
|
|
378
|
+
"idempotency_key": "charge-ord-1013"
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
expected_decision = {
|
|
382
|
+
"allow": False,
|
|
383
|
+
"reasons": [{"code": "oap.category_blocked", "message": "Item category is blocked"}]
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
387
|
+
response = await mock_policy_verification(expected_decision)()
|
|
388
|
+
result = await response.json()
|
|
389
|
+
assert result["allow"] is False
|
|
390
|
+
assert result["reasons"][0]["code"] == "oap.category_blocked"
|
|
391
|
+
|
|
392
|
+
@pytest.mark.asyncio
|
|
393
|
+
async def test_idempotency_validation(self, valid_passport):
|
|
394
|
+
"""Test idempotency key validation"""
|
|
395
|
+
# Test unique idempotency key
|
|
396
|
+
valid_context = {
|
|
397
|
+
"amount": 5000,
|
|
398
|
+
"currency": "USD",
|
|
399
|
+
"merchant_id": "merch_abc",
|
|
400
|
+
"region": "US",
|
|
401
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
402
|
+
"idempotency_key": "charge-ord-unique-123"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
406
|
+
response = await mock_policy_verification({"allow": True})()
|
|
407
|
+
result = await response.json()
|
|
408
|
+
assert result["allow"] is True
|
|
409
|
+
|
|
410
|
+
# Test duplicate idempotency key
|
|
411
|
+
invalid_context = {
|
|
412
|
+
"amount": 5000,
|
|
413
|
+
"currency": "USD",
|
|
414
|
+
"merchant_id": "merch_abc",
|
|
415
|
+
"region": "US",
|
|
416
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
417
|
+
"idempotency_key": "charge-ord-1001" # Already used
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
expected_decision = {
|
|
421
|
+
"allow": False,
|
|
422
|
+
"reasons": [{"code": "oap.idempotency_conflict", "message": "Idempotency key already used"}]
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
426
|
+
response = await mock_policy_verification(expected_decision)()
|
|
427
|
+
result = await response.json()
|
|
428
|
+
assert result["allow"] is False
|
|
429
|
+
assert result["reasons"][0]["code"] == "oap.idempotency_conflict"
|
|
430
|
+
|
|
431
|
+
@pytest.mark.asyncio
|
|
432
|
+
async def test_assurance_level_validation(self, valid_passport):
|
|
433
|
+
"""Test assurance level validation"""
|
|
434
|
+
# Test sufficient assurance level
|
|
435
|
+
valid_context = {
|
|
436
|
+
"amount": 5000,
|
|
437
|
+
"currency": "USD",
|
|
438
|
+
"merchant_id": "merch_abc",
|
|
439
|
+
"region": "US",
|
|
440
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
441
|
+
"idempotency_key": "charge-ord-1014"
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification({"allow": True})):
|
|
445
|
+
response = await mock_policy_verification({"allow": True})()
|
|
446
|
+
result = await response.json()
|
|
447
|
+
assert result["allow"] is True
|
|
448
|
+
|
|
449
|
+
# Test insufficient assurance level
|
|
450
|
+
invalid_context = {
|
|
451
|
+
"amount": 5000,
|
|
452
|
+
"currency": "USD",
|
|
453
|
+
"merchant_id": "merch_abc",
|
|
454
|
+
"region": "US",
|
|
455
|
+
"items": [{"sku": "SKU-1", "qty": 1}],
|
|
456
|
+
"idempotency_key": "charge-ord-1015"
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
expected_decision = {
|
|
460
|
+
"allow": False,
|
|
461
|
+
"reasons": [{"code": "oap.assurance_insufficient", "message": "Assurance level too low"}]
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification(expected_decision)):
|
|
465
|
+
response = await mock_policy_verification(expected_decision)()
|
|
466
|
+
result = await response.json()
|
|
467
|
+
assert result["allow"] is False
|
|
468
|
+
assert result["reasons"][0]["code"] == "oap.assurance_insufficient"
|
|
469
|
+
|
|
470
|
+
@pytest.mark.asyncio
|
|
471
|
+
async def test_error_handling(self):
|
|
472
|
+
"""Test error handling for policy verification"""
|
|
473
|
+
# Test policy verification error
|
|
474
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(500)):
|
|
475
|
+
response = await mock_policy_verification_error(500)()
|
|
476
|
+
assert not response.ok
|
|
477
|
+
assert response.status_code == 500
|
|
478
|
+
|
|
479
|
+
# Test malformed request
|
|
480
|
+
with patch('aiohttp.ClientSession.post', side_effect=mock_policy_verification_error(400)):
|
|
481
|
+
response = await mock_policy_verification_error(400)()
|
|
482
|
+
assert not response.ok
|
|
483
|
+
assert response.status_code == 400
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
pytest.main([__file__])
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
## Required Context
|
|
3
|
+
|
|
4
|
+
This policy requires the following context (JSON Schema):
|
|
5
|
+
|
|
6
|
+
```json
|
|
7
|
+
{
|
|
8
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": [
|
|
11
|
+
"amount",
|
|
12
|
+
"currency",
|
|
13
|
+
"destination_type",
|
|
14
|
+
"destination_id",
|
|
15
|
+
"payout_method",
|
|
16
|
+
"idempotency_key"
|
|
17
|
+
],
|
|
18
|
+
"properties": {
|
|
19
|
+
"amount": {
|
|
20
|
+
"type": "integer",
|
|
21
|
+
"minimum": 1,
|
|
22
|
+
"description": "Payout amount in minor units (e.g., cents)"
|
|
23
|
+
},
|
|
24
|
+
"currency": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"pattern": "^[A-Z]{3}$",
|
|
27
|
+
"description": "ISO 4217 currency code"
|
|
28
|
+
},
|
|
29
|
+
"destination_type": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": [
|
|
32
|
+
"bank_account",
|
|
33
|
+
"digital_wallet",
|
|
34
|
+
"crypto_address"
|
|
35
|
+
],
|
|
36
|
+
"description": "Type of destination account"
|
|
37
|
+
},
|
|
38
|
+
"destination_id": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Destination account identifier"
|
|
41
|
+
},
|
|
42
|
+
"payout_method": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": [
|
|
45
|
+
"wire_transfer",
|
|
46
|
+
"ach",
|
|
47
|
+
"crypto_transfer",
|
|
48
|
+
"digital_wallet"
|
|
49
|
+
],
|
|
50
|
+
"description": "Method of payout"
|
|
51
|
+
},
|
|
52
|
+
"idempotency_key": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"minLength": 8,
|
|
55
|
+
"description": "Idempotency key for duplicate prevention"
|
|
56
|
+
},
|
|
57
|
+
"description": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "Optional payout description"
|
|
60
|
+
},
|
|
61
|
+
"compliance_notes": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "Compliance verification notes"
|
|
64
|
+
},
|
|
65
|
+
"approval_required": {
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"description": "Whether payout requires manual approval"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can also fetch this live via the discovery endpoint:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
curl -s "https://aport.io/api/policies/finance.payment.payout.v1?format=schema"
|
|
77
|
+
```
|
|
78
|
+
|