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