Testing
You can test the whole signing flow in your application without sending mail and without writing to the real disk. The test builds its own small PDF with FPDF, which the package installs, so you need no fixture file.
What to fake
| Fake | Why |
|---|---|
Storage::fake($disk) | Originals, signatures and signed PDFs go to a temporary disk that is emptied between tests |
Notification::fake() | The invitation is a notification (Darvis\Signer\Notifications\SignatureRequested), so no mail goes out and you can assert that it was sent |
Event::fake([...]) with the event classes | Only when you want to assert the events; see the warning below |
Mail::fake() also stops the invitation, but Mail::assertSent() does not see it, because it is a notification and not a mailable. Use Notification::fake() when you want to assert.
A complete test
tests/Feature/ContractSigningTest.php, in Pest, with the RefreshDatabase trait active for feature tests:
<?php
use Darvis\Signer\Enums\DocumentStatus;
use Darvis\Signer\Events\DocumentCompleted;
use Darvis\Signer\Events\SignerSigned;
use Darvis\Signer\Facades\Signer;
use Darvis\Signer\Notifications\SignatureRequested;
use Darvis\Signer\Support\SignerConfig;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Storage;
it('completes the document when the signer signs', function () {
$disk = app(SignerConfig::class)->disk();
Storage::fake($disk);
Notification::fake();
Event::fake([SignerSigned::class, DocumentCompleted::class]);
// A real one page PDF.
$pdf = new FPDF;
$pdf->AddPage();
$pdf->SetFont('Helvetica', '', 14);
$pdf->Cell(0, 10, 'Test contract');
$path = tempnam(sys_get_temp_dir(), 'contract').'.pdf';
$pdf->Output('F', $path);
$document = Signer::document($path)
->title('Test contract')
->addSigner('Alice Jansen', 'alice@example.com', page: 1, x: 10, y: 80)
->send();
$signer = $document->signers->first();
Notification::assertSentTo($signer, SignatureRequested::class);
expect($document->status)->toBe(DocumentStatus::Pending);
// A real small PNG, like the signature pad sends.
$image = imagecreatetruecolor(120, 40);
imageline($image, 10, 30, 110, 10, imagecolorallocate($image, 255, 255, 255));
ob_start();
imagepng($image);
$signature = 'data:image/png;base64,'.base64_encode((string) ob_get_clean());
$this->post(route('signer.store', $signer), ['signature' => $signature])
->assertRedirect(route('signer.done', $signer));
$document->refresh();
expect($document->status)->toBe(DocumentStatus::Completed);
Storage::disk($disk)->assertExists($document->signed_path);
Event::assertDispatched(SignerSigned::class);
Event::assertDispatched(DocumentCompleted::class);
});
The test sends a document, checks the invitation, posts a signature the way the signing page does, and asserts that the document is completed and the signed PDF exists. In a PHPUnit class the same calls work inside a test method.
Things that trip up a test
- Pass the event classes to
Event::fake([...]). A bareEvent::fake()also silences the model events that fill theuuidcolumns, andsend()then fails with a database error aboutsigner_documents.uuid. - The signature must be a real PNG. Any other value is a validation error on the field
signature(assertSessionHasErrors('signature')) and nothing is stored. -
Open the signing page with a signed link.
route('signer.show', $signer)without a signature answers 403. Build the link the way the mail does:use Illuminate\Support\Facades\URL; $url = URL::temporarySignedRoute('signer.show', now()->addHour(), ['signer' => $signer->uuid]); $this->get($url)->assertOk(); - Test the expiry with time travel. After
$this->travel(73)->hours()the routessigner.pdfandsigner.storeanswer 403 with the default of 72 hours. - The portal login is rate limited. A test that logs in more than five times with one email address gets a 429; call
$this->travel(61)->seconds()in between. - Log in to the portal with
actingAs().$this->actingAs($user)->get(route('signer.portal.dashboard'))answers 200; a guest is redirected tosigner.portal.login. - Set config inside the test, before the code runs, with
config()->set('signer.link_expires_after_hours', 1). The route prefixes, the middleware andportal.enabledcannot be changed after the application has booted.
Testing a failed stamp
To see what your application does when stamping fails, bind a stamper that throws:
use Darvis\Signer\Enums\SignerStatus;
use Darvis\Signer\Models\Document;
use Darvis\Signer\Services\SignatureStamper;
use Darvis\Signer\Support\SignerConfig;
$this->app->instance(SignatureStamper::class, new class(app(SignerConfig::class)) extends SignatureStamper
{
public function stamp(Document $document): string
{
throw new RuntimeException('The stamper broke.');
}
});
$this->post(route('signer.store', $signer), ['signature' => $signature])
->assertSessionHasErrors('signature');
expect($signer->refresh()->status)->toBe(SignerStatus::Pending);
The signer stays Pending, no event is dispatched and the signer can submit again, see When signing fails.