📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
# TDD Session: Bug Fix
|
||||
|
||||
TDD workflow for fixing a production bug with test reproduction first.
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Issue**: Division by zero crashes calculator service
|
||||
**Reporter**: User reported 500 error when calculating percentage with zero total
|
||||
**Stack Trace**:
|
||||
|
||||
```
|
||||
Error: Division by zero
|
||||
at calculatePercentage (calculator.ts:45)
|
||||
at handler (api/stats.ts:23)
|
||||
```
|
||||
|
||||
**Priority**: High — causes service crash
|
||||
|
||||
## TDD Workflow
|
||||
|
||||
### Task Setup
|
||||
|
||||
```text
|
||||
1. Write failing test reproducing bug [in_progress]
|
||||
2. Fix bug with minimal code [pending]
|
||||
3. Refactor if needed [pending]
|
||||
4. Verify fix [pending]
|
||||
```
|
||||
|
||||
## RED Phase: Reproduce Bug (4 min)
|
||||
|
||||
Confidence: `▓▓░░░` → Can reproduce, know the fix
|
||||
|
||||
First, understand the current implementation:
|
||||
|
||||
**Read**: `src/calculator/percentage.ts`
|
||||
|
||||
```typescript
|
||||
export function calculatePercentage(value: number, total: number): number {
|
||||
return (value / total) * 100
|
||||
}
|
||||
```
|
||||
|
||||
Issue clear: No validation for zero total.
|
||||
|
||||
**Write failing test**: `src/calculator/percentage.test.ts`
|
||||
|
||||
```typescript
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
import { calculatePercentage } from './percentage'
|
||||
|
||||
describe('calculatePercentage', () => {
|
||||
// Existing tests
|
||||
test('calculates percentage correctly', () => {
|
||||
expect(calculatePercentage(25, 100)).toBe(25)
|
||||
expect(calculatePercentage(50, 200)).toBe(25)
|
||||
})
|
||||
|
||||
// NEW: Bug reproduction test
|
||||
test('handles division by zero gracefully', () => {
|
||||
const result = calculatePercentage(10, 0)
|
||||
|
||||
expect(result).toBe(0)
|
||||
// OR throw error approach:
|
||||
// expect(() => calculatePercentage(10, 0)).toThrow('Total cannot be zero')
|
||||
})
|
||||
|
||||
test('handles zero value with valid total', () => {
|
||||
expect(calculatePercentage(0, 100)).toBe(0)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run test**: `bun test percentage.test.ts`
|
||||
|
||||
```
|
||||
calculatePercentage
|
||||
✓ calculates percentage correctly
|
||||
✗ handles division by zero gracefully
|
||||
Expected: 0
|
||||
Received: Infinity
|
||||
✓ handles zero value with valid total
|
||||
|
||||
1 failing
|
||||
```
|
||||
|
||||
Bug reproduced successfully — test fails as expected.
|
||||
|
||||
**Commit**: `test: add failing test for division by zero bug`
|
||||
|
||||
**Task Update**:
|
||||
|
||||
```text
|
||||
1. Write failing test reproducing bug [completed]
|
||||
2. Fix bug with minimal code [in_progress]
|
||||
3. Refactor if needed [pending]
|
||||
4. Verify fix [pending]
|
||||
```
|
||||
|
||||
## GREEN Phase: Fix Bug (3 min)
|
||||
|
||||
Confidence: `▓▓▓▓░` → Know exact fix needed
|
||||
|
||||
**Update**: `src/calculator/percentage.ts`
|
||||
|
||||
```typescript
|
||||
export function calculatePercentage(value: number, total: number): number {
|
||||
// Fix: Handle division by zero
|
||||
if (total === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (value / total) * 100
|
||||
}
|
||||
```
|
||||
|
||||
**Run tests**: `bun test percentage.test.ts`
|
||||
|
||||
```
|
||||
calculatePercentage
|
||||
✓ calculates percentage correctly
|
||||
✓ handles division by zero gracefully
|
||||
✓ handles zero value with valid total
|
||||
|
||||
3 passing
|
||||
```
|
||||
|
||||
All tests pass — bug fixed.
|
||||
|
||||
**Verify no regressions**: `bun test`
|
||||
|
||||
```
|
||||
All test suites passing (23 tests total)
|
||||
```
|
||||
|
||||
No other tests broken — safe fix.
|
||||
|
||||
**Commit**: `fix: handle division by zero in percentage calculation`
|
||||
|
||||
**Task Update**:
|
||||
|
||||
```text
|
||||
1. Write failing test reproducing bug [completed]
|
||||
2. Fix bug with minimal code [completed]
|
||||
3. Refactor if needed [in_progress]
|
||||
4. Verify fix [pending]
|
||||
```
|
||||
|
||||
## REFACTOR Phase: Consider Improvements (5 min)
|
||||
|
||||
Current fix works but could be more robust. Consider:
|
||||
|
||||
1. Should negative totals be handled?
|
||||
2. Should we return error instead of 0?
|
||||
3. Type safety improvements?
|
||||
|
||||
**Decision**: Add input validation, use Result type for better error handling.
|
||||
|
||||
**Refactored**: `src/calculator/percentage.ts`
|
||||
|
||||
```typescript
|
||||
type PercentageResult =
|
||||
| { type: 'success'; value: number }
|
||||
| { type: 'error'; code: 'ZERO_TOTAL' | 'NEGATIVE_TOTAL' }
|
||||
|
||||
export function calculatePercentage(
|
||||
value: number,
|
||||
total: number
|
||||
): PercentageResult {
|
||||
// Validate total
|
||||
if (total === 0) {
|
||||
return { type: 'error', code: 'ZERO_TOTAL' }
|
||||
}
|
||||
|
||||
if (total < 0) {
|
||||
return { type: 'error', code: 'NEGATIVE_TOTAL' }
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'success',
|
||||
value: (value / total) * 100,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Update tests** to match new signature:
|
||||
|
||||
```typescript
|
||||
describe('calculatePercentage', () => {
|
||||
test('calculates percentage correctly', () => {
|
||||
const result1 = calculatePercentage(25, 100)
|
||||
const result2 = calculatePercentage(50, 200)
|
||||
|
||||
expect(result1).toEqual({ type: 'success', value: 25 })
|
||||
expect(result2).toEqual({ type: 'success', value: 25 })
|
||||
})
|
||||
|
||||
test('returns error for division by zero', () => {
|
||||
const result = calculatePercentage(10, 0)
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'error',
|
||||
code: 'ZERO_TOTAL',
|
||||
})
|
||||
})
|
||||
|
||||
test('returns error for negative total', () => {
|
||||
const result = calculatePercentage(10, -100)
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'error',
|
||||
code: 'NEGATIVE_TOTAL',
|
||||
})
|
||||
})
|
||||
|
||||
test('handles zero value with valid total', () => {
|
||||
const result = calculatePercentage(0, 100)
|
||||
|
||||
expect(result).toEqual({ type: 'success', value: 0 })
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run tests**: `bun test percentage.test.ts`
|
||||
|
||||
```
|
||||
calculatePercentage
|
||||
✓ calculates percentage correctly
|
||||
✓ returns error for division by zero
|
||||
✓ returns error for negative total
|
||||
✓ handles zero value with valid total
|
||||
|
||||
4 passing
|
||||
```
|
||||
|
||||
**Update usage** in `api/stats.ts`:
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const percentage = calculatePercentage(completed, total)
|
||||
return { percentage }
|
||||
|
||||
// After
|
||||
const result = calculatePercentage(completed, total)
|
||||
if (result.type === 'error') {
|
||||
return { error: 'Invalid calculation', code: result.code }
|
||||
}
|
||||
return { percentage: result.value }
|
||||
```
|
||||
|
||||
**Run all tests**: `bun test`
|
||||
|
||||
```
|
||||
All tests passing (24 tests total)
|
||||
```
|
||||
|
||||
No regressions — refactor successful.
|
||||
|
||||
**Commit**: `refactor: use Result type for percentage calculation errors`
|
||||
|
||||
**Task Update**:
|
||||
|
||||
```text
|
||||
1. Write failing test reproducing bug [completed]
|
||||
2. Fix bug with minimal code [completed]
|
||||
3. Refactor if needed [completed]
|
||||
4. Verify fix [in_progress]
|
||||
```
|
||||
|
||||
## VERIFY Phase: Comprehensive Validation (4 min)
|
||||
|
||||
Run full test suite with coverage:
|
||||
|
||||
```bash
|
||||
bun test --coverage
|
||||
```
|
||||
|
||||
```
|
||||
Coverage Summary:
|
||||
percentage.ts: 100% statements, 100% branches
|
||||
|
||||
All tests passing (24 total)
|
||||
```
|
||||
|
||||
Run mutation testing on updated code:
|
||||
|
||||
```bash
|
||||
bun x stryker run --mutate "src/calculator/percentage.ts"
|
||||
```
|
||||
|
||||
```
|
||||
Mutation Score: 100%
|
||||
All mutants killed
|
||||
|
||||
Mutations tested:
|
||||
✓ === → !== (killed by "returns error for division by zero")
|
||||
✓ < → <= (killed by "returns error for negative total")
|
||||
✓ / → * (killed by "calculates percentage correctly")
|
||||
✓ 0 → 1 (killed by "handles zero value with valid total")
|
||||
```
|
||||
|
||||
Perfect mutation score — tests thoroughly verify behavior.
|
||||
|
||||
**Integration test** to verify API fix:
|
||||
|
||||
```typescript
|
||||
describe('GET /api/stats', () => {
|
||||
test('handles zero total gracefully', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/stats')
|
||||
.query({ completed: 10, total: 0 })
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toMatchObject({
|
||||
error: 'Invalid calculation',
|
||||
code: 'ZERO_TOTAL',
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Run integration tests**: `bun test tests/integration/`
|
||||
|
||||
```
|
||||
API Integration Tests
|
||||
✓ handles zero total gracefully
|
||||
|
||||
All integration tests passing
|
||||
```
|
||||
|
||||
**Task Update**:
|
||||
|
||||
```text
|
||||
1. Write failing test reproducing bug [completed]
|
||||
2. Fix bug with minimal code [completed]
|
||||
3. Refactor if needed [completed]
|
||||
4. Verify fix [completed]
|
||||
```
|
||||
|
||||
## Session Summary
|
||||
|
||||
**Duration**: 16 minutes total
|
||||
- RED: 4 min
|
||||
- GREEN: 3 min
|
||||
- REFACTOR: 5 min
|
||||
- VERIFY: 4 min
|
||||
|
||||
**Bug**: Division by zero crash
|
||||
**Fix**: Added validation with Result type
|
||||
**Tests**: 4 new tests + 1 integration test
|
||||
**Coverage**: 100% on changed code
|
||||
**Mutation Score**: 100%
|
||||
|
||||
**Improvements beyond minimal fix**:
|
||||
- Used discriminated union for error handling
|
||||
- Added negative total validation
|
||||
- Updated API to handle error results
|
||||
- Added integration test
|
||||
|
||||
**Production deployment**:
|
||||
- All tests passing
|
||||
- No regressions detected
|
||||
- Error handling verified
|
||||
- Ready to deploy
|
||||
|
||||
## Key TDD Bug Fix Principles
|
||||
|
||||
1. **RED first**: Always reproduce bug with failing test before fixing
|
||||
2. **Minimal GREEN**: Fix the immediate issue first
|
||||
3. **Refactor for robustness**: Improve error handling and edge cases
|
||||
4. **Verify thoroughly**: Run full suite + mutation tests + integration tests
|
||||
5. **Document in test**: Test name describes the bug being fixed
|
||||
|
||||
## Anti-patterns Avoided
|
||||
|
||||
Avoided jumping straight to fix without test:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong approach
|
||||
// 1. See bug report
|
||||
// 2. Add if (total === 0) return 0
|
||||
// 3. Deploy and hope
|
||||
|
||||
// ✓ Correct TDD approach
|
||||
// 1. Write failing test reproducing bug
|
||||
// 2. Verify test fails
|
||||
// 3. Add minimal fix
|
||||
// 4. Verify test passes
|
||||
// 5. Refactor for robustness
|
||||
// 6. Verify with mutation testing
|
||||
```
|
||||
|
||||
Avoided over-engineering initial fix:
|
||||
|
||||
```typescript
|
||||
// ❌ Too complex for first fix
|
||||
if (total === 0 || total < 0 || !isFinite(total) || isNaN(total)) {
|
||||
throw new ValidationError(...)
|
||||
}
|
||||
|
||||
// ✓ Minimal fix first (GREEN phase)
|
||||
if (total === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// ✓ Then refactor with proper error handling (REFACTOR phase)
|
||||
if (total === 0) {
|
||||
return { type: 'error', code: 'ZERO_TOTAL' }
|
||||
}
|
||||
```
|
||||
|
||||
## Commit History
|
||||
|
||||
```
|
||||
test: add failing test for division by zero bug
|
||||
fix: handle division by zero in percentage calculation
|
||||
refactor: use Result type for percentage calculation errors
|
||||
```
|
||||
|
||||
Clean, focused commits showing TDD progression.
|
||||
@@ -0,0 +1,686 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user