import { test } from 'node:test' import assert from 'node:assert' import Fastify from 'fastify' import type { FastifyInstance } from 'fastify' import apophisPlugin from '../index.js' // Extend FastifyInstance type for tests import type { TestResult } from '../types.js' type TestFastifyInstance = FastifyInstance & { apophis: { contract: (opts?: { runs?: number; scope?: string; seed?: number }) => Promise spec: () => Record } } test('scope isolation: routes with x-scope are filtered by scope parameter', async () => { const fastify = Fastify() as unknown as TestFastifyInstance try { await fastify.register(import('@fastify/swagger'), {}) await fastify.register(apophisPlugin, { runtime: 'off' }) // Public route - no scope (runs for all scopes) fastify.get('/public', { schema: { 'x-category': 'observer', 'x-ensures': ['status:200'], response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } } } }, async () => ({ ok: true })) // Admin route - admin scope only fastify.get('/admin', { schema: { 'x-category': 'observer', 'x-scope': 'admin', 'x-ensures': ['status:200'], response: { 200: { type: 'object', properties: { admin: { type: 'boolean' } } } } } }, async () => ({ admin: true })) // User route - user scope only fastify.get('/user', { schema: { 'x-category': 'observer', 'x-scope': 'user', 'x-ensures': ['status:200'], response: { 200: { type: 'object', properties: { user: { type: 'boolean' } } } } } }, async () => ({ user: true })) await fastify.ready() // Test with no scope - should discover all 3 routes const allResult = await fastify.apophis.contract({ runs: 10, scope: undefined }) const allPaths = new Set(allResult.tests.map((t: TestResult) => t.name.split(' ')[1])) assert.ok(allPaths.has('/public'), 'public route should be in all scope') assert.ok(allPaths.has('/admin'), 'admin route should be in all scope') assert.ok(allPaths.has('/user'), 'user route should be in all scope') // Test with admin scope - should only get public + admin const adminResult = await fastify.apophis.contract({ runs: 10, scope: 'admin' }) const adminPaths = new Set(adminResult.tests.map((t: TestResult) => t.name.split(' ')[1])) assert.ok(adminPaths.has('/public'), 'public route should be in admin scope') assert.ok(adminPaths.has('/admin'), 'admin route should be in admin scope') assert.ok(!adminPaths.has('/user'), 'user route should NOT be in admin scope') // Test with user scope - should only get public + user const userResult = await fastify.apophis.contract({ runs: 10, scope: 'user' }) const userPaths = new Set(userResult.tests.map((t: TestResult) => t.name.split(' ')[1])) assert.ok(userPaths.has('/public'), 'public route should be in user scope') assert.ok(!userPaths.has('/admin'), 'admin route should NOT be in user scope') assert.ok(userPaths.has('/user'), 'user route should be in user scope') } finally { await fastify.close() } }) test('scope isolation: scope headers are passed to requests', async () => { const fastify = Fastify() as unknown as TestFastifyInstance try { let receivedHeaders: Record = {} await fastify.register(import('@fastify/swagger'), {}) await fastify.register(apophisPlugin, { runtime: 'off' }) fastify.get('/headers', { schema: { 'x-category': 'observer', 'x-scope': 'test', 'x-ensures': ['status:200'], response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } } } }, async (request) => { receivedHeaders = (request as { headers: Record }).headers return { ok: true } }) await fastify.ready() // Register scope with custom header ;(fastify as any).apophis.scope.register('test', { headers: { 'x-custom-header': 'test-value' }, metadata: {} }) await fastify.apophis.contract({ runs: 10, scope: 'test' }) assert.strictEqual(receivedHeaders['x-custom-header'], 'test-value', 'scope header should be passed to request') } finally { await fastify.close() } }) test('scope registry: malformed env var is handled gracefully', async () => { const originalEnv = { ...process.env } try { // Set a malformed JSON env var process.env.APOPHIS_SCOPE_MALFORMED = 'not-json-at-all' // Should not throw const { ScopeRegistry } = await import('../infrastructure/scope-registry.js') const registry = new ScopeRegistry() // Should not have the malformed scope assert.strictEqual(registry.scopes.has('malformed'), false, 'malformed scope should be ignored') // Other scopes should still work process.env.APOPHIS_SCOPE_VALID = '{"headers":{"x-test":"value"}}' const registry2 = new ScopeRegistry() assert.strictEqual(registry2.scopes.has('valid'), true, 'valid scope should be parsed') assert.deepStrictEqual(registry2.getHeaders('valid'), { 'x-test': 'value' }) } finally { // Restore env Object.keys(process.env).forEach(key => delete process.env[key]) Object.assign(process.env, originalEnv) } }) test('scope isolation: non-matching scope returns empty test suite', async () => { const fastify = Fastify() as unknown as TestFastifyInstance try { await fastify.register(import('@fastify/swagger'), {}) await fastify.register(apophisPlugin, { runtime: 'off' }) fastify.get('/scoped', { schema: { 'x-category': 'observer', 'x-scope': 'private', 'x-ensures': ['status:200'], response: { 200: { type: 'object', properties: { ok: { type: 'boolean' } } } } } }, async () => ({ ok: true })) await fastify.ready() // Test with non-matching scope const result = await fastify.apophis.contract({ runs: 10, scope: 'other' }) assert.strictEqual(result.tests.length, 0, 'no tests should run for non-matching scope') assert.strictEqual(result.summary.passed, 0, 'no tests should pass') assert.strictEqual(result.summary.failed, 0, 'no tests should fail') } finally { await fastify.close() } })