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,57 @@
<?php
declare(strict_types=1);
use App\Framework\Deployment\Ssl\ValueObjects\CertificateMode;
describe('CertificateMode', function () {
it('has production mode', function () {
expect(CertificateMode::PRODUCTION->value)->toBe('production');
});
it('has staging mode', function () {
expect(CertificateMode::STAGING->value)->toBe('staging');
});
it('detects production mode', function () {
$mode = CertificateMode::PRODUCTION;
expect($mode->isProduction())->toBeTrue();
expect($mode->isStaging())->toBeFalse();
});
it('detects staging mode', function () {
$mode = CertificateMode::STAGING;
expect($mode->isStaging())->toBeTrue();
expect($mode->isProduction())->toBeFalse();
});
it('returns correct certbot flag for production', function () {
$mode = CertificateMode::PRODUCTION;
expect($mode->toCertbotFlag())->toBe('');
});
it('returns correct certbot flag for staging', function () {
$mode = CertificateMode::STAGING;
expect($mode->toCertbotFlag())->toBe('--staging');
});
it('has descriptive text for production', function () {
$mode = CertificateMode::PRODUCTION;
expect($mode->getDescription())
->toContain('Production')
->toContain('Let\'s Encrypt');
});
it('has descriptive text for staging', function () {
$mode = CertificateMode::STAGING;
expect($mode->getDescription())
->toContain('Staging')
->toContain('Testing');
});
});

View File

