Skip to content

Testing your own code

Signing for real in an application's test suite means a PKCS#12 bundle in its repository and a real CMS built for every case that merely passes through. Neither is necessary.

The package ships its test doubles, because consumers need them too. They are installed through Signet's constructor rather than through a container, since there is no container.

Signing without a certificate

php
use LSNepomuceno\Signet\Signet;
use LSNepomuceno\Signet\Testing\FakeCertificateReader;
use LSNepomuceno\Signet\Testing\FakePdfSigner;
use LSNepomuceno\Signet\Enums\SignatureProfile;

$signer = new FakePdfSigner();

$signet = new Signet(
    signer: $signer,
    certificateReader: new FakeCertificateReader(),
);

// The code under test, unchanged.
$signet->newSignature()->certificate('anything.pfx', '')->pdfContents($pdf)->sign();

$signer->assertSigned();
$signer->assertSignedTimes(1);
$signer->assertSignedWithProfile(SignatureProfile::PadesBT);
$signer->assertCertified();
$signer->assertSealed();
$signer->assertNothingSigned();

assertSigned() takes an optional string the signed document must contain, for the case where what matters is which document was signed rather than that one was.

Two-phase signing is faked too. prepare() returns a real Data\PreparedSignature over the faked document, digest included, so an application can exercise the whole round trip without a certificate:

php
$prepared = $signet->newSignature()->pdfContents($pdf)->prepare();

$signer->assertPrepared();

$signet->complete($prepared, $cmsFromYourService);

$signer->assertCompleted();

What the fake records is on $signer->prepared and $signer->completed. See Two-phase signing.

A real certificate, generated on the spot

When the test needs real key material rather than a double, Testing\DebugCertificate builds throwaway bundles through the ext-openssl functions. No openssl binary is involved, and nothing is written to the repository.

php
use LSNepomuceno\Signet\Testing\DebugCertificate;

DebugCertificate::make();            // a PKCS#12 bundle
DebugCertificate::makePem();         // PEM, certificate and key
DebugCertificate::makeEc();          // an elliptic-curve bundle, P-256 or P-384
DebugCertificate::makeChain();       // a chain, for path validation
DebugCertificate::makeWithKeySize(); // a deliberately weak key, to test a finding
DebugCertificate::makeForPurpose();  // one whose extensions forbid document signing
DebugCertificate::icpBrasil();       // one carrying Brazilian identity extensions
DebugCertificate::makeRevocable();   // one an OCSP or CRL fixture can speak about

The last three exist for the cases a report has to be able to raise: a 1024-bit key, an extendedKeyUsage that permits everything except signing, and a certificate an OCSP response or CRL can be written about.

Timestamps, revocation and processes

The three remaining seams have local substitutes, which is what lets a suite exercise pades-b-t and above without reaching a live authority:

php
use LSNepomuceno\Signet\Testing\LocalTimestampAuthority;
use LSNepomuceno\Signet\Testing\LocalRevocationAuthority;
use LSNepomuceno\Signet\Testing\FakeProcessRunner;

$signet = new Signet(
    processes: new FakeProcessRunner(),
    transport: new LocalTimestampAuthority(),
);

A suite that cannot start a process at all can validate too, by selecting the verifier that needs none:

php
use LSNepomuceno\Signet\Validation\NativeSignatureVerifier;

$signet = new Signet(
    processes: new FakeProcessRunner(),
    verifier: new NativeSignatureVerifier(),
);

LocalTimestampAuthority answers timestamp(), ocsp() and crl() locally. FakeProcessRunner records what would have been executed:

php
$processes->commands();   // every command it was asked to run
$processes->ran($needle);
$processes->count();

Why these ship rather than living in tests/

Because an application testing its own signing path needs exactly the same doubles this package needs, and a double that only exists in a repository's test directory is one every consumer has to write again, slightly differently.

They are excluded from nothing: Testing\ is part of the public API and changing it is a versioned change like any other.

What the package's own suite does

Worth knowing if you are contributing rather than consuming: nothing skips. composer test carries --fail-on-skipped, because every check has to run somewhere and a skipped test is how one quietly stops. Tests in the network group hit a live authority and are the exception, gated offline through LocalTimestampAuthority:

bash
docker compose -f .docker/compose.yaml run --rm php vendor/bin/pest --exclude-group=network

Released under the MIT Licence.