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,142 @@
<?php
declare(strict_types=1);
use App\Framework\QrCode\QrCodeGenerator;
use App\Framework\QrCode\ValueObjects\EncodingMode;
use App\Framework\QrCode\ValueObjects\ErrorCorrectionLevel;
use App\Framework\QrCode\ValueObjects\QrCodeConfig;
use App\Framework\QrCode\ValueObjects\QrCodeVersion;
test('can generate QR code with default config', function () {
$data = 'Hello World';
$matrix = QrCodeGenerator::generate($data);
expect($matrix)->toBeInstanceOf(\App\Framework\QrCode\ValueObjects\QrCodeMatrix::class)
->and($matrix->getSize())->toBeGreaterThan(0)
->and($matrix->getVersion()->getVersionNumber())->toBe(1);
});
test('can generate QR code with auto-size config', function () {
$shortData = 'Test';
$longData = str_repeat('A', 30);
$matrix1 = QrCodeGenerator::generate($shortData);
$matrix2 = QrCodeGenerator::generate($longData);
expect($matrix1->getVersion()->getVersionNumber())->toBe(1)
->and($matrix2->getVersion()->getVersionNumber())->toBeGreaterThanOrEqual(2);
});
test('can generate QR code with explicit version', function () {
$data = 'Test';
$config = QrCodeConfig::withVersion(2);
$matrix = QrCodeGenerator::generate($data, $config);
expect($matrix->getVersion()->getVersionNumber())->toBe(2)
->and($matrix->getSize())->toBe(25); // Version 2 = 25x25
});
test('generates proper matrix size for different versions', function () {
$data = 'Test';
$matrix1 = QrCodeGenerator::generate($data, QrCodeConfig::withVersion(1));
$matrix2 = QrCodeGenerator::generate($data, QrCodeConfig::withVersion(2));
$matrix3 = QrCodeGenerator::generate($data, QrCodeConfig::withVersion(3));
expect($matrix1->getSize())->toBe(21) // Version 1: 21x21
->and($matrix2->getSize())->toBe(25) // Version 2: 25x25
->and($matrix3->getSize())->toBe(29); // Version 3: 29x29
});
test('matrix contains finder patterns', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
// Check top-left finder pattern (corner module should be dark)
expect($matrix->getModuleAt(0, 0)->isDark())->toBeTrue()
->and($matrix->getModuleAt(6, 6)->isDark())->toBeTrue();
});
test('matrix contains timing patterns', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
// Row 6 should have alternating modules (timing pattern)
expect($matrix->getModuleAt(6, 8)->isDark())->toBeTrue()
->and($matrix->getModuleAt(6, 9)->isLight())->toBeTrue()
->and($matrix->getModuleAt(6, 10)->isDark())->toBeTrue();
});
test('throws exception when data too long for version', function () {
$data = str_repeat('A', 100); // Too long for version 1-3
QrCodeGenerator::generate($data);
})->throws(\App\Framework\Exception\FrameworkException::class);
test('matrix has correct dark module count', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$darkCount = $matrix->countDarkModules();
// Matrix should have some dark modules (at least finder patterns)
expect($darkCount)->toBeGreaterThan(50) // Finder patterns alone = ~63 modules
->and($darkCount)->toBeLessThan($matrix->getSize() * $matrix->getSize());
});
test('can generate ASCII representation', function () {
$data = 'A';
$matrix = QrCodeGenerator::generate($data);
$ascii = $matrix->toAsciiArt();
expect($ascii)->toBeString()
->and($ascii)->toContain('█') // Should contain dark modules
->and($ascii)->toContain('░'); // Should contain light modules
});
test('can generate binary representation', function () {
$data = 'A';
$matrix = QrCodeGenerator::generate($data);
$binary = $matrix->toBinaryString();
expect($binary)->toBeString()
->and($binary)->toContain('1') // Should contain 1s (dark)
->and($binary)->toContain('0'); // Should contain 0s (light)
});
test('generates deterministic output for same input', function () {
$data = 'Test123';
$matrix1 = QrCodeGenerator::generate($data);
$matrix2 = QrCodeGenerator::generate($data);
expect($matrix1->toBinaryString())->toBe($matrix2->toBinaryString());
});
test('supports different data types', function () {
// Numeric-like data (but still byte mode in Phase 1)
$numeric = '1234567890';
$matrix1 = QrCodeGenerator::generate($numeric);
// Alphanumeric-like data
$alphanumeric = 'HELLO WORLD 123';
$matrix2 = QrCodeGenerator::generate($alphanumeric);
// Special characters
$special = 'test@example.com';
$matrix3 = QrCodeGenerator::generate($special);
// UTF-8
$utf8 = 'Hëllö Wörld';
$matrix4 = QrCodeGenerator::generate($utf8);
expect($matrix1)->toBeInstanceOf(\App\Framework\QrCode\ValueObjects\QrCodeMatrix::class)
->and($matrix2)->toBeInstanceOf(\App\Framework\QrCode\ValueObjects\QrCodeMatrix::class)
->and($matrix3)->toBeInstanceOf(\App\Framework\QrCode\ValueObjects\QrCodeMatrix::class)
->and($matrix4)->toBeInstanceOf(\App\Framework\QrCode\ValueObjects\QrCodeMatrix::class);
});

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
use App\Framework\QrCode\QrCodeGenerator;
use App\Framework\QrCode\QrCodeRenderer;
use App\Framework\QrCode\ValueObjects\QrCodeStyle;
use App\Framework\Svg\ValueObjects\Styling\SvgColor;
test('can render QR code to SVG', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg = $renderer->renderSvg($matrix);
expect($svg)->toBeString()
->and($svg)->toContain('<?xml')
->and($svg)->toContain('<svg')
->and($svg)->toContain('</svg>');
});
test('can render QR code to inline SVG', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg = $renderer->renderInlineSvg($matrix);
expect($svg)->toBeString()
->and($svg)->not->toContain('<?xml')
->and($svg)->toContain('<svg')
->and($svg)->toContain('</svg>');
});
test('renders with default style', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg = $renderer->renderSvg($matrix);
// Should contain rectangles for modules
expect($svg)->toContain('<rect')
->and($svg)->toContain('width=')
->and($svg)->toContain('height=');
});
test('renders with custom style', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$style = QrCodeStyle::withColors(
SvgColor::fromHex('#0000ff'), // Blue dark modules
SvgColor::fromHex('#ffff00') // Yellow light modules
);
$svg = $renderer->renderCustom($matrix, $style);
expect($svg)->toBeString()
->and($svg)->toContain('<rect');
});
test('renders with compact style', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$style = QrCodeStyle::compact();
$svg = $renderer->renderSvg($matrix, $style);
// Compact style has smaller modules (5px)
expect($svg)->toContain('<svg')
->and($svg)->toContain('width=');
});
test('renders with large style', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$style = QrCodeStyle::large();
$svg = $renderer->renderSvg($matrix, $style);
// Large style has bigger modules (20px)
expect($svg)->toContain('<svg')
->and($svg)->toContain('width=');
});
test('renders with inverted style', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$style = QrCodeStyle::inverted();
$svg = $renderer->renderSvg($matrix, $style);
// Should render (white modules on black background)
expect($svg)->toContain('<rect');
});
test('can generate data URL', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$dataUrl = $renderer->toDataUrl($matrix);
expect($dataUrl)->toBeString()
->and($dataUrl)->toStartWith('data:image/svg+xml;base64,')
->and(strlen($dataUrl))->toBeGreaterThan(100);
});
test('data URL contains valid base64', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$dataUrl = $renderer->toDataUrl($matrix);
$base64 = substr($dataUrl, strlen('data:image/svg+xml;base64,'));
$decoded = base64_decode($base64, true);
expect($decoded)->not->toBeFalse()
->and($decoded)->toContain('<svg');
});
test('renders QR code with title and description', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg = $renderer->renderSvg($matrix);
expect($svg)->toContain('<title>QR Code</title>')
->and($svg)->toContain('<desc>QR Code Version');
});
test('renders different sizes correctly', function () {
$data = 'Test';
$matrix1 = QrCodeGenerator::generate($data, \App\Framework\QrCode\ValueObjects\QrCodeConfig::withVersion(1));
$matrix2 = QrCodeGenerator::generate($data, \App\Framework\QrCode\ValueObjects\QrCodeConfig::withVersion(2));
$renderer = new QrCodeRenderer();
$style = QrCodeStyle::default();
$svg1 = $renderer->renderSvg($matrix1, $style);
$svg2 = $renderer->renderSvg($matrix2, $style);
// Larger version should have larger SVG
expect(strlen($svg2))->toBeGreaterThan(strlen($svg1));
});
test('can render to file', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$filepath = __DIR__ . '/../../../../tests/tmp/test-qrcode.svg';
// Ensure directory exists
if (!is_dir(dirname($filepath))) {
mkdir(dirname($filepath), 0777, true);
}
$renderer->renderToFile($matrix, $filepath);
expect(file_exists($filepath))->toBeTrue()
->and(filesize($filepath))->toBeGreaterThan(0);
// Cleanup
if (file_exists($filepath)) {
unlink($filepath);
}
});
test('rendered SVG has correct viewBox', function () {
$data = 'Test';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg = $renderer->renderSvg($matrix);
// Should contain viewBox attribute
expect($svg)->toContain('viewBox=');
});
test('renders consistent output for same input', function () {
$data = 'Test123';
$matrix = QrCodeGenerator::generate($data);
$renderer = new QrCodeRenderer();
$svg1 = $renderer->renderSvg($matrix);
$svg2 = $renderer->renderSvg($matrix);
expect($svg1)->toBe($svg2);
});

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
use App\Framework\QrCode\ValueObjects\Module;
test('can create dark module', function () {
$module = Module::dark();
expect($module->isDark())->toBeTrue()
->and($module->isLight())->toBeFalse();
});
test('can create light module', function () {
$module = Module::light();
expect($module->isLight())->toBeTrue()
->and($module->isDark())->toBeFalse();
});
test('can create from boolean', function () {
$dark = Module::fromBool(true);
$light = Module::fromBool(false);
expect($dark->isDark())->toBeTrue()
->and($light->isLight())->toBeTrue();
});
test('can create from bit', function () {
$dark = Module::fromBit(1);
$light = Module::fromBit(0);
expect($dark->isDark())->toBeTrue()
->and($light->isLight())->toBeTrue();
});
test('can convert to bit', function () {
$dark = Module::dark();
$light = Module::light();
expect($dark->toBit())->toBe(1)
->and($light->toBit())->toBe(0);
});
test('can invert module', function () {
$dark = Module::dark();
$light = Module::light();
$invertedDark = $dark->invert();
$invertedLight = $light->invert();
expect($invertedDark->isLight())->toBeTrue()
->and($invertedLight->isDark())->toBeTrue();
});
test('invert returns new instance', function () {
$original = Module::dark();
$inverted = $original->invert();
// Original unchanged
expect($original->isDark())->toBeTrue()
->and($inverted->isLight())->toBeTrue();
});
test('can convert to string', function () {
$dark = Module::dark();
$light = Module::light();
expect($dark->toString())->toBe('█')
->and($light->toString())->toBe('░');
});
test('dark and light are opposites', function () {
$dark = Module::dark();
$light = Module::light();
expect($dark->isDark())->not->toBe($light->isDark())
->and($dark->isLight())->not->toBe($light->isLight());
});
test('module is immutable', function () {
$module = Module::dark();
// Attempting to invert doesn't change original
$module->invert();
expect($module->isDark())->toBeTrue();
});

