593 lines
18 KiB
Markdown
593 lines
18 KiB
Markdown
# Scenario Testing Patterns
|
|
|
|
Common end-to-end scenario patterns with real dependencies.
|
|
|
|
## Authentication Flows
|
|
|
|
### Login Success
|
|
|
|
```typescript
|
|
// .scratch/test-auth-login-success.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
import { hash } from '../src/crypto'
|
|
|
|
async function testLoginSuccess() {
|
|
// Setup: create real test user
|
|
const password = 'test-password-123'
|
|
const user = await db.users.create({
|
|
email: 'test@example.com',
|
|
password: await hash(password)
|
|
})
|
|
|
|
try {
|
|
// Execute: real login request
|
|
const res = await api.post('/auth/login', {
|
|
email: user.email,
|
|
password
|
|
})
|
|
|
|
// Verify: actual response
|
|
console.assert(res.status === 200, 'Login should return 200')
|
|
console.assert(res.body.token, 'Should receive JWT token')
|
|
console.assert(res.body.user.id === user.id, 'Should return user data')
|
|
|
|
console.log('✓ Login success validated')
|
|
} finally {
|
|
// Cleanup: remove test user
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testLoginSuccess().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"auth-login-success","description":"User logs in with valid credentials","setup":"Create test user in database with hashed password","steps":["POST /auth/login with email and password","Receive 200 response","Extract JWT token from response","Verify user data in response"],"expected":"200 OK with JWT token and user object","tags":["auth","jwt","happy-path"],"duration_ms":150}
|
|
```
|
|
|
|
### Login Failure
|
|
|
|
```typescript
|
|
// .scratch/test-auth-login-failure.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
import { hash } from '../src/crypto'
|
|
|
|
async function testLoginFailure() {
|
|
const user = await db.users.create({
|
|
email: 'test@example.com',
|
|
password: await hash('correct-password')
|
|
})
|
|
|
|
try {
|
|
// Execute: login with wrong password
|
|
const res = await api.post('/auth/login', {
|
|
email: user.email,
|
|
password: 'wrong-password'
|
|
})
|
|
|
|
// Verify: rejection
|
|
console.assert(res.status === 401, 'Should return 401 Unauthorized')
|
|
console.assert(!res.body.token, 'Should not issue token')
|
|
console.assert(res.body.error, 'Should include error message')
|
|
|
|
console.log('✓ Login failure validated')
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testLoginFailure().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"auth-login-invalid","description":"Login fails with incorrect password","setup":"Create test user with known password","steps":["POST /auth/login with wrong password"],"expected":"401 Unauthorized, no token issued, error message present","tags":["auth","error-handling","security"],"duration_ms":100}
|
|
```
|
|
|
|
### Token Validation
|
|
|
|
```typescript
|
|
// .scratch/test-auth-token-validation.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
import { hash } from '../src/crypto'
|
|
|
|
async function testTokenValidation() {
|
|
const user = await db.users.create({
|
|
email: 'test@example.com',
|
|
password: await hash('password')
|
|
})
|
|
|
|
try {
|
|
// Get real token
|
|
const loginRes = await api.post('/auth/login', {
|
|
email: user.email,
|
|
password: 'password'
|
|
})
|
|
const token = loginRes.body.token
|
|
|
|
// Verify: valid token grants access
|
|
const validRes = await api.get('/auth/me', {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
console.assert(validRes.status === 200, 'Valid token should grant access')
|
|
console.assert(validRes.body.id === user.id, 'Should return correct user')
|
|
|
|
// Verify: invalid token denied
|
|
const invalidRes = await api.get('/auth/me', {
|
|
headers: { Authorization: 'Bearer invalid-token' }
|
|
})
|
|
console.assert(invalidRes.status === 401, 'Invalid token should be rejected')
|
|
|
|
// Verify: missing token denied
|
|
const missingRes = await api.get('/auth/me')
|
|
console.assert(missingRes.status === 401, 'Missing token should be rejected')
|
|
|
|
console.log('✓ Token validation scenarios passed')
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testTokenValidation().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"auth-token-validation","description":"JWT token validation for protected endpoints","setup":"Create user and obtain valid JWT token","steps":["GET /auth/me with valid token","GET /auth/me with invalid token","GET /auth/me without token"],"expected":"Valid token: 200 + user data. Invalid: 401. Missing: 401.","tags":["auth","jwt","authorization"],"duration_ms":200}
|
|
```
|
|
|
|
## CRUD Operations
|
|
|
|
### Create Resource
|
|
|
|
```typescript
|
|
// .scratch/test-crud-create.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
|
|
async function testCreateResource() {
|
|
// Setup: authenticate
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const token = await api.login(user)
|
|
|
|
try {
|
|
// Execute: create resource
|
|
const res = await api.post('/api/posts', {
|
|
title: 'Test Post',
|
|
content: 'Test content'
|
|
}, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
// Verify: resource created
|
|
console.assert(res.status === 201, 'Should return 201 Created')
|
|
console.assert(res.body.id, 'Should return resource ID')
|
|
console.assert(res.body.title === 'Test Post', 'Should store title')
|
|
|
|
// Verify: resource in database
|
|
const dbPost = await db.posts.findOne({ id: res.body.id })
|
|
console.assert(dbPost, 'Should exist in database')
|
|
console.assert(dbPost.author_id === user.id, 'Should link to author')
|
|
|
|
console.log('✓ Create resource validated')
|
|
|
|
// Cleanup
|
|
await db.posts.delete({ id: res.body.id })
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testCreateResource().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"crud-create-success","description":"Create new resource via API","setup":"Authenticated user","steps":["POST /api/posts with resource data","Receive 201 Created","Verify resource in database"],"expected":"201 Created with resource ID, resource persisted in database","tags":["crud","create","api"],"duration_ms":120}
|
|
```
|
|
|
|
### Read Resource
|
|
|
|
```typescript
|
|
// .scratch/test-crud-read.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
|
|
async function testReadResource() {
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const post = await db.posts.create({
|
|
title: 'Test Post',
|
|
content: 'Test content',
|
|
author_id: user.id
|
|
})
|
|
|
|
try {
|
|
// Execute: read resource
|
|
const res = await api.get(`/api/posts/${post.id}`)
|
|
|
|
// Verify: correct data returned
|
|
console.assert(res.status === 200, 'Should return 200 OK')
|
|
console.assert(res.body.id === post.id, 'Should return correct post')
|
|
console.assert(res.body.title === post.title, 'Should include title')
|
|
console.assert(res.body.content === post.content, 'Should include content')
|
|
console.assert(res.body.author.id === user.id, 'Should include author')
|
|
|
|
console.log('✓ Read resource validated')
|
|
} finally {
|
|
await db.posts.delete({ id: post.id })
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testReadResource().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"crud-read-success","description":"Retrieve existing resource","setup":"Resource exists in database","steps":["GET /api/posts/{id}"],"expected":"200 OK with complete resource data including relations","tags":["crud","read","api"],"duration_ms":80}
|
|
```
|
|
|
|
### Update Resource
|
|
|
|
```typescript
|
|
// .scratch/test-crud-update.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
|
|
async function testUpdateResource() {
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const token = await api.login(user)
|
|
const post = await db.posts.create({
|
|
title: 'Original Title',
|
|
content: 'Original content',
|
|
author_id: user.id
|
|
})
|
|
|
|
try {
|
|
// Execute: update resource
|
|
const res = await api.put(`/api/posts/${post.id}`, {
|
|
title: 'Updated Title',
|
|
content: 'Updated content'
|
|
}, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
// Verify: update successful
|
|
console.assert(res.status === 200, 'Should return 200 OK')
|
|
console.assert(res.body.title === 'Updated Title', 'Should update title')
|
|
console.assert(res.body.content === 'Updated content', 'Should update content')
|
|
|
|
// Verify: database updated
|
|
const dbPost = await db.posts.findOne({ id: post.id })
|
|
console.assert(dbPost.title === 'Updated Title', 'Should persist title')
|
|
console.assert(dbPost.content === 'Updated content', 'Should persist content')
|
|
|
|
console.log('✓ Update resource validated')
|
|
} finally {
|
|
await db.posts.delete({ id: post.id })
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testUpdateResource().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"crud-update-success","description":"Update existing resource","setup":"Resource owned by authenticated user","steps":["PUT /api/posts/{id} with updated fields","Verify response data","Verify database persistence"],"expected":"200 OK with updated data, changes persisted in database","tags":["crud","update","api"],"duration_ms":130}
|
|
```
|
|
|
|
### Delete Resource
|
|
|
|
```typescript
|
|
// .scratch/test-crud-delete.ts
|
|
import { db } from '../src/db'
|
|
import { api } from '../src/api'
|
|
|
|
async function testDeleteResource() {
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const token = await api.login(user)
|
|
const post = await db.posts.create({
|
|
title: 'Test Post',
|
|
content: 'Test content',
|
|
author_id: user.id
|
|
})
|
|
|
|
try {
|
|
// Execute: delete resource
|
|
const res = await api.delete(`/api/posts/${post.id}`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
// Verify: deletion successful
|
|
console.assert(res.status === 204, 'Should return 204 No Content')
|
|
|
|
// Verify: removed from database
|
|
const dbPost = await db.posts.findOne({ id: post.id })
|
|
console.assert(!dbPost, 'Should be removed from database')
|
|
|
|
// Verify: subsequent reads fail
|
|
const readRes = await api.get(`/api/posts/${post.id}`)
|
|
console.assert(readRes.status === 404, 'Should return 404 Not Found')
|
|
|
|
console.log('✓ Delete resource validated')
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testDeleteResource().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"crud-delete-success","description":"Delete owned resource","setup":"Resource owned by authenticated user","steps":["DELETE /api/posts/{id}","Verify 204 response","Verify removal from database","Verify 404 on subsequent read"],"expected":"204 No Content, resource removed, subsequent reads return 404","tags":["crud","delete","api"],"duration_ms":140}
|
|
```
|
|
|
|
## API Integration Patterns
|
|
|
|
### Third-Party API Call
|
|
|
|
```typescript
|
|
// .scratch/test-stripe-create-customer.ts
|
|
import { stripe } from '../src/integrations/stripe'
|
|
import { db } from '../src/db'
|
|
|
|
async function testStripeCustomerCreation() {
|
|
// Setup: test user
|
|
const user = await db.users.create({
|
|
email: 'test@example.com',
|
|
name: 'Test User'
|
|
})
|
|
|
|
try {
|
|
// Execute: real Stripe API call (test mode)
|
|
const customer = await stripe.customers.create({
|
|
email: user.email,
|
|
name: user.name,
|
|
metadata: { user_id: user.id }
|
|
})
|
|
|
|
// Verify: customer created
|
|
console.assert(customer.id, 'Should receive Stripe customer ID')
|
|
console.assert(customer.email === user.email, 'Should store email')
|
|
console.assert(customer.metadata.user_id === user.id, 'Should store metadata')
|
|
|
|
// Verify: stored in database
|
|
await db.users.update({ id: user.id }, {
|
|
stripe_customer_id: customer.id
|
|
})
|
|
const dbUser = await db.users.findOne({ id: user.id })
|
|
console.assert(dbUser.stripe_customer_id === customer.id, 'Should link customer')
|
|
|
|
console.log('✓ Stripe customer creation validated')
|
|
|
|
// Cleanup: delete Stripe customer
|
|
await stripe.customers.del(customer.id)
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testStripeCustomerCreation().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"stripe-customer-create","description":"Create Stripe customer for new user","setup":"Test user in database, Stripe test mode API keys","steps":["Call stripe.customers.create()","Store customer ID in database","Verify linkage"],"expected":"Customer created in Stripe, ID stored in database, metadata linked","tags":["integration","stripe","api"],"env":"test","duration_ms":450}
|
|
```
|
|
|
|
### Webhook Processing
|
|
|
|
```typescript
|
|
// .scratch/test-stripe-webhook.ts
|
|
import { api } from '../src/api'
|
|
import { stripe } from '../src/integrations/stripe'
|
|
import { db } from '../src/db'
|
|
|
|
async function testStripeWebhook() {
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const customer = await stripe.customers.create({ email: user.email })
|
|
|
|
try {
|
|
// Execute: simulate webhook (real Stripe event)
|
|
const event = await stripe.events.create({
|
|
type: 'customer.subscription.created',
|
|
data: {
|
|
object: {
|
|
customer: customer.id,
|
|
status: 'active',
|
|
items: {
|
|
data: [{
|
|
price: { id: 'price_test_123' }
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Send to webhook endpoint
|
|
const res = await api.post('/webhooks/stripe', event, {
|
|
headers: {
|
|
'stripe-signature': generateSignature(event)
|
|
}
|
|
})
|
|
|
|
// Verify: webhook processed
|
|
console.assert(res.status === 200, 'Webhook should be accepted')
|
|
|
|
// Verify: database updated
|
|
const dbUser = await db.users.findOne({ id: user.id })
|
|
console.assert(dbUser.subscription_status === 'active', 'Should update status')
|
|
|
|
console.log('✓ Stripe webhook validated')
|
|
} finally {
|
|
await stripe.customers.del(customer.id)
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testStripeWebhook().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"stripe-webhook-subscription-created","description":"Process subscription created webhook","setup":"Stripe customer exists, webhook endpoint configured","steps":["Create subscription.created event","POST to /webhooks/stripe","Verify signature","Process event","Update database"],"expected":"200 OK response, user subscription status updated","tags":["integration","stripe","webhook"],"env":"test","duration_ms":600}
|
|
```
|
|
|
|
## Rate Limiting
|
|
|
|
```typescript
|
|
// .scratch/test-rate-limiting.ts
|
|
import { api } from '../src/api'
|
|
|
|
async function testRateLimiting() {
|
|
const ip = '192.168.1.100'
|
|
|
|
// Execute: burst of requests
|
|
const responses = await Promise.all(
|
|
Array.from({ length: 15 }, (_, i) =>
|
|
api.get('/api/public/status', {
|
|
headers: { 'X-Forwarded-For': ip }
|
|
}).then(res => ({ attempt: i + 1, status: res.status }))
|
|
)
|
|
)
|
|
|
|
// Verify: first N requests succeed
|
|
const successful = responses.filter(r => r.status === 200)
|
|
const rateLimited = responses.filter(r => r.status === 429)
|
|
|
|
console.assert(successful.length === 10, 'Should allow 10 requests')
|
|
console.assert(rateLimited.length === 5, 'Should rate-limit remaining')
|
|
console.assert(rateLimited[0].attempt === 11, 'Should start limiting at 11th')
|
|
|
|
console.log('✓ Rate limiting validated')
|
|
console.log(` Successful: ${successful.length}, Rate-limited: ${rateLimited.length}`)
|
|
}
|
|
|
|
testRateLimiting().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"rate-limit-ip-burst","description":"IP-based rate limiting under burst load","setup":"Clean rate limit state","steps":["Send 15 requests from same IP","Track response codes"],"expected":"First 10 requests: 200 OK. Remaining 5: 429 Too Many Requests","tags":["rate-limiting","security","api"],"duration_ms":250}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Validation Errors
|
|
|
|
```typescript
|
|
// .scratch/test-validation-errors.ts
|
|
import { api } from '../src/api'
|
|
import { db } from '../src/db'
|
|
|
|
async function testValidationErrors() {
|
|
const user = await db.users.create({ email: 'test@example.com' })
|
|
const token = await api.login(user)
|
|
|
|
try {
|
|
// Execute: invalid input
|
|
const res = await api.post('/api/posts', {
|
|
title: '', // empty - should fail validation
|
|
content: 'x'.repeat(10001) // too long - should fail validation
|
|
}, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
// Verify: validation error
|
|
console.assert(res.status === 400, 'Should return 400 Bad Request')
|
|
console.assert(res.body.errors, 'Should include errors array')
|
|
console.assert(
|
|
res.body.errors.some(e => e.field === 'title'),
|
|
'Should flag title error'
|
|
)
|
|
console.assert(
|
|
res.body.errors.some(e => e.field === 'content'),
|
|
'Should flag content error'
|
|
)
|
|
|
|
// Verify: no resource created
|
|
const posts = await db.posts.findMany({ author_id: user.id })
|
|
console.assert(posts.length === 0, 'Should not create invalid resource')
|
|
|
|
console.log('✓ Validation errors handled correctly')
|
|
} finally {
|
|
await db.users.delete({ id: user.id })
|
|
}
|
|
}
|
|
|
|
testValidationErrors().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl entry:
|
|
|
|
```jsonl
|
|
{"name":"validation-multiple-errors","description":"Multiple validation errors returned","setup":"Authenticated user","steps":["POST /api/posts with multiple invalid fields"],"expected":"400 Bad Request with errors array listing all validation failures, no resource created","tags":["validation","error-handling","api"],"duration_ms":90}
|
|
```
|
|
|
|
## Template Structure
|
|
|
|
Generic scenario template:
|
|
|
|
```typescript
|
|
// .scratch/test-{feature}-{scenario}.ts
|
|
import { /* real dependencies */ } from '../src'
|
|
|
|
async function test{FeatureScenario}() {
|
|
// Setup: prepare real state
|
|
const resource = await db.create({ /* test data */ })
|
|
|
|
try {
|
|
// Execute: perform real action
|
|
const result = await /* real operation */
|
|
|
|
// Verify: assert on actual behavior
|
|
console.assert(/* condition */, 'failure message')
|
|
|
|
console.log('✓ {Scenario} validated')
|
|
} finally {
|
|
// Cleanup: restore state
|
|
await db.delete({ id: resource.id })
|
|
}
|
|
}
|
|
|
|
test{FeatureScenario}().catch(console.error)
|
|
```
|
|
|
|
scenarios.jsonl template:
|
|
|
|
```jsonl
|
|
{"name":"feature-scenario","description":"Human-readable summary","setup":"Prerequisites and state","steps":["Action 1","Action 2","Action 3"],"expected":"Success criteria","tags":["category","subcategory"],"env":"test","duration_ms":100}
|
|
```
|
|
|
|
## Common Tags
|
|
|
|
- `auth` — authentication flows
|
|
- `authorization` — permission checks
|
|
- `crud` — create, read, update, delete
|
|
- `api` — HTTP API endpoints
|
|
- `integration` — third-party services
|
|
- `webhook` — webhook processing
|
|
- `validation` — input validation
|
|
- `error-handling` — error scenarios
|
|
- `security` — security-sensitive flows
|
|
- `rate-limiting` — rate limit enforcement
|
|
- `happy-path` — successful flows
|
|
- `edge-case` — boundary conditions
|
|
- `regression` — bug prevention
|