Initial public release of Apophis — invariant-driven automated API testing

This commit is contained in:
John Dvorak
2026-03-10 00:00:00 -07:00
parent d278c4b105
commit 3ac1daf7e9
82 changed files with 3902 additions and 1098 deletions
+38 -34
View File
@@ -2,19 +2,20 @@
Inject controlled failures into contract tests to validate resilience guarantees.
Chaos testing applies the invariant-driven verification approach from [Invariant-Driven Automated Testing](https://arxiv.org/abs/2602.23922) (Malhado Ribeiro, 2021) under adverse conditions: if a contract must hold, it should still hold when dependencies fail, responses are delayed, or payloads are corrupted.
## Usage
```typescript
```javascript
const result = await fastify.apophis.contract({
depth: 'standard',
runs: 50,
chaos: {
probability: 0.1, // 10% of requests get chaos
delay: { probability: 1, minMs: 100, maxMs: 500 },
error: { probability: 1, statusCode: 503 },
dropout: { probability: 1 },
corruption: { probability: 1 },
delay: { probability: 0.1, minMs: 100, maxMs: 500 },
error: { probability: 0.1, statusCode: 503 },
dropout: { probability: 0.05 },
corruption: { probability: 0.1 },
},
})
});
```
## Event Types
@@ -24,16 +25,18 @@ const result = await fastify.apophis.contract({
Adds artificial latency. Tests timeout contracts:
```apostl
timeout_occurred(this) == false
response_time(this) < 1000
```
**Note**: Delay events are generated by the chaos arbitrary but the inbound delay handler is currently a no-op. Use this for timeout contract documentation; actual delay injection requires the outbound delay strategy or a custom handler.
### Error
Forces HTTP status codes. Tests error-handling contracts:
```apostl
if status:503 then response_body(this).retry_after != null
// Behavioral: when the service is unavailable, the client receives a valid retry signal
if status:503 then response_headers(this).retry-after > 0
```
### Dropout
@@ -41,7 +44,8 @@ if status:503 then response_body(this).retry_after != null
Simulates network failure (status 0). Tests fallback contracts:
```apostl
status:200 || status:0
// Behavioral: partial failure must still return previously cached data
if status:0 then response_body(this).cached_data == previous(response_body(GET /cache/{request_params(this).key}))
```
### Corruption
@@ -49,38 +53,39 @@ status:200 || status:0
Mutates response bodies. Tests parsing robustness:
```apostl
response_body(this).id != null
// Behavioral: corrupted requests maintain traceability for debugging
if status:400 then response_body(this).request_id == request_headers(this).x-request-id
```
## Content-Type Aware Corruption
## Corruption Strategies
Built-in strategies for common formats:
Built-in strategies are content-type agnostic:
| Content-Type | Strategy | Effect |
|-------------|----------|--------|
| `application/json` | Truncate or null field | Removes fields or sets random field to null |
| `application/x-ndjson` | Chunk corrupt | Corrupts one NDJSON chunk |
| `text/event-stream` | Event corrupt | Adds malformed SSE line |
| `multipart/form-data` | Field corrupt | Replaces field with corrupted data |
| `text/plain` | Truncate | Cuts string in half |
| Strategy | Effect |
|----------|--------|
| `truncate` | Cuts response body short |
| `malformed` | Invalidates structural boundaries (e.g., unclosed JSON, bad headers) |
| `field-corrupt` | Replaces a random field value with corrupted data |
Extension strategies can add content-type-specific behavior if needed.
## Custom Corruption via Extensions
```typescript
```javascript
const myExtension = {
name: 'custom-corrupt',
corruptionStrategies: {
'application/vnd.api+json': (data) => ({
...data as object,
...data,
corrupted: true,
}),
'text/*': (data) => `CORRUPTED:${String(data)}`,
},
}
};
await fastify.register(apophis, {
extensions: [myExtension],
})
});
```
Extension strategies take precedence over built-ins. Wildcard patterns (`text/*`) match any subtype.
@@ -90,7 +95,7 @@ Extension strategies take precedence over built-ins. Wildcard patterns (`text/*`
Low-level contract chaos APIs require `NODE_ENV=test`. For CLI qualification, environment policy controls whether chaos gates may run.
```
Error: Chaos mode is only available in test environment.
Error: chaos is only available in test environment. Set NODE_ENV=test to enable quality features.
```
## Interpreting Results
@@ -100,7 +105,7 @@ Failed tests include chaos events in diagnostics:
```json
{
"statusCode": 503,
"error": "Contract violation: status:200",
"error": "Contract violation: if status:503 then response_headers(this).retry-after > 0",
"chaosEvents": [
{
"type": "error",
@@ -118,26 +123,25 @@ Failed tests include chaos events in diagnostics:
1. **Start small**: `probability: 0.05` (5% of requests)
2. **Test one failure mode at a time**: Comment out other chaos types
3. **Verify contracts handle chaos**: `if status:503 then response_body(this).error != null`
3. **Verify contracts handle chaos**: `if status:503 then response_code(GET /health) == 200`
4. **Use seeds for reproducibility**: `seed: 42` makes chaos deterministic
## Example: Testing Retry Logic
```typescript
```javascript
fastify.get('/data', {
schema: {
'x-ensures': [
'if status:503 then response_headers(this).retry-after != null',
'if status:503 then response_headers(this).retry-after > 0',
'redirect_count(this) <= 3',
],
},
}, handler)
}, handler);
// Test
const result = await fastify.apophis.contract({
chaos: {
probability: 0.2,
error: { probability: 1, statusCode: 503 },
error: { probability: 0.2, statusCode: 503 },
},
})
});
```