playbook/outfitter-agents/plugins/outfitter/skills/tdd/examples/feature-implementation.md

687 lines
15 KiB
Markdown

# TDD Session: Feature Implementation
Complete TDD session implementing user authentication feature from scratch.
## Session Setup
**Feature**: User authentication with email/password
**Tech Stack**: TypeScript, Bun, discriminated unions for results
**Starting Point**: No existing code
**Duration**: ~45 minutes (3 RED-GREEN-REFACTOR cycles)
## Task State Tracking
Initial todos:
```text
1. Write failing test for user authentication [in_progress]
2. Implement authentication to pass tests [pending]
3. Refactor authentication code [pending]
4. Verify implementation [pending]
```
## Cycle 1: Basic Authentication
### RED Phase (5 min)
Starting confidence: `▓░░░░` → Writing tests to define interface
**Created**: `src/auth/authenticate.test.ts`
```typescript
import { describe, test, expect } from 'bun:test'
import { authenticate } from './authenticate'
describe('authenticate', () => {
const validCreds = {
email: 'user@example.com',
password: 'ValidPass123!',
} as const
test('returns success result with valid credentials', async () => {
const result = await authenticate(validCreds)
expect(result.type).toBe('success')
if (result.type === 'success') {
expect(result.user.email).toBe(validCreds.email)
}
})
test('returns error result with invalid credentials', async () => {
const result = await authenticate({
email: 'wrong@example.com',
password: 'wrong',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_CREDENTIALS')
}
})
test('returns error result with empty password', async () => {
const result = await authenticate({
email: 'user@example.com',
password: '',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('MISSING_PASSWORD')
}
})
})
```
**Run tests**: `bun test`
```
authenticate
✗ returns success result with valid credentials
Error: Cannot find module "./authenticate"
✗ returns error result with invalid credentials
✗ returns error result with empty password
3 failing
```
Tests fail as expected — no implementation exists yet.
**Commit**: `test: add failing tests for user authentication`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [in_progress]
3. Refactor authentication code [pending]
4. Verify implementation [pending]
```
### GREEN Phase (8 min)
Confidence: `▓▓░░░` → Implementing minimal solution
**Created**: `src/auth/authenticate.ts`
```typescript
type User = {
id: string
email: string
passwordHash: string
}
type AuthSuccess = {
type: 'success'
user: User
}
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD'
}
type AuthResult = AuthSuccess | AuthError
// Minimal mock database
const users: User[] = [
{
id: '1',
email: 'user@example.com',
passwordHash: '$2a$10$hashedValidPass123!',
},
]
async function findUserByEmail(email: string): Promise<User | undefined> {
return users.find(u => u.email === email)
}
async function comparePassword(password: string, hash: string): Promise<boolean> {
// Simplified for testing - in production use bcrypt
return password === 'ValidPass123!' && hash === '$2a$10$hashedValidPass123!'
}
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check password not empty
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const passwordMatch = await comparePassword(credentials.password, user.passwordHash)
if (!passwordMatch) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
return { type: 'success', user }
}
```
**Run tests**: `bun test`
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
3 passing
```
All tests pass — implementation complete.
**Commit**: `feat: implement user authentication to pass tests`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [in_progress]
4. Verify implementation [pending]
```
### REFACTOR Phase (7 min)
Confidence: `▓▓▓░░` → Improving structure and types
Observations:
- Types scattered throughout file
- Password comparison hardcoded
- No clear separation of concerns
**Refactored**: `src/auth/authenticate.ts`
```typescript
// Extract types to top
type User = {
id: string
email: string
passwordHash: string
}
type AuthSuccess = {
type: 'success'
user: User
}
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD' | 'INVALID_EMAIL'
}
type AuthResult = AuthSuccess | AuthError
// Extract validation
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
if (!credentials.email.includes('@')) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
return null
}
// Mock database (unchanged)
const users: User[] = [
{
id: '1',
email: 'user@example.com',
passwordHash: '$2a$10$hashedValidPass123!',
},
]
async function findUserByEmail(email: string): Promise<User | undefined> {
return users.find(u => u.email === email)
}
// Extract password verification
async function verifyPassword(password: string, hash: string): Promise<boolean> {
// Simplified for testing - in production use bcrypt
return password === 'ValidPass123!' && hash === '$2a$10$hashedValidPass123!'
}
// Cleaner main function
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Validate input
const validationError = validateCredentials(credentials)
if (validationError) {
return validationError
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const isValid = await verifyPassword(credentials.password, user.passwordHash)
if (!isValid) {
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
return { type: 'success', user }
}
```
**Run tests**: `bun test`
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
3 passing
```
Tests still pass — refactoring successful.
**Commit**: `refactor: extract validation and password verification`
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [completed]
4. Verify implementation [in_progress]
```
### VERIFY Phase (3 min)
Run full test suite with coverage:
```bash
bun test --coverage
```
```
authenticate
✓ returns success result with valid credentials
✓ returns error result with invalid credentials
✓ returns error result with empty password
Coverage:
File | % Stmts | % Branch | % Funcs | % Lines
------------------|---------|----------|---------|--------
authenticate.ts | 95.45 | 100 | 100 | 95.45
3 passing
```
Coverage ≥80% — quality standards met.
**Task Update**:
```text
1. Write failing test for user authentication [completed]
2. Implement authentication to pass tests [completed]
3. Refactor authentication code [completed]
4. Verify implementation [completed]
```
## Cycle 2: Email Validation
Starting new cycle for email validation edge cases.
**Task Update**:
```text
1. Write failing test for email validation [in_progress]
2. Implement email validation to pass tests [pending]
3. Refactor email validation [pending]
4. Verify implementation [pending]
```
### RED Phase (4 min)
Add tests for email validation edge cases:
```typescript
describe('authenticate - email validation', () => {
test('returns error for invalid email format', async () => {
const result = await authenticate({
email: 'not-an-email',
password: 'ValidPass123!',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_EMAIL')
}
})
test('returns error for empty email', async () => {
const result = await authenticate({
email: '',
password: 'ValidPass123!',
})
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('INVALID_EMAIL')
}
})
})
```
**Run tests**: `bun test`
```
authenticate - email validation
✓ returns error for invalid email format # Already passes!
✗ returns error for empty email
Expected code: 'INVALID_EMAIL'
Received code: 'MISSING_PASSWORD'
1 failing
```
One test passes (basic email check exists), one fails.
**Commit**: `test: add email validation edge case tests`
### GREEN Phase (3 min)
Update validation to handle empty email:
```typescript
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!credentials.email) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
if (!credentials.email.includes('@')) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
return null
}
```
**Run tests**: `bun test`
```
authenticate - email validation
✓ returns error for invalid email format
✓ returns error for empty email
All tests passing (5 total)
```
**Commit**: `feat: validate empty email addresses`
### REFACTOR Phase (4 min)
Extract email validation to dedicated function:
```typescript
function isValidEmail(email: string): boolean {
return email.length > 0 && email.includes('@')
}
function validateCredentials(credentials: {
email: string
password: string
}): AuthError | null {
if (!isValidEmail(credentials.email)) {
return { type: 'error', code: 'INVALID_EMAIL' }
}
if (!credentials.password) {
return { type: 'error', code: 'MISSING_PASSWORD' }
}
return null
}
```
**Run tests**: `bun test` — All passing
**Commit**: `refactor: extract email validation function`
### VERIFY Phase (2 min)
```bash
bun test --coverage
```
Coverage: 96.2% — excellent.
## Cycle 3: Rate Limiting
Implementing rate limiting for failed authentication attempts.
### RED Phase (6 min)
Add tests for rate limiting:
```typescript
describe('authenticate - rate limiting', () => {
test('allows authentication after successful login', async () => {
const validCreds = {
email: 'user@example.com',
password: 'ValidPass123!',
}
const result1 = await authenticate(validCreds)
const result2 = await authenticate(validCreds)
expect(result1.type).toBe('success')
expect(result2.type).toBe('success')
})
test('blocks authentication after 3 failed attempts', async () => {
const invalidCreds = {
email: 'user@example.com',
password: 'wrong',
}
// 3 failed attempts
await authenticate(invalidCreds)
await authenticate(invalidCreds)
await authenticate(invalidCreds)
// 4th attempt should be rate limited
const result = await authenticate(invalidCreds)
expect(result.type).toBe('error')
if (result.type === 'error') {
expect(result.code).toBe('RATE_LIMITED')
}
})
})
```
**Run tests**: `bun test` — Rate limit tests fail as expected
**Commit**: `test: add rate limiting tests`
### GREEN Phase (10 min)
Implement basic rate limiting:
```typescript
type AuthError = {
type: 'error'
code: 'INVALID_CREDENTIALS' | 'MISSING_PASSWORD' | 'INVALID_EMAIL' | 'RATE_LIMITED'
}
// Track failed attempts
const failedAttempts = new Map<string, number>()
function incrementFailedAttempts(email: string): void {
const current = failedAttempts.get(email) || 0
failedAttempts.set(email, current + 1)
}
function resetFailedAttempts(email: string): void {
failedAttempts.delete(email)
}
function isRateLimited(email: string): boolean {
const attempts = failedAttempts.get(email) || 0
return attempts >= 3
}
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check rate limiting first
if (isRateLimited(credentials.email)) {
return { type: 'error', code: 'RATE_LIMITED' }
}
// Validate input
const validationError = validateCredentials(credentials)
if (validationError) {
return validationError
}
// Find user
const user = await findUserByEmail(credentials.email)
if (!user) {
incrementFailedAttempts(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Verify password
const isValid = await verifyPassword(credentials.password, user.passwordHash)
if (!isValid) {
incrementFailedAttempts(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// Reset on success
resetFailedAttempts(credentials.email)
return { type: 'success', user }
}
```
**Run tests**: `bun test` — All 7 tests passing
**Commit**: `feat: implement rate limiting for failed authentication`
### REFACTOR Phase (6 min)
Extract rate limiting to separate module for testability:
**Created**: `src/auth/rate-limiter.ts`
```typescript
export class RateLimiter {
private attempts = new Map<string, number>()
constructor(private maxAttempts: number = 3) {}
increment(key: string): void {
const current = this.attempts.get(key) || 0
this.attempts.set(key, current + 1)
}
reset(key: string): void {
this.attempts.delete(key)
}
isLimited(key: string): boolean {
const attempts = this.attempts.get(key) || 0
return attempts >= this.maxAttempts
}
}
```
Update `authenticate.ts` to use class:
```typescript
import { RateLimiter } from './rate-limiter'
const rateLimiter = new RateLimiter(3)
export async function authenticate(credentials: {
email: string
password: string
}): Promise<AuthResult> {
// Check rate limiting first
if (rateLimiter.isLimited(credentials.email)) {
return { type: 'error', code: 'RATE_LIMITED' }
}
// ... rest unchanged ...
// On failure
if (!isValid) {
rateLimiter.increment(credentials.email)
return { type: 'error', code: 'INVALID_CREDENTIALS' }
}
// On success
rateLimiter.reset(credentials.email)
return { type: 'success', user }
}
```
**Run tests**: `bun test` — All passing
**Commit**: `refactor: extract rate limiter to separate class`
### VERIFY Phase (5 min)
Final verification with mutation testing:
```bash
bun test --coverage
bun x stryker run
```
Results:
- Coverage: 94.8%
- Mutation score: 78.3%
- All tests passing
**Task**: All completed
## Session Summary
Duration: 45 minutes
Cycles: 3 complete RED-GREEN-REFACTOR cycles
Tests: 7 tests, all passing
Coverage: 94.8% line coverage
Mutation: 78.3% mutation score
Features implemented:
1. Basic authentication with email/password
2. Email validation
3. Rate limiting for failed attempts
Code quality:
- All types explicit
- Functions single-purpose
- Tests cover happy path and edge cases
- Mutation testing verifies test quality
Next steps:
- Add integration tests with real database
- Implement actual bcrypt password hashing
- Add time-based rate limit expiration