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:
119
apps/backend/tests/k6/public-browsing.js
Normal file
119
apps/backend/tests/k6/public-browsing.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Public Browsing Load Test
|
||||
*
|
||||
* Simulates 100 concurrent users browsing public pages
|
||||
* Tests:
|
||||
* - Homepage
|
||||
* - About page
|
||||
* - Solutions page
|
||||
* - Portfolio list
|
||||
* - Blog list
|
||||
* - Contact page
|
||||
*
|
||||
* NFR4 Requirements:
|
||||
* - p95 response time < 500ms
|
||||
* - Error rate < 1%
|
||||
* - 100 concurrent users sustained for 2 minutes
|
||||
*/
|
||||
|
||||
import { check, group } from 'k6';
|
||||
import { SharedArray } from 'k6/data';
|
||||
import { urls, thresholdGroups, config } from './lib/config.js';
|
||||
import { PageHelper, thinkTime, pickRandom } from './lib/helpers.js';
|
||||
|
||||
// Test configuration
|
||||
export const options = {
|
||||
stages: config.stages.publicBrowsing,
|
||||
thresholds: thresholdGroups.public,
|
||||
...config.requestOptions,
|
||||
};
|
||||
|
||||
// Page list to browse
|
||||
const publicPages = [
|
||||
urls.home,
|
||||
urls.about,
|
||||
urls.solutions,
|
||||
urls.portfolio,
|
||||
urls.blog,
|
||||
urls.contact,
|
||||
];
|
||||
|
||||
// Initialize page helper
|
||||
const pageHelper = new PageHelper(config.baseUrl);
|
||||
|
||||
/**
|
||||
* Main test scenario
|
||||
*/
|
||||
export default function () {
|
||||
// Scenario 1: Browse homepage (most common)
|
||||
group('Browse Homepage', () => {
|
||||
pageHelper.loadPage(urls.home);
|
||||
thinkTime(2, 4); // 2-4 seconds thinking
|
||||
});
|
||||
|
||||
// Scenario 2: Browse random pages (weighted)
|
||||
group('Browse Random Pages', () => {
|
||||
// Browse 3-6 random pages
|
||||
const pageCount = Math.floor(Math.random() * 4) + 3;
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const randomPage = pickRandom(publicPages);
|
||||
pageHelper.loadPage(randomPage);
|
||||
thinkTime(1, 3); // 1-3 seconds thinking
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 3: Navigate to contact (conversion intent)
|
||||
group('Navigate to Contact', () => {
|
||||
// 20% chance to visit contact page
|
||||
if (Math.random() < 0.2) {
|
||||
pageHelper.loadPage(urls.contact);
|
||||
thinkTime(3, 5); // More time on contact page
|
||||
}
|
||||
});
|
||||
|
||||
// Scenario 4: Deep dive into portfolio or blog
|
||||
group('Deep Dive', () => {
|
||||
// 30% chance to deep dive
|
||||
if (Math.random() < 0.3) {
|
||||
const section = Math.random() > 0.5 ? 'portfolio' : 'blog';
|
||||
|
||||
if (section === 'portfolio') {
|
||||
// Browse portfolio items
|
||||
pageHelper.loadPage(urls.portfolio);
|
||||
thinkTime(1, 2);
|
||||
// Note: In real scenario, we would click individual items
|
||||
// This requires parsing the page to get item URLs
|
||||
} else {
|
||||
// Browse blog posts
|
||||
pageHelper.loadPage(urls.blog);
|
||||
thinkTime(1, 2);
|
||||
// Note: In real scenario, we would click individual posts
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Small think time before next iteration
|
||||
thinkTime(2, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup function - runs once before test
|
||||
*/
|
||||
export function setup() {
|
||||
console.log('=== Public Browsing Load Test ===');
|
||||
console.log(`Target: ${config.baseUrl}`);
|
||||
console.log(`Pages to browse: ${publicPages.length}`);
|
||||
console.log('Starting test...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Teardown function - runs once after test
|
||||
*/
|
||||
export function teardown(data) {
|
||||
console.log('=== Test Complete ===');
|
||||
console.log('Check results above for:');
|
||||
console.log('- p95 response time < 500ms');
|
||||
console.log('- Error rate < 1%');
|
||||
console.log('- 100 concurrent users sustained');
|
||||
}
|
||||
Reference in New Issue
Block a user