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 processedKeys = new Set() const ledger = new Map() // PAYMENT — idempotent, must not double-charge fastify.post('/payments', { schema: { 'x-category': 'mutator', 'x-requires': [ 'request_header(this).idempotency-key != null', ], 'x-ensures': [ 'response_status(this) == 201 || response_status(this) == 200', 'response_body(this).id != null', // Core idempotency assurance: same key, same result 'response_body(this).amount == request_body(this).amount', ], headers: { type: 'object', properties: { 'idempotency-key': { type: 'string' }, }, required: ['idempotency-key'], }, body: { type: 'object', properties: { amount: { type: 'number', minimum: 1 }, currency: { type: 'string', minLength: 3, maxLength: 3 }, }, required: ['amount', 'currency'], }, response: { 201: { type: 'object', properties: { id: { type: 'string' }, idempotencyKey: { type: 'string' }, amount: { type: 'number' }, status: { type: 'string' }, }, }, 200: { type: 'object', properties: { id: { type: 'string' }, idempotencyKey: { type: 'string' }, amount: { type: 'number' }, status: { type: 'string' }, }, }, }, }, }, async (req, reply) => { const key = req.headers['idempotency-key'] as string // Idempotency check: if already processed, return the saved result if (processedKeys.has(key)) { for (const record of ledger.values()) { if (record.idempotencyKey === key) { return { ...record, status: 'duplicate' } } } } const id = `pay-${crypto.randomUUID().slice(0, 8)}` const record = { id, idempotencyKey: key, amount: req.body.amount, status: 'processed', createdAt: Date.now(), } processedKeys.add(key) ledger.set(id, record) reply.status(201) return record }) // REFUND — idempotent reversal fastify.post('/payments/:id/refund', { schema: { 'x-category': 'mutator', 'x-requires': [ 'response_code(GET /payments/{request_params(this).id}) == 200', ], 'x-ensures': [ 'response_body(this).refundId != null', 'response_body(this).amount == previous(response_body(GET /payments/{request_params(this).id})).amount', // Refund must be reversible only once ], headers: { type: 'object', properties: { 'idempotency-key': { type: 'string' }, }, required: ['idempotency-key'], }, params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], }, response: { 201: { type: 'object', properties: { refundId: { type: 'string' }, amount: { type: 'number' }, status: { type: 'string' }, }, }, }, }, }, async (req, reply) => { const payment = ledger.get(req.params.id) if (!payment) { reply.status(404) return { error: 'Payment not found' } } const key = req.headers['idempotency-key'] as string if (processedKeys.has(key)) { return { refundId: `ref-${payment.id}`, amount: payment.amount, status: 'duplicate', } } processedKeys.add(key) reply.status(201) return { refundId: `ref-${payment.id}`, amount: payment.amount, status: 'refunded', } }) // PAYMENT STATUS — observer fastify.get('/payments/:id', { schema: { 'x-category': 'observer', 'x-requires': [ 'response_status(this) == 200 || response_status(this) == 404', ], params: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'], }, }, }, async (req, reply) => { const payment = ledger.get(req.params.id) if (!payment) { reply.status(404) return { error: 'Payment not found' } } return payment }) 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)