Implement Sprint 1 stories: collections, RBAC, audit logging, load testing

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
This commit is contained in:
2026-01-31 17:20:35 +08:00
parent 0846318d6e
commit 7fd73e0e3d
48 changed files with 19497 additions and 5261 deletions

View File

@@ -0,0 +1,230 @@
/**
* API Performance Load Test
*
* Tests specific API endpoints for performance
* Tests:
* - REST API endpoints (Pages, Posts, Portfolio, Categories)
* - GraphQL API queries
* - Global API endpoint
* - Authentication endpoints
*
* NFR4 Requirements:
* - p95 response time < 300ms (faster for API)
* - Error rate < 0.5% (stricter for API)
* - Throughput > 100 requests/second
*/
import http from 'k6/http';
import { check, group } from 'k6';
import { urls, thresholdGroups, config } from './lib/config.js';
import { ApiHelper, testData, thinkTime } from './lib/helpers.js';
// Test configuration
export const options = {
stages: config.stages.apiPerformance,
thresholds: thresholdGroups.api,
...config.requestOptions,
};
// API helper (no auth needed for public endpoints)
const apiHelper = new ApiHelper(config.baseUrl);
/**
* Setup function
*/
export function setup() {
console.log('=== API Performance Load Test ===');
console.log(`Target: ${config.baseUrl}`);
console.log('Starting test...');
}
/**
* Main test scenario
*/
export default function () {
// Scenario 1: Global API endpoint (metadata)
group('Global API', () => {
const res = apiHelper.get('/global');
check(res, {
'has global data': (r) => {
try {
const body = r.json();
return body !== null;
} catch {
return false;
}
},
});
thinkTime(0.1, 0.3); // Minimal think time for API
});
// Scenario 2: Pages API
group('Pages API', () => {
// List pages
apiHelper.get('/pages', { limit: 10, depth: 1 });
// Try to get a specific page
try {
const list = apiHelper.get('/pages', { limit: 1, depth: 0, page: 1 });
if (list.status === 200 && list.json('totalDocs') > 0) {
const firstId = list.json('docs')[0].id;
apiHelper.get(`/pages/${firstId}`, { depth: 1 });
}
} catch (e) {
// Page might not exist
}
thinkTime(0.1, 0.3);
});
// Scenario 3: Posts API
group('Posts API', () => {
// List posts
apiHelper.get('/posts', { limit: 10, depth: 1 });
// List with pagination
apiHelper.get('/posts', { limit: 20, depth: 0, page: 1 });
thinkTime(0.1, 0.3);
});
// Scenario 4: Portfolio API
group('Portfolio API', () => {
// List portfolio items
apiHelper.get('/portfolio', { limit: 10, depth: 1 });
// Filter by category (if applicable)
try {
const categories = apiHelper.get('/categories', { limit: 1, depth: 0 });
if (categories.status === 200 && categories.json('totalDocs') > 0) {
const categoryId = categories.json('docs')[0].id;
apiHelper.get('/portfolio', {
limit: 10,
depth: 1,
where: {
category: { equals: categoryId },
},
});
}
} catch (e) {
// Categories might not exist
}
thinkTime(0.1, 0.3);
});
// Scenario 5: Categories API
group('Categories API', () => {
// List all categories
apiHelper.get('/categories', { limit: 10, depth: 1 });
thinkTime(0.1, 0.3);
});
// Scenario 6: GraphQL API
group('GraphQL API', () => {
// Query 1: Simple list
const simpleQuery = `
query {
Posts(limit: 5) {
docs {
id
title
slug
}
}
}
`;
apiHelper.graphql(simpleQuery);
thinkTime(0.1, 0.2);
// Query 2: With relationships
const complexQuery = `
query {
Posts(limit: 3, depth: 2) {
docs {
id
title
content
category {
id
name
}
}
}
}
`;
apiHelper.graphql(complexQuery);
thinkTime(0.1, 0.3);
});
// Scenario 7: Authentication endpoints
group('Auth API', () => {
// Note: These will fail with invalid credentials, but test the endpoint response
const loginRes = http.post(`${config.baseUrl}/api/users/login`, JSON.stringify({
email: 'test@example.com',
password: 'wrongpassword',
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login responds': (r) => [200, 400, 401].includes(r.status),
'login responds quickly': (r) => r.timings.duration < 500,
});
thinkTime(0.1, 0.2);
});
// Scenario 8: Concurrent API requests
group('Concurrent Requests', () => {
// Simulate multiple API calls in parallel
const requests = [
apiHelper.get('/pages', { limit: 5, depth: 1 }),
apiHelper.get('/posts', { limit: 5, depth: 1 }),
apiHelper.get('/portfolio', { limit: 5, depth: 1 }),
];
// Check all succeeded
const allSuccessful = requests.every(r => r.status === 200);
check(null, {
'all concurrent requests successful': () => allSuccessful,
});
thinkTime(0.2, 0.5);
});
// Scenario 9: Filtered queries
group('Filtered Queries', () => {
// Various filter combinations
const filters = [
{ where: { status: { equals: 'published' } } },
{ limit: 5, sort: '-createdAt' },
{ limit: 10, depth: 2 },
];
filters.forEach((filter, i) => {
apiHelper.get('/posts', filter);
thinkTime(0.05, 0.15);
});
});
// Minimal think time for API-focused test
thinkTime(0.5, 1);
}
/**
* Teardown function
*/
export function teardown(data) {
console.log('=== API Performance Test Complete ===');
console.log('Check results above for:');
console.log('- p95 response time < 300ms');
console.log('- Error rate < 0.5%');
console.log('- Throughput > 100 req/s');
}