Sometimes you have code that depends on time in one way or another. One example I recently had to deal with was a rate limiter in one of our Laravel projects at Ideenreich. It was the rate limiter for login attempts, which only allows 3 login attempts within 10 minutes.
This is an important security feature to prevent brute-force attacks, where an attacker submits many passwords in hopes of guessing correctly. Because it's an essential feature, I wanted to write tests to ensure it behaves as expected and doesn’t break in the future.
But how do you test that the user really is banned for 10 minutes? Of course, you could add a programmatic pause in your test, something like
sleep(60 * 10);
to wait 10 minutes before continuing. However, tests should be as fast as possible, right? Even if a 10-minute coffee break every time you run this test sounds tempting, it would be far from efficient.
Meet Laravel's travel test helper
Fortunately, as in many cases, Laravel has you covered. Since Laravel 8, the base feature test class includes a travel helper method. This helper allows you to manipulate time in various useful ways. Let’s look at the basic usage. For example, to fake that 10 minutes have passed in your test, you can use:
$this->travel(10)->minutes();
Here, you’re telling Laravel to "travel" 10 units into the future, where the unit is minutes.
Of course, you can use other units too:
// Travel 10 milliseconds into the future
$this->travel(10)->milliseconds();
// Travel 10 seconds into the future
$this->travel(10)->seconds();
// Travel 10 hours into the future
$this->travel(10)->hours();
// Travel 10 days into the future
$this->travel(10)->days();
// Travel 10 weeks into the future
$this->travel(10)->weeks();
// Travel 10 years into the future
$this->travel(10)->years();
It's nice to travel into the future, but what about going back in time? It’s just as easy. Add a minus sign in front of your units in the travel() method. For example, to travel 10 minutes into the past, you can do:
$this->travel(-10)->minutes();
The travel helper also makes it easy to travel to a specific date:
$this->travelTo(now()->addMinutes(10));
And, of course, you can always travel back to the present:
$this->travelBack();
As you can see, the travel helper is really powerful and handy when it comes to testing time-related code. An additional advantage, e.g. compared to the Carbon::setTestNow() method, is that Laravel automatically resets the time after each test. You can read more about it in the Laravel Documentation.
What does this mean for our rate limit test?
With this helper in our toolkit, we can easily write a performant test that performs the following steps:
- Make 3 incorrect login attempts
- Travel 9 minutes into the future
- Check that the user is still suspended
- Travel 1 additional minute into the future
- Check that the user can now log in
And this is what the final test looks like:
public function test_rate_limiting_for_login_blocks_users_for_10_minutes()
{
// Create a user for the test
$user = User::factory()->create([
'email' => 'user@example.com',
'password' => Hash::make('password'),
]);
// Make 3 incorrect login attempts with a wrong password
$this->post('/login', [
'email' => $user->email,
'password' => 'passwort',
]);
$this->post('/login', [
'email' => $user->email,
'password' => 'passwort',
]);
$this->post('/login', [
'email' => $user->email,
'password' => 'passwort',
]);
// Check that the rate limit works and the user is suspended
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])
->assertSessionHasErrors();
$this->assertGuest();
// Travel 9 minutes into the future
$this->travel(9)->minutes();
// Check that the user is still suspended
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])
->assertSessionHasErrors();
$this->assertGuest();
// Travel one more minute into the future
$this->travel(1)->minutes();
// Check that the user can now log in
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])
->assertRedirect('/');
$this->assertAuthenticatedAs($user);
}
As you can see, the travel helper is extremely useful when dealing with time-related code in tests. I hope you’ll find it helpful too.