Complete 6 Sprint 1 stories for Epic 1 web migration infrastructure. Portfolio Collection: - Add 7 fields: title, slug, url, image, description, websiteType, tags - Configure R2 storage and authenticated access control Categories Collection: - Add nameEn, order, textColor, backgroundColor fields - Add color picker UI configuration Posts Collection: - Add excerpt with 200 char limit and ogImage for social sharing - Add showInFooter checkbox and status select (draft/review/published) Role-Based Access Control: - Add role field to Users collection (admin/editor) - Create adminOnly and authenticated access functions - Apply access rules to Portfolio, Categories, Posts, Users collections Audit Logging System (NFR9): - Create Audit collection with timestamps for 90-day retention - Add auditLogger utility for login/logout/content change tracking - Add auditChange and auditGlobalChange hooks to all collections and globals - Add cleanupAuditLogs job with 90-day retention policy Load Testing Framework (NFR4): - Add k6 load testing with 3 scripts: public-browsing, admin-operations, api-performance - Configure targets: p95 < 500ms, error rate < 1%, 100 concurrent users - Add verification script and comprehensive documentation Other Changes: - Remove unused Form blocks - Add Header/Footer audit hooks - Regenerate Payload TypeScript types
196 lines
5.2 KiB
JavaScript
196 lines
5.2 KiB
JavaScript
/**
|
|
* K6 Test Configuration
|
|
* Centralized configuration for all load tests
|
|
*/
|
|
|
|
// Get environment variables with defaults
|
|
export const config = {
|
|
// Base URL for the target server
|
|
get baseUrl() {
|
|
return __ENV.BASE_URL || 'http://localhost:3000';
|
|
},
|
|
|
|
// Admin credentials (from environment)
|
|
get adminEmail() {
|
|
return __ENV.ADMIN_EMAIL || 'admin@enchun.tw';
|
|
},
|
|
|
|
get adminPassword() {
|
|
return __ENV.ADMIN_PASSWORD || 'admin123';
|
|
},
|
|
|
|
// Load testing thresholds (NFR4 requirements)
|
|
thresholds: {
|
|
// Response time thresholds
|
|
httpReqDuration: ['p(95) < 500'], // 95th percentile < 500ms
|
|
httpReqDurationApi: ['p(95) < 300'], // API endpoints < 300ms
|
|
httpReqDurationAdmin: ['p(95) < 700'], // Admin operations < 700ms
|
|
|
|
// Error rate thresholds
|
|
httpReqFailed: ['rate < 0.01'], // < 1% error rate
|
|
httpReqFailedApi: ['rate < 0.005'], // API < 0.5% error rate
|
|
|
|
// Throughput thresholds
|
|
httpReqs: ['rate > 50'], // > 50 requests/second for pages
|
|
httpReqsApi: ['rate > 100'], // > 100 requests/second for API
|
|
|
|
// Check success rate
|
|
checks: ['rate > 0.99'], // 99% of checks should pass
|
|
},
|
|
|
|
// Stage configuration for gradual ramp-up
|
|
stages: {
|
|
// Public browsing: 100 users
|
|
publicBrowsing: [
|
|
{ duration: '30s', target: 20 }, // Ramp up to 20 users
|
|
{ duration: '30s', target: 50 }, // Ramp up to 50 users
|
|
{ duration: '1m', target: 100 }, // Ramp up to 100 users
|
|
{ duration: '2m', target: 100 }, // Stay at 100 users
|
|
{ duration: '30s', target: 0 }, // Ramp down
|
|
],
|
|
|
|
// Admin operations: 20 users
|
|
adminOperations: [
|
|
{ duration: '30s', target: 5 }, // Ramp up to 5 users
|
|
{ duration: '30s', target: 10 }, // Ramp up to 10 users
|
|
{ duration: '30s', target: 20 }, // Ramp up to 20 users
|
|
{ duration: '3m', target: 20 }, // Stay at 20 users
|
|
{ duration: '30s', target: 0 }, // Ramp down
|
|
],
|
|
|
|
// API performance: 50 users
|
|
apiPerformance: [
|
|
{ duration: '1m', target: 10 }, // Ramp up to 10 users
|
|
{ duration: '1m', target: 25 }, // Ramp up to 25 users
|
|
{ duration: '1m', target: 50 }, // Ramp up to 50 users
|
|
{ duration: '5m', target: 50 }, // Stay at 50 users
|
|
{ duration: '1m', target: 0 }, // Ramp down
|
|
],
|
|
},
|
|
|
|
// Common request options
|
|
requestOptions: {
|
|
timeout: '30s', // Request timeout
|
|
maxRedirects: 10, // Maximum redirects to follow
|
|
discardResponseBodies: true, // Discard response bodies to save memory
|
|
},
|
|
|
|
// Common headers
|
|
headers: {
|
|
'User-Agent': 'k6-load-test',
|
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'Accept-Language': 'en-US,en;q=0.9,zh-TW;q=0.8',
|
|
},
|
|
};
|
|
|
|
// URL helpers
|
|
export const urls = {
|
|
get baseUrl() {
|
|
return config.baseUrl;
|
|
},
|
|
|
|
// Public pages
|
|
get home() {
|
|
return `${config.baseUrl}/`;
|
|
},
|
|
get about() {
|
|
return `${config.baseUrl}/about`;
|
|
},
|
|
get solutions() {
|
|
return `${config.baseUrl}/solutions`;
|
|
},
|
|
get portfolio() {
|
|
return `${config.baseUrl}/portfolio`;
|
|
},
|
|
get blog() {
|
|
return `${config.baseUrl}/blog`;
|
|
},
|
|
get contact() {
|
|
return `${config.baseUrl}/contact`;
|
|
},
|
|
|
|
// API endpoints
|
|
get api() {
|
|
return `${config.baseUrl}/api`;
|
|
},
|
|
get graphql() {
|
|
return `${config.baseUrl}/api/graphql`;
|
|
},
|
|
get global() {
|
|
return `${config.baseUrl}/api/global`;
|
|
},
|
|
|
|
// Admin endpoints
|
|
get admin() {
|
|
return `${config.baseUrl}/admin`;
|
|
},
|
|
get collections() {
|
|
return `${config.baseUrl}/api/collections`;
|
|
},
|
|
get pages() {
|
|
return `${config.baseUrl}/api/pages`;
|
|
},
|
|
get posts() {
|
|
return `${config.baseUrl}/api/posts`;
|
|
},
|
|
get portfolioItems() {
|
|
return `${config.baseUrl}/api/portfolio`;
|
|
},
|
|
get categories() {
|
|
return `${config.baseUrl}/api/categories`;
|
|
},
|
|
|
|
// Auth endpoints
|
|
get login() {
|
|
return `${config.baseUrl}/api/users/login`;
|
|
},
|
|
get logout() {
|
|
return `${config.baseUrl}/api/users/logout`;
|
|
},
|
|
get me() {
|
|
return `${config.baseUrl}/api/users/me`;
|
|
},
|
|
};
|
|
|
|
// Common checks
|
|
export const checks = {
|
|
// HTTP status checks
|
|
status200: (res) => res.status === 200,
|
|
status201: (res) => res.status === 201,
|
|
status204: (res) => res.status === 204,
|
|
|
|
// Response time checks
|
|
responseTimeFast: (res) => res.timings.duration < 200,
|
|
responseTimeOk: (res) => res.timings.duration < 500,
|
|
responseTimeSlow: (res) => res.timings.duration < 1000,
|
|
|
|
// Content checks
|
|
hasContent: (res) => res.body.length > 0,
|
|
hasJson: (res) => res.headers['Content-Type'].includes('application/json'),
|
|
|
|
// Performance checks (NFR4)
|
|
nfr4ResponseTime: (res) => res.timings.duration < 500, // p95 < 500ms
|
|
nfr4ApiResponseTime: (res) => res.timings.duration < 300, // API < 300ms
|
|
};
|
|
|
|
// Threshold groups for different test types
|
|
export const thresholdGroups = {
|
|
public: {
|
|
http_req_duration: ['p(95) < 500', 'p(99) < 1000'],
|
|
http_req_failed: ['rate < 0.01'],
|
|
checks: ['rate > 0.99'],
|
|
},
|
|
|
|
admin: {
|
|
http_req_duration: ['p(95) < 700', 'p(99) < 1500'],
|
|
http_req_failed: ['rate < 0.01'],
|
|
checks: ['rate > 0.99'],
|
|
},
|
|
|
|
api: {
|
|
http_req_duration: ['p(95) < 300', 'p(99) < 500'],
|
|
http_req_failed: ['rate < 0.005'],
|
|
checks: ['rate > 0.995'],
|
|
},
|
|
};
|