- 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.
302 lines
11 KiB
PHP
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);
|
|
});
|
|
});
|
|
});
|