Files
apophis-fastify/src/test/domain.test.ts
T

654 lines
19 KiB
TypeScript

/**
* Domain Module Unit Tests
* Tests for category inference, contract extraction, and route discovery.
* Uses Node's built-in test runner with AAA (Arrange, Act, Assert) pattern.
*/
import { test } from 'node:test'
import assert from 'node:assert'
import { inferCategory } from '../domain/category.js'
import { extractContract } from '../domain/contract.js'
import { discoverRoutes } from '../domain/discovery.js'
// ============================================================================
import type { RouteContract } from '../types.js'
// Category Inference Tests
// ============================================================================
test('inferCategory returns utility for exact utility path /reset', () => {
// Arrange
const path = '/reset'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'utility')
})
test('inferCategory returns utility for utility path with trailing slash', () => {
// Arrange
const path = '/health/'
const method = 'GET'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'utility')
})
test('inferCategory returns utility for all registered utility paths', () => {
// Arrange
const utilityPaths = ['/ping', '/login', '/logout', '/auth', '/callback', '/purge', '/clear', '/initialize', '/setup', '/webhook']
// Act & Assert
for (const path of utilityPaths) {
const result = inferCategory(path, 'GET', undefined)
assert.strictEqual(result, 'utility', `Expected utility for path ${path}`)
}
})
test('inferCategory returns observer for GET method on non-utility path', () => {
// Arrange
const path = '/users'
const method = 'GET'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory returns observer for observer suffix /search', () => {
// Arrange
const path = '/users/search'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory returns observer for observer suffix /count', () => {
// Arrange
const path = '/items/count'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory returns observer for observer suffix /stats', () => {
// Arrange
const path = '/metrics/stats'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory returns observer for observer suffix /status', () => {
// Arrange
const path = '/system/status'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory returns constructor for POST on collection path', () => {
// Arrange
const path = '/users'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'constructor')
})
test('inferCategory returns constructor for POST on nested collection path', () => {
// Arrange
const path = '/api/v1/users'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'constructor')
})
test('inferCategory returns mutator for PUT method', () => {
// Arrange
const path = '/users/:id'
const method = 'PUT'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'mutator')
})
test('inferCategory returns mutator for PATCH method', () => {
// Arrange
const path = '/users/:id'
const method = 'PATCH'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'mutator')
})
test('inferCategory returns mutator for DELETE method', () => {
// Arrange
const path = '/users/:id'
const method = 'DELETE'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'mutator')
})
test('inferCategory returns mutator for POST with path parameter', () => {
// Arrange
const path = '/users/:id'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'mutator')
})
test('inferCategory returns observer for POST without collection path or path param', () => {
// Arrange
const path = '/search'
const method = 'POST'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory handles method case insensitively', () => {
// Arrange
const path = '/users'
const method = 'get'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory respects override when provided', () => {
// Arrange
const path = '/users'
const method = 'POST'
const override = 'observer'
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
test('inferCategory ignores empty string override', () => {
// Arrange
const path = '/users'
const method = 'POST'
const override = ''
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'constructor')
})
test('inferCategory returns utility when override is utility', () => {
// Arrange
const path = '/users'
const method = 'POST'
const override = 'utility'
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'utility')
})
test('inferCategory returns mutator when override is mutator', () => {
// Arrange
const path = '/users'
const method = 'GET'
const override = 'mutator'
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'mutator')
})
test('inferCategory returns observer as default fallback', () => {
// Arrange
const path = '/'
const method = 'HEAD'
const override = undefined
// Act
const result = inferCategory(path, method, override)
// Assert
assert.strictEqual(result, 'observer')
})
// ============================================================================
// Contract Extraction Tests
// ============================================================================
test('extractContract extracts basic contract with defaults', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = undefined
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.path, '/users')
assert.strictEqual(result.method, 'GET')
assert.strictEqual(result.category, 'observer')
assert.deepStrictEqual(result.requires, [])
assert.deepStrictEqual(result.ensures, [])
assert.deepStrictEqual(result.invariants, [])
assert.deepStrictEqual(result.regexPatterns, {})
assert.strictEqual(result.validateRuntime, true)
assert.deepStrictEqual(result.schema, {})
})
test('extractContract extracts x-requires array', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-requires': ['admin', 'authenticated'],
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.requires, ['admin', 'authenticated'])
})
test('extractContract extracts x-ensures array', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-ensures': ['user.created', 'email.sent'],
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.ensures, ['user.created', 'email.sent'])
})
test('extractContract ignores x-invariants (removed in v1.0)', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-invariants': ['unique.email', 'active.status'],
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.invariants, [])
})
test('extractContract ignores x-regex (removed in v1.0)', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-regex': {
email: '^[\\w.-]+@[\\w.-]+\\.\\w+$',
phone: '^\\+?[1-9]\\d{1,14}$',
},
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.regexPatterns, {})
})
test('extractContract handles x-validate-runtime false', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {
'x-validate-runtime': false,
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.validateRuntime, false)
})
test('extractContract defaults validateRuntime to true when not specified', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.validateRuntime, true)
})
test('extractContract respects x-category override', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-category': 'utility',
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.category, 'utility')
})
test('extractContract ignores non-string x-category', () => {
// Arrange
const path = '/users'
const method = 'POST'
const schema = {
'x-category': 123,
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.category, 'constructor')
})
test('extractContract handles empty schema object', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.requires, [])
assert.deepStrictEqual(result.ensures, [])
assert.deepStrictEqual(result.invariants, [])
assert.deepStrictEqual(result.regexPatterns, {})
assert.strictEqual(result.validateRuntime, true)
})
test('extractContract handles null x-regex gracefully', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {
'x-regex': null,
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.regexPatterns, {})
})
test('extractContract handles non-object x-regex gracefully', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {
'x-regex': 'invalid',
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.regexPatterns, {})
})
test('extractContract normalizes method to uppercase', () => {
// Arrange
const path = '/users'
const method = 'post'
const schema = undefined
// Act
const result = extractContract(path, method, schema)
// Assert
assert.strictEqual(result.method, 'POST')
})
test('extractContract preserves original schema in contract', () => {
// Arrange
const path = '/users'
const method = 'GET'
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
},
}
// Act
const result = extractContract(path, method, schema)
// Assert
assert.deepStrictEqual(result.schema, schema)
})
// ============================================================================
// Route Discovery Tests
// ============================================================================
test('discoverRoutes returns empty array for empty routes', () => {
// Arrange
const instance = { routes: [] }
// Act
const result = discoverRoutes(instance)
// Assert
assert.deepStrictEqual(result, [])
})
test('discoverRoutes returns empty array when routes is undefined', () => {
// Arrange
const instance = {}
// Act
const result = discoverRoutes(instance)
// Assert
assert.deepStrictEqual(result, [])
})
test('discoverRoutes discovers single route', () => {
// Arrange
const instance = {
routes: [
{ method: 'GET', url: '/users', schema: {} },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0]!.path, '/users')
assert.strictEqual(result[0]!.method, 'GET')
assert.strictEqual(result[0]!.category, 'observer')
})
test('discoverRoutes discovers multiple routes', () => {
// Arrange
const instance = {
routes: [
{ method: 'GET', url: '/users' },
{ method: 'POST', url: '/users' },
{ method: 'GET', url: '/users/:id' },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 3)
assert.strictEqual(result[0]!.category, 'observer')
assert.strictEqual(result[1]!.category, 'constructor')
assert.strictEqual(result[2]!.category, 'observer')
})
test('discoverRoutes handles routes with schemas', () => {
// Arrange
const instance = {
routes: [
{
method: 'POST',
url: '/users',
schema: {
'x-requires': ['admin'],
'x-ensures': ['user.created'],
'x-category': 'constructor',
},
},
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0]!.path, '/users')
assert.deepStrictEqual(result[0]!.requires, ['admin'])
assert.deepStrictEqual(result[0]!.ensures, ['user.created'])
assert.strictEqual(result[0]!.category, 'constructor')
})
test('discoverRoutes handles routes without schemas', () => {
// Arrange
const instance = {
routes: [
{ method: 'DELETE', url: '/users/:id' },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0]!.path, '/users/:id')
assert.strictEqual(result[0]!.method, 'DELETE')
assert.strictEqual(result[0]!.category, 'mutator')
assert.deepStrictEqual(result[0]!.requires, [])
})
test('discoverRoutes handles mixed route configurations', () => {
// Arrange
const instance = {
routes: [
{ method: 'GET', url: '/health' },
{ method: 'POST', url: '/users', schema: { 'x-requires': ['auth'] } },
{ method: 'GET', url: '/users/:id' },
{ method: 'DELETE', url: '/users/:id' },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 4)
assert.strictEqual(result[0]!.category, 'utility')
assert.deepStrictEqual(result[0]!.requires, [])
assert.strictEqual(result[1]!.category, 'constructor')
assert.deepStrictEqual(result[1]!.requires, ['auth'])
assert.strictEqual(result[2]!.category, 'observer')
assert.deepStrictEqual(result[2]!.invariants, [])
assert.strictEqual(result[3]!.category, 'mutator')
})
test('discoverRoutes ignores x-regex (removed in v1.0)', () => {
// Arrange
const instance = {
routes: [
{
method: 'POST',
url: '/users',
schema: {
'x-regex': {
email: '^[\\w.-]+@[\\w.-]+\\.\\w+$',
},
},
},
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.deepStrictEqual(result[0]!.regexPatterns, {})
})
test('discoverRoutes handles route with validateRuntime disabled', () => {
// Arrange
const instance = {
routes: [
{
method: 'GET',
url: '/public',
schema: {
'x-validate-runtime': false,
},
},
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result[0]!.validateRuntime, false)
})
test('discoverRoutes discovers utility routes correctly', () => {
// Arrange
const instance = {
routes: [
{ method: 'GET', url: '/reset' },
{ method: 'POST', url: '/login' },
{ method: 'GET', url: '/callback' },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 3)
for (const contract of result) {
assert.strictEqual(contract.category, 'utility')
}
})
test('discoverRoutes discovers observer suffix routes', () => {
// Arrange
const instance = {
routes: [
{ method: 'POST', url: '/users/search' },
{ method: 'GET', url: '/items/count' },
{ method: 'POST', url: '/system/stats' },
{ method: 'GET', url: '/service/status' },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 4)
for (const contract of result) {
assert.strictEqual(contract.category, 'observer')
}
})
test('discoverRoutes handles non-array routes property', () => {
// Arrange
const instance = {
routes: 'invalid' as unknown as Array<{ method: string; url: string; schema?: Record<string, unknown> }>,
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.deepStrictEqual(result, [])
})
test('discoverRoutes handles null instance gracefully', () => {
// Arrange
const instance = null as unknown as { routes?: Array<{ method: string; url: string; schema?: Record<string, unknown> }> }
// Act & Assert
assert.throws(() => {
discoverRoutes(instance)
}, /Cannot read properties of null/)
})
test('discoverRoutes handles route with empty schema', () => {
// Arrange
const instance = {
routes: [
{ method: 'GET', url: '/empty', schema: {} },
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 1)
assert.strictEqual(result[0]!.path, '/empty')
assert.deepStrictEqual(result[0]!.requires, [])
assert.deepStrictEqual(result[0]!.ensures, [])
assert.deepStrictEqual(result[0]!.invariants, [])
})
test('discoverRoutes handles route with all x-annotations', () => {
// Arrange
const instance = {
routes: [
{
method: 'POST',
url: '/users',
schema: {
'x-category': 'constructor',
'x-requires': ['auth', 'admin'],
'x-ensures': ['created'],
'x-invariants': ['unique'],
'x-regex': { name: '^[a-z]+$' },
'x-validate-runtime': true,
},
},
],
}
// Act
const result = discoverRoutes(instance)
// Assert
assert.strictEqual(result.length, 1)
const contract = result[0]!
assert.strictEqual(contract.category, 'constructor')
assert.deepStrictEqual(contract.requires, ['auth', 'admin'])
assert.deepStrictEqual(contract.ensures, ['created'])
assert.deepStrictEqual(contract.invariants, [])
assert.deepStrictEqual(contract.regexPatterns, {})
assert.strictEqual(contract.validateRuntime, true)
})