Manual Testing Guide: Approval Gate (v0.13.0)
Prerequisites
- Python 3.11+ with
uvinstalled - Node.js 18+ (for dashboard)
- mcp-hangar checked out on
feature/enterprise-migration - Optional: Slack workspace with webhook configured
1. Configuration
1.1 Dashboard Channel (default)
Add to your config.yaml:
enterprise:
approvals:
channel: dashboard1.2 Slack Channel
enterprise:
approvals:
channel: slack
slack:
webhook_url: "https://hooks.slack.com/services/T.../B.../xxx"
signing_secret: "your-slack-signing-secret"1.3 NoOp Channel (for testing without notifications)
enterprise:
approvals:
channel: noop2. Policy Configuration
Add approval_list to a provider's tool access policy:
providers:
grafana:
tool_access_policy:
deny_list:
- "admin_*"
approval_list:
- "delete_*"
- "create_alert_rule"
approval_timeout_seconds: 300
approval_channel: dashboardPolicy Precedence
| List | Effect |
|---|---|
deny_list | Blocked (highest) |
approval_list | Held for approval |
allow_list | Immediate execution |
| (none) | Unrestricted |
A tool on deny_list is always blocked -- even if also on approval_list.
3. Test Scenarios
3.1 Approve Flow (Dashboard)
Steps:
Start mcp-hangar:
bashcd mcp-hangar && uv run mcp-hangarStart the dashboard:
bashcd hangar-app && npm run devOpen the dashboard at
http://localhost:5173Navigate to Approvals in the sidebar (under Governance)
From an MCP client (e.g., Claude Code), invoke a tool matching the
approval_listpattern:delete_dashboard(id="dash-123")Observe in the dashboard:
- The "Approvals" page shows a new pending request
- Card shows: provider ID, tool name, countdown timer, arguments
- Badge shows pending count
Click Approve
Observe:
- The tool execution completes in the MCP client
- The card moves to "Approved" tab
- The card shows
decided_byinfo
Expected Result: Tool executes successfully after approval.
3.2 Deny Flow (Dashboard)
- Invoke a tool matching
approval_list - In the dashboard, expand the card and optionally enter a deny reason
- Click Deny
Expected Result: MCP client receives an error response with error_code: "approval_denied" and the deny reason.
3.3 Timeout Flow
- Set
approval_timeout_seconds: 10in policy (short timeout for testing) - Invoke a tool matching
approval_list - Do NOT approve or deny -- wait for timeout
Expected Result: After 10 seconds, MCP client receives error with error_code: "approval_timeout", message "No response within timeout".
3.4 Deny-List Override
Configure a tool that matches BOTH
deny_listandapproval_list:yamldeny_list: - "admin_*" approval_list: - "admin_*"Invoke
admin_reset()
Expected Result: Tool is blocked immediately (deny_list wins). No approval request is created.
3.5 Sensitive Argument Redaction
Invoke a tool with sensitive arguments:
connect_database(host="localhost", password="secret123", api_token="tok_abc")Check the approval card in the dashboard
Expected Result: Arguments show password: "[REDACTED]" and api_token: "[REDACTED]", while host shows the actual value.
4. REST API Testing (curl)
4.1 List Pending Approvals
curl -s http://localhost:8080/enterprise/approvals?state=pending | jq4.2 Get Single Approval
curl -s http://localhost:8080/enterprise/approvals/{approval_id} | jq4.3 Approve via API
curl -X POST http://localhost:8080/enterprise/approvals/{approval_id}/resolve \
-H "Content-Type: application/json" \
-H "x-principal-id: manual-tester" \
-d '{"decision": "approve"}'4.4 Deny via API
curl -X POST http://localhost:8080/enterprise/approvals/{approval_id}/resolve \
-H "Content-Type: application/json" \
-H "x-principal-id: manual-tester" \
-d '{"decision": "deny", "reason": "Not authorized for production"}'4.5 Double Resolve (idempotency check)
After resolving once, send the same request again:
# Should return 409 Conflict
curl -s -o /dev/null -w "%{http_code}" -X POST \
http://localhost:8080/enterprise/approvals/{approval_id}/resolve \
-H "Content-Type: application/json" \
-d '{"decision": "approve"}'Expected: HTTP 409
5. Slack Integration Testing
5.1 Prerequisite Setup
- Create a Slack App with Interactivity enabled
- Set the Request URL to:
https://your-domain/enterprise/approvals/{approval_id}/resolve - Copy the Signing Secret to config
- Set up an Incoming Webhook
5.2 Notification Test
- Configure
channel: slackin config - Invoke a tool matching
approval_list
Expected: Slack message appears with:
- Header: "Approval Required"
- Provider and tool name
- Sanitized arguments in a code block
- Expiry countdown
- "Approve" (green) and "Deny" (red) buttons
5.3 Slack Approve/Deny
- Click Approve or Deny in Slack
- Verify the tool execution completes (or fails with denied)
- Verify the
decided_byshowsslack:{user_id}
6. Permission Verification
6.1 Roles
| Role | Can view approvals | Can resolve |
|---|---|---|
| provider_admin | Yes | Yes |
| auditor | Yes | No |
| viewer | No | No |
6.2 Test Steps
Log in as
auditorroleNavigate to Approvals page -- should see pending requests
Try to approve -- should be blocked (no
approval:resolvepermission)Log in as
provider_adminNavigate to Approvals page
Approve/Deny -- should succeed
7. Domain Event Verification
After each approval action, verify events in the event store/log:
| Action | Expected Event |
|---|---|
| Request | ToolApprovalRequested |
| Approve | ToolApprovalGranted |
| Deny | ToolApprovalDenied |
| Timeout | ToolApprovalExpired |
Check via:
# If event store exposed via API:
curl -s http://localhost:8080/api/events?type=ToolApprovalRequested | jqOr check server logs for approval_id entries.
8. Automated Test Suite
Run all approval-related tests:
cd mcp-hangar
# Unit tests (106 tests)
uv run pytest tests/unit/domain/value_objects/test_tool_access_policy_approval.py \
tests/unit/enterprise/approvals/ -v
# Integration tests (14 tests)
uv run pytest tests/integration/test_approval_flow.py \
tests/integration/test_approval_api_e2e.py -v
# Fuzz tests (serialization round-trip)
uv run pytest tests/unit/test_event_serialization_fuzz.py -v
# Enterprise boundary check
bash scripts/check_enterprise_boundary.sh9. Checklist
- [ ] Approve flow works via dashboard
- [ ] Deny flow works with reason
- [ ] Timeout expires correctly
- [ ] deny_list overrides approval_list
- [ ] Sensitive args are redacted
- [ ] REST API returns correct status codes (200, 400, 404, 409)
- [ ] Double resolve returns 409
- [ ] Slack notifications arrive (if configured)
- [ ] Slack buttons resolve correctly
- [ ] provider_admin can resolve, auditor can only view
- [ ] Domain events published for all transitions
- [ ] Concurrent approvals do not interfere
- [ ] All automated tests pass (unit + 14 integration)