Skip to content

Timebox against 8.x #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 8.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Illuminate/Auth/AuthManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Illuminate\Contracts\Auth\Factory as FactoryContract;
use Illuminate\Support\Timebox;
use InvalidArgumentException;

class AuthManager implements FactoryContract
Expand Down Expand Up @@ -122,7 +123,7 @@ public function createSessionDriver($name, $config)
{
$provider = $this->createUserProvider($config['provider'] ?? null);

$guard = new SessionGuard($name, $provider, $this->app['session.store']);
$guard = new SessionGuard($name, $provider, $this->app['session.store'], null, new Timebox);

// When using the remember me functionality of the authentication services we
// will need to be set the encryption instance of the guard, which allows
Expand Down
37 changes: 31 additions & 6 deletions src/Illuminate/Auth/SessionGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Support\Timebox;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use RuntimeException;
Expand Down Expand Up @@ -88,6 +89,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected $events;

/**
* The timebox implementation.
*
* @var \Illuminate\Support\Timebox
*/
protected $timebox;

/**
* Indicates if the logout method has been called.
*
Expand All @@ -109,17 +117,20 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Contracts\Session\Session $session
* @param \Symfony\Component\HttpFoundation\Request|null $request
* @param \Illuminate\Support\Timebox|null $timebox
* @return void
*/
public function __construct($name,
UserProvider $provider,
Session $session,
Request $request = null)
Request $request = null,
Timebox $timebox = null)
{
$this->name = $name;
$this->session = $session;
$this->request = $request;
$this->provider = $provider;
$this->timebox = $timebox;
}

/**
Expand Down Expand Up @@ -419,13 +430,17 @@ public function attemptWhen(array $credentials = [], $callbacks = null, $remembe
*/
protected function hasValidCredentials($user, $credentials)
{
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
return $this->timebox->make(function ($timebox) use ($user, $credentials) {
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);

if ($validated) {
$this->fireValidatedEvent($user);
}
if ($validated) {
$timebox->returnEarly();

$this->fireValidatedEvent($user);
}

return $validated;
return $validated;
}, 200000);
}

/**
Expand Down Expand Up @@ -904,6 +919,16 @@ public function getSession()
return $this->session;
}

/**
* Get the timebox instance used by the guard.
*
* @return \Illuminate\Support\Timebox
*/
public function getTimebox()
{
return $this->timebox;
}

/**
* Return the currently cached user.
*
Expand Down
22 changes: 22 additions & 0 deletions src/Illuminate/Support/Facades/Timebox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Illuminate\Support\Facades;


/**
* @method static \Illuminate\Support\Timebox make(callable $callback, int $microseconds): mixed
*
* @see \Illuminate\Support\Timebox
*/
class Timebox extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return \Illuminate\Support\Timebox::class;
}
}
56 changes: 56 additions & 0 deletions src/Illuminate/Support/Timebox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace Illuminate\Support;

class Timebox
{
/**
* Is the timebox allowed to do an early return
*
* @var bool
*/
public $earlyReturn = false;

/**
* @param callable $callback
* @param int $microseconds
* @return mixed
*/
public function make(callable $callback, int $microseconds)
{
$start = microtime(true);

$result = $callback($this);

$remainder = $microseconds - ((microtime(true) - $start) * 1000000);

if (! $this->earlyReturn && $remainder > 0) {
$this->usleep($remainder);
}

return $result;
}

public function returnEarly(): self
{
$this->earlyReturn = true;

return $this;
}

public function dontReturnEarly(): self
{
$this->earlyReturn = false;

return $this;
}

/**
* @param $microseconds
* @return void
*/
protected function usleep($microseconds)
{
usleep($microseconds);
}
}
46 changes: 36 additions & 10 deletions tests/Auth/AuthGuardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Session\Session;
use Illuminate\Cookie\CookieJar;
use Illuminate\Support\Timebox;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Cookie;
Expand Down Expand Up @@ -94,6 +95,10 @@ public function testAttemptCallsRetrieveByCredentials()
{
$guard = $this->getGuard();
$guard->setDispatcher($events = m::mock(Dispatcher::class));
$timebox = $guard->getTimebox();
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback) use ($timebox) {
return $callback($timebox);
});
$events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class));
$events->shouldReceive('dispatch')->once()->with(m::type(Failed::class));
$events->shouldNotReceive('dispatch')->with(m::type(Validated::class));
Expand All @@ -103,9 +108,12 @@ public function testAttemptCallsRetrieveByCredentials()

