Skip to main content

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:

FieldWhat it is
statusthe HTTP status
locationthe redirect target
status_linethe status the response flashed to the session
errorsthe validation errors it flashed
bodythe 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 () {
User::factory()->create(['email' => '[email protected]']);

$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
{
User::factory()->create(['email' => '[email protected]']);

$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.