diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index cc23eb8..139f66f 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Contracts\Auth\Factory as FactoryContract; +use Illuminate\Support\Timebox; use InvalidArgumentException; class AuthManager implements FactoryContract @@ -120,7 +121,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 diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 652835f..a63e187 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -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; @@ -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. * @@ -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; } /** @@ -423,13 +434,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); } /** @@ -882,6 +897,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. * diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php index 219e599..798457d 100755 --- a/src/Illuminate/Support/Facades/Facade.php +++ b/src/Illuminate/Support/Facades/Facade.php @@ -290,6 +290,7 @@ public static function defaultAliases() 'Session' => Session::class, 'Storage' => Storage::class, 'Str' => Str::class, + 'Timebox' => Timebox::class, 'URL' => URL::class, 'Validator' => Validator::class, 'View' => View::class, diff --git a/src/Illuminate/Support/Facades/Timebox.php b/src/Illuminate/Support/Facades/Timebox.php new file mode 100755 index 0000000..3123916 --- /dev/null +++ b/src/Illuminate/Support/Facades/Timebox.php @@ -0,0 +1,22 @@ +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); + } +} diff --git a/tests/Auth/AuthGuardTest.php b/tests/Auth/AuthGuardTest.php index f062cbc..8f30fb7 100755 --- a/tests/Auth/AuthGuardTest.php +++ b/tests/Auth/AuthGuardTest.php @@ -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; @@ -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)); @@ -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); @@ -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)); @@ -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)); @@ -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)); @@ -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); @@ -555,9 +577,12 @@ 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'])); @@ -565,9 +590,9 @@ public function testLoginOnceFailure() 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() @@ -577,6 +602,7 @@ protected function getMocks() m::mock(UserProvider::class), Request::create('/', 'GET'), m::mock(CookieJar::class), + m::mock(Timebox::class), ]; } diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 2f75cca..ed33181 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -571,7 +571,7 @@ public function something() public function testRetry() { - $startTime = microtime(true); + $startTime = hrtime(true); $attempts = retry(2, function ($attempts) { if ($attempts > 1) { @@ -585,12 +585,12 @@ 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, (hrtime(true) - $startTime) / 1000000000, 0.02); } public function testRetryWithPassingSleepCallback() { - $startTime = microtime(true); + $startTime = hrtime(true); $attempts = retry(3, function ($attempts) { if ($attempts > 2) { @@ -608,12 +608,12 @@ 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, (hrtime(true) - $startTime) / 1000000000, 0.02); } public function testRetryWithPassingWhenCallback() { - $startTime = microtime(true); + $startTime = hrtime(true); $attempts = retry(2, function ($attempts) { if ($attempts > 1) { @@ -629,7 +629,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, (hrtime(true) - $startTime) / 1000000000, 0.02); } public function testRetryWithFailingWhenCallback() @@ -649,7 +649,7 @@ public function testRetryWithFailingWhenCallback() public function testRetryWithBackoff() { - $startTime = microtime(true); + $startTime = hrtime(true); $attempts = retry([50, 100, 200], function ($attempts) { if ($attempts > 3) { return $attempts; @@ -661,7 +661,7 @@ public function testRetryWithBackoff() // Make sure we made four attempts $this->assertEquals(4, $attempts); - $this->assertEqualsWithDelta(0.05 + 0.1 + 0.2, microtime(true) - $startTime, 0.02); + $this->assertEqualsWithDelta(0.05 + 0.1 + 0.2, (hrtime(true) - $startTime) / 1000000000, 0.02); } public function testTransform() diff --git a/tests/Support/SupportTimeboxTest.php b/tests/Support/SupportTimeboxTest.php new file mode 100755 index 0000000..b11b46e --- /dev/null +++ b/tests/Support/SupportTimeboxTest.php @@ -0,0 +1,52 @@ +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(); + } +}