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();
$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.
| Assertion | What 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.