📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
---
|
||||
name: scenarios
|
||||
description: This skill should be used when validating features end-to-end without mocks, testing integrations, or when "scenario test", "e2e test", or "no mocks" are mentioned.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Scenario Testing
|
||||
|
||||
End-to-end validation using real dependencies, no mocks ever.
|
||||
|
||||
<when_to_use>
|
||||
|
||||
- End-to-end feature validation
|
||||
- Integration testing across services
|
||||
- Proof programs demonstrating behavior
|
||||
- Real-world workflow testing
|
||||
- API contract verification
|
||||
- Authentication flow validation
|
||||
|
||||
NOT for: unit testing, mock testing, performance benchmarking, load testing
|
||||
|
||||
</when_to_use>
|
||||
|
||||
<iron_law>
|
||||
|
||||
NO MOCKS EVER.
|
||||
|
||||
Truth hierarchy:
|
||||
1. **Scenarios** — real dependencies, actual behavior
|
||||
2. **Unit tests** — isolated logic, synthetic inputs
|
||||
3. **Mocks** — assumptions about how things work
|
||||
|
||||
Mocks test your assumptions, not reality. When mocks pass but production fails, the mock lied. When scenarios fail, reality spoke.
|
||||
|
||||
Test against real databases, real APIs, real services. Use test credentials, staging environments, local instances — but always real implementations.
|
||||
|
||||
</iron_law>
|
||||
|
||||
<directory_structure>
|
||||
|
||||
## .scratch/ (gitignored)
|
||||
|
||||
Throwaway test scripts for quick validation. Self-contained, runnable, disposable.
|
||||
|
||||
CRITICAL: Verify .scratch/ in .gitignore before first use.
|
||||
|
||||
## scenarios.jsonl (committed)
|
||||
|
||||
Successful scenario patterns documented as JSONL. One scenario per line, each a complete JSON object.
|
||||
|
||||
Purpose: capture proven patterns, regression indicators, reusable test cases.
|
||||
|
||||
Structure:
|
||||
|
||||
```jsonl
|
||||
{"name":"auth-login-success","description":"User logs in with valid credentials","setup":"Create test user with known password","steps":["POST /auth/login with credentials","Receive JWT token","GET /auth/me with token"],"expected":"User profile returned with correct data","tags":["auth","jwt","happy-path"]}
|
||||
{"name":"auth-login-invalid","description":"Login fails with wrong password","setup":"Test user exists","steps":["POST /auth/login with wrong password"],"expected":"401 Unauthorized, no token issued","tags":["auth","error-handling"]}
|
||||
```
|
||||
|
||||
</directory_structure>
|
||||
|
||||
<scratch_directory>
|
||||
|
||||
## Purpose
|
||||
|
||||
Quick validation without ceremony. Write script, run against real deps, verify behavior, delete or document.
|
||||
|
||||
## Characteristics
|
||||
|
||||
- **Gitignored** — never committed, purely local
|
||||
- **Disposable** — delete after validation or promote to permanent tests
|
||||
- **Self-contained** — runnable with single command
|
||||
- **Real dependencies** — actual DB, real APIs, live services
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- `test-{feature}.ts` — feature validation (test-auth-flow.ts)
|
||||
- `debug-{issue}.ts` — investigate specific bug (debug-token-expiry.ts)
|
||||
- `prove-{behavior}.ts` — demonstrate expected behavior (prove-rate-limiting.ts)
|
||||
- `explore-{api}.ts` — learn external API behavior (explore-stripe-webhooks.ts)
|
||||
|
||||
## Example Structure
|
||||
|
||||
```typescript
|
||||
// .scratch/test-auth-flow.ts
|
||||
import { db } from '../src/db'
|
||||
import { api } from '../src/api'
|
||||
|
||||
async function testAuthFlow() {
|
||||
// Setup: real test user in real database
|
||||
const user = await db.users.create({
|
||||
email: 'test@example.com',
|
||||
password: 'hashed-test-password'
|
||||
})
|
||||
|
||||
// Execute: real HTTP requests
|
||||
const loginRes = await api.post('/auth/login', {
|
||||
email: user.email,
|
||||
password: 'test-password'
|
||||
})
|
||||
|
||||
// Verify: actual response
|
||||
console.assert(loginRes.status === 200, 'Login should succeed')
|
||||
console.assert(loginRes.body.token, 'Should receive JWT token')
|
||||
|
||||
const meRes = await api.get('/auth/me', {
|
||||
headers: { Authorization: `Bearer ${loginRes.body.token}` }
|
||||
})
|
||||
|
||||
console.assert(meRes.status === 200, 'Auth should work')
|
||||
console.assert(meRes.body.email === user.email, 'Should return correct user')
|
||||
|
||||
// Cleanup
|
||||
await db.users.delete({ id: user.id })
|
||||
|
||||
console.log('✓ Auth flow validated')
|
||||
}
|
||||
|
||||
testAuthFlow().catch(console.error)
|
||||
```
|
||||
|
||||
</scratch_directory>
|
||||
|
||||
<scenarios_jsonl>
|
||||
|
||||
## Format
|
||||
|
||||
Each line is complete JSON object with fields:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: string // unique identifier (kebab-case)
|
||||
description: string // human-readable summary
|
||||
setup: string // prerequisites and state preparation
|
||||
steps: string[] // ordered actions to execute
|
||||
expected: string // success criteria
|
||||
tags: string[] // categorization (auth, api, error, etc)
|
||||
env?: string // required environment (staging, local, prod-readonly)
|
||||
duration_ms?: number // typical execution time
|
||||
}
|
||||
```
|
||||
|
||||
## Purpose
|
||||
|
||||
- **Pattern library** — proven scenarios for regression testing
|
||||
- **Documentation** — executable specification of system behavior
|
||||
- **Regression detection** — compare new behavior against known-good patterns
|
||||
- **Test generation** — source material for permanent test suites
|
||||
|
||||
## When to Document
|
||||
|
||||
Document in scenarios.jsonl when:
|
||||
- Scenario validates critical user path
|
||||
- Bug was caught by this scenario (regression prevention)
|
||||
- Behavior is non-obvious or frequently questioned
|
||||
- Integration pattern is reusable across features
|
||||
|
||||
Delete from .scratch/ when:
|
||||
- One-time debugging script
|
||||
- Exploratory testing that didn't find issues
|
||||
- Temporary verification during development
|
||||
|
||||
</scenarios_jsonl>
|
||||
|
||||
<workflow>
|
||||
|
||||
Loop: Write → Execute → Document → Cleanup
|
||||
|
||||
1. **Write proof program** — self-contained script in .scratch/
|
||||
2. **Run against real dependencies** — actual DB, live APIs, real services
|
||||
3. **Verify behavior** — assertions on actual responses
|
||||
4. **Document if successful** — add pattern to scenarios.jsonl
|
||||
5. **Cleanup** — delete script or promote to permanent tests
|
||||
|
||||
Each iteration:
|
||||
- Script is throwaway (lives in .scratch/)
|
||||
- Dependencies are real (no mocks, no stubs)
|
||||
- Validation is concrete (actual behavior observed)
|
||||
- Pattern captured if valuable (scenarios.jsonl)
|
||||
|
||||
</workflow>
|
||||
|
||||
<gitignore_check>
|
||||
|
||||
MANDATORY before first .scratch/ use:
|
||||
|
||||
```bash
|
||||
grep -q '.scratch/' .gitignore || echo '.scratch/' >> .gitignore
|
||||
```
|
||||
|
||||
Verify .scratch/ directory will not be committed. All test scripts are local-only.
|
||||
|
||||
If .gitignore doesn't exist, create it:
|
||||
|
||||
```bash
|
||||
[ -f .gitignore ] || touch .gitignore
|
||||
grep -q '.scratch/' .gitignore || echo '.scratch/' >> .gitignore
|
||||
```
|
||||
|
||||
</gitignore_check>
|
||||
|
||||
<stages>
|
||||
|
||||
## 1. Setup → Setting up scenario environment
|
||||
|
||||
Prepare real dependencies:
|
||||
- Spin up local database (Docker, embedded)
|
||||
- Configure test API keys (staging credentials)
|
||||
- Initialize test data (real records, not fixtures)
|
||||
- Verify service connectivity
|
||||
|
||||
## 2. Script → Writing proof program
|
||||
|
||||
Create .scratch/ test script:
|
||||
- Import real dependencies (no mocks)
|
||||
- Setup stage: prepare state
|
||||
- Execute stage: perform actions
|
||||
- Verify stage: assert on results
|
||||
- Cleanup stage: restore state
|
||||
|
||||
## 3. Execute → Running against real dependencies
|
||||
|
||||
Run proof program:
|
||||
- Execute with real database connection
|
||||
- Call actual API endpoints
|
||||
- Use live service instances
|
||||
- Observe actual behavior (no simulation)
|
||||
|
||||
## 4. Document → Capturing successful patterns
|
||||
|
||||
If scenario validates behavior:
|
||||
- Extract pattern to scenarios.jsonl
|
||||
- Document setup requirements
|
||||
- Record expected outcomes
|
||||
- Tag for categorization
|
||||
|
||||
Delete .scratch/ script or promote to permanent test suite.
|
||||
|
||||
</stages>
|
||||
|
||||
<rules>
|
||||
|
||||
ALWAYS:
|
||||
- Verify .scratch/ in .gitignore before first use
|
||||
- Test against real dependencies (actual DB, live APIs)
|
||||
- Use self-contained scripts (runnable with single command)
|
||||
- Document successful scenarios in scenarios.jsonl
|
||||
- Cleanup test data after execution
|
||||
- Tag scenarios for easy filtering
|
||||
- Include cleanup stage in all scripts
|
||||
- Use test credentials (never production)
|
||||
|
||||
NEVER:
|
||||
- Use mocks, stubs, or test doubles
|
||||
- Commit .scratch/ directory contents
|
||||
- Test against production data
|
||||
- Skip cleanup stage
|
||||
- Assume behavior without verification
|
||||
- Promote assumptions to truth
|
||||
- Test mocked behavior instead of reality
|
||||
- Leave test data in shared environments
|
||||
|
||||
ESCALATE when:
|
||||
- No staging environment available
|
||||
- Real dependencies too expensive to test
|
||||
- Test requires destructive production operations
|
||||
- Cannot obtain test credentials
|
||||
|
||||
</rules>
|
||||
|
||||
<references>
|
||||
|
||||
Patterns and examples:
|
||||
- [patterns.md](references/patterns.md) — common scenario patterns and templates
|
||||
|
||||
Related skills:
|
||||
- debugging — investigation methodology (scenarios help reproduce bugs)
|
||||
- tdd — TDD workflow (scenarios validate features)
|
||||
- codebase-recon — evidence gathering (scenarios provide empirical data)
|
||||
|
||||
External resources:
|
||||
- [Growing Object-Oriented Software, Guided by Tests](http://www.growing-object-oriented-software.com/) — end-to-end testing philosophy
|
||||
- [Testing Without Mocks](https://www.jamesshore.com/v2/blog/2018/testing-without-mocks) — James Shore's pattern library
|
||||
|
||||
</references>
|
||||
@@ -0,0 +1,592 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user