# Story 1.17-a: Add Load Testing for NFR4 (100 Concurrent Users)

**Status:** Draft
**Epic:** Epic 1 - Webflow to Payload CMS + Astro Migration
**Priority:** P1 (High - NFR4 Validation Required)
**Estimated Time:** 3 hours

## Story

**As a** Development Team,
**I want** a load testing framework that validates system performance under concurrent user load,
**So that** we can ensure NFR4 compliance (100 concurrent users) before production deployment.

## Context

This is a Sprint 1 addition story. NFR4 requires the system to support at least 100 concurrent users without performance degradation. Load testing was only implied in the original plan and needs explicit validation.

**Story Source:**
- NFR4 from `docs/prd/02-requirements.md`
- Sprint 1 adjustments in `sprint-status.yaml`
- Task specs from `docs/prd/epic-1-stories-1.3-1.17-tasks.md`

**NFR4 Requirement:**
> "The system must support at least 100 concurrent users without performance degradation."

## Acceptance Criteria

### Part 1: Load Testing Framework
1. **AC1 - Tool Selected**: k6 or Artillery chosen and installed
2. **AC2 - Test Scripts Created**: Scripts for public browsing and admin operations

### Part 2: Test Scenarios
3. **AC3 - Public Browsing Test**: 100 concurrent users browsing pages
4. **AC4 - Admin Operations Test**: 20 concurrent admin users
5. **AC5 - API Performance Test**: Payload CMS API endpoints under load

### Part 3: Performance Targets
6. **AC6 - Response Time**: 95th percentile response time < 500ms
7. **AC7 - Error Rate**: Error rate < 1%
8. **AC8 - Cloudflare Limits**: Validated within Workers limits

### Part 4: Reporting
9. **AC9 - Test Report**: Generated report with results and recommendations
10. **AC10 - CI Integration**: Tests can be run on demand

## Dev Technical Guidance

### Task 1: Select and Install Load Testing Tool

