diff --git a/src/Application/Model/Webauthn.php b/src/Application/Model/Webauthn.php index 28c2f97..c7e88f3 100644 --- a/src/Application/Model/Webauthn.php +++ b/src/Application/Model/Webauthn.php @@ -16,6 +16,7 @@ declare(strict_types=1); namespace D3\Webauthn\Application\Model; use Assert\AssertionFailedException; +use D3\TestingTools\Production\IsMockable; use D3\Webauthn\Application\Model\Credential\PublicKeyCredential; use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList; use D3\Webauthn\Application\Model\Exceptions\WebauthnException; @@ -27,16 +28,22 @@ use Exception; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7Server\ServerRequestCreator; use OxidEsales\Eshop\Application\Model\User; -use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Config; +use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\UtilsView; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; +use Throwable; use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialDescriptor; use Webauthn\PublicKeyCredentialRequestOptions; use Webauthn\PublicKeyCredentialSource; use Webauthn\Server; class Webauthn { + use IsMockable; + public const SESSION_CREATIONS_OPTIONS = 'd3WebAuthnCreationOptions'; public const SESSION_ASSERTION_OPTIONS = 'd3WebAuthnAssertionOptions'; @@ -49,14 +56,14 @@ class Webauthn !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' || in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1']) || // is localhost - preg_match('/.*\.localhost$/mi', $_SERVER['REMOTE_ADDR']) // localhost is TLD + (isset($_SERVER['REMOTE_ADDR']) && preg_match('/.*\.localhost$/mi', $_SERVER['REMOTE_ADDR']) ) // localhost is TLD ) { return true; } $e = oxNew(WebauthnException::class, 'D3_WEBAUTHN_ERR_UNSECURECONNECTION'); - Registry::getLogger()->info($e->getDetailedErrorMessage()); - Registry::getUtilsView()->addErrorToDisplay($e); + $this->d3GetMockableLogger()->info($e->getDetailedErrorMessage()); + $this->d3GetMockableRegistryObject(UtilsView::class)->addErrorToDisplay($e); return false; } @@ -71,25 +78,18 @@ class Webauthn */ public function getCreationOptions(User $user): string { - $userEntity = oxNew(UserEntity::class, $user); + $userEntity = $this->d3GetMockableOxNewObject(UserEntity::class, $user); - /** @var PublicKeyCredentialList $credentialSourceRepository */ - $credentialSourceRepository = oxNew(PublicKeyCredentialList::class); - $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity); - $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) { - return $credential->getPublicKeyCredentialDescriptor(); - }, $credentialSources); - - $server = $this->getServer(); - $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions( + $publicKeyCredentialCreationOptions = $this->getServer()->generatePublicKeyCredentialCreationOptions( $userEntity, PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, - $excludeCredentials + $this->getExistingCredentials($userEntity) ); - Registry::getSession()->setVariable(self::SESSION_CREATIONS_OPTIONS, $publicKeyCredentialCreationOptions); + $this->d3GetMockableRegistryObject(Session::class) + ->setVariable(self::SESSION_CREATIONS_OPTIONS, $publicKeyCredentialCreationOptions); - $json = json_encode($publicKeyCredentialCreationOptions,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $json = $this->jsonEncode($publicKeyCredentialCreationOptions); if ($json === false) { throw oxNew(Exception::class, "can't encode creation options"); @@ -98,6 +98,34 @@ class Webauthn return $json; } + /** + * @param UserEntity $userEntity + * @return PublicKeyCredentialDescriptor[] + * @throws DoctrineDriverException + * @throws DoctrineException + */ + public function getExistingCredentials(UserEntity $userEntity): array + { + // Get the list of authenticators associated to the user + /** @var PublicKeyCredentialList $credentialSourceRepository */ + $credentialList = $this->d3GetMockableOxNewObject(PublicKeyCredentialList::class); + $credentialSources = $credentialList->findAllForUserEntity($userEntity); + + // Convert the Credential Sources into Public Key Credential Descriptors + return array_map(function (PublicKeyCredentialSource $credential) { + return $credential->getPublicKeyCredentialDescriptor(); + }, $credentialSources); + } + + /** + * @param PublicKeyCredentialCreationOptions|PublicKeyCredentialRequestOptions $creationOptions + * @return false|string + */ + protected function jsonEncode($creationOptions) + { + return json_encode($creationOptions,JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + /** * @param string $userId * @return string @@ -107,30 +135,20 @@ class Webauthn public function getRequestOptions(string $userId): string { /** @var d3_User_Webauthn $user */ - $user = oxNew(User::class); + $user = $this->d3GetMockableOxNewObject(User::class); $user->load($userId); - $userEntity = oxNew(UserEntity::class, $user); - - // Get the list of authenticators associated to the user - $credentialList = oxNew(PublicKeyCredentialList::class); - $credentialSources = $credentialList->findAllForUserEntity($userEntity); - - // Convert the Credential Sources into Public Key Credential Descriptors - $allowedCredentials = array_map(function (PublicKeyCredentialSource $credential) { - return $credential->getPublicKeyCredentialDescriptor(); - }, $credentialSources); - - $server = $this->getServer(); + $userEntity = $this->d3GetMockableOxNewObject(UserEntity::class, $user); // We generate the set of options. - $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions( + $publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions( PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value - $allowedCredentials + $this->getExistingCredentials($userEntity) ); - Registry::getSession()->setVariable(self::SESSION_ASSERTION_OPTIONS, $publicKeyCredentialRequestOptions); + $this->d3GetMockableRegistryObject(Session::class) + ->setVariable(self::SESSION_ASSERTION_OPTIONS, $publicKeyCredentialRequestOptions); - $json = json_encode($publicKeyCredentialRequestOptions, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $json = $this->jsonEncode($publicKeyCredentialRequestOptions); if ($json === false) { throw oxNew(Exception::class, "can't encode request options"); @@ -142,25 +160,28 @@ class Webauthn /** * @return Server */ - public function getServer(): Server + protected function getServer(): Server { /** @var RelyingPartyEntity $rpEntity */ - $rpEntity = oxNew(RelyingPartyEntity::class); + $rpEntity = $this->d3GetMockableOxNewObject(RelyingPartyEntity::class); /** @var Server $server */ - $server = oxNew(Server::class, $rpEntity, oxNew(PublicKeyCredentialList::class)); - $server->setLogger(Registry::getLogger()); + $server = $this->d3GetMockableOxNewObject( + Server::class, + $rpEntity, + $this->d3GetMockableOxNewObject(PublicKeyCredentialList::class) + ); + $server->setLogger($this->d3GetMockableLogger()); return $server; } /** - * @param string $credential + * @param string $credential * @param string|null $keyName * - * @throws ContainerExceptionInterface + * @throws AssertionFailedException * @throws DoctrineDriverException * @throws DoctrineException - * @throws NotFoundExceptionInterface - * @throws Exception + * @throws Throwable */ public function saveAuthn(string $credential, string $keyName = null): void { @@ -175,11 +196,11 @@ class Webauthn $publicKeyCredentialSource = $this->getServer()->loadAndCheckAttestationResponse( html_entity_decode($credential), - Registry::getSession()->getVariable(self::SESSION_CREATIONS_OPTIONS), + $this->d3GetMockableRegistryObject(Session::class)->getVariable(self::SESSION_CREATIONS_OPTIONS), $serverRequest ); - $pkCredential = oxNew(PublicKeyCredential::class); + $pkCredential = $this->d3GetMockableOxNewObject(PublicKeyCredential::class); $pkCredential->saveCredentialSource($publicKeyCredentialSource, $keyName); } @@ -200,20 +221,13 @@ class Webauthn ); $serverRequest = $creator->fromGlobals(); - /** @var User $user */ - $user = oxNew(User::class); - $user->load( - isAdmin() ? - Registry::getSession()->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) : - Registry::getSession()->getVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER) - ); - /** @var UserEntity $userEntity */ - $userEntity = oxNew(UserEntity::class, $user); + $userEntity = $this->getUserEntityFrom($this->getSavedUserIdFromSession()); try { $this->getServer()->loadAndCheckAssertionResponse( html_entity_decode( $response ), - Registry::getSession()->getVariable( self::SESSION_ASSERTION_OPTIONS ), + $this->d3GetMockableRegistryObject(Session::class) + ->getVariable( self::SESSION_ASSERTION_OPTIONS ), $userEntity, $serverRequest ); @@ -226,6 +240,39 @@ class Webauthn return true; } + /** + * @param $userId + * @return UserEntity + */ + protected function getUserEntityFrom($userId): UserEntity + { + /** @var User $user */ + $user = $this->d3GetMockableOxNewObject(User::class); + $user->load($userId); + /** @var UserEntity $userEntity */ + return $this->d3GetMockableOxNewObject(UserEntity::class, $user); + } + + /** + * @return string|null + */ + protected function getSavedUserIdFromSession(): ?string + { + $session = $this->d3GetMockableRegistryObject(Session::class); + + return $this->isAdmin() ? + $session->getVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) : + $session->getVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER); + } + + /** + * @return bool + */ + public function isAdmin(): bool + { + return isAdmin(); + } + /** * @param $userId * @return bool @@ -236,8 +283,10 @@ class Webauthn */ public function isActive($userId): bool { - return !Registry::getConfig()->getConfigParam(WebauthnConf::GLOBAL_SWITCH) - && !Registry::getSession()->getVariable(WebauthnConf::GLOBAL_SWITCH) + return !$this->d3GetMockableRegistryObject(Config::class) + ->getConfigParam(WebauthnConf::GLOBAL_SWITCH) + && !$this->d3GetMockableRegistryObject(Session::class) + ->getVariable(WebauthnConf::GLOBAL_SWITCH) && $this->UserUseWebauthn($userId); } @@ -251,14 +300,10 @@ class Webauthn */ public function UserUseWebauthn($userId): bool { - /** @var User $user */ - $user = oxNew(User::class); - $user->load($userId); - /** @var UserEntity $entity */ - $entity = oxNew(UserEntity::class, $user); + $entity = $this->getUserEntityFrom($userId); /** @var PublicKeyCredentialList $credentialList */ - $credentialList = oxNew(PublicKeyCredentialList::class); + $credentialList = $this->d3GetMockableOxNewObject(PublicKeyCredentialList::class); $list = $credentialList->findAllForUserEntity($entity); return is_array($list) && count($list); diff --git a/src/tests/unit/Application/Model/WebauthnTest.php b/src/tests/unit/Application/Model/WebauthnTest.php new file mode 100644 index 0000000..3ed215f --- /dev/null +++ b/src/tests/unit/Application/Model/WebauthnTest.php @@ -0,0 +1,886 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\tests\unit\Application\Model; + +use Assert\InvalidArgumentException; +use D3\TestingTools\Development\CanAccessRestricted; +use D3\Webauthn\Application\Model\Credential\PublicKeyCredential; +use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList; +use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; +use D3\Webauthn\Application\Model\RelyingPartyEntity; +use D3\Webauthn\Application\Model\UserEntity; +use D3\Webauthn\Application\Model\Webauthn; +use D3\Webauthn\Application\Model\WebauthnConf; +use Exception; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Config; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\UtilsView; +use OxidEsales\TestingLibrary\UnitTestCase; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionException; +use stdClass; +use Webauthn\PublicKeyCredentialCreationOptions; +use Webauthn\PublicKeyCredentialDescriptor; +use Webauthn\PublicKeyCredentialRequestOptions; +use Webauthn\PublicKeyCredentialSource; +use Webauthn\Server; + +class WebauthnTest extends UnitTestCase +{ + use CanAccessRestricted; + + /** + * @test + * @param $https + * @param $forwardedProto + * @param $forwardedSSL + * @param $remoteAddr + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canCheckIsAvailableDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::isAvailable + */ + public function canCheckIsAvailable($https, $forwardedProto, $forwardedSSL, $remoteAddr, $expected) + { + $_SERVER['HTTPS'] = $https; + $_SERVER['HTTP_X_FORWARDED_PROTO'] = $forwardedProto; + $_SERVER['HTTP_X_FORWARDED_SSL'] = $forwardedSSL; + $_SERVER['REMOTE_ADDR'] = $remoteAddr; + + /** @var UtilsView|MockObject $utilsViewMock */ + $utilsViewMock = $this->getMockBuilder(UtilsView::class) + ->onlyMethods(['addErrorToDisplay']) + ->getMock(); + $utilsViewMock->expects($this->exactly((int) !$expected))->method('addErrorToDisplay'); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableRegistryObject']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($utilsViewMock) { + $args = func_get_args(); + switch ($args[0]) { + case UtilsView::class: + return $utilsViewMock; + default: + return Registry::get($args[0]); + } + } + ); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'isAvailable' + ) + ); + } + + /** + * @return array[] + */ + public function canCheckIsAvailableDataProvider(): array + { + return [ + 'https' => ['on', null, null, null, true], + 'HTTP_X_FORWARDED_PROTO' => [null, 'https', null, null, true], + 'HTTP_X_FORWARDED_SSL' => [null, null, 'on', null, true], + 'REMOTE_ADDR v4' => [null, null, null, '127.0.0.1', true], + 'REMOTE_ADDR v6' => [null, null, null, '::1', true], + 'REMOTE_ADDR localhost' => [null, null, null, 'some.localhost', true], + 'unset' => [null, null, null, null, false], + 'not valid' => ['off', 'http', 'off', '160.158.23.7', false] + ]; + } + + /** + * @test + * @param $jsonReturn + * @return void + * @throws ReflectionException + * @dataProvider canGetOptionsDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::getCreationOptions + */ + public function canGetCreationOptions($jsonReturn) + { + /** @var PublicKeyCredentialDescriptor|MockObject $pubKeyCredDescriptorMock */ + $pubKeyCredDescriptorMock = $this->getMockBuilder(PublicKeyCredentialDescriptor::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var PublicKeyCredentialCreationOptions|MockObject $pubKeyCredCreationOptionsMock */ + $pubKeyCredCreationOptionsMock = $this->getMockBuilder(PublicKeyCredentialCreationOptions::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Server|MockObject $serverMock */ + $serverMock = $this->getMockBuilder(Server::class) + ->onlyMethods(['generatePublicKeyCredentialCreationOptions']) + ->disableOriginalConstructor() + ->getMock(); + $serverMock->expects($this->once())->method('generatePublicKeyCredentialCreationOptions')->with( + $this->anything(), + $this->anything(), + $this->identicalTo([$pubKeyCredDescriptorMock]) + )->willReturn($pubKeyCredCreationOptionsMock); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['setVariable']) + ->getMock(); + $sessionMock->expects($this->once())->method('setVariable')->with( + $this->identicalTo(Webauthn::SESSION_CREATIONS_OPTIONS), + $this->identicalTo($pubKeyCredCreationOptionsMock) + ); + + /** @var UserEntity|MockObject $userEntityMock */ + $userEntityMock = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var User|MockObject $userMock */ + $userMock = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableOxNewObject', 'getServer', 'd3GetMockableRegistryObject', 'jsonEncode', + 'getExistingCredentials' + ]) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($userEntityMock) { + $args = func_get_args(); + switch ($args[0]) { + case UserEntity::class: + return $userEntityMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + $sut->method('getServer')->willReturn($serverMock); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->expects($this->once())->method('jsonEncode')->willReturn($jsonReturn); + $sut->expects($this->once())->method('getExistingCredentials')->willReturn([ + $pubKeyCredDescriptorMock + ]); + + if (!$jsonReturn) { + $this->expectException(Exception::class); + } + + $return = $this->callMethod( + $sut, + 'getCreationOptions', + [$userMock] + ); + + if ($jsonReturn) { + $this->assertSame( + $jsonReturn, + $return + ); + } + } + + /** + * @return array + */ + public function canGetOptionsDataProvider(): array + { + return [ + 'json encoded' => ['jsonstring'], + 'json failed' => [false], + ]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Application\Model\Webauthn::getExistingCredentials + */ + public function canGetExistingCredentials() + { + /** @var PublicKeyCredentialDescriptor|MockObject $pubKeyCredDescriptorMock */ + $pubKeyCredDescriptorMock = $this->getMockBuilder(PublicKeyCredentialDescriptor::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var PublicKeyCredentialSource|MockObject $pubKeyCredSourceMock */ + $pubKeyCredSourceMock = $this->getMockBuilder(PublicKeyCredentialSource::class) + ->disableOriginalConstructor() + ->onlyMethods(['getPublicKeyCredentialDescriptor']) + ->getMock(); + $pubKeyCredSourceMock->method('getPublicKeyCredentialDescriptor')->willReturn($pubKeyCredDescriptorMock); + + /** @var PublicKeyCredentialList|MockObject $pubKeyCredListMock */ + $pubKeyCredListMock = $this->getMockBuilder(PublicKeyCredentialList::class) + ->onlyMethods(['findAllForUserEntity']) + ->getMock(); + $pubKeyCredListMock->method('findAllForUserEntity')->willReturn( + [$pubKeyCredSourceMock] + ); + + /** @var UserEntity|MockObject $userEntityMock */ + $userEntityMock = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableOxNewObject']) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($pubKeyCredListMock) { + $args = func_get_args(); + switch ($args[0]) { + case PublicKeyCredentialList::class: + return $pubKeyCredListMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + + $return = $this->callMethod( + $sut, + 'getExistingCredentials', + [$userEntityMock] + ); + + $this->assertIsArray($return); + $this->assertContains($pubKeyCredDescriptorMock, $return); + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Application\Model\Webauthn::jsonEncode + */ + public function canJsonEncode() + { + /** @var PublicKeyCredentialCreationOptions|MockObject $pubKeyCredCreationsOptions */ + $pubKeyCredCreationsOptions = $this->getMockBuilder(PublicKeyCredentialCreationOptions::class) + ->disableOriginalConstructor() + ->getMock(); + + $sut = oxNew(Webauthn::class); + + $this->assertJson( + $this->callMethod( + $sut, + 'jsonEncode', + [$pubKeyCredCreationsOptions] + ) + ); + } + + /** + * @test + * @param $jsonReturn + * @return void + * @throws ReflectionException + * @dataProvider canGetOptionsDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::getRequestOptions + */ + public function canGetRequestOptions($jsonReturn) + { + /** @var PublicKeyCredentialDescriptor|MockObject $pubKeyCredDescriptorMock */ + $pubKeyCredDescriptorMock = $this->getMockBuilder(PublicKeyCredentialDescriptor::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var PublicKeyCredentialRequestOptions|MockObject $pubKeyCredRequestOptionsMock */ + $pubKeyCredRequestOptionsMock = $this->getMockBuilder(PublicKeyCredentialRequestOptions::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Server|MockObject $serverMock */ + $serverMock = $this->getMockBuilder(Server::class) + ->onlyMethods(['generatePublicKeyCredentialRequestOptions']) + ->disableOriginalConstructor() + ->getMock(); + $serverMock->expects($this->once())->method('generatePublicKeyCredentialRequestOptions')->with( + $this->anything(), + $this->identicalTo([$pubKeyCredDescriptorMock]) + )->willReturn($pubKeyCredRequestOptionsMock); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['setVariable']) + ->getMock(); + $sessionMock->expects($this->once())->method('setVariable')->with( + $this->identicalTo(Webauthn::SESSION_ASSERTION_OPTIONS), + $this->identicalTo($pubKeyCredRequestOptionsMock) + ); + + /** @var User|MockObject $userMock */ + $userMock = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->onlyMethods(['load']) + ->getMock(); + $userMock->method('load')->willReturn(true); + + /** @var UserEntity|MockObject $userEntityMock */ + $userEntityMock = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableOxNewObject', 'getServer', 'd3GetMockableRegistryObject', 'jsonEncode', + 'getExistingCredentials' + ]) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($userEntityMock, $userMock) { + $args = func_get_args(); + switch ($args[0]) { + case UserEntity::class: + return $userEntityMock; + case User::class: + return $userMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + $sut->method('getServer')->willReturn($serverMock); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->expects($this->once())->method('jsonEncode')->willReturn($jsonReturn); + $sut->expects($this->once())->method('getExistingCredentials')->willReturn([ + $pubKeyCredDescriptorMock + ]); + + if (!$jsonReturn) { + $this->expectException(Exception::class); + } + + $return = $this->callMethod( + $sut, + 'getRequestOptions', + ['userId'] + ); + + if ($jsonReturn) { + $this->assertSame( + $jsonReturn, + $return + ); + } + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Application\Model\Webauthn::getServer + */ + public function canGetServer() + { + /** @var PublicKeyCredentialList|MockObject $pubKeyCredListMock */ + $pubKeyCredListMock = $this->getMockBuilder(PublicKeyCredentialList::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Server|MockObject $serverMock */ + $serverMock = $this->getMockBuilder(Server::class) + ->disableOriginalConstructor() + ->onlyMethods(['setLogger']) + ->getMock(); + $serverMock->expects($this->atLeastOnce())->method('setLogger'); + + /** @var RelyingPartyEntity|MockObject $rpEntityMock */ + $rpEntityMock = $this->getMockBuilder(RelyingPartyEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableOxNewObject']) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($rpEntityMock, $serverMock, $pubKeyCredListMock) { + $args = func_get_args(); + switch ($args[0]) { + case RelyingPartyEntity::class: + return $rpEntityMock; + case Server::class: + return $serverMock; + case PublicKeyCredentialList::class: + return $pubKeyCredListMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + + $this->assertSame( + $serverMock, + $this->callMethod( + $sut, + 'getServer' + ) + ); + } + + /** + * @test + * @param $throwsException + * @return void + * @throws ReflectionException + * @dataProvider loadAndCheckAssertionResponseDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::saveAuthn + */ + public function canSaveAuthn($throwsException) + { + /** @var PublicKeyCredentialCreationOptions|MockObject $pubKeyCredCreationsOptionsMock */ + $pubKeyCredCreationsOptionsMock = $this->getMockBuilder(PublicKeyCredentialCreationOptions::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var PublicKeyCredential|MockObject $pubKeyCredMock */ + $pubKeyCredMock = $this->getMockBuilder(PublicKeyCredential::class) + ->onlyMethods(['saveCredentialSource']) + ->getMock(); + $pubKeyCredMock->expects($this->exactly((int) !$throwsException))->method('saveCredentialSource'); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->willReturn($pubKeyCredCreationsOptionsMock); + + /** @var Server|MockObject $serverMock */ + $serverMock = $this->getMockBuilder(Server::class) + ->onlyMethods(['loadAndCheckAttestationResponse']) + ->disableOriginalConstructor() + ->getMock(); + if ($throwsException) { + $serverMock->expects($this->atLeastOnce())->method('loadAndCheckAttestationResponse') + ->willThrowException(new InvalidArgumentException('msg', 20)); + } else { + $serverMock->expects($this->atLeastOnce())->method('loadAndCheckAttestationResponse'); + } + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableRegistryObject', 'd3GetMockableOxNewObject', 'getServer']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($pubKeyCredMock) { + $args = func_get_args(); + switch ($args[0]) { + case PublicKeyCredential::class: + return $pubKeyCredMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + $sut->method('getServer')->willReturn($serverMock); + + if ($throwsException) { + $this->expectException(InvalidArgumentException::class); + } + + $this->callMethod( + $sut, + 'saveAuthn', + ['credential', 'keyName'] + ); + } + + /** + * @return array + */ + public function loadAndCheckAssertionResponseDataProvider(): array + { + return [ + 'check failed' => [true], + 'check passed' => [false], + ]; + } + + + /** + * @test + * @param $throwsException + * @return void + * @throws ReflectionException + * @dataProvider loadAndCheckAssertionResponseDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::assertAuthn + */ + public function canAssertAuthn($throwsException) + { + /** @var PublicKeyCredentialRequestOptions|MockObject $pubKeyCredRequestOptionsMock */ + $pubKeyCredRequestOptionsMock = $this->getMockBuilder(PublicKeyCredentialRequestOptions::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->willReturn($pubKeyCredRequestOptionsMock); + + /** @var Server|MockObject $serverMock */ + $serverMock = $this->getMockBuilder(Server::class) + ->disableOriginalConstructor() + ->onlyMethods(['loadAndCheckAssertionResponse']) + ->getMock(); + if ($throwsException) { + $serverMock->expects($this->atLeastOnce())->method('loadAndCheckAssertionResponse') + ->willThrowException(new InvalidArgumentException('msg', 20)); + } else { + $serverMock->expects($this->atLeastOnce())->method('loadAndCheckAssertionResponse'); + } + + /** @var UserEntity|MockObject $userEntity */ + $userEntity = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['getUserEntityFrom', 'getServer', 'd3GetMockableRegistryObject', 'getSavedUserIdFromSession']) + ->getMock(); + $sut->method('getUserEntityFrom')->willReturn($userEntity); + $sut->method('getServer')->willReturn($serverMock); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->method('getSavedUserIdFromSession')->willReturn('userId'); + + if ($throwsException) { + $this->expectException(WebauthnGetException::class); + } + + $return = $this->callMethod( + $sut, + 'assertAuthn', + ['responseString'] + ); + + if (!$throwsException) { + $this->assertTrue($return); + } + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Application\Model\Webauthn::getUserEntityFrom + */ + public function cangetUserEntityFrom() + { + /** @var UserEntity|MockObject $userEntityMock */ + $userEntityMock = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var User|MockObject $userMock */ + $userMock = $this->getMockBuilder(User::class) + ->onlyMethods(['load']) + ->getMock(); + $userMock->method('load'); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableOxNewObject']) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($userMock, $userEntityMock) { + $args = func_get_args(); + switch ($args[0]) { + case User::class: + return $userMock; + case UserEntity::class: + return $userEntityMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + + $this->assertSame( + $userEntityMock, + $this->callMethod( + $sut, + 'getUserEntityFrom', + ['userId'] + ) + ); + } + + /** + * @test + * @param $isAdmin + * @param $adminUser + * @param $frontendUser + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canGetSavedUserIdFromSessionDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::getSavedUserIdFromSession + */ + public function canGetSavedUserIdFromSession($isAdmin, $adminUser, $frontendUser, $expected) + { + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->willReturnMap( + [ + [WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER, $adminUser], + [WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER, $frontendUser] + ] + ); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableRegistryObject', 'isAdmin']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->method('isAdmin')->willReturn($isAdmin); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'getSavedUserIdFromSession' + ) + ); + } + + /** + * @return array[] + */ + public function canGetSavedUserIdFromSessionDataProvider(): array + { + return [ + 'admin' => [true, 'admUsr', 'frontendUsr', 'admUsr'], + 'frontend' => [false, 'admUsr', 'frontendUsr', 'frontendUsr'] + ]; + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Application\Model\Webauthn::isAdmin + */ + public function canCheckIsAdmin() + { + /** @var Webauthn $sut */ + $sut = oxNew(Webauthn::class); + + $this->assertIsBool( + $this->callMethod( + $sut, + 'isAdmin' + ) + ); + } + + /** + * @test + * @param $sessionGlobalSwitch + * @param $configGlobalSwitch + * @param $userUseWebauthn + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canCheckIsActiveDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::isActive + */ + public function canCheckIsActive($sessionGlobalSwitch, $configGlobalSwitch, $userUseWebauthn, $expected) + { + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->willReturn($sessionGlobalSwitch); + + /** @var Config|MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->onlyMethods(['getConfigParam']) + ->getMock(); + $configMock->method('getConfigParam')->willReturn($configGlobalSwitch); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['d3GetMockableRegistryObject', 'UserUseWebauthn']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($configMock, $sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Config::class: + return $configMock; + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->method('UserUseWebauthn')->willReturn($userUseWebauthn); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'isActive', + ['userId'] + ) + ); + } + + /** + * @return array + */ + public function canCheckIsActiveDataProvider(): array + { + return [ + 'user use webauthn' => [false, false, true, true], + 'user use webauthn, config disabled' => [true, false, true, false], + 'user use webauthn, session disabled' => [false, true, true, false], + 'user use webauthn, both disabled' => [true, true, true, false], + 'user dont use ' => [false, false, false, false] + ]; + } + + /** + * @test + * @param $credList + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canCheckUserUseWebauthnDataProvider + * @covers \D3\Webauthn\Application\Model\Webauthn::UserUseWebauthn + */ + public function canCheckUserUseWebauthn($credList, $expected) + { + /** @var PublicKeyCredentialList|MockObject $pubKeyCredListMock */ + $pubKeyCredListMock = $this->getMockBuilder(stdClass::class) + ->addMethods(['findAllForUserEntity']) + ->getMock(); + $pubKeyCredListMock->method('findAllForUserEntity')->willReturn($credList); + + /** @var UserEntity|MockObject $userEntityMock */ + $userEntityMock = $this->getMockBuilder(UserEntity::class) + ->disableOriginalConstructor() + ->getMock(); + + /** @var Webauthn|MockObject $sut */ + $sut = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['getUserEntityFrom', 'd3GetMockableOxNewObject']) + ->getMock(); + $sut->method('getUserEntityFrom')->willReturn($userEntityMock); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($pubKeyCredListMock) { + $args = func_get_args(); + switch ($args[0]) { + case PublicKeyCredentialList::class: + return $pubKeyCredListMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'UserUseWebauthn', + ['userId'] + ) + ); + } + + /** + * @return array[] + */ + public function canCheckUserUseWebauthnDataProvider(): array + { + return [ + 'no array' => [null, false], + 'no count' => [[], false], + 'filled array' => [['abc'], true], + ]; + } +} \ No newline at end of file