public function testAttemptReturnsUserInterface()
{
[$session, $provider, $request, $cookie] = $this->getMocks();
$guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login'])->setConstructorArgs(['default', $provider, $session, $request])->getMock();
[$session, $provider, $request, $cookie, $timebox] = $this->getMocks();
$guard = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['login'])->setConstructorArgs(['default', $provider, $session, $request, $timebox])->getMock();
$guard->setDispatcher($events = m::mock(Dispatcher::class));
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback, $microseconds) use ($timebox) {
return $callback($timebox->shouldReceive('returnEarly')->once()->getMock());
});
$events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class));
$events->shouldReceive('dispatch')->once()->with(m::type(Validated::class));
$user = $this->createMock(Authenticatable::class);
Expand All @@ -119,6 +127,10 @@ public function testAttemptReturnsFalseIfUserNotGiven()
{
$mock = $this->getGuard();
$mock->setDispatcher($events = m::mock(Dispatcher::class));
$timebox = $mock->getTimebox();
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback, $microseconds) use ($timebox) {
return $callback($timebox);
});
$events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class));
$events->shouldReceive('dispatch')->once()->with(m::type(Failed::class));
$events->shouldNotReceive('dispatch')->with(m::type(Validated::class));
Expand All @@ -128,9 +140,12 @@ public function testAttemptReturnsFalseIfUserNotGiven()

public function testAttemptAndWithCallbacks()
{
[$session, $provider, $request, $cookie] = $this->getMocks();
$mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName'])->setConstructorArgs(['default', $provider, $session, $request])->getMock();
[$session, $provider, $request, $cookie, $timebox] = $this->getMocks();
$mock = $this->getMockBuilder(SessionGuard::class)->onlyMethods(['getName'])->setConstructorArgs(['default', $provider, $session, $request, $timebox])->getMock();
$mock->setDispatcher($events = m::mock(Dispatcher::class));
$timebox->shouldReceive('make')->andReturnUsing(function ($callback) use ($timebox) {
return $callback($timebox->shouldReceive('returnEarly')->getMock());
});
$user = m::mock(Authenticatable::class);
$events->shouldReceive('dispatch')->times(3)->with(m::type(Attempting::class));
$events->shouldReceive('dispatch')->once()->with(m::type(Login::class));
Expand Down Expand Up @@ -212,6 +227,10 @@ public function testFailedAttemptFiresFailedEvent()
{
$guard = $this->getGuard();
$guard->setDispatcher($events = m::mock(Dispatcher::class));
$timebox = $guard->getTimebox();
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback, $microseconds) use ($timebox) {
return $callback($timebox);
});
$events->shouldReceive('dispatch')->once()->with(m::type(Attempting::class));
$events->shouldReceive('dispatch')->once()->with(m::type(Failed::class));
$events->shouldNotReceive('dispatch')->with(m::type(Validated::class));
Expand Down Expand Up @@ -544,9 +563,12 @@ public function testUserUsesRememberCookieIfItExists()

public function testLoginOnceSetsUser()
{
[$session, $provider, $request, $cookie] = $this->getMocks();
$guard = m::mock(SessionGuard::class, ['default', $provider, $session])->makePartial();
[$session, $provider, $request, $cookie, $timebox] = $this->getMocks();
$guard = m::mock(SessionGuard::class, ['default', $provider, $session, $request, $timebox])->makePartial();
$user = m::mock(Authenticatable::class);
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback) use ($timebox) {
return $callback($timebox->shouldReceive('returnEarly')->once()->getMock());
});
$guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user);
$guard->getProvider()->shouldReceive('validateCredentials')->once()->with($user, ['foo'])->andReturn(true);
$guard->shouldReceive('setUser')->once()->with($user);
Expand All @@ -555,19 +577,22 @@ public function testLoginOnceSetsUser()

