- 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.
274 lines
8.7 KiB
PHP
274 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Config\Environment;
|
|
use App\Framework\Core\ValueObjects\Email;
|
|
use App\Framework\Deployment\Ssl\Jobs\SslCertificateRenewalJob;
|
|
use App\Framework\Deployment\Ssl\Services\SslCertificateService;
|
|
use App\Framework\Deployment\Ssl\ValueObjects\CertificateMode;
|
|
use App\Framework\Deployment\Ssl\ValueObjects\CertificateStatus;
|
|
use App\Framework\Deployment\Ssl\ValueObjects\DomainName;
|
|
use App\Framework\Deployment\Ssl\ValueObjects\SslConfiguration;
|
|
use App\Framework\Filesystem\FilePath;
|
|
use App\Framework\Logging\Logger;
|
|
use App\Framework\Worker\Every;
|
|
use App\Framework\Worker\Schedule;
|
|
|
|
describe('SslCertificateRenewalJob', function () {
|
|
beforeEach(function () {
|
|
$this->sslService = Mockery::mock(SslCertificateService::class);
|
|
$this->environment = Mockery::mock(Environment::class);
|
|
$this->logger = Mockery::mock(Logger::class);
|
|
|
|
// Setup environment mock
|
|
$this->environment->shouldReceive('get')
|
|
->with('DOMAIN_NAME', 'michaelschiemer.de')
|
|
->andReturn('example.com');
|
|
$this->environment->shouldReceive('get')
|
|
->with('SSL_EMAIL', 'mail@michaelschiemer.de')
|
|
->andReturn('admin@example.com');
|
|
$this->environment->shouldReceive('get')
|
|
->with('LETSENCRYPT_STAGING', '0')
|
|
->andReturn('0');
|
|
|
|
$this->job = new SslCertificateRenewalJob(
|
|
$this->sslService,
|
|
$this->environment,
|
|
$this->logger
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('has schedule attribute', function () {
|
|
$reflection = new ReflectionClass(SslCertificateRenewalJob::class);
|
|
$attributes = $reflection->getAttributes(Schedule::class);
|
|
|
|
expect($attributes)->toHaveCount(1);
|
|
|
|
$schedule = $attributes[0]->newInstance();
|
|
expect($schedule->at)->toBeInstanceOf(Every::class);
|
|
expect($schedule->at->days)->toBe(1);
|
|
});
|
|
|
|
it('skips renewal when certificate does not exist', function () {
|
|
$status = CertificateStatus::notFound();
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($status);
|
|
|
|
// Renewal should not be attempted
|
|
$this->sslService->shouldNotReceive('renew');
|
|
|
|
$this->logger->shouldReceive('info')->once();
|
|
$this->logger->shouldReceive('warning')->once();
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeFalse();
|
|
expect($result['reason'])->toBe('certificate_not_found');
|
|
});
|
|
|
|
it('skips renewal when certificate is valid and not expiring', function () {
|
|
$status = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('-30 days'),
|
|
notAfter: new DateTimeImmutable('+60 days'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 60,
|
|
isExpiring: false,
|
|
isExpired: false
|
|
);
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($status);
|
|
|
|
// Renewal should not be attempted
|
|
$this->sslService->shouldNotReceive('renew');
|
|
|
|
$this->logger->shouldReceive('info')->twice();
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect($result['renewed'])->toBeFalse();
|
|
expect($result['reason'])->toBe('not_needed');
|
|
expect($result['days_until_expiry'])->toBe(60);
|
|
});
|
|
|
|
it('renews certificate when expiring', function () {
|
|
$oldStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('-60 days'),
|
|
notAfter: new DateTimeImmutable('+20 days'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 20,
|
|
isExpiring: true,
|
|
isExpired: false
|
|
);
|
|
|
|
$newStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('now'),
|
|
notAfter: new DateTimeImmutable('+90 days'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 90,
|
|
isExpiring: false,
|
|
isExpired: false
|
|
);
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($oldStatus);
|
|
|
|
$this->sslService->shouldReceive('renew')
|
|
->once()
|
|
->andReturn($newStatus);
|
|
|
|
$this->logger->shouldReceive('info')->times(3);
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect($result['renewed'])->toBeTrue();
|
|
expect($result['days_until_expiry'])->toBe(90);
|
|
});
|
|
|
|
it('renews certificate when expired', function () {
|
|
$oldStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: false,
|
|
notBefore: new DateTimeImmutable('-180 days'),
|
|
notAfter: new DateTimeImmutable('-10 days'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: -10,
|
|
isExpiring: false,
|
|
isExpired: true
|
|
);
|
|
|
|
$newStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('now'),
|
|
notAfter: new DateTimeImmutable('+90 days'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 90,
|
|
isExpiring: false,
|
|
isExpired: false
|
|
);
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($oldStatus);
|
|
|
|
$this->sslService->shouldReceive('renew')
|
|
->once()
|
|
->andReturn($newStatus);
|
|
|
|
$this->logger->shouldReceive('info')->times(3);
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect($result['renewed'])->toBeTrue();
|
|
});
|
|
|
|
it('handles renewal exception gracefully', function () {
|
|
$status = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: null,
|
|
notAfter: null,
|
|
issuer: null,
|
|
subject: null,
|
|
daysUntilExpiry: 20,
|
|
isExpiring: true,
|
|
isExpired: false
|
|
);
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($status);
|
|
|
|
$this->sslService->shouldReceive('renew')
|
|
->once()
|
|
->andThrow(new \RuntimeException('Renewal failed'));
|
|
|
|
$this->logger->shouldReceive('info')->twice();
|
|
$this->logger->shouldReceive('error')->once();
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeFalse();
|
|
expect($result['error'])->toBe('Renewal failed');
|
|
});
|
|
|
|
it('logs renewal details', function () {
|
|
$oldStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('2024-01-01'),
|
|
notAfter: new DateTimeImmutable('2024-03-15'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 20,
|
|
isExpiring: true,
|
|
isExpired: false
|
|
);
|
|
|
|
$newStatus = new CertificateStatus(
|
|
exists: true,
|
|
isValid: true,
|
|
notBefore: new DateTimeImmutable('2024-03-01'),
|
|
notAfter: new DateTimeImmutable('2024-05-30'),
|
|
issuer: 'Let\'s Encrypt',
|
|
subject: 'example.com',
|
|
daysUntilExpiry: 90,
|
|
isExpiring: false,
|
|
isExpired: false
|
|
);
|
|
|
|
$this->sslService->shouldReceive('getStatus')
|
|
->once()
|
|
->andReturn($oldStatus);
|
|
|
|
$this->sslService->shouldReceive('renew')
|
|
->once()
|
|
->andReturn($newStatus);
|
|
|
|
$this->logger->shouldReceive('info')
|
|
->with('Running scheduled SSL certificate renewal check', Mockery::any())
|
|
->once();
|
|
|
|
$this->logger->shouldReceive('info')
|
|
->with('SSL certificate renewal needed, starting renewal', Mockery::any())
|
|
->once();
|
|
|
|
$this->logger->shouldReceive('info')
|
|
->with('SSL certificate renewed successfully', Mockery::on(function ($context) {
|
|
return isset($context['old_expiry'])
|
|
&& isset($context['new_expiry'])
|
|
&& isset($context['new_days_until_expiry']);
|
|
}))
|
|
->once();
|
|
|
|
$result = $this->job->handle();
|
|
|
|
expect($result['success'])->toBeTrue();
|
|
expect($result['renewed'])->toBeTrue();
|
|
});
|
|
});
|