---
title: "Testing — Crypto for Laravel | Roundly"
description: "Why there is no Crypto::fake(), plus ready-made test keys, known OTP vectors, throwaway certificate chains and opt-in Pest expectations."
url: https://roundly-consulting.com/open-source/docs/crypto-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Crypto for Laravel](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)

# Testing

There is no Crypto::fake(), and nothing needs one: the facade makes no database, queue, event, mail or HTTP call. Apart from randomness and the key loaders, every call is a pure function of its arguments, so run it for real — and for random output, assert the shape rather than the value.

The key loaders read your config or a disk, and fromStorageOrGenerate() writes on first boot. That goes through Laravel’s Storage and config(), so Storage::fake('local') and config(\[...\]) already cover fromStorageOrGenerate() and the \*FromConfig loaders in your tests.

## Test keys and OTP vectors

The Testing namespace gives your suite ready-made key material and known OTP vectors instead of hand-rolled ones. It ships in src/ but imports no PHPUnit or Pest symbol:

```php
use RoundlyConsulting\Crypto\Testing\TestKeys;
use RoundlyConsulting\Crypto\Testing\TestOtp;

$secret = TestKeys::hmacSecret();      // a fixed, valid 64-byte secret (every HS tier)
$rsa    = TestKeys::rsa();             // ephemeral 2048-bit private key
$ec     = TestKeys::ec('P-384');       // ephemeral EC private key

if (TestKeys::supportsEd25519()) {
    $okp = TestKeys::ed25519();        // guard on hosts without ext-sodium
}

$code = TestOtp::codeAt(time());       // a valid TOTP code for TestOtp::SECRET
```

TestKeys::hmacSecret() is a fixed, valid 64-byte secret — long enough for every HS tier, HS512 included — and TestOtp::SECRET a fixed base32 secret; the RSA, EC and Ed25519 factories mint fresh ephemeral keys on each call. Never use any of them in production.

## Throwaway certificate chains

TestCertificates mints genuinely linked chains — writing its own temporary openssl.cnf with the sections it needs — so no suite has to hand-roll CSRs and CA extensions. CA keys are EC P-256; the leaf may be EC or RSA:

```php
use RoundlyConsulting\Crypto\Hash\HashAlgorithm;
use RoundlyConsulting\Crypto\Testing\TestCertificates;

$ca = TestCertificates::chain();                  // leaf → intermediate → root, genuinely linked
$ca->leafKey;                                     // the leaf's PRIVATE key — sign your test token with it
$ca->x5c();                                       // ready to drop into a JWS `x5c` header
$ca->pinnedFingerprints();                        // the [intermediate, root] slice a pinning verifier compares (SHA-256)
$ca->pinnedFingerprints(HashAlgorithm::Sha1);     // for a verifier that pins SHA-1
$ca->pemBundle();

$rogue = TestCertificates::rogueLeaf($ca);        // same subject, a different CA — breaks isLinked()
$self  = TestCertificates::selfSigned(['app.test']);
```

fingerprints() and pinnedFingerprints() default to SHA-256, like Chain::fingerprints(). A verifier that pins SHA-1 needs pinnedFingerprints(HashAlgorithm::Sha1) — with the default it would compare SHA-256 values and reject every chain for the wrong reason.

ext-openssl always stamps notBefore at signing time, so produce expired or not-yet-valid scenarios by evaluating at another instant rather than backdating a certificate:

```php
$leaf = $ca->leaf();

$leaf->isExpiredAt($leaf->notAfter()->addDay());        // expired
$leaf->isNotYetValidAt($leaf->notBefore()->subDay());   // not yet valid
CarbonImmutable::setTestNow($leaf->notAfter()->addYear());
```

## Leaves that carry extensions

TestLeafOptions decorates the leaf with raw extensions, extended key usage OIDs, a directoryName SAN, an empty subject or an OU — so you can test your own reading of an extension through Certificate::extension() and DerDecoder:

```php
use RoundlyConsulting\Crypto\Testing\TestCertificates;
use RoundlyConsulting\Crypto\Testing\TestLeafOptions;

$nonce = random_bytes(32);

$chain = TestCertificates::chain(length: 2, leafOptions: new TestLeafOptions(
    rawExtensions: ['1.2.840.113635.100.8.2' => "\x30\x24\xA1\x22\x04\x20".$nonce],
    subjectOrganizationalUnit: 'Authenticator Attestation',
));

$chain->leaf()->extension('1.2.840.113635.100.8.2')->der;   // exactly the bytes above
```

## Opt-in Pest expectations

Five Pest expectations ship in a non-autoloaded file. Require it from your own tests/Pest.php — it is guarded by function\_exists('expect'), so it never loads at runtime:

```php
// tests/Pest.php
require dirname(__DIR__).'/vendor/roundly-consulting/crypto-for-laravel/src/Testing/pest-expectations.php';

expect($token)->toBeValidJws($verifier, Algorithm::RS256);
expect($code)->toBeValidTotp($secret);
expect($ca->chain)->toBeLinked();                     // the math, not trust
expect($ca->leaf())->toBeSignedBy($ca->chain->get(1));  // the intermediate signed the leaf
expect($ca->chain->get(1))->toBeSignedBy($ca->root());
expect($jwk)->toHaveThumbprint('NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs');
```

## A complete test

```php
use RoundlyConsulting\Crypto\Jose\Jws;
use RoundlyConsulting\Crypto\Signature\Algorithm;
use RoundlyConsulting\Crypto\Signature\Es;
use RoundlyConsulting\Crypto\Signature\Key\EcKey;
use RoundlyConsulting\Crypto\Testing\TestKeys;
use RoundlyConsulting\Crypto\Testing\TestOtp;

it('issues a verifiable ES256 token', function () {
    $key = TestKeys::ec();                                  // ephemeral P-256 private key
    $token = (new Jws)->sign([], ['sub' => 'alice'], new Es($key));

    expect($token)->toBeValidJws(new Es(EcKey::public($key->publicPem())), Algorithm::ES256);
});

it('accepts the current TOTP code', function () {
    expect(TestOtp::codeAt(time()))->toBeValidTotp(TestOtp::SECRET);
});
```

## Pinning the clock

Totp and Claims::assertTemporal() read the current time via CarbonImmutable::now(), so pin it with Laravel’s helper:

```php
use Illuminate\Support\Carbon;

Carbon::setTestNow('2026-07-10 12:00:00');
// … assert TOTP codes / token expiry deterministically …
Carbon::setTestNow();   // clear
```

## The package’s own suite

The package pins its behaviour with committed RFC test vectors, Project Wycheproof corpora and fuzz targets on every attacker-facing input. To run it from a clone of the repository:

```bash
composer test
CRYPTO_FUZZ_ITERATIONS=100000 composer test   # a longer fuzz run
```

## 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/crypto-for-laravel.md)
