Testing
This comprehensive guide covers everything you need to know about testing the Foundry REST API, including setup, running tests, understanding test infrastructure, and generating documentation examples.
Overview
The Foundry REST API uses a testing framework that:
- Tests against multiple Foundry VTT versions simultaneously
- Supports both headless browser automation and existing sessions
- Captures real API responses for documentation generation
- Manages entity lifecycle with automatic cleanup
- Sources test data from system compendiums for properly initialized entities
- Runs system-specific tests (e.g., D&D 5e) only on matching Foundry instances
- Tests permission filtering with the
userIdparameter - Uses a custom test sequencer to ensure proper execution order
Prerequisites
Before running tests, you need:
- Relay Server: The REST API relay server running (this project)
- Foundry Instances: One or more Foundry VTT instances running with the REST API module installed
- Test Worlds: Foundry worlds ready for testing (entities will be created/deleted during tests)
Environment Configuration
Setting Up the Test Environment
Create a .env.test file based on the example:
cp .env.test.example .env.test
Configuration Options
Required Settings
# Relay Server Configuration
TEST_BASE_URL=http://localhost:3010
TEST_API_KEY=your-relay-api-key-here
# Foundry Instance URLs
FOUNDRY_V12_URL=http://localhost:30012
FOUNDRY_V13_URL=http://localhost:30013
# Which versions to test (comma-separated)
TEST_FOUNDRY_VERSIONS=12,13
Session Mode: Existing Session (Recommended for Development)
Using an existing session is faster and doesn't require browser automation:
USE_EXISTING_SESSION=true
# Client IDs per version (get from /clients endpoint or browser console)
TEST_CLIENT_ID_V12=foundry-abc123
TEST_CLIENT_ID_V13=foundry-xyz789
When using existing sessions, the session endpoint tests (session-endpoints.test.ts) are automatically skipped since session creation/authentication is not needed.
How to get your client ID:
- Open Foundry in your browser and log in to a world as a GM
- The REST API module will connect to the relay server
- Visit
http://localhost:3010/clientswith your API key header to see connected clients - Or check the browser console for the client ID
When using existing sessions with multiple Foundry versions in the same browser, be aware that browsers share cookies across localhost ports. This can cause authentication issues. Use different browser profiles or incognito windows for each Foundry version.
Session Mode: Headless Automation (For CI/CD)
For automated testing, the framework can create headless browser sessions:
USE_EXISTING_SESSION=false
# Foundry login credentials
FOUNDRY_USERNAME=Gamemaster
FOUNDRY_PASSWORD=your-password
Headless mode may fail if there are already GMs connected to the Foundry world, as they may retain "primary GM" status and the relay will connect to them instead of the headless session. Ensure no other GM clients are connected when running headless tests. This will be improved in a future update.
# Default world name
TEST_DEFAULT_WORLD=test-world
# Version-specific world names (optional)
FOUNDRY_V12_WORLD=test-world-v12
FOUNDRY_V13_WORLD=test-world-v13
Permission Filtering Tests (Optional)
To test userId permission filtering with a real non-GM player:
# Create a non-GM player in your Foundry world, then provide their ID or username
TEST_PLAYER_USER_ID_V12=Player1
TEST_PLAYER_USER_ID_V13=Player1
Without this, only invalid-userId error handling tests run. With it, additional tests verify:
- Search result filtering (player sees fewer results than GM)
- Write permission denial (player can't update/delete GM-owned entities)
- Chat whisper visibility (player can't see GM-only whispers)
Running Tests
Full Test Suite
pnpm test
This runs all integration tests in the correct order using a custom test sequencer.
What Happens During a Test Run
-
Session Setup (
session-endpoints.test.ts)- Creates or validates Foundry sessions
- Stores client IDs, session data, and system IDs for other tests
-
Entity Creation (
entity-endpoints.test.ts)- Fetches entity data from system compendiums (e.g.,
dnd5e.heroesfor actors) - Falls back to minimal
type: 'base'entities if compendium lookup fails - Creates test actors, items, journals, macros
- Registers entities for automatic cleanup
- Fetches entity data from system compendiums (e.g.,
-
Auth Validation (
auth-requirements.test.ts)- Dynamically scans all route files for endpoints
- Verifies API key and clientId requirements on every endpoint
-
Scene + Canvas (
scene-endpoints.test.ts,canvas-endpoints.test.ts)- Creates and activates a test scene
- Tests full CRUD on all 8 canvas document types (tokens, tiles, drawings, lights, sounds, notes, templates, walls)
- Creates a persistent token for utility/encounter tests
-
Core Functionality Tests
- Structure, search, roll (including SSE streaming), sheet, macro endpoints
- Utility endpoints (select, players, world info, token movement, measure distance)
- Encounter management (uses persistent token on the test scene; rolls initiative when compendium actors are available)
- File system operations (upload, download, browse)
- Chat (including SSE streaming, whisper, flush)
- Playlist control and sound playback
-
Permission Filtering (
permission-filtering.test.ts)- Dynamically scans route files for all endpoints accepting
userId - Tests invalid userId rejection on every endpoint
- Tests player permission scoping (if
TEST_PLAYER_USER_IDis configured)
- Dynamically scans route files for all endpoints accepting
6b. Feature Tests
- Scoped API key management and auto-routing
- Hook and combat event subscriptions (WebSocket)
- Scene screenshots and raw background images
- User CRUD operations
- WebSocket API relay
-
System-Specific Tests (
dnd5e-endpoints.test.ts)- Only runs on Foundry instances with the matching system
- Sets up system-specific test data (gives actor spells and consumables from compendiums)
- Tests actor details, modify experience, use abilities/spells/items
- Skill checks, ability saves, death saves, short/long rest
- Concentration tracking and saves
- Inventory management (equip, attune, transfer currency)
-
Cleanup (
cleanup-entities.test.ts,scene-cleanup.test.ts,end-sessions.test.ts,auth-cleanup.test.ts)- Deletes all created test entities
- Restores the original active scene and deletes the test scene
- Closes headless sessions (if applicable)
Running Specific Tests
Most test files cannot be run individually. Tests depend on shared state (client IDs, entity UUIDs) that is established by earlier tests in the sequence. For example, entity-endpoints.test.ts requires session-endpoints.test.ts to have run first to set up the client ID.
Always run the full test suite with pnpm test to ensure proper test ordering and state management.
This may be improved in the future.
# Run the full test suite
pnpm test
Skipping Cleanup (For Debugging)
To keep test entities for inspection:
SKIP_CLEANUP=true pnpm test
Validate Setup
Before running full tests, validate your configuration:
pnpm validate-setup
This checks:
.env.testfile exists and has required variables- Relay server connectivity (at
TEST_BASE_URL) - Foundry instance availability (for each
FOUNDRY_VX_URL) - API key authentication
Test Architecture
Directory Structure
tests/
├── integration/ # API integration tests
│ ├── session-endpoints.test.ts # Session management (runs first)
│ ├── client-endpoints.test.ts # Connected client listing
│ ├── entity-endpoints.test.ts # Entity CRUD (creates test data)
│ ├── auth-requirements.test.ts # Auth validation (scans all routes)
│ ├── auth-endpoints.test.ts # Auth endpoint CRUD tests
│ ├── scene-endpoints.test.ts # Scene CRUD + activate test scene
│ ├── canvas-endpoints.test.ts # All canvas document types
│ ├── structure-endpoints.test.ts # World structure queries
│ ├── search-endpoints.test.ts # Search functionality
│ ├── roll-endpoints.test.ts # Dice rolling + SSE streaming
│ ├── sheet-endpoints.test.ts # Actor sheet screenshots
│ ├── macro-endpoints.test.ts # Macro execution
│ ├── utility-endpoints.test.ts # Utility endpoints (select, players, world-info, etc.)
│ ├── encounter-endpoints.test.ts # Combat/encounters
│ ├── fileSystem-endpoints.test.ts # File operations
│ ├── chat-endpoints.test.ts # Chat messages + SSE streaming
│ ├── permission-filtering.test.ts # userId permission scoping
│ ├── scoped-keys-endpoints.test.ts # Scoped API key CRUD + auto-routing
│ ├── playlist-endpoints.test.ts # Playlist control + sound playback
│ ├── hooks-subscribe.test.ts # Hook event subscriptions (SSE/WS)
│ ├── combat-subscribe.test.ts # Combat event subscriptions (WS)
│ ├── scene-image.test.ts # Scene screenshots + raw backgrounds
│ ├── user-endpoints.test.ts # User CRUD (self-contained)
│ ├── websocket-api.test.ts # Raw WebSocket API relay
│ ├── effects-endpoints.test.ts # ActiveEffect CRUD + status effects list
│ ├── dnd5e-endpoints.test.ts # D&D 5e system tests (includes concentration + inventory)
│ ├── cleanup-entities.test.ts # Deletes test entities
│ ├── scene-cleanup.test.ts # Restores original scene
│ ├── end-sessions.test.ts # Session cleanup (runs last)
│ └── auth-cleanup.test.ts # Auth test user cleanup
├── helpers/ # Test utilities
│ ├── apiRequest.ts # HTTP request helper
│ ├── captureExample.ts # Documentation capture (HTTP)
│ ├── captureWsExample.ts # Documentation capture (WebSocket)
│ ├── compendiumData.ts # Compendium entity fetching
│ ├── endpointMetadata.ts # Dynamic endpoint discovery
│ ├── globalVariables.ts # Cross-file state
│ ├── multiVersion.ts # Multi-version test runner
│ ├── systemSetup.ts # Per-system test data setup
│ ├── testEntities.ts # Entity lifecycle management
│ ├── testSequencer.ts # Test execution order
│ ├── testVariables.ts # Environment variables
│ └── wsClient.ts # WebSocket test client
├── setup.ts # Jest setup (loads .env.test)
└── globalTeardown.ts # Cleanup after all tests
Test Execution Order
Tests run in a specific order defined in tests/helpers/testSequencer.ts. The order follows these phases:
- Session Setup - Creates/validates Foundry sessions
- Entity Creation - Creates test actors, items, journals, etc. (compendium-sourced when possible)
- Auth Validation - Verifies authentication requirements
- Scene + Canvas - Creates a test scene, activates it, tests canvas CRUD for all document types
- Core Functionality - Tests all remaining API endpoints (structure, search, roll, sheet, macro, utility, encounter, fileSystem, chat)
- Permission Filtering - Tests userId validation and player permission scoping
- System-Specific - D&D 5e tests (only on dnd5e systems)
- Cleanup - Deletes test entities, restores original scene, closes sessions
New test files must be added to TEST_ORDER in testSequencer.ts at an appropriate position. Tests not in this array won't run as part of the suite. See the actual file for the current test order.
Test Helpers
Multi-Version Testing (multiVersion.ts)
Run tests across all configured Foundry versions:
import { forEachVersion } from '../helpers/multiVersion';
forEachVersion((version, getClientId) => {
describe(`/my-endpoint (v${version})`, () => {
test('GET /my-endpoint', async () => {
const clientId = getClientId();
// ... test code
});
});
});
For system-specific tests:
import { forEachVersionWithSystem } from '../helpers/multiVersion';
forEachVersionWithSystem('dnd5e', (version, getClientId) => {
describe(`D&D 5e tests (v${version})`, () => {
// Only runs on clients with dnd5e system
});
});
Compendium Data (compendiumData.ts)
Fetch properly initialized entity data from the active system's compendiums:
import { fetchCompendiumEntityData } from '../helpers/compendiumData';
// Fetch any actor from the system's compendiums
const actorData = await fetchCompendiumEntityData(version, 'Actor');
// Fetch from a specific pack
const heroData = await fetchCompendiumEntityData(version, 'Actor', {
packFilter: 'heroes',
});
// Fetch a specific entry by name
const spellData = await fetchCompendiumEntityData(version, 'Item', {
packFilter: 'spells',
entryFilter: 'fireball',
});
Compendium data is:
- Cached per version+entityType+options (one
/structurecall per combination per run) - Cleaned for creation (
_id,_stats,folder,ownership,flagsremoved) - Prefixed with
test-in the name for identification - System-agnostic — works with any system (dnd5e, pf2e, etc.) by filtering packs by
systemId
System Setup (systemSetup.ts)
Set up system-specific test data on actors (spells, consumables, etc.):
import { setupSystemTestData } from '../helpers/systemSetup';
const result = await setupSystemTestData(version, actorUuid);
// result: { spellName: 'Acid Splash' | null, consumableName: 'Potion of Healing' | null }
To add support for a new system, add a SystemTestConfig entry to the SYSTEM_CONFIGS map in systemSetup.ts:
const pf2eConfig: SystemTestConfig = {
id: 'pf2e',
compendiumPacks: { actors: 'heroes', spells: 'spells-srd', consumables: 'equipment-srd' },
giveSpell: async (version, actorUuid) => { /* pf2e-specific logic */ },
giveConsumable: async (version, actorUuid) => { /* pf2e-specific logic */ },
};
SYSTEM_CONFIGS['pf2e'] = pf2eConfig;
Entity Management (testEntities.ts)
Create test entities with automatic cleanup. Actors and Items are automatically sourced from system compendiums when available, falling back to minimal type: 'base' entities:
import { createTestEntities, getEntityUuid } from '../helpers/testEntities';
// Create multiple entities (actors/items auto-sourced from compendiums)
await createTestEntities(version, [
{ key: 'primary', entityType: 'Actor', captureForDocs: true },
{ key: 'secondary', entityType: 'Actor' },
{ key: 'expendable', entityType: 'Actor' }, // For delete tests
], { capturedExamples });
// Later, retrieve the UUID
const actorUuid = getEntityUuid(version, 'Actor', 'primary');
Entity types supported:
Actor,Item,JournalEntry,Scene,Macro,RollTable,Playlist
API Request Helper (apiRequest.ts)
Make API requests with a configuration:
import { ApiRequestConfig, makeRequest, replaceVariables } from '../helpers/apiRequest';
import { testVariables } from '../helpers/testVariables';
const requestConfig: ApiRequestConfig = {
url: {
raw: '{{baseUrl}}/get',
host: ['{{baseUrl}}'],
path: ['get'],
query: [
{ key: 'clientId', value: '{{clientId}}' },
{ key: 'uuid', value: 'Actor.abc123' }
]
},
method: 'GET',
header: [
{ key: 'x-api-key', value: '{{apiKey}}', type: 'text' }
]
};
const resolvedConfig = replaceVariables(requestConfig, testVariables);
const response = await makeRequest(resolvedConfig);
Global Variables (globalVariables.ts)
Share state across test files using file-based persistence:
import { setGlobalVariable, getGlobalVariable } from '../helpers/globalVariables';
// Store a value
setGlobalVariable('13', 'clientId', 'foundry-abc123');
// Retrieve it in another test file
const clientId = getGlobalVariable('13', 'clientId');
Capturing Examples (captureExample.ts)
Capture API requests and responses for documentation:
import { captureExample, saveExamples } from '../helpers/captureExample';
const captured = await captureExample(
requestConfig,
testVariables,
'/my-endpoint - Description'
);
// Assertions on the captured response
expect(captured.response.status).toBe(200);
// Save examples for documentation generation
capturedExamples.push(captured);
afterAll(() => {
saveExamples(capturedExamples, 'docs/examples/my-examples.json');
});
Captured examples include auto-generated code snippets in:
- JavaScript (fetch)
- TypeScript (axios)
- Python (requests)
- cURL
- Emojicode (for fun!)
Writing New Tests
Basic Test Structure
/**
* @file my-endpoints.test.ts
* @description My Endpoint Tests
* @endpoints GET /my-endpoint, POST /my-endpoint
*/
import { describe, test, expect, afterAll } from '@jest/globals';
import { ApiRequestConfig } from '../helpers/apiRequest';
import { testVariables, setVariable } from '../helpers/testVariables';
import { captureExample, saveExamples } from '../helpers/captureExample';
import { forEachVersion } from '../helpers/multiVersion';
import * as path from 'path';
const capturedExamples: any[] = [];
describe('My Endpoint', () => {
afterAll(() => {
const outputPath = path.join(__dirname, '../../docs/examples/my-examples.json');
saveExamples(capturedExamples, outputPath);
});
forEachVersion((version, getClientId) => {
describe(`/my-endpoint (v${version})`, () => {
test('GET /my-endpoint', async () => {
setVariable('clientId', getClientId());
const requestConfig: ApiRequestConfig = {
// ... request configuration
};
const captured = await captureExample(
requestConfig,
testVariables,
'/my-endpoint'
);
capturedExamples.push(captured);
// Assertions
expect(captured.response.status).toBe(200);
});
});
});
});
Adding to Test Order
Add your test file to tests/helpers/testSequencer.ts in the TEST_ORDER array at an appropriate position:
- Add after any tests that create data your tests depend on
- If your tests need canvas documents (tokens, lights, walls), add them after
scene-endpoints.test.tsand beforescene-cleanup.test.ts - System-specific tests go in Phase 7 (after permission filtering, before cleanup)
- Add before cleanup tests (
cleanup-entities.test.ts,scene-cleanup.test.ts,end-sessions.test.ts)
Tests not in TEST_ORDER won't run as part of pnpm test.
Adding a System-Specific Test
- Create
tests/integration/mystem-endpoints.test.tsusingforEachVersionWithSystem('mystem', ...) - Add a system config to
tests/helpers/systemSetup.tsin theSYSTEM_CONFIGSmap - Add the test file to
TEST_ORDERin Phase 7 - The test will auto-skip on Foundry instances that don't have your system
Documentation Generation
Capturing Examples During Tests
Tests automatically capture API examples when using captureExample(). These are saved to docs/examples/.
Generating API Documentation
# Generate API docs from route files
pnpm docs:generate
# Update docs with captured examples
pnpm docs:examples
# Full documentation update
pnpm docs:update
# Full documentation update and build (does all of the above + builds the site)
pnpm docs:full
The doc generator automatically expands parameterized routes (like /canvas/:documentType) into concrete endpoints (e.g., /canvas/tokens, /canvas/walls, etc.) using the ROUTE_PARAM_EXPANSIONS table in generateApiDocs.js.
Running tests captures real API responses, which updates docs/examples/*.json files. This regenerates code examples with current response data, potentially modifying documentation for endpoints you didn't touch.
Before committing, discard changes to documentation files for endpoints you're not working on:
# Review what changed
git diff docs/
# Discard changes to files you didn't intentionally modify
git checkout docs/examples/entity-examples.json # example
Only commit documentation changes that are relevant to your PR.
Building Documentation Site
# Install docs dependencies
pnpm docs:install
# Build docs site
pnpm docs:build
Troubleshooting
Common Issues
"No clientId found for version X"
- Ensure session tests ran successfully
- Check that
USE_EXISTING_SESSIONis properly set - Verify
TEST_CLIENT_ID_VXenvironment variable
"Invalid client ID"
- The Foundry client may have disconnected
- Restart Foundry and re-run tests
- Check relay server logs for connection issues
"Session creation failed"
- Verify Foundry is running and world is loaded
- Check username/password credentials
- Ensure no popup dialogs are blocking
"Request timed out" (408)
- Increase timeout in test:
}, 30000); - Check network connectivity between relay and Foundry
- Verify the REST API module is active in Foundry
- For system-specific tests: ensure the Foundry module is rebuilt and the page is refreshed
"User not found" on permission tests
- The
TEST_PLAYER_USER_IDmust match a real Foundry user ID or username - Check the
/playersendpoint to see available users
Compendium data not loading
- Session tests must run first (they store
systemId) - Check that the Foundry world has system compendium packs
- Look for
⚠ No compendiumPackswarnings in test output
Cookie isolation issues (multi-version testing)
- Use different browser profiles for each Foundry version
- Or use headless mode (
USE_EXISTING_SESSION=false)
Debugging
Check the docs/examples/ folder to see the full requests, responses, and auto-generated code examples captured during tests. This is very helpful for debugging API issues.
Browser Console Logging
When debugging headless session issues, you can capture the browser's console output to log files by setting the CAPTURE_BROWSER_CONSOLE environment variable:
CAPTURE_BROWSER_CONSOLE=debug pnpm test
Valid levels (each includes levels above it):
error— page errors and failed requests onlywarn— warnings + errorsdebug— all console output
Logs are written to data/browser-logs/ with filenames like {world}_{username}_{foundryVersion}_{timestamp}.log. Previous logs for the same world/username are automatically cleaned up when a new session starts.
You can also pass captureBrowserConsole in the request body of POST /start-session to enable logging for individual sessions.
Debugging Test Output
After running tests, check the generated outputs for debugging:
Test Reports (generated in test-results/):
test-report.html- Visual HTML report of all test resultsjunit.xml- JUnit format report for CI/CD integration
Captured Examples (generated in docs/examples/):
- JSON files containing full request/response data for each endpoint
- Auto-generated code snippets (JavaScript, Python, cURL, etc.)
- Useful for verifying API behavior and debugging failures
The tests/.global-vars.json file stores state during test execution but is automatically deleted after tests complete.
Best Practices
- Use semantic entity keys:
primary,secondary,expendableinstead oftest1,test2 - Always register for cleanup: Use
createTestEntities()to automatically track entities - Capture examples for docs: Use
captureExample()for endpoints that should be documented - Test all versions: Use
forEachVersion()to ensure compatibility - Extended timeouts: Use longer timeouts for complex operations:
}, 30000); - Descriptive assertions: Test specific response properties, not just status codes
- Data-driven tests: Use tables/loops for endpoints with multiple variants (see canvas tests)
- Graceful degradation: System-specific tests should skip gracefully when data isn't available
Generating Test Files
For new endpoints, you can auto-generate test file boilerplate:
pnpm test:generate
This script parses route files and generates integration test stubs for endpoints that don't have tests yet. It extracts:
- Endpoint paths and methods
- Required and optional parameters from
createApiRouteconfiguration - JSDoc descriptions for test documentation (from the
@routetag)
Manual Steps Required
Generated test files are starting points that need customization:
- Replace placeholder parameter values with actual test data
- Use test helpers for dynamic values:
getEntityUuid(version, 'Actor', 'primary')- Get UUIDs from test entitiesgetGlobalVariable(version, 'key')- Get values stored by other testssetGlobalVariable(version, 'key', value)- Store values for other tests
- Add meaningful assertions that verify the response data
- Add the test file to
testSequencer.tsin the appropriate phase
Creating Additional Test Entities
If your tests need specific entities that don't exist, add them in entity-endpoints.test.ts:
await createTestEntities(version, [
{ key: 'my-special-actor', entityType: 'Actor', data: { /* custom data */ } },
], { capturedExamples });
These entities will be automatically cleaned up after tests complete.
Useful Resources
- Foundry VTT API Documentation: https://foundryvtt.com/api/
- Project Discord: https://discord.gg/U634xNGRAC
- Contributing Guide: Contributing Documentation