View File

@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
use App\Framework\QrCode\ValueObjects\Module;
use App\Framework\QrCode\ValueObjects\ModulePosition;
use App\Framework\QrCode\ValueObjects\QrCodeMatrix;
use App\Framework\QrCode\ValueObjects\QrCodeVersion;
test('can create empty matrix', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
expect($matrix->getSize())->toBe(21)
->and($matrix->getVersion()->getVersionNumber())->toBe(1);
});
test('empty matrix has all light modules', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$module = $matrix->getModuleAt(0, 0);
expect($module->isLight())->toBeTrue();
});
test('can set and get module', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$position = ModulePosition::at(5, 5);
$matrix = $matrix->setModule($position, Module::dark());
expect($matrix->getModule($position)->isDark())->toBeTrue();
});
test('setting module returns new instance', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix1 = QrCodeMatrix::create($version);
$position = ModulePosition::at(5, 5);
$matrix2 = $matrix1->setModule($position, Module::dark());
// Original matrix unchanged (immutability)
expect($matrix1->getModule($position)->isLight())->toBeTrue()
->and($matrix2->getModule($position)->isDark())->toBeTrue();
});
test('can set module using coordinates', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$matrix = $matrix->setModuleAt(10, 10, Module::dark());
expect($matrix->getModuleAt(10, 10)->isDark())->toBeTrue();
});
test('can use helper methods setDark and setLight', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$pos1 = ModulePosition::at(5, 5);
$pos2 = ModulePosition::at(10, 10);
$matrix = $matrix->setDark($pos1);
$matrix = $matrix->setLight($pos2);
expect($matrix->getModule($pos1)->isDark())->toBeTrue()
->and($matrix->getModule($pos2)->isLight())->toBeTrue();
});
test('can check if position is dark', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$position = ModulePosition::at(5, 5);
$matrix = $matrix->setDark($position);
expect($matrix->isDark($position))->toBeTrue();
});
test('throws exception for out of bounds position', function () {
$version = QrCodeVersion::fromNumber(1); // Size = 21
$matrix = QrCodeMatrix::create($version);
$position = ModulePosition::at(25, 25); // Out of bounds
$matrix->getModule($position);
})->throws(\App\Framework\Exception\FrameworkException::class);
test('can convert to array', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$array = $matrix->toArray();
expect($array)->toBeArray()
->and(count($array))->toBe(21)
->and(count($array[0]))->toBe(21);
});
test('can convert to binary string', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$matrix = $matrix->setModuleAt(0, 0, Module::dark());
$matrix = $matrix->setModuleAt(0, 1, Module::light());
$binary = $matrix->toBinaryString();
expect($binary)->toBeString()
->and($binary)->toContain('1')
->and($binary)->toContain('0')
->and($binary)->toContain("\n");
});
test('can convert to ASCII art', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
$matrix = $matrix->setModuleAt(0, 0, Module::dark());
$matrix = $matrix->setModuleAt(0, 1, Module::light());
$ascii = $matrix->toAsciiArt();
expect($ascii)->toBeString()
->and($ascii)->toContain('█')
->and($ascii)->toContain('░');
});
test('can count dark modules', function () {
$version = QrCodeVersion::fromNumber(1);
$matrix = QrCodeMatrix::create($version);
// Add some dark modules
$matrix = $matrix->setModuleAt(0, 0, Module::dark());
$matrix = $matrix->setModuleAt(1, 1, Module::dark());
$matrix = $matrix->setModuleAt(2, 2, Module::dark());
$count = $matrix->countDarkModules();
expect($count)->toBe(3);
});
test('matrix size scales with version', function () {
$matrix1 = QrCodeMatrix::create(QrCodeVersion::fromNumber(1));
$matrix2 = QrCodeMatrix::create(QrCodeVersion::fromNumber(2));
$matrix3 = QrCodeMatrix::create(QrCodeVersion::fromNumber(3));
expect($matrix1->getSize())->toBe(21)
->and($matrix2->getSize())->toBe(25)
->and($matrix3->getSize())->toBe(29);
});