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,381 @@
<?php
declare(strict_types=1);
use App\Framework\Core\Services\LinkValidator;
use App\Framework\Core\ValueObjects\AccessibleLink;
use App\Framework\Core\ValueObjects\HtmlLink;
use App\Framework\Core\ValueObjects\LinkRel;
use App\Framework\Core\ValueObjects\LinkTarget;
// Helper function to check if array contains string (case-insensitive)
function arrayContainsString(array $array, string $needle): bool
{
$needleLower = strtolower($needle);
foreach ($array as $item) {
if (str_contains(strtolower($item), $needleLower)) {
return true;
}
}
return false;
}
describe('LinkValidator Service', function () {
beforeEach(function () {
$this->validator = new LinkValidator();
});
describe('Security Validation', function () {
it('detects missing noopener on external blank links', function () {
$link = new HtmlLink(
href: 'https://external.com',
text: 'External',
target: LinkTarget::BLANK,
rel: [] // Missing noopener
);
$result = $this->validator->validate($link);
expect($result->isValid)->toBeFalse()
->and($result->hasErrors())->toBeTrue()
->and(arrayContainsString($result->errors, 'noopener'))->toBeTrue();
});
it('passes external blank link with proper security', function () {
$link = HtmlLink::external('https://external.com', 'External');
$result = $this->validator->validate($link);
expect($result->isValid)->toBeTrue()
->and($result->hasErrors())->toBeFalse();
});
it('warns about javascript hrefs', function () {
$link = HtmlLink::create('javascript:alert("test")', 'Bad Link');
$result = $this->validator->validate($link);
expect($result->isValid)->toBeFalse()
->and(arrayContainsString($result->errors, 'JavaScript hrefs'))->toBeTrue();
});
it('warns about download without filename', function () {
$link = HtmlLink::download('/file.pdf', 'Download');
$result = $this->validator->validate($link);
expect($result->hasWarnings())->toBeTrue();
$hasFilenameWarning = arrayContainsString($result->errors, 'filename')
|| arrayContainsString($result->warnings, 'filename')
|| arrayContainsString($result->suggestions, 'filename');
expect($hasFilenameWarning)->toBeTrue();
});
});
describe('Accessibility Validation', function () {
it('warns about generic link text', function () {
$link = HtmlLink::create('/page', 'Click here');
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasGenericText = arrayContainsString($result->errors, 'generic link text')
|| arrayContainsString($result->warnings, 'generic link text')
|| arrayContainsString($result->suggestions, 'generic link text');
expect($hasGenericText)->toBeTrue();
});
it('suggests indicating new window for external links', function () {
$link = HtmlLink::external('https://external.com', 'External');
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasNewWindowSuggestion = arrayContainsString($result->errors, 'new window')
|| arrayContainsString($result->warnings, 'new window')
|| arrayContainsString($result->suggestions, 'new window');
expect($hasNewWindowSuggestion)->toBeTrue();
});
it('warns about very short link text', function () {
$link = HtmlLink::create('/page', 'X');
$result = $this->validator->validate($link);
expect($result->hasWarnings())->toBeTrue();
$hasTooShortWarning = arrayContainsString($result->errors, 'too short')
|| arrayContainsString($result->warnings, 'too short')
|| arrayContainsString($result->suggestions, 'too short');
expect($hasTooShortWarning)->toBeTrue();
});
it('suggests improvements for disabled links', function () {
$link = HtmlLink::create('/page', 'Page')->withDisabled(true);
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasAccessibleLinkSuggestion = arrayContainsString($result->errors, 'AccessibleLink')
|| arrayContainsString($result->warnings, 'AccessibleLink')
|| arrayContainsString($result->suggestions, 'AccessibleLink');
expect($hasAccessibleLinkSuggestion)->toBeTrue();
});
});
describe('SEO Validation', function () {
it('warns about nofollow on internal links', function () {
$link = new HtmlLink(
href: '/internal-page',
text: 'Internal',
rel: [LinkRel::NOFOLLOW]
);
$result = $this->validator->validate($link);
expect($result->hasWarnings())->toBeTrue();
$hasInternalLinkWarning = arrayContainsString($result->errors, 'Internal link')
|| arrayContainsString($result->warnings, 'Internal link')
|| arrayContainsString($result->suggestions, 'Internal link');
expect($hasInternalLinkWarning)->toBeTrue();
});
it('suggests pagination rels for pagination URLs', function () {
$link = HtmlLink::create('/posts?page=2', 'Page 2');
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasPaginationSuggestion = arrayContainsString($result->errors, 'pagination')
|| arrayContainsString($result->warnings, 'pagination')
|| arrayContainsString($result->suggestions, 'pagination');
expect($hasPaginationSuggestion)->toBeTrue();
});
it('warns about relative canonical URLs', function () {
$link = new HtmlLink(
href: '/canonical-page',
text: 'Canonical',
rel: [LinkRel::CANONICAL]
);
$result = $this->validator->validate($link);
expect($result->hasWarnings())->toBeTrue();
$hasAbsoluteUrlWarning = arrayContainsString($result->errors, 'absolute URL')
|| arrayContainsString($result->warnings, 'absolute URL')
|| arrayContainsString($result->suggestions, 'absolute URL');
expect($hasAbsoluteUrlWarning)->toBeTrue();
});
});
describe('AccessibleLink Validation', function () {
it('validates ARIA attributes', function () {
$link = new AccessibleLink(
baseLink: HtmlLink::create('/page', 'Page'),
ariaCurrent: true,
ariaDisabled: true // Conflicting states
);
$result = $this->validator->validateAccessible($link);
expect($result->hasWarnings())->toBeTrue();
$hasConflictingStatesWarning = arrayContainsString($result->errors, 'conflicting states')
|| arrayContainsString($result->warnings, 'conflicting states')
|| arrayContainsString($result->suggestions, 'conflicting states');
expect($hasConflictingStatesWarning)->toBeTrue();
});
it('warns about aria-hidden without tabindex=-1', function () {
$link = new AccessibleLink(
baseLink: HtmlLink::create('/page', 'Page'),
ariaHidden: true,
tabindex: 0
);
$result = $this->validator->validateAccessible($link);
expect($result->hasWarnings())->toBeTrue();
$hasTabindexWarning = arrayContainsString($result->errors, 'tabindex="-1"')
|| arrayContainsString($result->warnings, 'tabindex="-1"')
|| arrayContainsString($result->suggestions, 'tabindex="-1"');
expect($hasTabindexWarning)->toBeTrue();
});
it('suggests tabindex=-1 for current page', function () {
$link = new AccessibleLink(
baseLink: HtmlLink::create('/current', 'Current'),
ariaCurrent: true,
tabindex: 0
);
$result = $this->validator->validateAccessible($link);
expect($result->hasSuggestions())->toBeTrue();
$hasCurrentPageSuggestion = arrayContainsString($result->errors, 'Current page link')
|| arrayContainsString($result->warnings, 'Current page link')
|| arrayContainsString($result->suggestions, 'Current page link');
expect($hasCurrentPageSuggestion)->toBeTrue();
});
});
describe('Best Practices Validation', function () {
it('warns about empty href', function () {
$link = HtmlLink::create('#', 'Empty');
$result = $this->validator->validate($link);
expect($result->hasWarnings())->toBeTrue();
$hasEmptyHrefWarning = arrayContainsString($result->errors, 'empty or hash-only')
|| arrayContainsString($result->warnings, 'empty or hash-only')
|| arrayContainsString($result->suggestions, 'empty or hash-only');
expect($hasEmptyHrefWarning)->toBeTrue();
});
it('suggests shortening very long link text', function () {
$longText = str_repeat('Very long link text ', 10);
$link = HtmlLink::create('/page', $longText);
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasVeryLongSuggestion = arrayContainsString($result->errors, 'very long')
|| arrayContainsString($result->warnings, 'very long')
|| arrayContainsString($result->suggestions, 'very long');
expect($hasVeryLongSuggestion)->toBeTrue();
});
it('suggests file type indication for downloads', function () {
$link = HtmlLink::download('/document', 'Download Document');
$result = $this->validator->validate($link);
expect($result->hasSuggestions())->toBeTrue();
$hasFileTypeSuggestion = arrayContainsString($result->errors, 'file type')
|| arrayContainsString($result->warnings, 'file type')
|| arrayContainsString($result->suggestions, 'file type');
expect($hasFileTypeSuggestion)->toBeTrue();
});
});
describe('Batch Validation', function () {
it('validates multiple links', function () {
$links = [
HtmlLink::create('https://example.com', 'Example'),
HtmlLink::external('https://external.com', 'External'),
HtmlLink::create('/internal', 'Internal'),
];
$results = $this->validator->validateBatch($links);
expect($results)->toHaveCount(3)
->and($results[0])->toBeInstanceOf(\App\Framework\Core\ValueObjects\LinkValidationResult::class);
});
it('generates validation summary', function () {
$results = [
\App\Framework\Core\ValueObjects\LinkValidationResult::valid(),
\App\Framework\Core\ValueObjects\LinkValidationResult::invalid(['Error 1']),
\App\Framework\Core\ValueObjects\LinkValidationResult::valid(),
];
$summary = $this->validator->getSummary($results);
expect($summary['total_links'])->toBe(3)
->and($summary['valid_links'])->toBe(2)
->and($summary['invalid_links'])->toBe(1)
->and($summary['total_errors'])->toBe(1)
->and($summary['validation_rate'])->toBe(66.67);
});
});
describe('Strict Mode', function () {
it('treats warnings as errors in strict mode', function () {
$strictValidator = new LinkValidator(strictMode: true);
$link = HtmlLink::download('/file.pdf', 'Download'); // Has warnings
$result = $strictValidator->validate($link);
expect($result->isValid)->toBeFalse() // Warnings fail in strict mode
->and($result->hasWarnings())->toBeTrue();
});
});
});
describe('LinkValidationResult Value Object', function () {
it('can create valid result', function () {
$result = \App\Framework\Core\ValueObjects\LinkValidationResult::valid();
expect($result->isValid)->toBeTrue()
->and($result->hasErrors())->toBeFalse();
});
it('can create invalid result', function () {
$result = \App\Framework\Core\ValueObjects\LinkValidationResult::invalid(
['Error 1', 'Error 2'],
['Warning 1']
);
expect($result->isValid)->toBeFalse()
->and($result->hasErrors())->toBeTrue()
->and($result->hasWarnings())->toBeTrue()
->and($result->getIssueCount())->toBe(3);
});
it('can get all issues combined', function () {
$result = new \App\Framework\Core\ValueObjects\LinkValidationResult(
isValid: false,
errors: ['Error 1'],
warnings: ['Warning 1', 'Warning 2']
);
$issues = $result->getAllIssues();
expect($issues)->toHaveCount(3)
->and($issues)->toContain('Error 1')
->and($issues)->toContain('Warning 1');
});
it('converts to array for logging', function () {
$result = \App\Framework\Core\ValueObjects\LinkValidationResult::invalid(
['Error 1'],
['Warning 1']
);
$array = $result->toArray();
expect($array)->toHaveKey('is_valid')
->and($array)->toHaveKey('errors')
->and($array)->toHaveKey('warnings')
->and($array['error_count'])->toBe(1)
->and($array['warning_count'])->toBe(1);
});
});