NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Certificates::fake() swaps an in-memory Testing\CertificatesFake in for the facade and the container, so constructor-injected CertificatesManagers, the for() handle and HasCertificates::requestCertificate() are all recorded. It returns the fake, and every assertion is callable on it or on the facade:

use RoundlyConsulting\Certificates\Facades\Certificates;

$fake = Certificates::fake();

$this->post('/sites', ['domain' => 'app.example.com']);

Certificates::assertIssued('app.example.com');
Certificates::assertNothingRevoked();
  • It never touches a provider, the registry, the queue or the event bus — certificates live in memory and every driver() is an in-memory ArrayProvider.
  • Invalid domains (InvalidDomainException), unknown domains and illegal status moves — renewLater() included — still throw, exactly as they do for real.
  • renewDue() works on the in-memory store and returns a real RenewalReport; sync() and prune() return 0.

Assertions

Recorded byAssertions
issue, issueIfMissing, generate, for()->issue(), requestCertificate()assertIssued($domain), assertNotIssued($domain), assertIssuedCount($n), assertNothingIssued(), assertRequested($domain)
recordFailure($domain)assertFailed($domain)
renew, for()->renew() (and inline renewDue)assertRenewed($domain), assertNotRenewed($domain), assertNothingRenewed()
renewLater, for()->renewLater() (and renewDue(queue: true))assertRenewedLater($domain), assertNothingRenewedLater()
renewDueassertRenewedDue(?$thresholdDays), assertNothingRenewedDue()
a renewal failed via failRenewalOf()assertRenewalFailed($domain), assertNoRenewalFailures()
revoke, for()->revoke()assertRevoked($domain, ?$reason), assertNotRevoked($domain), assertNothingRevoked()
expire, for()->expire()assertExpired($domain), assertNothingExpired()
syncassertSynced(?$driver), assertNothingSynced()
pruneassertPruned(?$days), assertNothingPruned()

Every assertion in use:

use RoundlyConsulting\Certificates\Facades\Certificates;

$fake = Certificates::fake();

// Issuance — issue, issueIfMissing, generate, for()->issue(), requestCertificate()
Certificates::issueIfMissing('a.example.com');
Certificates::for('b.example.com')->issue();
$fake->recordFailure('c.example.com');

Certificates::assertIssued('a.example.com');
Certificates::assertNotIssued('c.example.com');
Certificates::assertIssuedCount(2);
Certificates::assertRequested('c.example.com');   // issued, or recorded as failed
Certificates::assertFailed('c.example.com');

// Renewals — renew, renewLater, renewDue
Certificates::renew('a.example.com');
Certificates::for('b.example.com')->renewLater();
Certificates::renewDue(7);

Certificates::assertRenewed('a.example.com');
Certificates::assertNotRenewed('b.example.com');
Certificates::assertRenewedLater('b.example.com');
Certificates::assertRenewedDue(7);                // a run with exactly this threshold
Certificates::assertNoRenewalFailures();

// Revoke and expire
Certificates::revoke('a.example.com', 'key compromise');
Certificates::for('b.example.com')->expire();

Certificates::assertRevoked('a.example.com', 'key compromise');
Certificates::assertNotRevoked('b.example.com');
Certificates::assertExpired('b.example.com');

// Registry maintenance
Certificates::sync('kubernetes');
Certificates::prune(30);

Certificates::assertSynced('kubernetes');
Certificates::assertPruned(30);

And the “nothing happened” counterparts:

Certificates::fake();

// ...code that must leave certificates alone...

Certificates::assertNothingIssued();
Certificates::assertNothingRenewed();
Certificates::assertNothingRenewedLater();
Certificates::assertNothingRenewedDue();
Certificates::assertNothingRevoked();
Certificates::assertNothingExpired();
Certificates::assertNothingSynced();
Certificates::assertNothingPruned();

Seeding and failing renewals

MethodPurpose
seed(Certificate ...$certificates)Store certificates in memory without recording an issuance, to renew, revoke or expire something the test did not issue. Returns the fake.
failRenewalOf(string ...$domains)Make those renewals fail: the row turns Failed, renew() throws, renewDue() reports it under failed and carries on. Returns the fake.
recordFailure(string $domain)Record a failed issuance — drives assertFailed() and assertRequested().

seed() and failRenewalOf() let you test how your code handles a RenewalReport with failures:

use RoundlyConsulting\Certificates\Facades\Certificates;
use RoundlyConsulting\Certificates\Models\Certificate;

$fake = Certificates::fake()
    ->seed(Certificate::factory()->forDomain('a.example.com')->expiring(5)->make())
    ->failRenewalOf('a.example.com');

$report = Certificates::renewDue();

expect($report->hasFailures())->toBeTrue()
    ->and($report->failedDomains())->toBe(['a.example.com']);

Certificates::assertRenewalFailed('a.example.com');

Expiry monitors

monitorExpiry() builds an alerts schedule, so fake it with the alerts Health::fake() and assertMonitored():

use RoundlyConsulting\Alerts\Facades\Health;
use RoundlyConsulting\Certificates\Alerts\CertificateExpiryCheck;
use RoundlyConsulting\Certificates\Facades\Certificates;

$health = Health::fake();

Certificates::monitorExpiry($certificate, $opsTeam)->daily()->save();

$health->assertMonitored(CertificateExpiryCheck::class, $opsTeam);

Against the array driver

To exercise the real issue path — registry rows, events, owners — skip the fake and switch the default driver to the in-memory array driver. Run with the migrations loaded (RefreshDatabase):

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Certificates\Events\CertificateIssued;
use RoundlyConsulting\Certificates\Facades\Certificates;

it('records an issued certificate', function () {
    config(['certificates.default' => 'array']);
    Event::fake([CertificateIssued::class]);

    $certificate = Certificates::issueIfMissing('app.example.com');

    expect($certificate->status->isActive())->toBeTrue()
        ->and($certificate->expires_at)->not->toBeNull();

    Event::assertDispatched(CertificateIssued::class);
});

Model factory

The registry model ships a factory with states for the common scenarios:

use RoundlyConsulting\Certificates\Models\Certificate;

Certificate::factory()->create();                // pending, random domain, kubernetes driver
Certificate::factory()->issued()->create();      // issued, expires in 90 days
Certificate::factory()->expiring(5)->create();   // issued, expires in 5 days
Certificate::factory()->expired()->create();     // expired 5 days ago
Certificate::factory()->failed()->create();      // failed, with last_error
Certificate::factory()->forDomain('shop.example.com')->issued()->create();

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.