*/ class TokenServiceTest extends TestCase { /** * @var TokenService */ private $subject; /** * @var TokenStore|MockObject */ private $tokenStoreMock; protected function setUp(): void { parent::setUp(); $this->tokenStoreMock = $this->createMock(TokenStore::class); $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => 10, 'timeLimit' => 0, 'validateTokens' => true ] ); } public function testValidateToken(): void { $token = $this->createTokenToValidate(); static::assertTrue($this->subject->validateToken($token->getValue())); } public function testValidateExpiredToken(): void { $subject = clone $this->subject; $subject->setOption(TokenService::TIME_LIMIT_OPT, 1); $token = $this->createTokenToValidate(); $token->setCreatedAt(0); $this->expectException(UnauthorizedException::class); $subject->validateToken($token->getValue()); } public function testValidateIncorrectToken(): void { $invalidTokenValue = 'foo'; $this->tokenStoreMock ->method('getToken') ->with($invalidTokenValue) ->willReturn(null); $this->expectException(UnauthorizedException::class); $this->subject->validateToken($invalidTokenValue); } public function testInstantiateNoStore(): void { $this->expectException(InvalidService::class); $service = new TokenService(); $service->checkToken('unusedString'); } public function testInstantiateBadStore(): void { $this->expectException(InvalidService::class); $service = new TokenService([ 'store' => [] ]); $service->checkToken('unusedString'); } public function testCreateToken_WhenCalledTwice_ThenReturnsDifferentNewTokes(): void { $this->tokenStoreMock ->expects(self::exactly(2)) ->method('getAll') ->willReturn([]); $this->tokenStoreMock ->expects(self::exactly(2)) ->method('setToken'); $token1 = $this->subject->createToken(); $token2 = $this->subject->createToken(); self::assertNotEquals($token1->getValue(), $token2->getValue(), 'Method must return new token on each call.'); } public function testCheckToken_WhenValidTokenObject_ThenReturnTrue(): void { $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => 12345, ]; $token = new Token($tokenData); $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn($token); self::assertTrue($this->subject->checkToken($token), 'Method must return TRUE for valid token object.'); } public function testCheckToken_WhenValidTokenString_ThenReturnTrue(): void { $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => 12345, ]; $token = new Token($tokenData); $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn($token); self::assertTrue($this->subject->checkToken($tokenString), 'Method must return TRUE for valid token string.'); } public function testCheckToken_WhenTokeDontExist_ThenReturnFalse(): void { $tokenString = 'TOKEN_STRING'; $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn(null); self::assertFalse( $this->subject->checkToken($tokenString), 'Method must return FALSE when token does not exist.' ); } public function testCheckToken_WhenInvalidTokenString_ThenReturnFalse(): void { $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => 12345, ]; $token1 = new Token($tokenData); $token2 = new Token(); $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn($token2); $this->subject->checkToken($token1); self::assertFalse( $this->subject->checkToken($token1), 'Method must return FALSE when value does not match.' ); } public function testCheckToken_WhenValidExpiredTokenTimeLimitOn_ThenReturnFalse(): void { $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => 10, 'timeLimit' => 1, 'validateTokens' => true ] ); $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => microtime(true) - 100, ]; $token = new Token($tokenData); $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn($token); self::assertFalse( $this->subject->checkToken($token), 'Method must return FALSE when token is expired.' ); } public function testCheckToken_WhenValidExpiredTokenTimeLimitOff_ThenReturnTrue(): void { $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => 10, 'timeLimit' => 0, // Big time limit for token to not expire during test execution 'validateTokens' => true ] ); $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => microtime(true), ]; $token = new Token($tokenData); $this->tokenStoreMock ->method('getToken') ->with($tokenString) ->willReturn($token); self::assertTrue( $this->subject->checkToken($token), 'Method must return TRUE when token value match and token is not expired.' ); } public function testCheckFormToken_WhenCalled_ThenWillCheckCorrectToken(): void { $tokenString = 'TOKEN_STRING'; $tokenData = [ 'token' => $tokenString, 'ts' => 12345, ]; $token = new Token($tokenData); // Assert that token retrieved using form namespace $this->tokenStoreMock ->expects(self::once()) ->method('getToken') ->with(TokenService::FORM_TOKEN_NAMESPACE) ->willReturn($token); self::assertTrue($this->subject->checkFormToken($token), 'Method must return TRUE for valid form token object.'); } public function testInvalidateRemovesExpiredTokens(): void { $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => 10, 'timeLimit' => 1, 'validateTokens' => true ] ); $tokenString1 = 'TOKEN_1'; $expiredTokenData1 = [ 'token' => $tokenString1, 'ts' => microtime(true) - 1000, ]; $tokenString2 = 'TOKEN_2'; $expiredTokenData2 = [ 'token' => $tokenString2, 'ts' => microtime(true) - 1000, ]; $tokenString3 = 'TOKEN_3'; $validTokenData = [ 'token' => $tokenString3, 'ts' => microtime(true) + 1000, ]; $expiredToken1 = new Token($expiredTokenData1); $expiredToken2 = new Token($expiredTokenData2); $validToken = new Token($validTokenData); $tokensPool = [$expiredToken1, $expiredToken2, $validToken]; // Assert that expired tokens will be deleted. $this->tokenStoreMock ->method('getAll') ->willReturn($tokensPool); $this->tokenStoreMock ->expects(self::exactly(2)) ->method('removeToken') ->withConsecutive( [$tokenString1], [$tokenString2] ); $this->subject->createToken(); } public function testInvalidateRemovesOldestTokensWhenPoolIfFull(): void { $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => 2, 'timeLimit' => 1000, 'validateTokens' => true ] ); $tokenString1 = 'TOKEN_1'; $tokenData1 = [ 'token' => $tokenString1, 'ts' => microtime(true) - 3, ]; $tokenString2 = 'TOKEN_2'; $tokenData2 = [ 'token' => $tokenString2, 'ts' => microtime(true) - 2, ]; $tokenString3 = 'TOKEN_3'; $tokenData3 = [ 'token' => $tokenString3, 'ts' => microtime(true), ]; $token1 = new Token($tokenData1); $token2 = new Token($tokenData2); $token3 = new Token($tokenData3); $tokensPool = [$token1, $token2, $token3]; // Assert that oldest tokens will be deleted when pool is full. $this->tokenStoreMock ->method('getAll') ->willReturn($tokensPool); $this->tokenStoreMock ->expects(self::exactly(2)) ->method('removeToken') ->withConsecutive( [$tokenString1], [$tokenString2] ); $this->subject->createToken(); } /** * @param int $poolSizeOption * @param bool $withForm * @param bool $hasFormToken * @param int $expectedResult * @throws InvalidService * * @dataProvider dataProviderTestGetPoolSize */ public function testGetPoolSize(int $poolSizeOption, bool $withForm, bool $hasFormToken, int $expectedResult): void { $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSizeOption, 'timeLimit' => 0, 'validateTokens' => true ] ); $this->tokenStoreMock ->method('hasToken') ->with('form_token') ->willReturn($hasFormToken); $result = $this->subject->getPoolSize($withForm); self::assertSame($expectedResult, $result, 'Method must return correct pool size value.'); } public function testGetPoolSize_WhenSizeNotConfigured_WillReturnDefaultValue(): void { $defaultPoolSize = 6; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'timeLimit' => 0, 'validateTokens' => true ] ); $result = $this->subject->getPoolSize(); self::assertSame($defaultPoolSize, $result, 'Method must return default pool size if it is not configured.'); } public function testGenerateTokenPool_WhenNoTokensStored_ThenGeneratesNewPool(): void { $poolSize = 5; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSize, 'timeLimit' => 0, 'validateTokens' => true ] ); $this->tokenStoreMock ->method('getAll') ->willReturn([]); $this->tokenStoreMock ->expects(self::exactly($poolSize)) ->method('setToken'); $result = $this->subject->generateTokenPool(); self::assertCount($poolSize, $result, 'Method must return a tokens pool of correct size.'); } public function testGenerateTokenPool_WhenStoredPoolNotFull_ThenGeneratesMissingTokens(): void { $poolSize = 5; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSize, 'timeLimit' => 0, 'validateTokens' => true ] ); $storedTokens = [ new Token(), new Token(), ]; $this->tokenStoreMock ->method('getAll') ->willReturn($storedTokens); $this->tokenStoreMock ->expects(self::exactly(3)) ->method('setToken'); $result = $this->subject->generateTokenPool(); self::assertCount($poolSize, $result, 'Method must return a tokens pool of correct size.'); } public function testGenerateTokenPool_WhenStoredPoolIsFull_ThenReturnStoredTokens(): void { $poolSize = 3; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSize, 'timeLimit' => 0, 'validateTokens' => true ] ); $storedTokens = [ new Token(), new Token(), new Token(), ]; $this->tokenStoreMock ->method('getAll') ->willReturn($storedTokens); $this->tokenStoreMock ->expects(self::never()) ->method('setToken'); $result = $this->subject->generateTokenPool(); self::assertCount($poolSize, $result, 'Method must return a tokens pool of correct size.'); self::assertSame($storedTokens, $result, 'Method must return a pool of stored tokens.'); } public function testGenerateTokenPool_WhenStoredPoolHasExpiredTokens_ThenGeneratesNewTokens(): void { $poolSize = 3; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSize, 'timeLimit' => 1000, 'validateTokens' => true ] ); $expiredTokenValue = 'EXPIRED_TOKEN_VALUE'; $expiredTokenData = [ 'token' => $expiredTokenValue, 'ts' => microtime(true) - 1000, ]; $storedTokens = [ new Token(), new Token(), new Token($expiredTokenData) ]; $this->tokenStoreMock ->method('getAll') ->willReturn($storedTokens); $this->tokenStoreMock ->expects(self::once()) ->method('removeToken') ->with($expiredTokenValue); $this->tokenStoreMock ->expects(self::once()) ->method('setToken'); $result = $this->subject->generateTokenPool(); self::assertCount($poolSize, $result, 'Method must return a tokens pool of correct size.'); } public function testGetClientConfig(): void { $poolSize = 5; $this->subject = new TokenService( [ 'store' => $this->tokenStoreMock, 'poolSize' => $poolSize, 'timeLimit' => 60, 'validateTokens' => true ] ); $result = $this->subject->getClientConfig(); self::assertArrayHasKey('tokenTimeLimit', $result, 'Client config must contain time limit value.'); self::assertArrayHasKey('maxSize', $result, 'Client config must contain pool size value.'); self::assertArrayHasKey('tokens', $result, 'Client config must contain list of stored tokens.'); self::assertArrayHasKey('validateTokens', $result, 'Client config must contain validate tokens flag.'); self::assertArrayHasKey('store', $result, 'Client config must contain client tokens store configuration.'); } public function testAddFormToken(): void { $this->tokenStoreMock ->expects(self::once()) ->method('setToken') ->with( 'form_token', self::callback(function (Token $token) { return true; }) ); $this->subject->addFormToken(); } public function testGetFormToken_WhenFormTokenExist_ReturnStoredToken(): void { $storedFormToken = new Token(); $this->tokenStoreMock ->method('getToken') ->with('form_token') ->willReturn($storedFormToken); $result = $this->subject->getFormToken(); self::assertSame($storedFormToken, $result, 'Method must return form tokens from tokens store.'); } public function testGetFormToken_WhenFormTokenDontExist_ReturnStoredToken(): void { // Form token does not exist in token store during the first call but exists during the second call. $this->tokenStoreMock ->method('getToken') ->with('form_token') ->willReturnOnConsecutiveCalls( null, new Token() ); // Assert that new form token is generated and stored $this->tokenStoreMock ->expects(self::once()) ->method('setToken') ->with( 'form_token', self::callback(function (Token $token) { return true; }) ); $this->subject->getFormToken(); } public function dataProviderTestGetPoolSize(): array { return [ 'With form, with form token in storage' => [ 'poolSizeOption' => 10, 'withForm' => true, 'hasFormToken' => true, 'expectedResult' => 11, ], 'Without form, with form token in storage' => [ 'poolSizeOption' => 10, 'withForm' => false, 'hasFormToken' => true, 'expectedResult' => 10, ], 'With form, without form token in storage' => [ 'poolSizeOption' => 10, 'withForm' => true, 'hasFormToken' => false, 'expectedResult' => 10, ], 'Without form, without form token in storage' => [ 'poolSizeOption' => 10, 'withForm' => false, 'hasFormToken' => false, 'expectedResult' => 10, ], ]; } private function createTokenToValidate(): Token { $token = $this->createStoredToken(); $this->tokenStoreMock ->expects(static::once()) ->method('removeToken') ->with($token->getValue()) ->willReturn(true); return $token; } private function createStoredToken(): Token { $token = $this->subject->createToken(); $this->tokenStoreMock ->method('getToken') ->with($token->getValue()) ->willReturn($token); return $token; } }