**Recommended: k6** (Grafana's load testing tool)

**Installation:**
```bash
# Install k6
pnpm add -D k6

# Or globally
brew install k6  # macOS
```

**Why k6:**
- JavaScript-based tests (familiar to devs)
- Good Cloudflare Workers support
- Excellent reporting and metrics
- Easy CI/CD integration

### Task 2: Create Load Test Scripts

**File:** `apps/frontend/tests/load/public-browsing.js`

```javascript
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate } from 'k6/metrics'

// Custom metrics
const errorRate = new Rate('errors')

// Test configuration
export const options = {
  stages: [
    { duration: '30s', target: 20 },   // Ramp up to 20 users
    { duration: '1m', target: 50 },     // Ramp up to 50 users
    { duration: '2m', target: 100 },    // Ramp up to 100 users (NFR4 target)
    { duration: '2m', target: 100 },    // Stay at 100 users
    { duration: '30s', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],   // 95% of requests under 500ms
    errors: ['rate<0.01'],               // Error rate < 1%
    http_req_failed: ['rate<0.01'],      // Failed requests < 1%
  },
}

const BASE_URL = __ENV.BASE_URL || 'http://localhost:4321'

// Pages to test
const pages = [
  '/',
  '/about',
  '/solutions',
  '/contact',
  '/blog',
  '/portfolio',
]

export function setup() {
  // Optional: Login and get auth token for admin tests
  const loginRes = http.post(`${BASE_URL}/api/login`, JSON.stringify({
    email: 'test@example.com',
    password: 'test123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  })

  if (loginRes.status === 200) {
    return { token: loginRes.json('token') }
  }
  return {}
}

export default function(data) {
  // Pick a random page
  const page = pages[Math.floor(Math.random() * pages.length)]

  // Make request
  const res = http.get(`${BASE_URL}${page}`, {
    tags: { name: `Page: ${page}` },
  })

  // Check response
  const success = check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'page contains content': (r) => r.html().find('body').length > 0,
  })

  errorRate.add(!success)

  // Think time between requests (2-8 seconds)
  sleep(Math.random() * 6 + 2)
}

export function teardown(data) {
  console.log('Load test completed')
}
```

**File:** `apps/frontend/tests/load/admin-operations.js`

```javascript
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate } from 'k6/metrics'

const errorRate = new Rate('errors')

export const options = {
  stages: [
    { duration: '20s', target: 5 },     // Ramp up to 5 admin users
    { duration: '1m', target: 10 },     // Ramp up to 10 admin users
    { duration: '2m', target: 20 },     // Ramp up to 20 admin users
    { duration: '1m', target: 20 },     // Stay at 20 users
    { duration: '20s', target: 0 },     // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],
    errors: ['rate<0.01'],
  },
}

const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
const API_URL = __ENV.API_URL || 'http://localhost:3000/api'

export function setup() {
  // Login as admin
  const loginRes = http.post(`${API_URL}/users/login`, JSON.stringify({
    email: 'admin@enchun.tw',
    password: 'admin123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  })

  if (loginRes.status !== 200) {
    throw new Error('Login failed')
  }

  return { token: loginRes.json('token') }
}

export default function(data) {
  const headers = {
    'Authorization': `JWT ${data.token}`,
    'Content-Type': 'application/json',
  }

  // Test different admin operations
  const operations = [
    // List posts
    () => {
      const res = http.get(`${API_URL}/posts?limit=10`, { headers })
      check(res, { 'posts list status': (r) => r.status === 200 })
    },
    // List categories
    () => {
      const res = http.get(`${API_URL}/categories?limit=20`, { headers })
      check(res, { 'categories list status': (r) => r.status === 200 })
    },
    // Get globals
    () => {
      const res = http.get(`${API_URL}/globals/header`, { headers })
      check(res, { 'globals status': (r) => r.status === 200 })
    },
    // Get media
    () => {
      const res = http.get(`${API_URL}/media?limit=10`, { headers })
      check(res, { 'media list status': (r) => r.status === 200 })
    },
  ]

  // Execute random operation
  const operation = operations[Math.floor(Math.random() * operations.length)]
  const res = operation()

  errorRate.add(!res)

  // Think time
  sleep(Math.random() * 3 + 1)
}
```

**File:** `apps/backend/tests/load/api-performance.js`

```javascript
import http from 'k6/http'
import { check } from 'k6'
import { Rate } from 'k6/metrics'

const errorRate = new Rate('errors')

export const options = {
  scenarios: {
    concurrent_readers: {
      executor: 'constant-vus',
      vus: 50,
      duration: '2m',
      gracefulStop: '30s',
    },
    concurrent_writers: {
      executor: 'constant-vus',
      vus: 10,
      duration: '2m',
      startTime: '30s',
      gracefulStop: '30s',
    },
  },
  thresholds: {
    http_req_duration: ['p(95)<500'],
    errors: ['rate<0.01'],
  },
}

const API_URL = __ENV.API_URL || 'http://localhost:3000/api'

export function setup() {
  // Create test user for write operations
  const loginRes = http.post(`${API_URL}/users/login`, JSON.stringify({
    email: 'test-writer@example.com',
    password: 'test123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  })

  return { token: loginRes.json('token') || '' }
}

export default function(data) {
  const headers = {
    'Content-Type': 'application/json',
  }

  if (data.token) {
    headers['Authorization'] = `JWT ${data.token}`
  }

  // Mix of read operations
  const endpoints = [
    () => http.get(`${API_URL}/posts?limit=10&depth=1`, { headers }),
    () => http.get(`${API_URL}/categories`, { headers }),
    () => http.get(`${API_URL}/globals/header`, { headers }),
    () => http.get(`${API_URL}/globals/footer`, { headers }),
  ]

  const res = endpoints[Math.floor(Math.random() * endpoints.length)]()

  check(res, {
    'status 200': (r) => r.status === 200,
    'response < 500ms': (r) => r.timings.duration < 500,
  })

  errorRate.add(res.status !== 200)
}
```

### Task 3: Create Test Runner Scripts

**File:** `package.json` scripts

```json
{
  "scripts": {
    "test:load": "npm-run-all -p test:load:public test:load:admin",
    "test:load:public": "k6 run apps/frontend/tests/load/public-browsing.js",
    "test:load:admin": "k6 run apps/frontend/tests/load/admin-operations.js",
    "test:load:api": "k6 run apps/backend/tests/load/api-performance.js",
    "test:load:report": "k6 run --out json=load-test-results.json apps/frontend/tests/load/public-browsing.js"
  }
}
```

### Task 4: Cloudflare Workers Limits Validation

**Create documentation file:** `docs/load-testing-cloudflare-limits.md`

```markdown
# Cloudflare Workers Limits for Load Testing

## Relevant Limits

| Resource | Limit | Notes |
|----------|-------|-------|
| CPU Time | 10ms (Free), 50ms (Paid) | Per request |
| Request Timeout | 30 seconds | Wall-clock time |
| Concurrent Requests | No limit | But fair use applies |
| Workers Requests | 100,000/day (Free) | Paid plan has more |

## Testing Strategy

1. Monitor CPU time per request during load tests
2. Ensure 95th percentile < 50ms CPU time
3. Use `wrangler dev` local mode for initial testing
4. Test on preview deployment before production

## Monitoring

Use Cloudflare Analytics to verify:
- Request count by endpoint
- CPU usage percentage
- Cache hit rate
- Error rate
```

### File Structure

```
apps/
├── frontend/
│   └── tests/
│       └── load/
│           ├── public-browsing.js     ← CREATE
│           └── admin-operations.js    ← CREATE
├── backend/
│   └── tests/
│       └── load/
│           └── api-performance.js     ← CREATE
docs/
└── load-testing-cloudflare-limits.md ← CREATE
package.json                           ← MODIFY (add scripts)
```

## Tasks / Subtasks

### Part 1: Setup
- [ ] **Task 1.1**: Install k6
  - [ ] Add k6 as dev dependency
  - [ ] Verify installation
  - [ ] Add npm scripts

- [ ] **Task 1.2**: Create test directories
  - [ ] Create apps/frontend/tests/load
  - [ ] Create apps/backend/tests/load

### Part 2: Public Browsing Test
- [ ] **Task 2.1**: Create public-browsing.js
  - [ ] Implement 100 user ramp-up
  - [ ] Add page navigation logic
  - [ ] Configure thresholds

- [ ] **Task 2.2**: Run initial test
  - [ ] Test against local dev server
  - [ ] Verify results
  - [ ] Adjust think times if needed

### Part 3: Admin Operations Test
- [ ] **Task 3.1**: Create admin-operations.js
  - [ ] Implement auth flow
  - [ ] Add admin operations
  - [ ] Configure 20 user target

- [ ] **Task 3.2**: Run admin test
  - [ ] Verify auth works
  - [ ] Check performance metrics

### Part 4: API Performance Test
- [ ] **Task 4.1**: Create api-performance.js
  - [ ] Implement concurrent readers/writers
  - [ ] Add all key endpoints

- [ ] **Task 4.2**: Run API test
  - [ ] Verify API performance
  - [ ] Check for bottlenecks

### Part 5: Cloudflare Validation
- [ ] **Task 5.1**: Create limits documentation
- [ ] **Task 5.2**: Test on Workers environment
- [ ] **Task 5.3**: Verify CPU time limits

### Part 6: Reporting
- [ ] **Task 6.1**: Generate test report
  - [ ] Run all tests
  - [ ] Generate HTML report
  - [ ] Document results

- [ ] **Task 6.2**: Create recommendations
  - [ ] Identify bottlenecks
  - [ ] Suggest optimizations
  - [ ] Document for deployment

## Testing Requirements

### Performance Targets

| Metric | Target | Threshold |
|--------|--------|-----------|
| Response Time (p95) | < 500ms | NFR5 |
| Error Rate | < 1% | Acceptable |
| Concurrent Users | 100 | NFR4 |
| Admin Users | 20 | Target |

### Test Scenarios

1. **Public Browsing** (100 users):
   - Homepage
   - About page
   - Solutions page
   - Contact page
   - Blog listing
   - Portfolio listing

2. **Admin Operations** (20 users):
   - List posts
   - List categories
   - Get globals (Header/Footer)
   - List media

3. **API Performance** (50 readers + 10 writers):
   - GET /api/posts
   - GET /api/categories
   - GET /api/globals/*
   - Mixed read/write operations

### Manual Testing Checklist
- [ ] k6 installed successfully
- [ ] Public browsing test runs
- [ ] Admin operations test runs
- [ ] API performance test runs
- [ ] All tests pass thresholds
- [ ] Results documented
- [ ] Cloudflare limits validated
- [ ] Recommendations created

## Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|------------|--------|------------|
| Local testing not representative | Medium | Medium | Test on preview deployment |
| Cloudflare limits exceeded | Low | High | Monitor CPU time, optimize |
| Test data pollution | Medium | Low | Use test environment |
| False failures | Low | Low | Calibrate thresholds properly |

## Definition of Done

- [ ] k6 installed and configured
- [ ] Public browsing test script created
- [ ] Admin operations test script created
- [ ] API performance test script created
- [ ] All tests run successfully
- [ ] Performance targets met (p95 < 500ms, errors < 1%)
- [ ] 100 concurrent user test passed
- [ ] Cloudflare Workers limits validated
- [ ] Test report generated
- [ ] Recommendations documented
- [ ] sprint-status.yaml updated

## Dev Agent Record

### Agent Model Used
*To be filled by Dev Agent*

### Debug Log References
*To be filled by Dev Agent*

### Completion Notes
*To be filled by Dev Agent*

### File List
*To be filled by Dev Agent*

## Change Log

| Date | Action | Author |
|------|--------|--------|
| 2026-01-31 | Story created (Draft) | SM Agent (Bob) |
