Testing Guide¶
End-to-end curl examples for every major feature. Run these against a local or staging instance before going to production.
Setup¶
Set environment variables once per session.
All examples below use Bash syntax.
1. License Types¶
Create licenses¶
Test activation¶
LICENSE_KEY="LIC-XXXX-YYYY-ZZZZ" # from create response
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$LICENSE_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"machineId": "test-machine-001"
}' | jq .
Expected: "success": true, "token": "<jwt>".
Test validation¶
curl -s -X POST "$BASE_URL/api/licenses/validate" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$LICENSE_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"machineId": "test-machine-001"
}' | jq .
Test deactivation¶
curl -s -X POST "$BASE_URL/api/licenses/deactivate" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$LICENSE_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"machineId": "test-machine-001"
}' | jq .
Reset all machines (admin)¶
curl -s -X POST "$BASE_URL/api/admin/licenses/$LICENSE_KEY/reset-machines" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq .
2. Site Licenses¶
Site licenses allow unlimited activations from any machine. Geo checks are skipped.
SITE_KEY="LIC-SITE-YYYY-ZZZZ"
# Activate machine 1
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$SITE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"farm-node-001"}' | jq .
# Activate machine 2 (different country simulation - should still pass)
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$SITE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"farm-node-002"}' | jq .
# Activate 20 more - all should succeed
for i in $(seq 3 22); do
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$SITE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"farm-node-'"$i"'"}' \
| jq .success
done
3. Trial Codes¶
Create trial codes¶
curl -s -X POST "$BASE_URL/api/admin/trial-codes" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"durationDays": 14,
"machineLimit": 1,
"quantity": 3,
"note": "test-batch"
}' | jq .
List trial codes¶
curl -s "$BASE_URL/api/admin/trial-codes?productId=$PRODUCT_ID" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq .
Redeem via webhook (simulating a purchase)¶
TRIAL_CODE="TRIAL-XXXX-YYYY"
curl -s -X POST "$BASE_URL/api/webhooks/redeem-trial" \
-H "Content-Type: application/json" \
-d '{
"trialCode": "'"$TRIAL_CODE"'",
"customerEmail": "trialer@test.com",
"productId": "'"$PRODUCT_ID"'"
}' | jq .
Test trial limits (should fail after limit)¶
TRIAL_KEY="LIC-TRIAL-YYYY" # from redeem response
# First activation (should succeed)
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$TRIAL_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"trial-machine-001"}' | jq .
# Second activation on different machine (should fail - limit 1)
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$TRIAL_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"trial-machine-002"}' | jq .
# Expected: "success": false, error code: MACHINE_LIMIT_REACHED
4. Threat Detection¶
Simulate geo-spread (activate from 3+ countries rapidly)¶
GEO_KEY="LIC-GEO-YYYY-ZZZZ"
# These use X-Forwarded-For to simulate different IPs / geos
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 85.214.0.1" \
-d '{"licenseKey":"'"$GEO_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"machine-de"}' | jq .
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 68.85.0.1" \
-d '{"licenseKey":"'"$GEO_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"machine-us"}' | jq .
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-H "X-Forwarded-For: 177.131.0.1" \
-d '{"licenseKey":"'"$GEO_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"machine-br"}' | jq .
Simulate machine churn¶
CHURN_KEY="LIC-CHURN-YYYY-ZZZZ"
for i in $(seq 1 10); do
# Activate
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$CHURN_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"churn-machine-'"$i"'"}' > /dev/null
# Deactivate immediately
curl -s -X POST "$BASE_URL/api/licenses/deactivate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$CHURN_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"churn-machine-'"$i"'"}' > /dev/null
done
Check threat status¶
curl -s "$BASE_URL/api/admin/licenses/$GEO_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq '.threats'
Resolve a threat (clear flag)¶
curl -s -X POST "$BASE_URL/api/admin/licenses/$GEO_KEY/clear-flag" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"reason": "Verified legitimate user traveling internationally"}' | jq .
5. Chargeback and Refund¶
Simulate a refund webhook¶
curl -s -X POST "$BASE_URL/cgloungeWebhook" \
-H "Content-Type: application/json" \
-H "x-webhook-secret: $WEBHOOK_SECRET" \
-d '{
"type": "purchase.refunded",
"email": "refund@test.com",
"productId": "'"$PRODUCT_ID"'",
"purchaseId": "refund-test-001"
}' | jq .
Expected: license status changes to revoked.
Simulate a chargeback webhook¶
curl -s -X POST "$BASE_URL/api/webhooks/payment" \
-H "Content-Type: application/json" \
-d '{
"type": "chargeback",
"licenseKey": "LIC-XXXX-YYYY-ZZZZ",
"productId": "'"$PRODUCT_ID"'",
"customerEmail": "chargeback@test.com"
}' | jq .
Expected: license revoked, threat flag added.
Verify post-revoke activation fails¶
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"LIC-XXXX-YYYY-ZZZZ","productId":"'"$PRODUCT_ID"'","machineId":"test-001"}' | jq .
# Expected: "success": false, error: LICENSE_REVOKED
6. Floating Licenses¶
Check out a floating seat (heartbeat session start)¶
FLOAT_KEY="LIC-FLOAT-YYYY-ZZZZ"
curl -s -X POST "$BASE_URL/api/licenses/floating/checkout" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$FLOAT_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"machineId": "workstation-a",
"sessionId": "session-abc123"
}' | jq .
Send heartbeat¶
curl -s -X POST "$BASE_URL/api/licenses/floating/heartbeat" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$FLOAT_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"sessionId": "session-abc123"
}' | jq .
Release a floating seat¶
curl -s -X POST "$BASE_URL/api/licenses/floating/release" \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "'"$FLOAT_KEY"'",
"productId": "'"$PRODUCT_ID"'",
"sessionId": "session-abc123"
}' | jq .
Test seat limit (4th checkout should fail for 3-seat license)¶
for i in 1 2 3; do
curl -s -X POST "$BASE_URL/api/licenses/floating/checkout" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$FLOAT_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"ws-'"$i"'","sessionId":"session-'"$i"'"}' \
| jq .success
done
# This one should fail
curl -s -X POST "$BASE_URL/api/licenses/floating/checkout" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$FLOAT_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"ws-4","sessionId":"session-4"}' | jq .
# Expected: "success": false, error: MACHINE_LIMIT_REACHED
7. Revoke and Reinstate¶
Revoke¶
curl -s -X POST "$BASE_URL/api/admin/licenses/$LICENSE_KEY/revoke" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"reason": "Terms of service violation"}' | jq .
Verify revoked license rejects activation¶
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$LICENSE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"any"}' | jq .
# Expected: "success": false, error: LICENSE_REVOKED
Reinstate¶
curl -s -X POST "$BASE_URL/api/admin/licenses/$LICENSE_KEY/reinstate" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq .
Verify reinstatement works¶
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$LICENSE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"post-reinstate-001"}' | jq .
# Expected: "success": true
8. Expiring Licenses¶
Create a license that expires in 2 days¶
EXPIRY_DATE=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+2d +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X POST "$BASE_URL/api/admin/licenses" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"variant": "indie", "licenseType": "per-machine",
"machineLimit": 1,
"customerEmail": "expiring@test.com",
"expiresAt": "'"$EXPIRY_DATE"'"
}' | jq .
Activate and validate¶
EXPIRY_KEY="LIC-EXPIRY-YYYY"
curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$EXPIRY_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"expiry-test-001"}' | jq '.license.expiresAt'
Manually expire (admin backdating for testing)¶
curl -s -X PATCH "$BASE_URL/api/admin/licenses/$EXPIRY_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"expiresAt": "2020-01-01T00:00:00Z"}' | jq .
Verify expired license rejects¶
curl -s -X POST "$BASE_URL/api/licenses/validate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$EXPIRY_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"expiry-test-001"}' | jq .
# Expected: "success": false, error: LICENSE_EXPIRED
9. Paid Duration (durationDays)¶
Create a variant with durationDays¶
curl -s -X POST "$BASE_URL/api/admin/variants" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"name": "Annual Subscription",
"priceUsd": 4900,
"durationDays": 365,
"machineLimit": 2
}' | jq .
Purchase (simulate webhook creating a license with expiry)¶
VARIANT_ID="var_annual_xyz"
PURCHASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EXPIRY_DATE=$(date -u -d "+365 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+365d +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X POST "$BASE_URL/api/admin/licenses" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"variantId": "'"$VARIANT_ID"'",
"variant": "indie", "licenseType": "per-machine",
"machineLimit": 2,
"customerEmail": "subscriber@test.com",
"expiresAt": "'"$EXPIRY_DATE"'"
}' | jq .
Verify expiry is correct¶
DURATION_KEY="LIC-DURATION-YYYY"
curl -s "$BASE_URL/api/admin/licenses/$DURATION_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq '.expiresAt'
Simulate renewal (extend expiry by 365 days)¶
NEW_EXPIRY=$(date -u -d "+730 days" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -v+730d +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X PATCH "$BASE_URL/api/admin/licenses/$DURATION_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"expiresAt": "'"$NEW_EXPIRY"'"}' | jq .
Simulate cancellation (set expiry to now)¶
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
curl -s -X PATCH "$BASE_URL/api/admin/licenses/$DURATION_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"expiresAt": "'"$NOW"'"}' | jq .
Priority test: perpetual beats subscription¶
Create one perpetual and one subscription for the same product and verify the perpetual does not gain an expiry via any renewal logic.
# Perpetual license - should never have expiresAt set to non-null
curl -s -X POST "$BASE_URL/api/admin/licenses" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"variant": "indie", "licenseType": "per-machine",
"machineLimit": 2,
"customerEmail": "perpetual@test.com"
}' | jq '.expiresAt'
# Expected: null
10. Variant Management¶
Create a variant¶
curl -s -X POST "$BASE_URL/api/admin/variants" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{
"productId": "'"$PRODUCT_ID"'",
"name": "Indie",
"priceUsd": 2900,
"machineLimit": 2,
"durationDays": null
}' | jq .
List active variants¶
curl -s "$BASE_URL/api/products/$PRODUCT_ID/variants" | jq .
# Returns only variants where active: true (default filter)
Deactivate a variant¶
VARIANT_ID="var_indie_abc"
curl -s -X PATCH "$BASE_URL/api/admin/variants/$VARIANT_ID" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"active": false}' | jq .
List variants including inactive¶
curl -s "$BASE_URL/api/admin/variants?productId=$PRODUCT_ID&includeInactive=true" \
-H "Authorization: Bearer $ADMIN_SECRET" | jq .
Quick Test Script¶
Runs the happy path for all major features against a local server.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://localhost:3000}"
ADMIN_SECRET="${ADMIN_SECRET:-changeme}"
PRODUCT_ID="${PRODUCT_ID:-prod_test123}"
echo "=== CG Lounge License Server: Quick Test ==="
echo "Target: $BASE_URL"
echo ""
fail() { echo "FAIL: $1"; exit 1; }
pass() { echo "PASS: $1"; }
# Health check
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health")
[ "$STATUS" = "200" ] || fail "Health check ($STATUS)"
pass "Health check"
# Create license
RESPONSE=$(curl -s -X POST "$BASE_URL/api/admin/licenses" \
-H "Authorization: Bearer $ADMIN_SECRET" \
-H "Content-Type: application/json" \
-d '{"productId":"'"$PRODUCT_ID"'","licenseType":"indie","machineLimit":2,"customerEmail":"quicktest@test.com"}')
KEY=$(echo "$RESPONSE" | jq -r '.key // empty')
[ -n "$KEY" ] || fail "Create license: $RESPONSE"
pass "Create license: $KEY"
# Activate
RESPONSE=$(curl -s -X POST "$BASE_URL/api/licenses/activate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"quicktest-001"}')
TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty')
[ -n "$TOKEN" ] || fail "Activate: $RESPONSE"
pass "Activate"
# Validate
RESPONSE=$(curl -s -X POST "$BASE_URL/api/licenses/validate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"quicktest-001"}')
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
[ "$SUCCESS" = "true" ] || fail "Validate: $RESPONSE"
pass "Validate"
# Deactivate
RESPONSE=$(curl -s -X POST "$BASE_URL/api/licenses/deactivate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"quicktest-001"}')
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
[ "$SUCCESS" = "true" ] || fail "Deactivate: $RESPONSE"
pass "Deactivate"
# Cleanup
curl -s -X DELETE "$BASE_URL/api/admin/licenses/$KEY" \
-H "Authorization: Bearer $ADMIN_SECRET" > /dev/null
pass "Cleanup"
echo ""
echo "All tests passed."
Cleanup Commands¶
Remove all test data after a test session.
# Delete a specific license
curl -s -X DELETE "$BASE_URL/api/admin/licenses/$LICENSE_KEY" \
-H "Authorization: Bearer $ADMIN_SECRET"
# Delete all licenses for a product (dangerous: use only on test products)
curl -s -X DELETE "$BASE_URL/api/admin/products/$PRODUCT_ID/licenses" \
-H "Authorization: Bearer $ADMIN_SECRET"
# Delete a variant
curl -s -X DELETE "$BASE_URL/api/admin/variants/$VARIANT_ID" \
-H "Authorization: Bearer $ADMIN_SECRET"
# Purge all trial codes for a product
curl -s -X DELETE "$BASE_URL/api/admin/trial-codes?productId=$PRODUCT_ID" \
-H "Authorization: Bearer $ADMIN_SECRET"
Troubleshooting¶
401 Unauthorized on admin endpoints¶
Check that Authorization: Bearer $ADMIN_SECRET is set and the secret matches what the server was started with.
404 Not Found on license key¶
The key does not exist or belongs to a different product. Check productId matches the one used at creation time.
MACHINE_LIMIT_REACHED on second activation¶
Expected if the machine limit is 1. Deactivate first:
curl -s -X POST "$BASE_URL/api/licenses/deactivate" \
-H "Content-Type: application/json" \
-d '{"licenseKey":"'"$LICENSE_KEY"'","productId":"'"$PRODUCT_ID"'","machineId":"test-machine-001"}' | jq .
Webhook signature errors¶
The webhook secret is compared as a plain string match. Ensure the x-webhook-secret header value exactly matches what's configured. Test with:
# Test that the secret matches (should return 200)
curl -s -X POST "$BASE_URL/cgloungeWebhook" \
-H "Content-Type: application/json" \
-H "x-webhook-secret: $WEBHOOK_SECRET" \
-d '{"type":"purchase.completed","email":"test@x.com","productId":"test","purchaseId":"sig-test"}'
# Test with wrong secret (should return 401)
curl -s -X POST "$BASE_URL/cgloungeWebhook" \
-H "Content-Type: application/json" \
-H "x-webhook-secret: wrong-secret" \
-d '{"type":"purchase.completed","email":"test@x.com","productId":"test","purchaseId":"sig-test2"}'
jq not found¶
Install jq:
# Ubuntu/Debian
apt-get install -y jq
# macOS
brew install jq
# Windows (Chocolatey)
choco install jq
Date commands differ between macOS and Linux¶
The test examples use both formats:
# Linux (GNU date)
date -u -d "+365 days" +"%Y-%m-%dT%H:%M:%SZ"
# macOS (BSD date)
date -u -v+365d +"%Y-%m-%dT%H:%M:%SZ"
The quick test script uses the 2>/dev/null || fallback pattern to handle both.