public function testLoginOnceFailure()
{
[$session, $provider, $request, $cookie] = $this->getMocks();
$guard = m::mock(SessionGuard::class, ['default', $provider, $session])->makePartial();
[$session, $provider, $request, $cookie, $timebox] = $this->getMocks();
$guard = m::mock(SessionGuard::class, ['default', $provider, $session, $request, $timebox])->makePartial();
$user = m::mock(Authenticatable::class);
$timebox->shouldReceive('make')->once()->andReturnUsing(function ($callback) use ($timebox) {
return $callback($timebox);
});
$guard->getProvider()->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user);
$guard->getProvider()->shouldReceive('validateCredentials')->once()->with($user, ['foo'])->andReturn(false);
$this->assertFalse($guard->once(['foo']));
}

protected function getGuard()
{
[$session, $provider, $request, $cookie] = $this->getMocks();
[$session, $provider, $request, $cookie, $timebox] = $this->getMocks();

return new SessionGuard('default', $provider, $session, $request);
return new SessionGuard('default', $provider, $session, $request, $timebox);
}

protected function getMocks()
Expand All @@ -577,6 +602,7 @@ protected function getMocks()
m::mock(UserProvider::class),
Request::create('/', 'GET'),
m::mock(CookieJar::class),
m::mock(Timebox::class),
];
}

Expand Down
6 changes: 3 additions & 3 deletions tests/Support/SupportHelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ public function testRetry()
$this->assertEquals(2, $attempts);

// Make sure we waited 100ms for the first attempt
$this->assertEqualsWithDelta(0.1, microtime(true) - $startTime, 0.02);
$this->assertEqualsWithDelta(0.1, microtime(true) - $startTime, 0.03);
}

public function testRetryWithPassingSleepCallback()
Expand All @@ -573,7 +573,7 @@ public function testRetryWithPassingSleepCallback()
$this->assertEquals(3, $attempts);

// Make sure we waited 300ms for the first two attempts
$this->assertEqualsWithDelta(0.3, microtime(true) - $startTime, 0.02);
$this->assertEqualsWithDelta(0.3, microtime(true) - $startTime, 0.03);
}

public function testRetryWithPassingWhenCallback()
Expand All @@ -594,7 +594,7 @@ public function testRetryWithPassingWhenCallback()
$this->assertEquals(2, $attempts);

// Make sure we waited 100ms for the first attempt
$this->assertEqualsWithDelta(0.1, microtime(true) - $startTime, 0.02);
$this->assertEqualsWithDelta(0.1, microtime(true) - $startTime, 0.03);
}

public function testRetryWithFailingWhenCallback()
Expand Down
52 changes: 52 additions & 0 deletions tests/Support/SupportTimeboxTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Illuminate\Tests\Support;

use Illuminate\Support\Timebox;
use PHPUnit\Framework\TestCase;
use Mockery as m;

class SupportTimeboxTest extends TestCase
{
public function testMakeExecutesCallback()
{
$callback = function () {
$this->assertTrue(true);
};

(new Timebox)->make($callback, 0);
}

public function testMakeWaitsForMicroseconds()
{
$mock = m::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial();
$mock->shouldReceive('usleep')->once();

$mock->make(function () {}, 10000);

$mock->shouldHaveReceived('usleep')->once();
}

public function testMakeShouldNotSleepWhenEarlyReturnHasBeenFlagged()
{
$mock = m::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial();
$mock->make(function ($timebox) {
$timebox->returnEarly();
}, 10000);

$mock->shouldNotHaveReceived('usleep');
}

public function testMakeShouldSleepWhenDontEarlyReturnHasBeenFlagged()
{
$mock = m::spy(Timebox::class)->shouldAllowMockingProtectedMethods()->makePartial();
$mock->shouldReceive('usleep')->once();

$mock->make(function ($timebox) {
$timebox->returnEarly();
$timebox->dontReturnEarly();
}, 10000);

$mock->shouldHaveReceived('usleep')->once();
}
}