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); }); }); });