test: final validation battery — expressive contracts, plugin contracts, chaos, outbound (46 tests)

This commit is contained in:
John Dvorak
2026-05-22 16:13:36 -07:00
parent 67d0d02c12
commit 9474e2d321
9 changed files with 1367 additions and 10 deletions
+2 -2
View File
@@ -71,13 +71,13 @@ export async function runChaosOnRoute(
for (const phase of Object.values(composed.phases)) {
for (const req of phase.requires) {
if (!route.requires.includes(req.formula)) {
route.requires.push(req.formula)
route.requires = [...route.requires, req.formula]
route.formulaSources[req.formula] = req.source
}
}
for (const ens of phase.ensures) {
if (!route.ensures.includes(ens.formula)) {
route.ensures.push(ens.formula)
route.ensures = [...route.ensures, ens.formula]
route.formulaSources[ens.formula] = ens.source
}
}
+2 -2
View File
@@ -527,13 +527,13 @@ export async function runVerify(deps: VerifyRunnerDeps): Promise<VerifyRunResult
for (const phase of Object.values(composed.phases)) {
for (const req of phase.requires) {
if (!route.requires.includes(req.formula)) {
route.requires.push(req.formula)
route.requires = [...route.requires, req.formula]
route.formulaSources[req.formula] = req.source
}
}
for (const ens of phase.ensures) {
if (!route.ensures.includes(ens.formula)) {
route.ensures.push(ens.formula)
route.ensures = [...route.ensures, ens.formula]
route.formulaSources[ens.formula] = ens.source
}
}
+2 -2
View File
@@ -122,13 +122,13 @@ export const runPetitTests = async (
for (const phase of Object.values(composed.phases)) {
for (const req of phase.requires) {
if (!route.requires.includes(req.formula)) {
route.requires.push(req.formula)
route.requires = [...route.requires, req.formula]
route.formulaSources[req.formula] = req.source
}
}
for (const ens of phase.ensures) {
if (!route.ensures.includes(ens.formula)) {
route.ensures.push(ens.formula)
route.ensures = [...route.ensures, ens.formula]
route.formulaSources[ens.formula] = ens.source
}
}
+2 -2
View File
@@ -172,13 +172,13 @@ export const runScenario = async (
for (const phase of Object.values(composed.phases)) {
for (const req of phase.requires) {
if (!route.requires.includes(req.formula)) {
route.requires.push(req.formula)
route.requires = [...route.requires, req.formula]
route.formulaSources[req.formula] = req.source
}
}
for (const ens of phase.ensures) {
if (!route.ensures.includes(ens.formula)) {
route.ensures.push(ens.formula)
route.ensures = [...route.ensures, ens.formula]
route.formulaSources[ens.formula] = ens.source
}
}
+2 -2
View File
@@ -109,13 +109,13 @@ export const runStatefulTests = async (
for (const phase of Object.values(composed.phases)) {
for (const req of phase.requires) {
if (!route.requires.includes(req.formula)) {
route.requires.push(req.formula)
route.requires = [...route.requires, req.formula]
route.formulaSources[req.formula] = req.source
}
}
for (const ens of phase.ensures) {
if (!route.ensures.includes(ens.formula)) {
route.ensures.push(ens.formula)
route.ensures = [...route.ensures, ens.formula]
route.formulaSources[ens.formula] = ens.source
}
}
+156
View File
@@ -0,0 +1,156 @@
import { test } from 'node:test'
import assert from 'node:assert'
import Fastify from 'fastify'
import * as fc from 'fast-check'
import apophisPlugin from '../index.js'
import { createChaosEventArbitrary } from '../quality/chaos-v3.js'
import { FlakeDetector } from '../quality/flake.js'
process.env.NODE_ENV = 'test'
test('chaos: no events means no disruption', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/ping', {
schema: {
'x-category': 'utility',
'x-ensures': ['status:200', 'response_body(this).pong == true'],
response: { 200: { type: 'object', properties: { pong: { type: 'boolean' } } } }
}
}, async () => {
return { pong: true }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({
runs: 5,
chaos: { probability: 0 }
})
assert.strictEqual(result.summary.failed, 0)
})
test('chaos: error injection breaks contracts', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/critical', {
schema: {
'x-category': 'constructor',
'x-ensures': ['status:201', 'response_body(this).id != null'],
body: { type: 'object', properties: { x: { type: 'number' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(201)
return { id: 'c-1' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({
runs: 10,
chaos: {
probability: 1,
error: { probability: 1, statusCode: 503 }
}
})
assert.ok(result.summary.failed > 0, 'chaos error injection should cause contract failures')
})
test('chaos: createChaosEventArbitrary determinism with fixed seed', async () => {
const arb = createChaosEventArbitrary(
{
probability: 0.5,
error: { probability: 0.5, statusCode: 503 },
delay: { probability: 0.5, minMs: 0, maxMs: 100 },
},
['test.contract']
)
const events1 = fc.sample(arb, { numRuns: 1, seed: 42 })[0]!
const events2 = fc.sample(arb, { numRuns: 1, seed: 42 })[0]!
assert.deepStrictEqual(events1, events2, 'same seed produces identical chaos events')
const events3 = fc.sample(arb, { numRuns: 1, seed: 43 })[0]!
assert.deepStrictEqual(events1, events2)
})
test('chaos: FlakeDetector classifies deterministic failures as not flaky', async () => {
let callCount = 0
const rerunFn = async (seed?: number) => {
callCount++
return { passed: false }
}
const detector = new FlakeDetector({ sameSeedReruns: 1, seedVariations: 2 })
const report = await detector.detectFlake(
{ ok: false, name: 'test', id: 1 },
rerunFn,
42
)
assert.strictEqual(report.isFlaky, false)
assert.strictEqual(report.confidence, 'high')
})
test('chaos: FlakeDetector with varying seeds detects intermittent failures', async () => {
let flip = false
const rerunFn = async (seed?: number) => {
flip = !flip
return { passed: flip }
}
const detector = new FlakeDetector({ sameSeedReruns: 1, seedVariations: 3 })
const report = await detector.detectFlake(
{ ok: false, name: 'test', id: 1 },
rerunFn,
42
)
assert.strictEqual(report.isFlaky, true)
})
test('chaos: chaos events with no strategies produce only none events', async () => {
const arb = createChaosEventArbitrary(
{ probability: 0.5 },
['svc.users']
)
const events = fc.sample(arb, { numRuns: 1, seed: 1 })[0]!
assert.strictEqual(events.filter((e: any) => e.type !== 'none').length, 0)
})
test('chaos: chaos events with error strategy generates non-none events', async () => {
const arb = createChaosEventArbitrary(
{ probability: 1, error: { probability: 1, statusCode: 503 } },
['svc.orders']
)
const samples = fc.sample(arb, { numRuns: 10, seed: 123 })
const activeEvents = samples.flat().filter((e: any) => e.type !== 'none')
assert.ok(activeEvents.length > 0, 'error strategy should produce chaos events')
})
test('chaos: contract runner with chaos no-ops ignores none events', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/stable', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200', 'response_body(this).stable == true'],
response: { 200: { type: 'object', properties: { stable: { type: 'boolean' } } } }
}
}, async () => {
return { stable: true }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({
runs: 10,
chaos: { probability: 0 }
})
assert.strictEqual(result.summary.failed, 0)
})
+555
View File
@@ -0,0 +1,555 @@
import { describe, test, before, after } from 'node:test'
import assert from 'node:assert'
import Fastify from 'fastify'
import apophisPlugin from '../index.js'
const FORMULA_EDGE_CASES = describe('expressive contracts: formula edge cases')
test('if/then/else nesting: status-dependent body check', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off', profile: 'platform-observe' })
fastify.post('/order', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'if status:201 then response_body(this).id != null else if status:400 then response_body(this).error != null else true'
],
body: { type: 'object', properties: { amount: { type: 'number' } } },
response: {
201: { type: 'object', properties: { id: { type: 'string' } } },
400: { type: 'object', properties: { error: { type: 'string' } } }
}
}
}, async (req, reply) => {
if (req.body.amount > 0) { reply.status(201); return { id: 'ord-1' } }
reply.status(400); return { error: 'amount must be positive' }
})
await fastify.ready()
t.after(() => fastify.close())
const res1 = await fastify.inject({ method: 'POST', url: '/order', payload: { amount: 100 } })
assert.strictEqual(res1.statusCode, 201)
assert.ok(res1.json().id)
const res2 = await fastify.inject({ method: 'POST', url: '/order', payload: { amount: 0 } })
assert.strictEqual(res2.statusCode, 400)
assert.ok(res2.json().error)
})
test('request_headers and request_body accessors', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/echo', {
schema: {
'x-category': 'mutator',
'x-ensures': [
'response_body(this).echoed == request_body(this).message',
],
body: { type: 'object', properties: { message: { type: 'string' } } },
response: { 200: { type: 'object', properties: { echoed: { type: 'string' } } } }
}
}, async (req) => {
return { echoed: req.body.message }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/echo', payload: { message: 'hello world' } })
assert.strictEqual(res.statusCode, 200)
assert.strictEqual(res.json().echoed, 'hello world')
})
test('response_headers validation: content-type and custom header', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/json', {
schema: {
'x-category': 'observer',
'x-ensures': [
'response_headers(this).content-type == "application/json; charset=utf-8"',
],
response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } }
}
}, async (req, reply) => {
reply.header('x-custom', 'foobar')
return { ok: true }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'GET', url: '/json' })
assert.strictEqual(res.statusCode, 200)
assert.ok(res.json().ok)
})
test('comparison operators: ==, !=, >, <, >=, <=', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/score', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'response_body(this).score >= 0',
'response_body(this).score <= 100',
'response_body(this).grade != null',
],
body: { type: 'object', properties: { raw: { type: 'number' } } },
response: { 201: { type: 'object', properties: { score: { type: 'number' }, grade: { type: 'string' } } } }
}
}, async (req, reply) => {
const score = Math.min(100, Math.max(0, req.body.raw))
const grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : 'F'
reply.status(201)
return { score, grade }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/score', payload: { raw: 85 } })
assert.strictEqual(res.statusCode, 201)
const body = res.json()
assert.ok(body.score >= 0 && body.score <= 100)
assert.ok(body.grade)
})
test('logical operators: and, or, not', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/widget', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'response_body(this).id != null',
'response_body(this).color == "red" || response_body(this).color == "blue"',
],
body: { type: 'object', properties: { color: { type: 'string' }, size: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, color: { type: 'string' }, size: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(201)
return { id: 'w-' + Math.random().toString(36).slice(2), color: req.body.color || 'red', size: req.body.size || 'm' }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/widget', payload: { color: 'blue' } })
assert.strictEqual(res.statusCode, 201)
const body = res.json()
assert.ok(body.id)
assert.ok(body.color === 'red' || body.color === 'blue')
})
test('cross-operation with previous(): verify created resource is retrievable', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off', profile: 'protocol-lab' })
fastify.post('/projects', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
// Cross-operation: fetch the project we just created and verify it's found
'previous(response_code(GET /projects/{response_body(this).name})) == 200',
],
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 201: { type: 'object', properties: { name: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(201)
return { name: req.body.name }
})
fastify.get('/projects/:name', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200', 'response_body(this).name == request_params(this).name'],
response: { 200: { type: 'object', properties: { name: { type: 'string' } } } }
}
}, async (req) => {
return { name: (req.params as any).name }
})
await fastify.ready()
t.after(() => fastify.close())
const createRes = await fastify.inject({ method: 'POST', url: '/projects', payload: { name: 'apophis-v2' } })
assert.strictEqual(createRes.statusCode, 201)
assert.strictEqual(createRes.json().name, 'apophis-v2')
const getRes = await fastify.inject({ method: 'GET', url: '/projects/apophis-v2' })
assert.strictEqual(getRes.statusCode, 200)
assert.strictEqual(getRes.json().name, 'apophis-v2')
})
test('complex HATEOAS: linked resource route_exists and compliance', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/tenants', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).id != null',
// HATEOAS: self link must exist
'response_body(this)._links.self.href != null',
],
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, _links: { type: 'object', properties: { self: { type: 'object', properties: { href: { type: 'string' } } } } } } } }
}
}, async (req, reply) => {
const id = 't-' + Math.random().toString(36).slice(2, 8)
reply.status(201)
return { id, name: req.body.name, _links: { self: { href: `/tenants/${id}` } } }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/tenants', payload: { name: 'acme' } })
assert.strictEqual(res.statusCode, 201)
const body = res.json()
assert.ok(body._links.self.href)
assert.ok(body.id)
})
test('null handling: optional fields in response', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/user-simple', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).id != null',
],
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' }, avatar: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(201)
return { id: 'u-1', name: req.body.name }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/user-simple', payload: { name: 'Test User' } })
assert.strictEqual(res.statusCode, 201)
assert.strictEqual(res.json().id, 'u-1')
})
test('api versioning with x-variants: different contracts per version', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/me', {
schema: {
'x-category': 'observer',
'x-variants': [
{ name: 'v1', headers: { 'x-api-version': 'v1' } },
{ name: 'v2', headers: { 'x-api-version': 'v2' } }
],
'x-ensures': [
'status:200',
'response_body(this).id != null',
],
response: { 200: { type: 'object', properties: { id: { type: 'string' }, email: { type: 'string' } } } }
}
}, async () => {
return { id: 'me-1', email: 'user@example.com' }
})
await fastify.ready()
t.after(() => fastify.close())
const resV1 = await fastify.inject({ method: 'GET', url: '/me', headers: { 'x-api-version': 'v1' } })
assert.strictEqual(resV1.statusCode, 200)
const resV2 = await fastify.inject({ method: 'GET', url: '/me', headers: { 'x-api-version': 'v2' } })
assert.strictEqual(resV2.statusCode, 200)
})
test('concatenation and string formulas in ensures', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/concatenate', {
schema: {
'x-category': 'mutator',
'x-ensures': [
'response_body(this).display == request_body(this).first && response_body(this).display != null',
],
body: { type: 'object', properties: { first: { type: 'string' }, last: { type: 'string' } } },
response: { 200: { type: 'object', properties: { display: { type: 'string' } } } }
}
}, async (req) => {
return { display: req.body.first + ' ' + req.body.last }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/concatenate', payload: { first: 'Jane', last: 'Doe' } })
assert.strictEqual(res.statusCode, 200)
assert.ok(res.json().display)
})
test('boolean response field: truthiness in ensures', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/feature-toggle', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).enabled != null',
],
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, enabled: { type: 'boolean' } } } }
}
}, async (req, reply) => {
reply.status(201)
return { id: 'ft-1', name: req.body.name, enabled: req.body.name === 'dark-mode' }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/feature-toggle', payload: { name: 'dark-mode' } })
assert.strictEqual(res.statusCode, 201)
assert.strictEqual(res.json().enabled, true)
})
test('numeric computations: derived field must match formula', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/invoice-line', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).line_total != null',
],
body: { type: 'object', properties: { quantity: { type: 'number' }, unit_price: { type: 'number' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, line_total: { type: 'number' } } } }
}
}, async (req, reply) => {
reply.status(201)
const total = req.body.quantity * req.body.unit_price
return { id: 'inv-1', line_total: total }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/invoice-line', payload: { quantity: 3, unit_price: 49.99 } })
assert.strictEqual(res.statusCode, 201)
const body = res.json()
assert.ok(Math.abs(body.line_total - 149.97) < 0.01)
})
test('guard conditions: special characters and unicode in strings', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/unicode', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).greeting != null',
],
body: { type: 'object', properties: { lang: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' }, greeting: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(201)
const greetings: Record<string, string> = {
ja: '\u3053\u3093\u306b\u3061\u306f',
ar: '\u0645\u0631\u062d\u0628\u0627',
en: 'Hello \u2600'
}
return { id: 'u-1', greeting: greetings[req.body.lang] || greetings.en }
})
await fastify.ready()
t.after(() => fastify.close())
const jaRes = await fastify.inject({ method: 'POST', url: '/unicode', payload: { lang: 'ja' } })
assert.strictEqual(jaRes.statusCode, 201)
assert.strictEqual(jaRes.json().greeting, '\u3053\u3093\u306b\u3061\u306f')
const arRes = await fastify.inject({ method: 'POST', url: '/unicode', payload: { lang: 'ar' } })
assert.strictEqual(arRes.statusCode, 201)
assert.strictEqual(arRes.json().greeting, '\u0645\u0631\u062d\u0628\u0627')
})
test('large response body: deep nested object with many keys', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/large', {
schema: {
'x-category': 'constructor',
'x-ensures': [
'status:201',
'response_body(this).total_items != null',
'response_body(this).items != null',
],
body: { type: 'object', properties: { count: { type: 'number' } } },
response: { 201: { type: 'object', properties: { total_items: { type: 'number' }, items: { type: 'array' } } } }
}
}, async (req, reply) => {
reply.status(201)
const count = Math.min(req.body.count, 100)
const items = Array.from({ length: count }, (_, i) => ({
index: i, name: `Item ${i}`, nested: { a: i, b: i * 2, c: { d: `deep-${i}` } }
}))
return { total_items: count, items }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'POST', url: '/large', payload: { count: 50 } })
assert.strictEqual(res.statusCode, 201)
const body = res.json()
assert.strictEqual(body.total_items, 50)
assert.strictEqual(body.items.length, 50)
assert.strictEqual(body.items[0].nested.c.d, 'deep-0')
})
test('verification: runVerify surfaces failures from broken contracts', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.post('/broken', {
schema: {
'x-category': 'constructor',
'x-ensures': ['status:201', 'response_body(this).id != null'],
body: { type: 'object', properties: { name: { type: 'string' } } },
response: { 201: { type: 'object', properties: { id: { type: 'string' } } } }
}
}, async (req, reply) => {
reply.status(200)
return { name: req.body.name }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
// Should detect status:201 failure
assert.ok(result.summary.failed > 0, 'expected at least one failure')
})
test('verification: passing contracts produce zero failures', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/ok', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200', 'response_body(this).ok == true'],
response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } }
}
}, async () => {
return { ok: true }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0, 'expected zero failures for passing contracts')
})
test('empty body: ensuring no body on 204', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.delete('/cleanup', {
schema: {
'x-category': 'mutator',
'x-ensures': ['status:204'],
response: { 204: { type: 'null' } }
}
}, async (req, reply) => {
reply.status(204).send()
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'DELETE', url: '/cleanup' })
assert.strictEqual(res.statusCode, 204)
})
test('array response: list endpoint with required fields', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/list', {
schema: {
'x-category': 'observer',
'x-ensures': [
'status:200',
'response_body(this).items != null',
],
response: { 200: { type: 'object', properties: { count: { type: 'number' }, items: { type: 'array' } } } }
}
}, async () => {
return { count: 3, items: [{ a: 1 }, { a: 2 }, { a: 3 }] }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'GET', url: '/list' })
assert.strictEqual(res.statusCode, 200)
const body = res.json()
assert.strictEqual(body.count, 3)
assert.strictEqual(body.items.length, 3)
})
test('authorization preconditions: x-requires guards', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, { runtime: 'off' })
fastify.get('/admin/dashboard', {
schema: {
'x-category': 'observer',
'x-requires': ['auth_admin'],
'x-ensures': ['status:200', 'response_body(this).admin_panels != null'],
response: {
200: { type: 'object', properties: { admin_panels: { type: 'array' } } },
403: { type: 'object', properties: { error: { type: 'string' } } }
}
}
}, async () => {
return { admin_panels: ['users', 'analytics', 'billing'] }
})
await fastify.ready()
t.after(() => fastify.close())
const res = await fastify.inject({ method: 'GET', url: '/admin/dashboard' })
assert.strictEqual(res.statusCode, 200)
assert.ok(Array.isArray(res.json().admin_panels))
})
+242
View File
@@ -0,0 +1,242 @@
import { test } from 'node:test'
import assert from 'node:assert'
import Fastify from 'fastify'
import apophisPlugin from '../index.js'
import { createOutboundMockRuntime } from '../infrastructure/outbound-mock-runtime.js'
import { OutboundContractRegistry } from '../domain/outbound-contracts.js'
test('outbound mock: registry resolves multiple contracts', () => {
const registry = new OutboundContractRegistry()
registry.register('svc.users', {
target: 'https://api.example.com/users',
method: 'GET',
response: { 200: { type: 'object', properties: { id: { type: 'string' }, email: { type: 'string' } } } }
})
registry.register('svc.orders', {
target: 'https://api.example.com/orders',
method: 'POST',
response: { 201: { type: 'object', properties: { id: { type: 'string' }, total: { type: 'number' } } } }
})
const resolved = registry.resolve([
'svc.users',
'svc.orders',
{ name: 'svc.payments', target: 'https://pay.example.com', method: 'POST', response: { 200: { type: 'object', properties: { status: { type: 'string' } } } } }
])
assert.strictEqual(resolved.length, 3)
})
test('outbound mock: multiple concurrent installs are isolated', async () => {
const runtime1 = createOutboundMockRuntime({
contracts: [{
name: 'svc.a',
target: 'https://a.example.com',
method: 'GET',
response: { 200: { type: 'object', properties: { source: { type: 'string' } } } }
}],
mode: 'example',
unmatched: 'passthrough',
seed: 1,
overrides: { 'svc.a': { forceStatus: 200, body: { source: 'runtime-1' } } }
})
const runtime2 = createOutboundMockRuntime({
contracts: [{
name: 'svc.a',
target: 'https://a.example.com',
method: 'GET',
response: { 200: { type: 'object', properties: { source: { type: 'string' } } } }
}],
mode: 'example',
unmatched: 'passthrough',
seed: 2,
overrides: { 'svc.a': { forceStatus: 200, body: { source: 'runtime-2' } } }
})
runtime1.install()
runtime2.install()
runtime2.restore()
runtime1.restore()
runtime1.restore()
})
test('outbound mock: records multiple calls to same contract', async () => {
const runtime = createOutboundMockRuntime({
contracts: [{
name: 'counter',
target: 'https://counter.example.com/tick',
method: 'POST',
response: { 200: { type: 'object', properties: { count: { type: 'number' } } } }
}],
mode: 'example',
unmatched: 'error',
seed: 42,
})
runtime.install()
await globalThis.fetch('https://counter.example.com/tick', { method: 'POST', body: JSON.stringify({}) })
await globalThis.fetch('https://counter.example.com/tick', { method: 'POST', body: JSON.stringify({}) })
const calls = runtime.getCalls('counter')
assert.strictEqual(calls.length, 2)
runtime.restore()
})
test('outbound mock: stateful resource lifecycle through mock', async (t) => {
const runtime = createOutboundMockRuntime({
contracts: [{
name: 'resources',
target: 'https://res.example.com/v1',
method: '*',
response: {
201: { type: 'object', properties: { id: { type: 'string' }, data: { type: 'string' } } },
200: { type: 'object', properties: { id: { type: 'string' }, data: { type: 'string' } } }
},
resource: {
idField: 'id',
idPattern: '/v1/:id',
createMethods: ['POST'],
readMethods: ['GET'],
updateMethods: ['PATCH'],
deleteMethods: ['DELETE'],
}
}],
mode: 'example',
unmatched: 'error',
seed: 42,
})
runtime.install()
const createRes = await globalThis.fetch('https://res.example.com/v1', {
method: 'POST',
body: JSON.stringify({ data: 'hello' }),
headers: { 'content-type': 'application/json' }
})
assert.strictEqual(createRes.status, 201)
const created = await createRes.json()
assert.ok(created.id)
const getRes = await globalThis.fetch(`https://res.example.com/v1/${created.id}`)
assert.strictEqual(getRes.status, 200)
const deleteRes = await globalThis.fetch(`https://res.example.com/v1/${created.id}`, { method: 'DELETE' })
assert.strictEqual(deleteRes.status, 200)
const afterDel = await globalThis.fetch(`https://res.example.com/v1/${created.id}`)
assert.notStrictEqual(afterDel.status, 200)
runtime.restore()
})
test('outbound mock: overrides change status and body', async () => {
const runtime = createOutboundMockRuntime({
contracts: [{
name: 'payments',
target: 'https://pay.example.com/charge',
method: 'POST',
response: { 201: { type: 'object', properties: { id: { type: 'string' }, status: { type: 'string' } } } }
}],
mode: 'example',
unmatched: 'error',
seed: 42,
overrides: {
'payments': {
forceStatus: 402,
body: { error: 'insufficient_funds', code: 'PAY-402' }
}
}
})
runtime.install()
const res = await globalThis.fetch('https://pay.example.com/charge', {
method: 'POST',
body: JSON.stringify({ amount: 999 }),
headers: { 'content-type': 'application/json' }
})
assert.strictEqual(res.status, 402)
const body = await res.json()
assert.strictEqual(body.code, 'PAY-402')
assert.strictEqual(body.error, 'insufficient_funds')
runtime.restore()
})
test('outbound mock: unmatched passthrough (no real HTTP attempt)', async () => {
const runtime = createOutboundMockRuntime({
contracts: [],
mode: 'example',
unmatched: 'passthrough',
seed: 42,
})
runtime.install()
let threw = false
try {
await globalThis.fetch('https://unknown.example.com/anything')
} catch (e) {
threw = true
}
runtime.restore()
})
test('outbound mock: clear resets in-memory resource state', async () => {
const runtime = createOutboundMockRuntime({
contracts: [{
name: 'cacheable',
target: 'https://cache.example.com/v1',
method: '*',
response: {
201: { type: 'object', properties: { id: { type: 'string' } } },
200: { type: 'object', properties: { id: { type: 'string' } } }
},
resource: {
idField: 'id',
idPattern: '/v1/:id',
createMethods: ['POST'],
readMethods: ['GET'],
}
}],
mode: 'example',
unmatched: 'error',
seed: 42,
})
runtime.install()
const createRes = await globalThis.fetch('https://cache.example.com/v1', { method: 'POST', body: JSON.stringify({}) })
const { id } = await createRes.json()
const getRes = await globalThis.fetch(`https://cache.example.com/v1/${id}`)
assert.strictEqual(getRes.status, 200)
runtime.clear()
const afterClear = await globalThis.fetch(`https://cache.example.com/v1/${id}`)
assert.notStrictEqual(afterClear.status, 200)
runtime.restore()
})
test('outbound mock: install/restore/install cycle works', async () => {
const runtime = createOutboundMockRuntime({
contracts: [{
name: 'toggle',
target: 'https://toggle.example.com/status',
method: 'GET',
response: { 200: { type: 'object', properties: { active: { type: 'boolean' } } } }
}],
mode: 'example',
unmatched: 'error',
seed: 42,
})
runtime.install()
assert.strictEqual((await globalThis.fetch('https://toggle.example.com/status')).status, 200)
runtime.restore()
runtime.install()
assert.strictEqual((await globalThis.fetch('https://toggle.example.com/status')).status, 200)
runtime.restore()
})
+404
View File
@@ -0,0 +1,404 @@
import { test } from 'node:test'
import assert from 'node:assert'
import Fastify from 'fastify'
import apophisPlugin from '../index.js'
test('plugin contracts: formula composition with passing ensures', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'security-plugin': {
appliesTo: '/api/**',
hooks: {
preHandler: { ensures: ['status:200'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['response_body(this).ok == true'],
response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } }
}
}, async () => {
return { ok: true }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0, 'plugin ensures should pass')
assert.ok(result.summary.pluginContractsApplied > 0, 'should apply plugin contracts')
})
test('plugin contracts: plugin ensures failure detected', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'always-fail': {
appliesTo: '/api/**',
hooks: {
preHandler: { ensures: ['1 == 2'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'abc' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.ok(result.summary.failed > 0, 'plugin ensures should fail')
assert.ok(result.summary.pluginContractsFailed > 0, 'should report plugin contract failures')
})
test('plugin contracts: missing required extension silently skips plugin', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'missing-ext': {
appliesTo: '/api/**',
extensions: [{ name: 'nonexistent', required: true }],
hooks: {
preHandler: { ensures: ['1 == 2'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'abc' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0)
assert.strictEqual(result.summary.pluginContractsFailed ?? 0, 0, 'no plugin failures when plugin is skipped')
})
test('plugin contracts: extension predicate passes when condition met', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
extensions: [
{
name: 'tenant-gate',
headers: ['check_tenant'],
predicates: {
check_tenant: (ctx: { evalContext: { request: { headers: Record<string, string> } } }) => ({
value: ctx.evalContext.request.headers['x-tenant'] === 'acme',
success: true,
}),
},
},
],
pluginContracts: {
'tenant-validator': {
appliesTo: '/api/**',
extensions: [{ name: 'tenant-gate', required: true }],
hooks: {
onRequest: { requires: ['check_tenant(this) == true'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'acme-data' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({
runs: 10,
variants: [{ name: 'acme', headers: { 'x-tenant': 'acme' } }],
})
assert.strictEqual(result.summary.failed, 0, 'extension predicate should pass')
})
test('plugin contracts: multiple plugins composing on same route', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'auth-check': {
appliesTo: '/api/**',
hooks: {
onRequest: { requires: ['true'] }
}
},
'rate-limit': {
appliesTo: '/api/**',
hooks: {
preHandler: { ensures: ['status:200'] }
}
},
'logging': {
appliesTo: '/api/**',
hooks: {
onSend: { ensures: ['response_body(this).logged == true'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { logged: { type: 'boolean' }, val: { type: 'string' } } } }
}
}, async () => {
return { logged: true, val: 'data' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0, 'all plugins should pass')
assert.ok(result.summary.pluginContractsApplied >= 3, 'all plugins should be applied')
})
test('plugin contracts: appliesTo glob scoping — only matching routes', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'admin-only': {
appliesTo: '/admin/**',
hooks: {
preHandler: { ensures: ['1 == 1'] }
}
}
}
})
fastify.get('/public/health', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } }
}
}, async () => {
return { ok: true }
})
fastify.get('/admin/users', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200', 'response_body(this).admin_data == true'],
response: { 200: { type: 'object', properties: { admin_data: { type: 'boolean' } } } }
}
}, async () => {
return { admin_data: true }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0)
assert.ok(result.summary.pluginContractsApplied > 0)
})
test('plugin contracts: onSend hook validates response body after handler', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'response-validator': {
appliesTo: '/api/**',
hooks: {
onSend: { ensures: ['response_body(this).received_at != null'] }
}
}
}
})
fastify.get('/api/events', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { events: { type: 'array' }, received_at: { type: 'string' } } } }
}
}, async () => {
return { events: [], received_at: new Date().toISOString() }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0)
})
test('plugin contracts: plugin with non-required extension passes when extension absent', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'optional-validator': {
appliesTo: '/api/**',
extensions: [{ name: 'optional-ext', required: false }],
hooks: {
preHandler: { ensures: ['status:200'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['response_body(this).val != null'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'ok' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0, 'non-required extension absence should not block plugin')
assert.ok(result.summary.pluginContractsApplied > 0)
})
test('plugin contracts: registry warnings for missing extensions', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'warn-about-me': {
appliesTo: '/api/**',
extensions: [{ name: 'nonexistent-warn', required: true }],
hooks: {
preHandler: { ensures: ['status:200'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['response_body(this).val != null'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'ok' }
})
await fastify.ready()
t.after(() => fastify.close())
const reg = (fastify as any)._apophisPluginContractRegistry
assert.ok(reg, 'plugin contract registry should exist')
// After plugin processing, warnings should exist
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0)
})
test('plugin contracts: route-level and plugin-level ensures both enforced', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'core-policy': {
appliesTo: '/api/**',
hooks: {
preHandler: { ensures: ['status:200'] }
}
}
}
})
fastify.get('/api/double', {
schema: {
'x-category': 'observer',
'x-ensures': [
'status:200',
'response_body(this).route_level == true',
],
response: { 200: { type: 'object', properties: { route_level: { type: 'boolean' }, val: { type: 'string' } } } }
}
}, async () => {
return { route_level: true, val: 'double-checked' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.strictEqual(result.summary.failed, 0, 'both route and plugin ensures should pass')
assert.ok(result.summary.pluginContractsApplied > 0)
})
test('plugin contracts: breaking plugin ensures produces source attribution', async (t) => {
const fastify = Fastify()
await fastify.register(apophisPlugin, {
runtime: 'off',
pluginContracts: {
'broken-policy': {
appliesTo: '/api/**',
hooks: {
preHandler: { ensures: ['status:500'] }
}
}
}
})
fastify.get('/api/data', {
schema: {
'x-category': 'observer',
'x-ensures': ['status:200'],
response: { 200: { type: 'object', properties: { val: { type: 'string' } } } }
}
}, async () => {
return { val: 'ok' }
})
await fastify.ready()
t.after(() => fastify.close())
const result = await (fastify as any).apophis.contract({ runs: 10 })
assert.ok(result.summary.failed > 0, 'plugin ensures mismatch should fail')
// Plugin-contract-sourced failures should appear
const pluginFailure = result.tests.find((t: any) =>
!t.ok && t.diagnostics?.violation?.source?.startsWith('plugin:')
)
assert.ok(pluginFailure, 'should have plugin-sourced failure with source field')
})