Designing a Secure Sandbox Environment for Testing High-Risk API Integrations
What This Guide Covers
You are designing a fully isolated testing environment for Genesys Cloud API integrations that involve high-risk operations - bulk user deletion, recording purge, GDPR erasure requests, telephony trunk modifications, and OAuth client management - where a misconfigured test against production would cause irreversible data loss or service disruption. When complete, your development team has a hermetic sandbox that mirrors the production org’s configuration, intercepts destructive API calls before they execute, generates realistic mock responses, and logs every API interaction for post-test audit - all without ever touching production data.
Prerequisites, Roles & Licensing
- Genesys Cloud: A dedicated sandbox/developer org (separate from production) - Genesys Cloud offers free developer orgs at developer.genesys.cloud; alternatively a paid non-production org that mirrors your production licensing
- Testing infrastructure: A mock server (Prism, WireMock, or a custom Node.js interceptor) positioned between your integration code and the Genesys Cloud API
- Access management: The sandbox org must have identical role/permission structure to production, but with no real agents, customers, or live telephony
- CI/CD integration: GitHub Actions, GitLab CI, or Jenkins for automated sandbox test execution
The Implementation Deep-Dive
1. The Three-Layer Sandbox Architecture
A production-safe sandbox requires isolation at three levels:
[Integration Code Under Test]
│
▼
[Layer 1: Environment Variable Routing]
GENESYS_ENV=sandbox → connects to sandbox org
GENESYS_ENV=mock → connects to mock server (no network calls)
GENESYS_ENV=prod → connects to production (requires explicit override)
│
▼
[Layer 2: API Guard Middleware]
Intercepts ALL destructive HTTP verbs (DELETE, PATCH on critical resources)
Logs intent, checks test mode flag, either executes or dry-runs
│
▼
[Layer 3: Sandbox Org or Mock Server]
Sandbox org: real Genesys Cloud API, isolated data
Mock server: offline, pre-recorded responses for CI/CD
Environment routing in code:
import os
from enum import Enum
class GenesysEnvironment(Enum):
PRODUCTION = "prod"
SANDBOX = "sandbox"
MOCK = "mock"
class GenesysClientFactory:
@staticmethod
def create(env: GenesysEnvironment | None = None):
env = env or GenesysEnvironment(os.getenv("GENESYS_ENV", "sandbox"))
if env == GenesysEnvironment.MOCK:
return MockGenesysClient(base_url="http://localhost:4010")
elif env == GenesysEnvironment.SANDBOX:
client_id = os.getenv("GENESYS_SANDBOX_CLIENT_ID")
client_secret = os.getenv("GENESYS_SANDBOX_CLIENT_SECRET")
base_url = os.getenv("GENESYS_SANDBOX_URL", "https://api.mypurecloud.com")
if not client_id or not client_secret:
raise EnvironmentError("Sandbox credentials not configured. Set GENESYS_SANDBOX_CLIENT_ID and GENESYS_SANDBOX_CLIENT_SECRET.")
return GenesysAPIClient(client_id, client_secret, base_url)
elif env == GenesysEnvironment.PRODUCTION:
# Require an explicit override to prevent accidental production access
if not os.getenv("ALLOW_PRODUCTION_ACCESS") == "I_KNOW_WHAT_I_AM_DOING":
raise PermissionError(
"Production access requires ALLOW_PRODUCTION_ACCESS=I_KNOW_WHAT_I_AM_DOING. "
"Did you mean GENESYS_ENV=sandbox?"
)
client_id = os.getenv("GENESYS_PROD_CLIENT_ID")
client_secret = os.getenv("GENESYS_PROD_CLIENT_SECRET")
return GenesysAPIClient(client_id, client_secret, "https://api.mypurecloud.com")
The Trap - relying on the developer’s discipline to not use production credentials during testing: Environment-based discipline fails under time pressure. The guard pattern above makes production access opt-in with a friction phrase, rather than the default. A developer running tests in their local shell won’t accidentally connect to production unless they deliberately set both GENESYS_ENV=prod and the override phrase.
2. API Guard Middleware: Dry-Run Mode for Destructive Operations
The API guard sits between your code and the actual HTTP client, intercepting destructive calls:
from typing import Optional
import logging
DESTRUCTIVE_PATTERNS = [
("DELETE", "/api/v2/users/"),
("DELETE", "/api/v2/gdpr/"),
("DELETE", "/api/v2/recordings/"),
("DELETE", "/api/v2/telephony/"),
("POST", "/api/v2/gdpr/requests"), # GDPR deletion trigger
("POST", "/api/v2/telephony/providers/"), # Trunk modification
("PATCH", "/api/v2/authorization/"), # Permission changes
("PUT", "/api/v2/authorization/"),
]
class APIGuardMiddleware:
def __init__(self, underlying_client, dry_run: bool = False, audit_log: str = None):
self.client = underlying_client
self.dry_run = dry_run
self.audit_log_path = audit_log
self.blocked_calls = []
def request(self, method: str, url: str, **kwargs) -> dict:
is_destructive = any(
method.upper() == m and pattern in url
for m, pattern in DESTRUCTIVE_PATTERNS
)
audit_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"method": method,
"url": url,
"body": kwargs.get("json"),
"is_destructive": is_destructive,
"dry_run_blocked": False
}
if is_destructive and self.dry_run:
audit_entry["dry_run_blocked"] = True
self.blocked_calls.append(audit_entry)
self._write_audit(audit_entry)
logging.warning(f"[DRY RUN BLOCKED] {method} {url}")
# Return a mock success response without executing
return {
"_dryRun": True,
"_blockedUrl": url,
"_blockedMethod": method,
"id": f"dry-run-{uuid.uuid4()}",
"status": "simulated"
}
# Execute the real call
result = self.client.request(method, url, **kwargs)
self._write_audit(audit_entry)
return result
def _write_audit(self, entry: dict):
if self.audit_log_path:
with open(self.audit_log_path, "a") as f:
f.write(json.dumps(entry) + "\n")
def get_blocked_call_report(self) -> list:
return self.blocked_calls
Usage in tests:
def test_gdpr_erasure_pipeline():
# Use dry-run mode - no actual deletion occurs
client = GenesysClientFactory.create(GenesysEnvironment.SANDBOX)
guard = APIGuardMiddleware(client, dry_run=True, audit_log="/tmp/test_audit.jsonl")
erasure_pipeline = GDPRErasurePipeline(api_client=guard)
# Run the pipeline - destructive calls are intercepted and logged
result = erasure_pipeline.process_erasure_request(
subject_id="test-subject-001",
email="test@example.com"
)
# Assert the pipeline attempted the correct operations
blocked = guard.get_blocked_call_report()
assert any("gdpr/requests" in call["url"] for call in blocked), "Should have attempted GDPR deletion"
assert all(call["dry_run_blocked"] for call in blocked if call["is_destructive"]), "All destructive calls blocked"
3. Mock Server: Offline Testing with Prism
For CI/CD pipelines where network access to even a sandbox org is impractical, use a mock server based on the Genesys Cloud OpenAPI spec:
# Install Prism (OpenAPI mock server)
npm install -g @stoplight/prism-cli
# Download Genesys Cloud OpenAPI spec
curl -o genesys_cloud_api.yaml https://developer.genesys.cloud/api/rest/v2/apis.yaml
# Start Prism mock server on port 4010
prism mock genesys_cloud_api.yaml --port 4010 --dynamic
With --dynamic, Prism generates realistic mock responses from the schema definitions rather than returning empty objects. Your code sees properly structured responses that match the Genesys Cloud API contract.
Custom response overrides for specific test scenarios:
Prism supports __examples__ in the spec or x-prism-examples extensions. For your test scenarios, create a custom mock overlay:
# mock_overrides.yaml
paths:
/api/v2/users:
get:
responses:
"200":
content:
application/json:
example:
entities:
- id: "test-user-001"
name: "Test Agent One"
email: "agent1@test.com"
state: "active"
- id: "test-user-002"
name: "Test Agent Two"
email: "agent2@test.com"
state: "active"
pageCount: 1
pageNumber: 1
pageSize: 25
total: 2
4. Sandbox Data Fixtures: Mirroring Production Structure
Your sandbox org must contain representative data for meaningful tests. Automate fixture creation:
def setup_sandbox_fixtures(access_token: str, sandbox_url: str):
"""
Idempotently create test data in the sandbox org.
Safe to run multiple times - checks existence before creating.
"""
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
fixtures = {
"queues": [
{"name": "TEST-Standard-Support", "mediaSettings": {"call": {"alertingTimeoutSeconds": 30}}},
{"name": "TEST-Enterprise-Priority", "mediaSettings": {"call": {"alertingTimeoutSeconds": 20}}},
],
"users": [
{"name": "Test Agent Alpha", "email": "agent.alpha@sandbox.test", "password": "TestPass123!"},
{"name": "Test Agent Beta", "email": "agent.beta@sandbox.test", "password": "TestPass123!"},
{"name": "Test Supervisor", "email": "supervisor@sandbox.test", "password": "TestPass123!"},
],
"wrapUpCodes": [
{"name": "TEST-Resolved"},
{"name": "TEST-Callback"},
{"name": "TEST-Escalated"}
]
}
created_resources = {}
for queue in fixtures["queues"]:
# Check if queue exists
existing = requests.get(
f"{sandbox_url}/api/v2/routing/queues",
headers=headers,
params={"name": queue["name"]}
).json().get("entities", [])
if not existing:
resp = requests.post(f"{sandbox_url}/api/v2/routing/queues", headers=headers, json=queue)
created_resources.setdefault("queues", []).append(resp.json()["id"])
else:
created_resources.setdefault("queues", []).append(existing[0]["id"])
return created_resources
5. CI/CD Integration: Automated Sandbox Tests in GitHub Actions
# .github/workflows/sandbox_tests.yml
name: Genesys Cloud Integration Tests (Sandbox)
on:
pull_request:
branches: [main, develop]
jobs:
sandbox_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with: { python-version: "3.11" }
- name: Install dependencies
run: pip install -r requirements.txt
- name: Start Prism mock server
run: |
npm install -g @stoplight/prism-cli
prism mock tests/fixtures/genesys_cloud_api.yaml --port 4010 --dynamic &
sleep 3 # Wait for Prism to start
- name: Run mock-mode tests (no network required)
env:
GENESYS_ENV: mock
run: pytest tests/unit/ -v --tb=short
- name: Run sandbox-mode tests (sandbox org)
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main'
env:
GENESYS_ENV: sandbox
GENESYS_SANDBOX_CLIENT_ID: ${{ secrets.GENESYS_SANDBOX_CLIENT_ID }}
GENESYS_SANDBOX_CLIENT_SECRET: ${{ secrets.GENESYS_SANDBOX_CLIENT_SECRET }}
GENESYS_SANDBOX_URL: https://api.mypurecloud.com
run: pytest tests/integration/ -v --tb=short -m "sandbox"
- name: Upload audit log
if: always()
uses: actions/upload-artifact@v4
with:
name: sandbox-api-audit-log
path: /tmp/test_audit.jsonl
Validation, Edge Cases & Troubleshooting
Edge Case 1: Sandbox Org Token Quota Exhaustion
Your sandbox org has the same OAuth token limits as your production org tier. If your CI/CD pipeline runs 50 parallel test jobs each requesting a new token, you may hit token issuance rate limits. Cache sandbox tokens in GitHub Actions secrets cache (using actions/cache for the token file) or use a shared token service that vends tokens to all test jobs from a single OAuth client.
Edge Case 2: Prism Dynamic Responses Not Matching Real API Behavior
Prism generates responses from the schema, but some Genesys Cloud API behaviors are non-obvious from the schema alone (e.g., pagination cursor behavior, conditional field presence). Supplement Prism with recorded responses captured during a real sandbox test run using your API guard middleware’s audit log. Convert the audit log to Prism example overrides for the most complex endpoints.
Edge Case 3: Sandbox Org Data Pollution Across Test Runs
If fixture creation doesn’t clean up test resources, your sandbox accumulates thousands of test users, queues, and wrap-up codes after 30 test runs. Add a teardown fixture that deletes all resources tagged with “TEST-” prefix. Run teardown at both test start (clear previous run) and test end (clean exit):
def teardown_sandbox():
"""Delete all TEST- prefixed resources from the sandbox org."""
test_users = get_users_matching_prefix("TEST-", access_token, sandbox_url)
for user in test_users:
delete_user(user["id"], access_token, sandbox_url)
Edge Case 4: Dry-Run Results Diverging from Real API Behavior
The API guard’s dry-run mode returns a synthetic {_dryRun: true, id: "dry-run-uuid"} response. If downstream code uses the returned id for a subsequent API call (e.g., creates a user, then assigns a role to the returned user ID), the chain breaks in dry-run mode - the fake ID is not a real resource. Structure your tests to isolate destructive operations rather than chaining them: test the deletion logic separately from the pre-deletion data fetching logic, with the destructive step asserted via the audit log rather than its downstream effects.