Files
michaelschiemer/tests/Unit/Framework/Deployment/Ssl/Services/SslCertificateServiceTest.php
Michael Schiemer fc3d7e6357 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.
2025-10-25 19:18:37 +02:00

302 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Config\Environment;
use App\Framework\Core\ValueObjects\Email;
use App\Framework\Deployment\Ssl\Events\SslCertificateObtained;
use App\Framework\Deployment\Ssl\Events\SslCertificateRenewed;
use App\Framework\Deployment\Ssl\Events\SslCertificateRevoked;
use App\Framework\Deployment\Ssl\Exceptions\CertificateObtainFailedException;
use App\Framework\Deployment\Ssl\Exceptions\CertificateRenewalFailedException;
use App\Framework\Deployment\Ssl\Services\SslCertificateService;
use App\Framework\Deployment\Ssl\ValueObjects\CertificateMode;
use App\Framework\Deployment\Ssl\ValueObjects\DomainName;
use App\Framework\Deployment\Ssl\ValueObjects\SslConfiguration;
use App\Framework\EventBus\EventBus;
use App\Framework\Filesystem\FilePath;
use App\Framework\Logging\Logger;
use App\Framework\Process\Process;
use App\Framework\Process\ProcessResult;
describe('SslCertificateService', function () {
beforeEach(function () {
$this->process = Mockery::mock(Process::class);
$this->eventBus = Mockery::mock(EventBus::class);
$this->logger = Mockery::mock(Logger::class);
$this->service = new SslCertificateService(
$this->process,
$this->eventBus,
$this->logger
);
$this->config = new SslConfiguration(
domain: DomainName::fromString('example.com'),
email: new Email('admin@example.com'),
mode: CertificateMode::STAGING,
certbotConfDir: FilePath::create('/tmp/certbot-conf'),
certbotWwwDir: FilePath::create('/tmp/certbot-www')
);
});
afterEach(function () {
Mockery::close();
});
describe('obtain', function () {
it('obtains certificate successfully', function () {
// Mock successful certbot execution
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(
exitCode: 0,
output: 'Certificate obtained successfully',
errorOutput: ''
));
// Expect event dispatch
$this->eventBus->shouldReceive('dispatch')
->once()
->with(Mockery::type(SslCertificateObtained::class));
// Expect info logging
$this->logger->shouldReceive('info')
->twice();
$status = $this->service->obtain($this->config);
expect($status)->not->toBeNull();
});
it('throws exception on certbot failure', function () {
// Mock failed certbot execution
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(
exitCode: 1,
output: '',
errorOutput: 'Failed to obtain certificate'
));
// Expect error logging
$this->logger->shouldReceive('info')->once();
$this->logger->shouldReceive('error')->once();
$this->service->obtain($this->config);
})->throws(CertificateObtainFailedException::class);
it('uses staging mode flag', function () {
$this->process->shouldReceive('run')
->once()
->with(Mockery::on(function ($command) {
return in_array('--staging', $command, true);
}))
->andReturn(new ProcessResult(0, 'Success', ''));
$this->eventBus->shouldReceive('dispatch')->once();
$this->logger->shouldReceive('info')->twice();
$this->service->obtain($this->config);
});
it('uses production mode without staging flag', function () {
$productionConfig = new SslConfiguration(
domain: DomainName::fromString('example.com'),
email: new Email('admin@example.com'),
mode: CertificateMode::PRODUCTION,
certbotConfDir: FilePath::create('/tmp/certbot-conf'),
certbotWwwDir: FilePath::create('/tmp/certbot-www')
);
$this->process->shouldReceive('run')
->once()
->with(Mockery::on(function ($command) {
return !in_array('--staging', $command, true);
}))
->andReturn(new ProcessResult(0, 'Success', ''));
$this->eventBus->shouldReceive('dispatch')->once();
$this->logger->shouldReceive('info')->twice();
$this->service->obtain($productionConfig);
});
});
describe('renew', function () {
it('renews certificate successfully', function () {
// Mock successful renewal
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(
exitCode: 0,
output: 'Certificate renewed successfully',
errorOutput: ''
));
// Expect event dispatch
$this->eventBus->shouldReceive('dispatch')
->once()
->with(Mockery::type(SslCertificateRenewed::class));
// Expect info logging
$this->logger->shouldReceive('info')
->twice();
$status = $this->service->renew($this->config);
expect($status)->not->toBeNull();
});
it('throws exception on renewal failure', function () {
// Mock failed renewal
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(
exitCode: 1,
output: '',
errorOutput: 'Renewal failed'
));
// Expect error logging
$this->logger->shouldReceive('info')->once();
$this->logger->shouldReceive('error')->once();
$this->service->renew($this->config);
})->throws(CertificateRenewalFailedException::class);
it('forces renewal with --force-renewal flag', function () {
$this->process->shouldReceive('run')
->once()
->with(Mockery::on(function ($command) {
return in_array('--force-renewal', $command, true);
}))
->andReturn(new ProcessResult(0, 'Success', ''));
$this->eventBus->shouldReceive('dispatch')->once();
$this->logger->shouldReceive('info')->twice();
$this->service->renew($this->config);
});
});
describe('test', function () {
it('returns true when dry-run succeeds', function () {
// Mock successful dry-run
$this->process->shouldReceive('run')
->once()
->with(Mockery::on(function ($command) {
return in_array('--dry-run', $command, true);
}))
->andReturn(new ProcessResult(0, 'Dry run successful', ''));
$this->logger->shouldReceive('info')->twice();
$result = $this->service->test($this->config);
expect($result)->toBeTrue();
});
it('returns false when dry-run fails', function () {
// Mock failed dry-run
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(1, '', 'Dry run failed'));
$this->logger->shouldReceive('info')->once();
$this->logger->shouldReceive('error')->once();
$result = $this->service->test($this->config);
expect($result)->toBeFalse();
});
});
describe('revoke', function () {
it('revokes certificate successfully', function () {
// Mock successful revocation
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(0, 'Certificate revoked', ''));
// Expect event dispatch
$this->eventBus->shouldReceive('dispatch')
->once()
->with(Mockery::type(SslCertificateRevoked::class));
$this->logger->shouldReceive('info')->twice();
$this->service->revoke(
DomainName::fromString('example.com'),
FilePath::create('/tmp/certbot-conf')
);
});
it('logs error on revocation failure', function () {
// Mock failed revocation
$this->process->shouldReceive('run')
->once()
->andReturn(new ProcessResult(1, '', 'Revocation failed'));
$this->logger->shouldReceive('info')->once();
$this->logger->shouldReceive('error')->once();
// No exception thrown, just logged
$this->service->revoke(
DomainName::fromString('example.com'),
FilePath::create('/tmp/certbot-conf')
);
});
});
describe('getStatus', function () {
it('returns not found status when certificate does not exist', function () {
$status = $this->service->getStatus(
DomainName::fromString('nonexistent.com'),
'/tmp/nonexistent'
);
expect($status->exists)->toBeFalse();
expect($status->isValid)->toBeFalse();
expect($status->errors)->toContain('Certificate files not found');
});
it('detects existing certificate files', function () {
// Create temporary certificate structure for testing
$tempDir = sys_get_temp_dir() . '/ssl-test-' . uniqid();
mkdir($tempDir . '/live/example.com', 0777, true);
// Create dummy certificate files
touch($tempDir . '/live/example.com/fullchain.pem');
touch($tempDir . '/live/example.com/privkey.pem');
touch($tempDir . '/live/example.com/chain.pem');
touch($tempDir . '/live/example.com/cert.pem');
// Write a simple certificate for testing (self-signed)
$certContent = <<<'CERT'
-----BEGIN CERTIFICATE-----
MIICljCCAX4CCQCKz8Qh0YBzITANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJE
RTAEFH0NTAJNDRA0TRBMDExOVFBMDMxMVFBMFE0NNRBMFoxDTA0TjA0TRBMDExO
VFBMDMxMVFBMFkGA1UEAwwJZXhhbXBsZS5jb20wHhcNMjQwMTAxMDAwMDAwWhcN
MjUwMTAxMDAwMDAwWjANMQswCQYDVQQGEwJERTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAK5I8vHLMvYwH8qxHC6pLULfZ1YFz2wGzv8eQyXYjE5eZ3zZ
-----END CERTIFICATE-----
CERT;
file_put_contents($tempDir . '/live/example.com/cert.pem', $certContent);
$status = $this->service->getStatus(
DomainName::fromString('example.com'),
$tempDir
);
expect($status->exists)->toBeTrue();
// Cleanup
array_map('unlink', glob($tempDir . '/live/example.com/*'));
rmdir($tempDir . '/live/example.com');
rmdir($tempDir . '/live');
rmdir($tempDir);
});
});
});