Skip to main content

Testing your integration

Consent::fake() swaps the consent manager for an in-memory double. Nothing it does touches a database, so your application's tests can exercise consent screens, the gate and the register form without migrating this package's tables into your test schema.

use Pushery\LegalConsent\Facades\Consent;

it('records acceptance when someone registers', function (): void {
$fake = Consent::fake();

$this->post('/register', ['email' => '[email protected]', 'terms' => '1']);

$fake->assertAccepted(User::firstOrFail(), 'terms');
});

The defaults describe a fully-consented subject

A fresh fake answers every read as if the subject owed nothing: outstanding() is empty, hasCurrent() is true, statusFor() and history() are empty, published() returns null, and registrationChecklist() is empty.

That direction is deliberate. A test about a checkout or a profile update must not start failing because a consent gate it never mentioned decided the subject owes a document. A test that cares about consent declares what it cares about; a test that does not gets out of the gate's way.

Declaring what the reads answer

$fake = Consent::fake();

// This subject still owes the terms. outstanding() returns it and hasCurrent('terms')
// answers false — the two reads a gate makes, kept consistent so you cannot arrange a
// subject who both owes a document and holds it.
$fake->owes($user, 'terms');

// What a public page renders. Note there is NO locale fallback, exactly as in production:
// published('terms', 'en') stays null unless you declared an 'en' document.
$fake->publishes(new PublishedDocument(/* … */));

// What a register form is told to show.
$fake->checklistIs($termsItem, $privacyItem);

// The status map and the Art. 15 history payload.
$fake->statusIs([...])->historyIs([...]);

The documents outstanding() hands back — from the fake and from the real manager alike — carry only the attributes a consent screen needs, not the full row. Under Model::shouldBeStrict() reaching for anything outside that set throws, and the fake is built to throw in the same place the real manager does. That symmetry is the point of a double: a test that passes here has to pass in production for the same reasons, and a fake that answered null where the real thing throws would be reassurance rather than a test.

Writes are recorded, never performed

record(), accept(), withdraw(), object(), terminate(), requestConfirmation() and confirm() return an unsaved LegalConsent carrying the attributes they were called with, so code that reads the returned model keeps working. $consent->exists stays false — the honest answer, since nothing was written, and code that persists or reloads it fails loudly instead of asserting against a row that never existed. forget() records the subject it was handed and answers with an all-zero SubjectErasure.

AssertionWhat it checks
assertRecorded($subject, $key, $action = null)An entry for that subject and document, optionally of that action
assertNotRecorded($subject, $key, $action = null)The same, negated
assertAccepted($subject, $key)The subject agreed — see the note below
assertWithdrawn($subject, $key)A withdrawal was recorded
assertForgotten($subject)Consent::forget() was called for that subject — see Retention
assertNotForgotten($subject)The half that catches an over-eager erasure
assertNothingRecorded()No write at all
assertRecordedCount($n)Exactly this many writes
recorded($subject = null, $key = null, $action = null)The raw entries, for anything else

The erasure pair is not simulated, only recorded: the fake never rewrites a ledger, because a second implementation of the chain walk would be worth less than none. When the rewrite itself is what a test is about, assert against the real manager.

A failure names what was recorded, so you are not sent back into the code to find out.

assertAccepted() and the four acceptance actions

It matches granted, acknowledged and re_accepted. Which of the three the real manager writes depends on the document's type and on the subject's history, and your test has no reason to know either — asserting on one of them would make the test break when a document's legal basis changes, which is not what it is about.

It deliberately does not match deemed_accepted. Silence counting as acceptance is the § 308 Nr. 5 legal fiction, not an act of the subject, so "the user accepted" must never be satisfied by the user having said nothing. Assert that one explicitly with assertRecorded($user, 'terms', ConsentAction::DeemedAccepted) when it is what you mean.

Why the fake ships with the package

The ConsentManager interface has grown with almost every release — double opt-in added two methods, the Art. 17 erasure a third. Hand-rolling a stub of it in your application works exactly once: the next release that adds a method breaks your stub, and it breaks at a moment that has nothing to do with the change you were making. A double that ships with the interface moves with it.