import Fastify from 'fastify' import apophisPlugin from 'apophis-fastify' import crypto from 'crypto' const fastify = Fastify() await fastify.register(apophisPlugin, { runtime: 'error', cleanup: true, }) const users = new Map() // CREATE — constructor // Behavioral: the created user must be retrievable. // Note: we do not write 'status:201' or 'response_body(this).id != null'. // The schema already validates status codes and required fields. // Contracts should test behavior across operations, not structure. fastify.post('/users', { schema: { 'x-category': 'constructor', 'x-ensures': [ // Round-trip: the server returns exactly what we sent (no mutation, no drops) 'response_body(this) == request_body(this)', // Cross-route: the created user must be retrievable 'response_code(GET /users/{response_body(this).id}) == 200', ], 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) => { const id = `usr-${crypto.createHash('sha256').update(req.body.email).digest('hex').slice(0, 8)}` const user = { id, email: req.body.email, name: req.body.name } users.set(id, user) reply.status(201) return user }) // READ — observer // Behavioral: the returned user must match the requested id. fastify.get('/users/:id', { schema: { 'x-category': 'observer', 'x-requires': [ // Precondition: the user must exist for this read to be valid 'response_code(GET /users/{request_params(this).id}) == 200' ], 'x-ensures': [ // The returned id must match the requested id (no mix-up) '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 // Behavioral: after update, the change must be visible on read. fastify.put('/users/:id', { schema: { 'x-category': 'mutator', 'x-requires': [ // The user must exist before updating 'response_code(GET /users/{request_params(this).id}) == 200' ], 'x-ensures': [ // Cross-route: after update, reading the user shows the new data 'response_body(GET /users/{request_params(this).id}).email == request_body(this).email', ], 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 // Behavioral: after deletion, the user must no longer exist. fastify.delete('/users/:id', { schema: { 'x-category': 'destructor', 'x-requires': [ // The user must exist before deleting 'response_code(GET /users/{request_params(this).id}) == 200' ], 'x-ensures': [ // After deletion, the user is gone 'response_code(GET /users/{request_params(this).id}) == 404', // The deleted user data is returned (matches pre-deletion read) 'response_body(this) == previous(response_body(GET /users/{request_params(this).id}))', ], params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } }, async (req, reply) => { const user = users.get(req.params.id) users.delete(req.params.id) reply.status(200) return user }) await fastify.ready() const result = await fastify.apophis.contract({ runs: 50 }) console.log('Contract tests:', result.summary) const stateful = await fastify.apophis.stateful({ runs: 50, seed: 42 }) console.log('Stateful tests:', stateful.summary) const check = await fastify.apophis.check('POST', '/users') console.log('POST /users check:', check.ok ? 'PASS' : 'FAIL')