140 lines
6.1 KiB
TypeScript
140 lines
6.1 KiB
TypeScript
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<any>
|
|
spec: () => Record<string, unknown>
|
|
}
|
|
}
|
|
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<string, string> = {}
|
|
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<string, string> }).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()
|
|
}
|
|
}) |