feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,596 @@
/**
* E2E Tests for LiveComponents Chunked Upload System
*
* Tests chunked upload functionality in real browser environment:
* - Upload initialization and session management
* - Chunk splitting and parallel uploads
* - Progress tracking via SSE
* - Resume capability after interruption
* - Integrity verification (SHA-256)
* - Error handling and retry logic
* - Quarantine system integration
* - Multiple file uploads
*
* Run with: npx playwright test chunked-upload.spec.js
*/
import { test, expect } from '@playwright/test';
import { createReadStream } from 'fs';
import { writeFile, unlink, mkdir } from 'fs/promises';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Test file generation
const TEST_FILES_DIR = join(__dirname, '../../tmp/upload-test-files');
/**
* Helper: Create test file of specified size
*/
async function createTestFile(filename, sizeInMB) {
await mkdir(TEST_FILES_DIR, { recursive: true });
const filePath = join(TEST_FILES_DIR, filename);
const sizeInBytes = sizeInMB * 1024 * 1024;
// Generate random data
const chunkSize = 1024 * 1024; // 1MB chunks
const chunks = Math.ceil(sizeInBytes / chunkSize);
const buffer = Buffer.alloc(chunkSize);
let written = 0;
const stream = require('fs').createWriteStream(filePath);
for (let i = 0; i < chunks; i++) {
const remaining = sizeInBytes - written;
const writeSize = Math.min(chunkSize, remaining);
// Fill with pattern for verification
for (let j = 0; j < writeSize; j++) {
buffer[j] = (i + j) % 256;
}
stream.write(buffer.slice(0, writeSize));
written += writeSize;
}
stream.end();
await new Promise((resolve) => stream.on('finish', resolve));
return filePath;
}
/**
* Helper: Clean up test files
*/
async function cleanupTestFiles() {
try {
const fs = await import('fs/promises');
const files = await fs.readdir(TEST_FILES_DIR);
for (const file of files) {
await fs.unlink(join(TEST_FILES_DIR, file));
}
} catch (error) {
// Directory might not exist
}
}
test.describe('Chunked Upload System', () => {
test.beforeAll(async () => {
// Clean up any existing test files
await cleanupTestFiles();
});
test.afterAll(async () => {
// Clean up test files
await cleanupTestFiles();
});
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/upload');
await page.waitForFunction(() => window.LiveComponents !== undefined);
await page.waitForFunction(() => window.ChunkedUploader !== undefined);
});
test('should initialize upload session successfully', async ({ page }) => {
// Create small test file (1MB)
const testFile = await createTestFile('test-small.bin', 1);
// Set file input
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
// Click upload button
await page.click('button#upload-btn');
// Wait for session initialization
await page.waitForTimeout(500);
// Verify session created
const sessionId = await page.evaluate(() => {
return window.__uploadSession?.sessionId;
});
expect(sessionId).toBeTruthy();
expect(sessionId).toMatch(/^[a-f0-9-]{36}$/); // UUID format
});
test('should split file into correct number of chunks', async ({ page }) => {
// Create 2MB file (should result in 4 chunks with 512KB chunk size)
const testFile = await createTestFile('test-chunks.bin', 2);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Wait for chunk splitting
await page.waitForTimeout(500);
const uploadInfo = await page.evaluate(() => {
return {
totalChunks: window.__uploadSession?.totalChunks,
chunkSize: window.__uploadSession?.chunkSize
};
});
expect(uploadInfo.totalChunks).toBe(4); // 2MB / 512KB = 4
expect(uploadInfo.chunkSize).toBe(512 * 1024); // 512KB
});
test('should upload chunks in parallel', async ({ page }) => {
// Create 5MB file
const testFile = await createTestFile('test-parallel.bin', 5);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
// Monitor network requests
const chunkRequests = [];
page.on('request', request => {
if (request.url().includes('/live-component/') &&
request.url().includes('/chunk')) {
chunkRequests.push({
time: Date.now(),
url: request.url()
});
}
});
await page.click('button#upload-btn');
// Wait for upload to complete
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Verify parallel uploads (chunks should overlap in time)
expect(chunkRequests.length).toBeGreaterThan(3);
// Check if requests were concurrent (within 100ms window)
const timeWindows = chunkRequests.reduce((windows, req) => {
const window = Math.floor(req.time / 100);
windows[window] = (windows[window] || 0) + 1;
return windows;
}, {});
const hasParallelUploads = Object.values(timeWindows).some(count => count > 1);
expect(hasParallelUploads).toBe(true);
});
test('should track upload progress accurately', async ({ page }) => {
// Create 3MB file
const testFile = await createTestFile('test-progress.bin', 3);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
const progressUpdates = [];
// Monitor progress updates
await page.evaluate(() => {
window.__progressUpdates = [];
const originalLog = console.log;
console.log = function(...args) {
if (args[0] && typeof args[0] === 'string' && args[0].includes('Progress:')) {
window.__progressUpdates.push(parseFloat(args[1]));
}
originalLog.apply(console, args);
};
});
await page.click('button#upload-btn');
// Wait for completion
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Get progress updates
const updates = await page.evaluate(() => window.__progressUpdates || []);
// Verify progress increases monotonically
for (let i = 1; i < updates.length; i++) {
expect(updates[i]).toBeGreaterThanOrEqual(updates[i - 1]);
}
// Verify final progress is 100%
expect(updates[updates.length - 1]).toBeCloseTo(100, 0);
});
test('should verify chunk integrity with SHA-256', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-integrity.bin', 1);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
// Monitor chunk upload requests
const chunkHashes = [];
page.on('requestfinished', async request => {
if (request.url().includes('/chunk')) {
const postData = request.postDataJSON();
if (postData && postData.chunkHash) {
chunkHashes.push(postData.chunkHash);
}
}
});
await page.click('button#upload-btn');
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Verify hashes were sent
expect(chunkHashes.length).toBeGreaterThan(0);
// Verify hash format (SHA-256 is 64 hex characters)
chunkHashes.forEach(hash => {
expect(hash).toMatch(/^[a-f0-9]{64}$/);
});
});
test('should handle upload interruption and resume', async ({ page }) => {
// Create larger file (10MB)
const testFile = await createTestFile('test-resume.bin', 10);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Wait for some chunks to upload
await page.waitForTimeout(2000);
// Get current progress
const progressBefore = await page.locator('#progress-text').textContent();
const percentBefore = parseFloat(progressBefore);
// Simulate interruption (reload page)
await page.reload();
await page.waitForFunction(() => window.LiveComponents !== undefined);
// Re-select file
const fileInput2 = page.locator('input[type="file"]');
await fileInput2.setInputFiles(testFile);
// Resume upload
await page.click('button#resume-upload-btn');
// Wait for completion
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Verify upload completed
const finalProgress = await page.locator('#progress-text').textContent();
expect(finalProgress).toBe('100%');
// Verify resume actually happened (should skip uploaded chunks)
const uploadedChunks = await page.evaluate(() => window.__uploadedChunks || 0);
expect(uploadedChunks).toBeGreaterThan(0);
});
test('should retry failed chunks with exponential backoff', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-retry.bin', 2);
// Intercept and fail first chunk upload attempt
let attemptCount = 0;
await page.route('**/live-component/**/chunk/**', (route, request) => {
attemptCount++;
if (attemptCount <= 2) {
// Fail first 2 attempts
route.abort('failed');
} else {
// Allow subsequent attempts
route.continue();
}
});
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Upload should eventually succeed after retries
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Verify retries occurred
expect(attemptCount).toBeGreaterThan(2);
});
test('should handle concurrent multi-file uploads', async ({ page }) => {
// Create multiple test files
const files = [
await createTestFile('test-multi-1.bin', 1),
await createTestFile('test-multi-2.bin', 1),
await createTestFile('test-multi-3.bin', 1)
];
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(files);
// Verify all files queued
const queuedCount = await page.evaluate(() => {
return window.__uploadQueue?.length || 0;
});
expect(queuedCount).toBe(3);
// Start uploads
await page.click('button#upload-all-btn');
// Wait for all uploads to complete
await page.waitForSelector('.all-uploads-complete', { timeout: 60000 });
// Verify all files uploaded
const completedFiles = await page.locator('.uploaded-file').count();
expect(completedFiles).toBe(3);
});
test('should receive real-time progress via SSE', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-sse.bin', 5);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
// Monitor SSE connections
let sseConnected = false;
page.on('requestfinished', request => {
if (request.url().includes('/sse/upload-progress')) {
sseConnected = true;
}
});
await page.click('button#upload-btn');
// Wait a bit for SSE connection
await page.waitForTimeout(1000);
// Verify SSE connection established
expect(sseConnected).toBe(true);
// Verify progress updates are real-time (not just on completion)
const progressElement = page.locator('#progress-text');
// Should see intermediate progress values
const intermediateProgress = [];
for (let i = 0; i < 5; i++) {
await page.waitForTimeout(500);
const progress = await progressElement.textContent();
intermediateProgress.push(parseFloat(progress));
}
// Should have varying progress values
const uniqueValues = new Set(intermediateProgress);
expect(uniqueValues.size).toBeGreaterThan(1);
});
test('should handle upload cancellation', async ({ page }) => {
// Create larger file
const testFile = await createTestFile('test-cancel.bin', 20);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Wait for upload to start
await page.waitForTimeout(1000);
// Get progress before cancellation
const progressBefore = await page.locator('#progress-text').textContent();
const percentBefore = parseFloat(progressBefore);
expect(percentBefore).toBeGreaterThan(0);
expect(percentBefore).toBeLessThan(100);
// Cancel upload
await page.click('button#cancel-upload-btn');
// Wait for cancellation
await page.waitForTimeout(500);
// Verify upload cancelled
const status = await page.locator('#upload-status').textContent();
expect(status).toContain('Cancelled');
// Verify progress stopped
await page.waitForTimeout(1000);
const progressAfter = await page.locator('#progress-text').textContent();
expect(progressAfter).toBe(progressBefore); // Should not have increased
});
test('should validate file size limits', async ({ page }) => {
// Create file exceeding limit (e.g., 100MB)
const testFile = await createTestFile('test-too-large.bin', 100);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Should show error
await page.waitForSelector('.upload-error', { timeout: 5000 });
const errorMessage = await page.locator('.upload-error').textContent();
expect(errorMessage).toContain('File too large');
});
test('should validate file types', async ({ page }) => {
// Create file with disallowed extension
const testFile = await createTestFile('test-invalid.exe', 1);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Should show error
await page.waitForSelector('.upload-error', { timeout: 5000 });
const errorMessage = await page.locator('.upload-error').textContent();
expect(errorMessage).toContain('File type not allowed');
});
test('should display uploaded file in component', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-display.jpg', 1);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
// Wait for upload complete
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Wait for component update
await page.waitForTimeout(500);
// Verify file appears in uploaded files list
const uploadedFile = page.locator('[data-lc-fragment="file-list"] li').first();
await expect(uploadedFile).toBeVisible();
const fileName = await uploadedFile.textContent();
expect(fileName).toContain('test-display.jpg');
});
test('should handle quarantine system integration', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-quarantine.bin', 1);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Check for quarantine status
const quarantineStatus = await page.evaluate(() => {
return window.__uploadResult?.quarantineStatus;
});
// Should have a quarantine status (scanning, passed, or failed)
expect(['scanning', 'passed', 'failed']).toContain(quarantineStatus);
});
test('should update component state after successful upload', async ({ page }) => {
// Get initial file count
const initialCount = await page.locator('[data-lc-fragment="file-list"] li').count();
// Create and upload file
const testFile = await createTestFile('test-state.pdf', 1);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
await page.waitForSelector('.upload-complete', { timeout: 30000 });
// Wait for component state update
await page.waitForTimeout(1000);
// Verify file count increased
const finalCount = await page.locator('[data-lc-fragment="file-list"] li').count();
expect(finalCount).toBe(initialCount + 1);
});
test('should handle network interruption gracefully', async ({ page }) => {
// Create test file
const testFile = await createTestFile('test-network.bin', 5);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
// Simulate network interruption after 2 seconds
setTimeout(() => {
page.route('**/live-component/**', route => route.abort('failed'));
}, 2000);
await page.click('button#upload-btn');
// Should show error or retry notification
await page.waitForSelector('.upload-error, .upload-retrying', { timeout: 10000 });
const status = await page.locator('#upload-status').textContent();
expect(status).toMatch(/Error|Retrying/);
});
});
test.describe('Chunked Upload Performance', () => {
test.beforeEach(async ({ page }) => {
await page.goto('https://localhost/livecomponents/test/upload');
await page.waitForFunction(() => window.LiveComponents !== undefined);
});
test('should upload 10MB file in under 30 seconds', async ({ page }) => {
const testFile = await createTestFile('test-perf-10mb.bin', 10);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
const startTime = Date.now();
await page.click('button#upload-btn');
await page.waitForSelector('.upload-complete', { timeout: 30000 });
const duration = Date.now() - startTime;
console.log(`10MB upload took ${duration}ms`);
expect(duration).toBeLessThan(30000); // < 30 seconds
});
test('should handle memory efficiently with large files', async ({ page }) => {
// Monitor memory if available (Chromium only)
const memoryBefore = await page.evaluate(() => {
return performance.memory ? performance.memory.usedJSHeapSize : 0;
});
// Upload 50MB file
const testFile = await createTestFile('test-memory-50mb.bin', 50);
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(testFile);
await page.click('button#upload-btn');
await page.waitForSelector('.upload-complete', { timeout: 120000 });
const memoryAfter = await page.evaluate(() => {
return performance.memory ? performance.memory.usedJSHeapSize : 0;
});
if (memoryBefore > 0) {
const memoryIncrease = memoryAfter - memoryBefore;
// Memory increase should be reasonable (not loading entire file into memory)
// Should be < 10MB for 50MB file (chunked processing)
expect(memoryIncrease).toBeLessThan(10 * 1024 * 1024);
console.log(`Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)} MB`);
}
});
});