2026-03-10 00:00:00 -07:00
|
|
|
import Fastify from 'fastify'
|
|
|
|
|
import apophisPlugin from 'apophis-fastify'
|
2026-04-30 11:25:30 -07:00
|
|
|
import crypto from 'crypto'
|
2026-03-10 00:00:00 -07:00
|
|
|
|
|
|
|
|
const fastify = Fastify()
|
|
|
|
|
|
|
|
|
|
await fastify.register(apophisPlugin, {
|
|
|
|
|
runtime: 'error', // Validate contracts on every request
|
|
|
|
|
cleanup: true, // Auto-cleanup resources on exit
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// In-memory store for demo
|
|
|
|
|
const users = new Map<string, { id: string; email: string; name: string }>()
|
|
|
|
|
|
|
|
|
|
// CREATE — constructor
|
|
|
|
|
fastify.post('/users', {
|
|
|
|
|
schema: {
|
|
|
|
|
'x-category': 'constructor',
|
|
|
|
|
'x-ensures': [
|
|
|
|
|
'status:201',
|
|
|
|
|
'response_body(this).id != null',
|
|
|
|
|
'response_body(this).email == request_body(this).email',
|
|
|
|
|
],
|
|
|
|
|
body: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
email: { type: 'string', format: 'email' },
|
|
|
|
|
name: { type: 'string', minLength: 1 }
|
|
|
|
|
},
|
|
|
|
|
required: ['email', 'name']
|
|
|
|
|
},
|
|
|
|
|
response: {
|
|
|
|
|
201: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' },
|
|
|
|
|
email: { type: 'string' },
|
|
|
|
|
name: { type: 'string' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, async (req, reply) => {
|
2026-04-30 11:25:30 -07:00
|
|
|
const id = `usr-${crypto.createHash('sha256').update(req.body.email).digest('hex').slice(0, 8)}`
|
2026-03-10 00:00:00 -07:00
|
|
|
const user = { id, email: req.body.email, name: req.body.name }
|
|
|
|
|
users.set(id, user)
|
|
|
|
|
reply.status(201)
|
|
|
|
|
return user
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// READ — observer
|
|
|
|
|
fastify.get('/users/:id', {
|
|
|
|
|
schema: {
|
|
|
|
|
'x-category': 'observer',
|
|
|
|
|
'x-requires': ['users:id'],
|
|
|
|
|
'x-ensures': [
|
|
|
|
|
'status:200',
|
|
|
|
|
'response_body(this).id == request_params(this).id',
|
|
|
|
|
],
|
|
|
|
|
params: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' }
|
|
|
|
|
},
|
|
|
|
|
required: ['id']
|
|
|
|
|
},
|
|
|
|
|
response: {
|
|
|
|
|
200: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' },
|
|
|
|
|
email: { type: 'string' },
|
|
|
|
|
name: { type: 'string' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, async (req) => {
|
|
|
|
|
const user = users.get(req.params.id)
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new Error('User not found')
|
|
|
|
|
}
|
|
|
|
|
return user
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// UPDATE — mutator
|
|
|
|
|
fastify.put('/users/:id', {
|
|
|
|
|
schema: {
|
|
|
|
|
'x-category': 'mutator',
|
|
|
|
|
'x-requires': ['users:id'],
|
|
|
|
|
'x-ensures': [
|
|
|
|
|
'status:200',
|
|
|
|
|
'response_body(this).id == request_params(this).id',
|
|
|
|
|
],
|
|
|
|
|
params: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' }
|
|
|
|
|
},
|
|
|
|
|
required: ['id']
|
|
|
|
|
},
|
|
|
|
|
body: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
email: { type: 'string', format: 'email' },
|
|
|
|
|
name: { type: 'string', minLength: 1 }
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
response: {
|
|
|
|
|
200: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' },
|
|
|
|
|
email: { type: 'string' },
|
|
|
|
|
name: { type: 'string' }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, async (req) => {
|
|
|
|
|
const user = users.get(req.params.id)
|
|
|
|
|
if (!user) {
|
|
|
|
|
throw new Error('User not found')
|
|
|
|
|
}
|
|
|
|
|
const updated = {
|
|
|
|
|
...user,
|
|
|
|
|
email: req.body.email ?? user.email,
|
|
|
|
|
name: req.body.name ?? user.name,
|
|
|
|
|
}
|
|
|
|
|
users.set(req.params.id, updated)
|
|
|
|
|
return updated
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// DELETE — destructor
|
|
|
|
|
fastify.delete('/users/:id', {
|
|
|
|
|
schema: {
|
|
|
|
|
'x-category': 'destructor',
|
|
|
|
|
'x-requires': ['users:id'],
|
|
|
|
|
'x-ensures': ['status:204'],
|
|
|
|
|
params: {
|
|
|
|
|
type: 'object',
|
|
|
|
|
properties: {
|
|
|
|
|
id: { type: 'string' }
|
|
|
|
|
},
|
|
|
|
|
required: ['id']
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, async (req, reply) => {
|
|
|
|
|
users.delete(req.params.id)
|
|
|
|
|
reply.status(204)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await fastify.ready()
|
|
|
|
|
|
|
|
|
|
// Run contract tests (all non-utility routes, property-based)
|
2026-04-30 13:56:39 -07:00
|
|
|
const result = await fastify.apophis.contract({ runs: 50 })
|
2026-03-10 00:00:00 -07:00
|
|
|
console.log('Contract tests:', result.summary)
|
|
|
|
|
|
|
|
|
|
// Run stateful tests (constructor→mutator→destructor sequences)
|
2026-04-30 13:56:39 -07:00
|
|
|
const stateful = await fastify.apophis.stateful({ runs: 50, seed: 42 })
|
2026-03-10 00:00:00 -07:00
|
|
|
console.log('Stateful tests:', stateful.summary)
|
|
|
|
|
|
|
|
|
|
// Validate a single route
|
|
|
|
|
const check = await fastify.apophis.check('POST', '/users')
|
|
|
|
|
console.log('POST /users check:', check.ok ? 'PASS' : 'FAIL')
|