Testing your application
The package proves its answers in its own suite. Your application can change them: a custom answer, a sign-in page that shows the status, a form request with more fields. So the package ships the assertion it uses, for your suite to prove the property where it matters.
The assertion
Pushery\FortifyHardening\Testing\AssertsEnumerationSafety provides assertEnumerationSafe(). It
sends the same request once for an identity that has an account and once for one that has none,
runs your own expectation on each response, and then requires the two to be indistinguishable to
the caller.
public function assertEnumerationSafe(
Closure $submit, // sends the request for one identity, returns the TestResponse
string $known, // an identity with an account
string $unknown, // an identity without one
?Closure $expect = null, // your expectation, run on each response
): void
Indistinguishable means the same fingerprint, which compares five things:
| Field | What it is |
|---|---|
status | the HTTP status |
location | the redirect target |
status_line | the status the response flashed to the session |
errors | the validation errors it flashed |
body | the body of a JSON response |
The body of an HTML page is left out, because a page carries a CSRF token of its own on every
request. enumerationFingerprint() is public and static, should you want to compare fingerprints
yourself. It reads the session the way Laravel's own session assertions do, so it also works with
session.serialization set to json.
With Pest
use Illuminate\Testing\TestResponse;
use Pushery\FortifyHardening\Testing\AssertsEnumerationSafety;
uses(AssertsEnumerationSafety::class);
it('answers a taken address as it answers a free one', function () {
$this->assertEnumerationSafe(
fn (string $email): TestResponse => $this->post('/register', [
'name' => 'Grace',
'email' => $email,
'password' => 'a-long-password',
'password_confirmation' => 'a-long-password',
]),
function (TestResponse $response): void {
$response->assertRedirect(route('login'))->assertSessionHasNoErrors();
$this->assertGuest();
},
);
});
With PHPUnit
use Illuminate\Testing\TestResponse;
use Pushery\FortifyHardening\Testing\AssertsEnumerationSafety;
use Tests\TestCase;
class PasswordResetLinkTest extends TestCase
{
use AssertsEnumerationSafety;
public function test_an_unknown_address_reads_like_a_sent_link(): void
{
$this->assertEnumerationSafe(
fn (string $email): TestResponse => $this->from('/forgot-password')
->post('/forgot-password', ['email' => $email]),
fn (TestResponse $response) => $response->assertSessionHas('status', __('passwords.sent')),
);
}
}
The password reset
A reset needs a token, and the property holds for a wrong one:
$this->assertEnumerationSafe(
fn (string $email): TestResponse => $this->from('/reset-password/a-token')->post('/reset-password', [
'token' => 'not-the-token',
'email' => $email,
'password' => 'a-new-password',
'password_confirmation' => 'a-new-password',
]),
fn (TestResponse $response) => $response->assertSessionHasErrors(['email' => __('passwords.token')]),
);
Proving that it can fail
An assertion that has only ever passed proves little. Run it once against an answer that does tell the cases apart, for example with the package's switch for that route turned off, and watch it fail before you rely on it.