Skip to content

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.

export BASE_URL="http://localhost:3000"
export ADMIN_SECRET="your-admin-secret"
export PRODUCT_ID="prod_test123"
$BASE_URL    = "http://localhost:3000"
$ADMIN_SECRET = "your-admin-secret"
$PRODUCT_ID  = "prod_test123"

All examples below use Bash syntax.


1. License Types

Create licenses

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": "indie@test.com"
  }' | jq .
curl -s -X POST "$BASE_URL/api/admin/licenses" \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "'"$PRODUCT_ID"'",
    "variant": "studio", "licenseType": "per-machine",
    "machineLimit": 5,
    "customerEmail": "studio@test.com"
  }' | jq .
curl -s -X POST "$BASE_URL/api/admin/licenses" \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "'"$PRODUCT_ID"'",
    "licenseType": "site",
    "customerEmail": "studio@bigcorp.com"
  }' | jq .
curl -s -X POST "$BASE_URL/api/admin/licenses" \
  -H "Authorization: Bearer $ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "'"$PRODUCT_ID"'",
    "licenseType": "floating",
    "machineLimit": 3,
    "customerEmail": "float@studio.com"
  }' | jq .

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.

echo $ADMIN_SECRET  # verify it's set

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.