@@ -0,0 +1,210 @@
<?php
declare(strict_types=1);
use App\Framework\Deployment\Ssl\ValueObjects\CertificateStatus;
describe('CertificateStatus', function () {
it('creates not found status', function () {
$status = CertificateStatus::notFound();
expect($status->exists)->toBeFalse();
expect($status->isValid)->toBeFalse();
expect($status->errors)->toContain('Certificate files not found');
});
it('creates status from certificate data', function () {
$notBefore = new DateTimeImmutable('2024-01-01');
$notAfter = new DateTimeImmutable('2024-12-31');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Let\'s Encrypt Authority X3',
'example.com'
);
expect($status->exists)->toBeTrue();
expect($status->issuer)->toBe('Let\'s Encrypt Authority X3');
expect($status->subject)->toBe('example.com');
});
it('detects expired certificate', function () {
$notBefore = new DateTimeImmutable('-2 months');
$notAfter = new DateTimeImmutable('-1 day');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->isExpired)->toBeTrue();
expect($status->isValid)->toBeFalse();
});
it('detects expiring certificate', function () {
$notBefore = new DateTimeImmutable('-2 months');
$notAfter = new DateTimeImmutable('+15 days');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->isExpiring)->toBeTrue();
expect($status->isExpired)->toBeFalse();
});
it('detects valid certificate', function () {
$notBefore = new DateTimeImmutable('-1 month');
$notAfter = new DateTimeImmutable('+60 days');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->isValid)->toBeTrue();
expect($status->isExpiring)->toBeFalse();
expect($status->isExpired)->toBeFalse();
});
it('determines renewal needed for expired certificate', function () {
$notBefore = new DateTimeImmutable('-2 months');
$notAfter = new DateTimeImmutable('-1 day');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->needsRenewal())->toBeTrue();
});
it('determines renewal needed for expiring certificate', function () {
$notBefore = new DateTimeImmutable('-1 month');
$notAfter = new DateTimeImmutable('+20 days');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->needsRenewal())->toBeTrue();
});
it('determines renewal not needed for valid certificate', function () {
$notBefore = new DateTimeImmutable('-1 month');
$notAfter = new DateTimeImmutable('+60 days');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->needsRenewal())->toBeFalse();
});
it('returns correct health status for expired', function () {
$status = new CertificateStatus(
exists: true,
isValid: false,
notBefore: null,
notAfter: null,
issuer: null,
subject: null,
daysUntilExpiry: -10,
isExpiring: false,
isExpired: true
);
expect($status->getHealthStatus())->toBe('expired');
});
it('returns correct health status for expiring', function () {
$status = new CertificateStatus(
exists: true,
isValid: true,
notBefore: null,
notAfter: null,
issuer: null,
subject: null,
daysUntilExpiry: 20,
isExpiring: true,
isExpired: false
);
expect($status->getHealthStatus())->toBe('expiring');
});
it('returns correct health status for invalid', function () {
$status = new CertificateStatus(
exists: true,
isValid: false,
notBefore: null,
notAfter: null,
issuer: null,
subject: null,
daysUntilExpiry: null,
isExpiring: false,
isExpired: false
);
expect($status->getHealthStatus())->toBe('invalid');
});
it('returns correct health status for missing', function () {
$status = CertificateStatus::notFound();
expect($status->getHealthStatus())->toBe('missing');
});
it('returns correct health status for healthy', function () {
$notBefore = new DateTimeImmutable('-1 month');
$notAfter = new DateTimeImmutable('+60 days');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
expect($status->getHealthStatus())->toBe('healthy');
});
it('converts to array', function () {
$notBefore = new DateTimeImmutable('2024-01-01');
$notAfter = new DateTimeImmutable('2024-12-31');
$status = CertificateStatus::fromCertificateData(
$notBefore,
$notAfter,
'Test Issuer',
'example.com'
);
$array = $status->toArray();
expect($array)->toHaveKey('exists');
expect($array)->toHaveKey('is_valid');
expect($array)->toHaveKey('not_before');
expect($array)->toHaveKey('not_after');
expect($array)->toHaveKey('issuer');
expect($array)->toHaveKey('subject');
expect($array)->toHaveKey('days_until_expiry');
expect($array)->toHaveKey('health_status');
});
});

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
use App\Framework\Deployment\Ssl\ValueObjects\DomainName;
describe('DomainName', function () {
it('creates domain name from valid string', function () {
$domain = DomainName::fromString('example.com');
expect($domain->value)->toBe('example.com');
});
it('accepts valid domain formats', function () {
$validDomains = [
'example.com',
'subdomain.example.com',
'sub.domain.example.com',
'example-site.com',
'example123.com',
'123example.com', // Starting with number in label is OK if not first character
'a.com',
'very-long-subdomain-name.example.com',
];
foreach ($validDomains as $domainStr) {
expect(fn() => DomainName::fromString($domainStr))
->not->toThrow(InvalidArgumentException::class);
}
});
it('rejects invalid domain formats', function () {
expect(fn() => DomainName::fromString(''))
->toThrow(InvalidArgumentException::class, 'Domain name cannot be empty');
});
it('rejects domain starting with hyphen', function () {
expect(fn() => DomainName::fromString('-example.com'))
->toThrow(InvalidArgumentException::class);
});
it('rejects domain with invalid characters', function () {
expect(fn() => DomainName::fromString('example$.com'))
->toThrow(InvalidArgumentException::class, 'Domain contains invalid characters');
});
it('rejects domain exceeding maximum length', function () {
$longDomain = str_repeat('a', 254) . '.com';
expect(fn() => DomainName::fromString($longDomain))
->toThrow(InvalidArgumentException::class, 'exceeds maximum length');
});
it('rejects label exceeding maximum length', function () {
$longLabel = str_repeat('a', 64);
expect(fn() => DomainName::fromString($longLabel . '.com'))
->toThrow(InvalidArgumentException::class, 'exceeds maximum length');
});
it('detects wildcard domains', function () {
$wildcard = DomainName::fromString('*.example.com');
expect($wildcard->isWildcard())->toBeTrue();
});
it('detects non-wildcard domains', function () {
$normal = DomainName::fromString('example.com');
expect($normal->isWildcard())->toBeFalse();
});
it('extracts TLD correctly', function () {
$domain = DomainName::fromString('subdomain.example.com');
expect($domain->getTld())->toBe('com');
});
it('extracts subdomain correctly', function () {
$domain = DomainName::fromString('sub.example.com');
expect($domain->getSubdomain())->toBe('sub');
});
it('returns null for domain without subdomain', function () {
$domain = DomainName::fromString('example.com');
expect($domain->getSubdomain())->toBeNull();
});
it('returns labels array', function () {
$domain = DomainName::fromString('sub.example.com');
expect($domain->getLabels())->toBe(['sub', 'example', 'com']);
});
it('converts to string', function () {
$domain = DomainName::fromString('example.com');
expect($domain->toString())->toBe('example.com');
expect((string) $domain)->toBe('example.com');
});
it('compares domains correctly', function () {
$domain1 = DomainName::fromString('example.com');
$domain2 = DomainName::fromString('example.com');
$domain3 = DomainName::fromString('other.com');
expect($domain1->equals($domain2))->toBeTrue();
expect($domain1->equals($domain3))->toBeFalse();
});
it('compares domains case-insensitively', function () {
$domain1 = DomainName::fromString('Example.COM');
$domain2 = DomainName::fromString('example.com');
expect($domain1->equals($domain2))->toBeTrue();
});
});