playbook/outfitter-agents/plugins/outfitter/skills/scenarios/references/patterns.md

18 KiB

Scenario Testing Patterns

Common end-to-end scenario patterns with real dependencies.

Authentication Flows

Login Success

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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

// .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:

{"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:

// .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:

{"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