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(), firstAcceptance() and owed() return it,
// and hasCurrent('terms') answers false — 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.

The fake identifies a subject and a locale the same way. A write records the subject under its morph alias, the value getMorphClass() returns and the real manager stores in subject_type, so under Relation::enforceMorphMap() an unmapped model throws in the fake where it throws in production. A read without a locale answers for legal-consent.default_locale, not for a fixed language, and a write without one records the context's locale or, failing that, the same default.

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.