6.7 KiB
Feedback for Apophis Team: Real-World Integration Challenges
Context
We're integrating Apophis v1.1 into Arbiter, a multi-tenant identity platform with complex auth, graph-based permissions, and LinkedDataFragment responses. The goal is to use Apophis for contract testing of our Fastify routes.
Issues Encountered
1. APOSTL Syntax: Mandatory else Clause is Undocumented ✅ FIXED in v2.0
else Clause is UndocumentedStatus: Resolved. APOPHIS v2.0 replaced APOSTL with Justin (plain JavaScript expressions).
What we wrote (v1.x):
if response_code(this) == 201 then response_body(this).data.ok == true else T
What v2.0 uses:
statusCode == 201 ? response.body.data.ok == true : true
// or simply:
!(statusCode == 201) || response.body.data.ok == true
Resolution: Justin uses standard JS ternary operators (? :) and boolean logic. No mandatory else clause, no custom syntax to learn.
2. Unclear Value Proposition vs Fastify Schema Validation
It took us time to understand what Apophis adds on top of Fastify's built-in JSON Schema validation.
Fastify already provides:
- Request body/query/params validation (via Ajv)
- Response serialization (via fast-json-stringify)
- Error formatting
We initially thought Apophis would:
- Validate responses against schemas (it doesn't — Fastify only serializes, doesn't validate responses)
- Replace our need for separate test files (it partially does, but only for behavioral contracts)
What Apophis actually adds:
- Behavioral contracts (
x-ensures) for side effects and state changes - Property-based test generation from schemas
- Stateful testing (constructor → mutator → destructor sequences)
Suggestion: Clarify in the "Getting Started" docs that Apophis is for behavioral contracts, not structural validation. Show a clear comparison table.
3. Testing Authenticated Routes is Underspecified
Our routes require:
- JWT tokens in Authorization header
- Tenant context (x-tenant-id header)
- Permission checks via graph-based auth
- Session cookies
The problem: Apophis generates requests programmatically, but there's no clear pattern for:
- Injecting auth tokens into generated requests
- Setting up prerequisite state (create user → login → get token → test route)
- Handling token refresh or session management
We tried:
- Using
scopesto inject headers, but this is static and can't handle dynamic tokens - Using
x-requiresfor preconditions, but it's unclear how to satisfy them
Suggestion: Document a pattern for authenticated routes. Examples:
// Option 1: Dynamic scope setup
await app.apophis.scope.register('authed', {
headers: async () => ({
'Authorization': `Bearer ${await getTestToken()}`
})
})
// Option 2: Test hooks
await app.apophis.contract({
beforeEach: async (req) => {
req.headers['Authorization'] = await getTestToken()
}
})
4. Running Against Real Server is Difficult
The docs show examples with inline route definitions, but we want to test our actual production routes.
Challenges:
- Server bootstraps databases, WAL stores, ledger connections
- Routes have complex dependency injection
- We need to clean up between tests (file system conflicts, port binding)
Example error:
Error: EEXIST: file already exists, mkdir 'server-data/wal.log'
Suggestion: Provide a guide for "Testing Existing Fastify Apps" that covers:
- Bootstrapping the server in test mode
- Cleaning up resources between runs
- Configuring Apophis after server creation but before
ready()
5. Contract Debugging is Hard
When contracts fail, the output is verbose but not actionable.
Example output:
{
"formula": "statusCode == 400 ? response.body.error != null : true",
"context": {
"expected": "non-null value",
"actual": "undefined (field missing)"
}
}
Problems:
- We don't see the actual request that was generated
- We don't see the full response body
- No suggestion for how to fix the contract
Suggestion: Include in failure output:
- The generated request (method, path, body)
- The full response body
- A suggestion like "Field 'error' missing from response. Check your handler returns error details."
6. No Clear CI/CD Pattern
We want to run Apophis in CI, but:
- How do we handle database migrations/seeding?
- How do we ensure deterministic runs (seed management)?
- How do we fail the build on contract violations?
Suggestion: Add a CI/CD section to docs with GitHub Actions example that shows:
- name: Contract Tests
run: |
npm run db:migrate:test
npm run test:contracts
# Exit code should be non-zero if contracts fail
What Works Well
- Schema-driven test generation is powerful
x-categoryauto-categorization reduces boilerplatecheck()for single-route validation is useful- Integration with
@fastify/swaggeris seamless
Recommendations
Make✅ Fixed in v2.0 — Justin uses standard JS ternary operatorselseoptional in APOSTL conditionals- Add "Auth Patterns" guide with examples for JWT, sessions, API keys
- Improve error messages with request/response context and fix suggestions
- Document real-world integration (existing Fastify apps, not just toy examples)
- Add CI/CD examples with database setup and deterministic testing
Our Current Workaround
We're using Apophis for:
- Schema discovery and validation
- Contract syntax checking
- Documentation generation
But for authenticated routes, we're writing traditional E2E tests with fastify.inject() because we can control auth headers and setup/teardown more easily.
Update: APOPHIS v2.0 Resolutions
APOPHIS v2.0 (released 2026-04-25) addresses all feedback items:
- ✅ APOSTL
elseclause: Replaced with Justin (standard JS ternary? :) - ✅ Value proposition: Documentation now clearly distinguishes structural vs behavioral validation
- ✅ Auth patterns: Extension system allows dynamic header injection via
onBuildRequesthook - ✅ Real-world integration: Guide added for testing existing Fastify apps with complex bootstrapping
- ✅ Contract debugging: Failure output now includes generated request, full response, and fix suggestions
- ✅ CI/CD patterns: GitHub Actions example with database migrations and deterministic seeds
Recommended next steps for Arbiter integration:
- Migrate contracts from APOSTL to Justin using the migration guide
- Use the Extension Plugin System for Arbiter-specific predicates (
graph_check,partial_graph,budget_check) - Register Arbiter extension to inject S2S headers and handle preflight/finalize lifecycle
See docs/extensions/EXTENSION-PLUGIN-SYSTEM.md for the Arbiter extension example.