Files
John Dvorak d0523fcc2d fix: harden engine, enrich failure diagnostics, close adoption gaps
- P0: CLI verify now honors  test budget with seeded multi-sample
- P0: Observe sampling enforced via Math.random() gate in hook-validator
- P1: Remove misleading undici-mock-agent isolation option
- P1: Qualify reuses shared discoverRouteDetails() with warnings
- P1: Chaos/scenario config exposed via preset schema
- P1: README/docs limitations updated to current state
- P2: Nested response annotations prefer 2xx deterministically
- P2: --changed documented as heuristic in verify.md

- Add observe sink tests (sampling 0/1, sink failure non-interference)
- Add verify runs regression tests (scale, determinism, variants)
- Add configured-scenario qualify test (independent of OAuth fixture)
- Add coverageBreakdown to qualify artifacts (per-gate route coverage)
- Add production-style observe example with real sink in docs/observe.md
- Add nightly/staging vs PR gating guidance to docs/qualify.md

- Enrich VerifyFailure with formula-aware diagnostics:
  status:201 => 'HTTP 200', body field checks => actual values
- Remove stale observe CLI activation message
- Document outbound mocks as process-global in getting-started.md
- Refresh APOPHIS_ADOPTION_AUDIT.md with current state

903 tests pass, build clean, typecheck clean.
2026-05-21 20:39:36 -07:00

142 lines
3.6 KiB
TypeScript

import Fastify from 'fastify'
import apophisPlugin from '@apophis/fastify'
import crypto from 'crypto'
const fastify = Fastify()
import { createAuthExtension } from '@apophis/fastify/extension/factories'
const authExtension = createAuthExtension({
name: 'bearer',
acquire: async () => {
// In real apps, this would call a login endpoint
const token = crypto.randomBytes(32).toString('hex')
return { token, userId: 'tester-1' }
},
})
await fastify.register(apophisPlugin, {
runtime: 'error',
extensions: [authExtension],
})
const sessions = new Map<string, { userId: string; createdAt: number }>()
// LOGIN — acquires a session token
fastify.post('/auth/login', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'response_body(this).token != null',
'response_body(this).expiresAt > request_time(this)',
],
body: {
type: 'object',
properties: {
username: { type: 'string' },
password: { type: 'string' },
},
required: ['username', 'password'],
},
response: {
200: {
type: 'object',
properties: {
token: { type: 'string' },
expiresAt: { type: 'number' },
},
},
},
},
}, async (req) => {
const token = crypto.randomBytes(48).toString('hex')
const expiresAt = Date.now() + 3600_000
sessions.set(token, { userId: `usr-${req.body.username}`, createdAt: Date.now() })
return { token, expiresAt }
})
// PROTECTED RESOURCE — requires valid auth
fastify.get('/auth/me', {
schema: {
'x-category': 'observer',
'x-requires': [
'response_status(this) == 200',
],
'x-ensures': [
'response_body(this).userId != null',
'response_body(this).authenticated == true',
],
headers: {
type: 'object',
properties: {
authorization: { type: 'string', pattern: '^Bearer ' },
},
required: ['authorization'],
},
response: {
200: {
type: 'object',
properties: {
userId: { type: 'string' },
authenticated: { type: 'boolean' },
},
},
},
},
}, async (req, reply) => {
const header = req.headers.authorization
if (!header) {
reply.status(401)
return { error: 'Missing Authorization header' }
}
const token = header.replace('Bearer ', '')
const session = sessions.get(token)
if (!session) {
reply.status(401)
return { error: 'Invalid or expired token' }
}
return { userId: session.userId, authenticated: true }
})
// LOGOUT — destroys a session, must not succeed twice
fastify.post('/auth/logout', {
schema: {
'x-category': 'destructor',
'x-requires': [
'response_status(this) == 200',
],
'x-ensures': [
// After logout, the same token should be rejected
'response_code(GET /auth/me) == 401',
],
headers: {
type: 'object',
properties: {
authorization: { type: 'string', pattern: '^Bearer ' },
},
required: ['authorization'],
},
},
}, async (req, reply) => {
const header = req.headers.authorization
if (!header) {
reply.status(401)
return { error: 'Missing Authorization header' }
}
const token = header.replace('Bearer ', '')
const existed = sessions.delete(token)
if (!existed) {
reply.status(404)
return { error: 'Session not found' }
}
return { ok: true }
})
await fastify.ready()
const result = await fastify.apophis.contract({ runs: 30 })
console.log('Contract tests:', result.summary)
const stateful = await fastify.apophis.stateful({ runs: 30, seed: 42 })
console.log('Stateful tests:', stateful.summary)