@reso/certification
@reso/certification
RESO certification testing toolkit for validating OData 4.01 server compliance against RESO Web API endorsement specifications. Ships as a single package with spec-specific modules for Add/Edit (RCP-010), Web API Core, and Data Dictionary.
Uses shared test infrastructure (src/test-runner/) providing OData protocol validators, metadata parsing (via @reso/odata-client), field validation (via @reso/validation), reporting, and HTTP client helpers.
Install
npm install @reso/certification
Project Structure
certification/
├── src/
│ ├── index.ts # Public SDK exports
│ ├── cli/index.ts # Unified CLI entry point (bin: reso-cert)
│ ├── test-runner/ # Shared infrastructure (validators, client, auth, metadata, reporter)
│ ├── add-edit/ # RCP-010 Add/Edit scenarios + mock server
│ ├── web-api-core/ # Web API Core 2.0.0 (stub)
│ └── data-dictionary/ # Data Dictionary 2.0 (stub)
├── tests/ # 60 Vitest tests
├── sample-payloads/ # JSON payload files for Add/Edit testing
├── sample-metadata.xml # Sample EDMX metadata for mock server
└── Dockerfile # Multi-stage Docker build for CI
Certification Modules
Add/Edit (RCP-010)
Validates OData CRUD operations against 8 Gherkin BDD certification scenarios:
- Create with
Prefer: return=representation— POST returns 201 with entity, OData annotations, and follow-up GET - Create with
Prefer: return=minimal— POST returns 204 with headers only, and follow-up GET - Create with invalid payload — POST returns 400 with OData error format and field-level details
- Update with
Prefer: return=representation— PATCH returns 200 with entity and@odata.etag - Update with
Prefer: return=minimal— PATCH returns 204 with headers only - Update with invalid payload — PATCH returns 400 with OData error format
- Delete succeeds — DELETE returns 204, follow-up GET returns 404
- Delete fails — DELETE to non-existent resource returns 4xx
Current status: 8 passed, 0 failed
Web API Core 2.0.0 (stub)
Future home of programmatic Web API Core certification. Currently tested via the RESO web-api-commander Docker workflow.
Current status: 42 passed, 0 failed, 3 skipped (via Docker)
Data Dictionary 2.0 (stub)
Future home of programmatic Data Dictionary certification. Currently tested via reso-certification-utils Docker workflow.
Current status: 1,034 passed, 570 skipped, 0 failed (via Docker)
CLI Usage
npm run build
# Run Add/Edit against a live server
npx reso-cert \
--url https://api.example.com \
--resource Property \
--payloads ./sample-payloads \
--auth-token <token>
# Run against the built-in mock server (offline testing / CI)
npx reso-cert \
--url http://localhost:8800 \
--resource Property \
--payloads ./sample-payloads \
--auth-token test \
--mock
# Generate a compliance report
npx reso-cert \
--url http://localhost:8080 \
--resource Property \
--payloads ./sample-payloads \
--auth-token test \
--compliance-report ./report.json \
--spec-version 2.0.0
CLI Options
| Option | Description |
|---|---|
--url <url> |
OData server base URL |
--resource <name> |
OData resource name (e.g., Property) |
--payloads <dir> |
Directory containing payload JSON files |
--auth-token <token> |
Pre-fetched bearer token |
--client-id <id> |
OAuth2 client ID (alternative to --auth-token) |
--client-secret <secret> |
OAuth2 client secret |
--token-url <url> |
OAuth2 token endpoint URL |
--metadata <path> |
Path to local XML metadata file |
--mock |
Start a built-in mock OData server on port 8800 |
--output <format> |
Output format: console (default) or json |
--compliance-report <path> |
Write structured JSON compliance report to file |
--spec-version <version> |
Spec version for the report (default: 2.0.0) |
Exit Codes
| Code | Meaning |
|---|---|
| 0 | All scenarios passed |
| 1 | One or more scenarios failed |
| 2 | Runtime error |
Programmatic API
import {
runAllScenarios,
generateComplianceReport,
startMockServer,
stopMockServer
} from '@reso/certification';
// Run all 8 Add/Edit scenarios
const report = await runAllScenarios({
serverUrl: 'http://localhost:8080',
resource: 'Property',
payloadsDir: './sample-payloads',
auth: { mode: 'token', authToken: 'test' }
});
console.log(`${report.summary.passed} passed, ${report.summary.failed} failed`);
Shared Test Infrastructure
The test-runner module is also exported for building custom certification scenarios:
import {
// Validators
validateODataVersionHeader,
validateStatusCode,
validateLocationHeader,
validateODataAnnotation,
validateODataError,
validateResponseContainsPayload,
// Client
odataRequest,
buildResourceUrl,
// Metadata
fetchMetadata,
parseMetadataXml,
getEntityType,
validatePayloadAgainstMetadata,
// Auth
resolveAuthToken,
// Reporter
formatConsoleReport,
formatJsonReport,
// Helpers
extractPrimaryKey,
buildScenarioResult
} from '@reso/certification';
Docker Compliance Testing
Run compliance tests against the RESO Reference Server using Docker Compose.
Setup
cd ../reso-reference-server
# Start server and seed data
docker compose up -d --build --wait
docker compose --profile seed up --exit-code-from seed seed
Web API Core 2.0.0
docker compose --profile compliance-core up --build --exit-code-from compliance-core
Add/Edit (RCP-010)
docker compose --profile compliance-addedit up --build --exit-code-from compliance-addedit
Data Dictionary 2.0
docker compose --profile compliance-dd up --build --exit-code-from compliance-dd
Local CLI (without Docker)
# Against a running reference server
npx reso-cert \
--url http://localhost:8080 \
--resource Property \
--payloads ./sample-payloads \
--auth-token test \
--compliance-report ./compliance-report.json \
--spec-version 2.0.0
Compliance Report Format
The --compliance-report flag generates a structured JSON file suitable for API submission. The report captures the outcome of each certification scenario along with the fields, enumerations, and expansions tested.
Top-Level Structure
{
"description": "Web API Add/Edit",
"version": "2.0.0",
"generatedOn": "2026-03-06T09:00:00.000Z",
"outcome": "passed",
"remarks": "Tested Create (representation, minimal), Update (representation, minimal), and Delete operations on Property with 5 of 652 fields.",
"scenarios": [...]
}
| Field | Type | Description |
|---|---|---|
description |
string |
Endorsement name ("Web API Add/Edit") |
version |
string |
Spec version (from --spec-version) |
generatedOn |
string |
ISO 8601 timestamp |
outcome |
"passed" \| "failed" |
Overall result |
remarks |
string |
Human-readable summary of operations, fields, and any failures |
scenarios |
ScenarioDetail[] |
Per-scenario results |
Scenario Detail
Each scenario includes the operation type, response preference, tested fields, enumerations, and any failures:
{
"name": "create-succeeds-representation",
"operation": "Create",
"preference": "return=representation",
"resource": "Property",
"status": "passed",
"durationMs": 42,
"fields": {
"count": 5,
"names": ["ListPrice", "City", "StateOrProvince", "PostalCode", "Country"]
},
"enumerations": {
"count": 1,
"values": [{ "fieldName": "StandardStatus", "value": "Active" }]
},
"expansions": [],
"failures": []
}
| Field | Type | Description |
|---|---|---|
name |
string |
Scenario identifier (e.g., create-succeeds-representation) |
operation |
"Create" \| "Update" \| "Delete" |
CRUD operation type |
preference |
string? |
return=representation or return=minimal (omitted for Delete) |
resource |
string |
OData resource tested |
status |
"passed" \| "failed" |
Scenario result |
durationMs |
number |
Execution time in milliseconds |
fields.count |
number |
Number of non-key fields in the payload |
fields.names |
string[] |
Field names tested |
enumerations.count |
number |
Number of enum fields with values |
enumerations.values |
{fieldName, value}[] |
Enum field names and their test values |
expansions |
string[] |
Navigation properties included in the payload |
failures |
FailureDetail[] |
Assertion failures (empty when passed) |
Failure Detail
When a scenario fails, each failing assertion is captured:
{
"assertion": "OData-Version header is 4.0 or 4.01",
"expected": "4.0 or 4.01",
"actual": "(missing)"
}
Payload Format
Place JSON files in the payloads directory. The tool expects 6 files:
| File | Purpose |
|---|---|
create-succeeds.json |
Known-good payload for POST |
create-fails.json |
Known-bad payload for POST (e.g., negative ListPrice) |
update-succeeds.json |
Known-good payload for PATCH (includes primary key) |
update-fails.json |
Known-bad payload for PATCH |
delete-succeeds.json |
{ "id": "<key>" } — existing record to delete |
delete-fails.json |
{ "id": "<key>" } — non-existent record |
Known-bad payloads use negative numeric values to trigger server-side validation errors (e.g., "ListPrice": -1).
Authentication
| Method | Options |
|---|---|
| Bearer token | --auth-token <token> |
| OAuth2 Client Credentials | --client-id <id> --client-secret <secret> --token-url <url> |
When using --mock, the mock server provides its own OAuth2 token endpoint automatically.
Mock Server
The --mock flag starts a built-in Express-based OData mock server on port 8800 that implements enough of the OData protocol to exercise all 8 scenarios. Useful for offline testing and CI.
The mock server:
- Serves EDMX metadata at
/$metadata - Implements CRUD at
/{Resource}and/{Resource}('{key}') - Returns OData-compliant headers, annotations, and error responses
- Triggers 400 validation errors for negative numeric field values
- Provides a mock OAuth2 token endpoint at
/oauth/token
Development
npm install
npm run build
npm test # 60 tests
License
See LICENSE in the repository root.