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() // 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)