From 9f0ad7d26fc41e874841e8d1ce8a1d2bc93657a1 Mon Sep 17 00:00:00 2001 From: Daniel Seifert Date: Sat, 10 Dec 2022 23:55:53 +0100 Subject: [PATCH] add further tests --- .../Component/d3_webauthn_UserComponent.php | 74 +++-- .../Application/Model/d3_User_Webauthn.php | 56 ++-- src/Setup/Actions.php | 195 ++++++++++++ src/Setup/Events.php | 186 +----------- .../Component/UserComponentWebauthnTest.php | 277 +++++++++++++++++- .../Application/Model/UserWebauthnTest.php | 218 +++++++++++++- 6 files changed, 787 insertions(+), 219 deletions(-) create mode 100644 src/Setup/Actions.php diff --git a/src/Modules/Application/Component/d3_webauthn_UserComponent.php b/src/Modules/Application/Component/d3_webauthn_UserComponent.php index 899c091..94efde6 100755 --- a/src/Modules/Application/Component/d3_webauthn_UserComponent.php +++ b/src/Modules/Application/Component/d3_webauthn_UserComponent.php @@ -25,7 +25,7 @@ use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Exception; use OxidEsales\Eshop\Application\Model\User; -use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Config; use OxidEsales\Eshop\Core\Request; use OxidEsales\Eshop\Core\Session; use OxidEsales\Eshop\Core\Utils; @@ -46,41 +46,79 @@ class d3_webauthn_UserComponent extends d3_webauthn_UserComponent_parent */ public function login() { - $lgn_user = $this->d3GetMockableRegistryObject(Request::class)->getRequestParameter('lgn_usr'); - $password = $this->d3GetMockableRegistryObject(Request::class)->getRequestParameter('lgn_pwd'); + $this->d3WebauthnLogin(); + + return $this->d3CallMockableFunction([d3_webauthn_UserComponent_parent::class, 'login']); + } + + /** + * @return void + * @throws DoctrineDriverException + * @throws Exception + */ + public function d3WebauthnLogin(): void + { + $lgn_user = $this->d3GetMockableRegistryObject(Request::class)->getRequestParameter( 'lgn_usr'); /** @var d3_User_Webauthn $user */ - $user = oxNew(User::class); + $user = $this->d3GetMockableOxNewObject(User::class); $userId = $user->d3GetLoginUserId($lgn_user); - if ($lgn_user && $userId && !strlen(trim((string) $password))) { - $webauthn = $this->d3GetMockableOxNewObject(Webauthn::class); - - if ($webauthn->isActive($userId) - && !Registry::getSession()->getVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH) - ) { - Registry::getSession()->setVariable( + if ( $this->d3CanUseWebauthn( $lgn_user, $userId)) { + if ($this->d3HasWebauthnButNotLoggedin($userId)) { + $session = $this->d3GetMockableRegistryObject(Session::class); + $session->setVariable( WebauthnConf::WEBAUTHN_SESSION_CURRENTCLASS, - $this->getParent()->getClassKey() != 'd3webauthnlogin' ? $this->getParent()->getClassKey() : 'start'); - Registry::getSession()->setVariable( + $this->getClassKey() != 'd3webauthnlogin' ? $this->getClassKey() : 'start' + ); + $session->setVariable( WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER, $userId ); - - Registry::getSession()->setVariable( + $session->setVariable( WebauthnConf::WEBAUTHN_SESSION_NAVPARAMS, $this->getParent()->getNavigationParams() ); - Registry::getSession()->setVariable( + $session->setVariable( WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS, $this->getParent()->getViewConfig()->getNavFormParams() ); - $sUrl = Registry::getConfig()->getShopHomeUrl() . 'cl=d3webauthnlogin'; + $sUrl = $this->d3GetMockableRegistryObject(Config::class)->getShopHomeUrl() . 'cl=d3webauthnlogin'; $this->d3GetMockableRegistryObject(Utils::class)->redirect($sUrl); } } + } - return parent::login(); + /** + * @param $lgn_user + * @param string|null $userId + * + * @return bool + */ + protected function d3CanUseWebauthn( $lgn_user, ?string $userId): bool + { + $password = $this->d3GetMockableRegistryObject(Request::class)->getRequestParameter( 'lgn_pwd'); + + return $lgn_user && + $userId && + false === $this->d3GetMockableRegistryObject(Session::class) + ->hasVariable( WebauthnConf::WEBAUTHN_SESSION_AUTH ) && + ( ! strlen( trim( (string) $password ) ) ); + } + + /** + * @param $userId + * @return bool + * @throws DoctrineDriverException + * @throws Exception + */ + protected function d3HasWebauthnButNotLoggedin($userId): bool + { + $webauthn = $this->d3GetMockableOxNewObject(Webauthn::class); + + return $webauthn->isActive($userId) + && !$this->d3GetMockableRegistryObject(Session::class) + ->getVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH); } /** diff --git a/src/Modules/Application/Model/d3_User_Webauthn.php b/src/Modules/Application/Model/d3_User_Webauthn.php index 279bde4..1f25a50 100755 --- a/src/Modules/Application/Model/d3_User_Webauthn.php +++ b/src/Modules/Application/Model/d3_User_Webauthn.php @@ -20,8 +20,7 @@ use D3\Webauthn\Application\Model\WebauthnConf; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Query\QueryBuilder; -use OxidEsales\Eshop\Core\Exception\UserException; -use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Config; use OxidEsales\Eshop\Core\Session; use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; @@ -37,7 +36,16 @@ class d3_User_Webauthn extends d3_User_Webauthn_parent public function logout() { $return = $this->d3CallMockableFunction([d3_User_Webauthn_parent::class, 'logout']); + $this->d3WebauthnLogout(); + return $return; + } + + /** + * @return void + */ + protected function d3WebauthnLogout(): void + { $session = $this->d3GetMockableRegistryObject(Session::class); $session->deleteVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH); $session->deleteVariable(WebauthnConf::WEBAUTHN_LOGIN_OBJECT); @@ -52,26 +60,36 @@ class d3_User_Webauthn extends d3_User_Webauthn_parent $session->deleteVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTCLASS); $session->deleteVariable(WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS); - - return $return; } /** * @param $userName * @param $password - * @param $setSessionCookie + * @param bool $setSessionCookie * @return bool - * @throws UserException * @throws ReflectionException */ public function login($userName, $password, $setSessionCookie = false) + { + $userName = $this->d3WebauthnLogin($userName); + + return $this->d3CallMockableFunction([d3_User_Webauthn_parent::class, 'login'], [$userName, $password, $setSessionCookie]); + } + + /** + * @param string $userName + * @return mixed|string|null + * @throws ReflectionException + */ + protected function d3WebauthnLogin(string $userName) { $session = $this->d3GetMockableRegistryObject(Session::class); - if ($session->getVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH)) { + if ($session->getVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH) && + $userName === $session->getVariable(WebauthnConf::WEBAUTHN_SESSION_LOGINUSER) + ) { $userName = $userName ?: $session->getVariable(WebauthnConf::WEBAUTHN_SESSION_LOGINUSER); - $config = Registry::getConfig(); - $shopId = $config->getShopId(); + $shopId = $this->d3GetMockableRegistryObject(Config::class)->getShopId(); /** private method is out of scope */ $class = new ReflectionClass($this); @@ -80,17 +98,16 @@ class d3_User_Webauthn extends d3_User_Webauthn_parent $method->invokeArgs( $this, [ - $session->getVariable(WebauthnConf::WEBAUTHN_SESSION_LOGINUSER), + $userName, $shopId ] ); } - - return parent::login($userName, $password, $setSessionCookie); + return $userName; } /** - * @param string $username + * @param string|null $username * @param string|null $rights * @return string|null * @throws ContainerExceptionInterface @@ -98,7 +115,7 @@ class d3_User_Webauthn extends d3_User_Webauthn_parent * @throws Exception * @throws NotFoundExceptionInterface */ - public function d3GetLoginUserId(string $username, string $rights = null): ?string + public function d3GetLoginUserId(?string $username, string $rights = null): ?string { if (empty($username)) { return null; @@ -116,12 +133,13 @@ class d3_User_Webauthn extends d3_User_Webauthn_parent ), $qb->expr()->eq( 'oxshopid', - $qb->createNamedParameter(Registry::getConfig()->getShopId()) + $qb->createNamedParameter($this->d3GetMockableRegistryObject(Config::class)->getShopId()) ), - $rights ? $qb->expr()->eq( - 'oxrights', - $qb->createNamedParameter($rights) - ) : '1' + $rights ? + $qb->expr()->eq( + 'oxrights', + $qb->createNamedParameter($rights) + ) : '1' ) )->setMaxResults(1); diff --git a/src/Setup/Actions.php b/src/Setup/Actions.php new file mode 100644 index 0000000..8dc1a68 --- /dev/null +++ b/src/Setup/Actions.php @@ -0,0 +1,195 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Webauthn\Setup; + +use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; +use Doctrine\DBAL\Exception as DoctrineException; +use Doctrine\DBAL\Query\QueryBuilder; +use Exception; +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\DbMetaDataHandler; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\UtilsView; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; +use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +class Actions +{ + /** + * SQL statement, that will be executed only at the first time of module installation. + * + * @var array + */ + protected $createCredentialSql = + "CREATE TABLE `d3wa_usercredentials` ( + `OXID` char(32) NOT NULL, + `OXUSERID` char(32) NOT NULL, + `OXSHOPID` int(11) NOT NULL, + `NAME` varchar(100) NOT NULL, + `CREDENTIALID` char(128) NOT NULL, + `CREDENTIAL` varchar(2000) NOT NULL, + `OXTIMESTAMP` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + PRIMARY KEY (`OXID`), + KEY `CREDENTIALID_IDX` (`CREDENTIALID`), + KEY `SHOPUSER_IDX` (`OXUSERID`,`OXSHOPID`) USING BTREE + ) ENGINE=InnoDB COMMENT='WebAuthn Credentials';"; + + /** + * Execute the sql at the first time of the module installation. + * @return void + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function setupModule() + { + if (!$this->tableExists('d3wa_usercredentials')) { + $this->executeSQL($this->createCredentialSql); + } + } + + /** + * Check if table exists + * + * @param string $sTableName table name + * + * @return bool + */ + public function tableExists(string $sTableName): bool + { + $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); + + return $oDbMetaDataHandler->tableExists($sTableName); + } + + /** + * Executes given sql statement. + * + * @param string $sSQL Sql to execute. + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function executeSQL(string $sSQL) + { + DatabaseProvider::getDb()->execute($sSQL); + } + + /** + * Check if field exists in table + * + * @param string $sFieldName field name + * @param string $sTableName table name + * + * @return bool + */ + public function fieldExists(string $sFieldName, string $sTableName): bool + { + $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); + + return $oDbMetaDataHandler->fieldExists($sFieldName, $sTableName); + } + + /** + * Regenerate views for changed tables + */ + public function regenerateViews() + { + $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); + $oDbMetaDataHandler->updateViews(); + } + + /** + * clear cache + */ + public function clearCache() + { + /** @var UtilsView $oUtilsView */ + $oUtilsView = Registry::getUtilsView(); + $sSmartyDir = $oUtilsView->getSmartyDir(); + + if ($sSmartyDir && is_readable($sSmartyDir)) { + foreach (glob($sSmartyDir . '*') as $sFile) { + if (!is_dir($sFile)) { + @unlink($sFile); + } + } + } + } + + /** + * @return void + */ + public function seoUrl() + { + try { + if (!self::hasSeoUrl()) { + self::createSeoUrl(); + } + } catch (Exception|NotFoundExceptionInterface|DoctrineDriverException|ContainerExceptionInterface $e) { + Registry::getUtilsView()->addErrorToDisplay('error wile creating SEO URLs: '.$e->getMessage()); + } + } + + /** + * @return bool + * @throws DoctrineDriverException + * @throws DoctrineException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function hasSeoUrl(): bool + { + /** @var QueryBuilder $qb */ + $qb = ContainerFactory::getInstance()->getContainer()->get(QueryBuilderFactoryInterface::class)->create(); + $qb->select('1') + ->from('oxseo') + ->where( + $qb->expr()->and( + $qb->expr()->eq( + 'oxstdurl', + $qb->createNamedParameter('index.php?cl=d3_account_webauthn') + ), + $qb->expr()->eq( + 'oxshopid', + $qb->createNamedParameter(Registry::getConfig()->getShopId()) + ), + $qb->expr()->eq( + 'oxlang', + $qb->createNamedParameter('1') + ) + ) + ) + ->setMaxResults(1); + return (bool) $qb->execute()->fetchOne(); + } + + /** + * @return void + * @throws DatabaseConnectionException + * @throws DatabaseErrorException + */ + public function createSeoUrl() + { + $query = "INSERT INTO `oxseo` (`OXOBJECTID`, `OXIDENT`, `OXSHOPID`, `OXLANG`, `OXSTDURL`, `OXSEOURL`, `OXTYPE`, `OXFIXED`, `OXEXPIRED`, `OXPARAMS`, `OXTIMESTAMP`) VALUES + ('ff57646b47249ee33c6b672741ac371a', 'bd3b6183c9a2f94682f4c62e714e4d6b', 1, 1, 'index.php?cl=d3_account_webauthn', 'en/key-authentication/', 'static', 0, 0, '', NOW()), + ('ff57646b47249ee33c6b672741ac371a', '94d0d3ec07f10e8838a71e54084be885', 1, 0, 'index.php?cl=d3_account_webauthn', 'sicherheitsschluessel/', 'static', 0, 0, '', NOW());"; + + DatabaseProvider::getDb()->execute($query); + } +} \ No newline at end of file diff --git a/src/Setup/Events.php b/src/Setup/Events.php index 9b56c02..a3d1a6c 100755 --- a/src/Setup/Events.php +++ b/src/Setup/Events.php @@ -15,44 +15,14 @@ declare(strict_types=1); namespace D3\Webauthn\Setup; -use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; -use Doctrine\DBAL\Exception as DoctrineException; -use Doctrine\DBAL\Query\QueryBuilder; -use Exception; -use OxidEsales\Eshop\Core\DatabaseProvider; -use OxidEsales\Eshop\Core\DbMetaDataHandler; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; -use OxidEsales\Eshop\Core\Registry; -use OxidEsales\Eshop\Core\UtilsView; -use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; -use OxidEsales\EshopCommunity\Internal\Framework\Database\QueryBuilderFactoryInterface; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; class Events { - /** - * SQL statement, that will be executed only at the first time of module installation. - * - * @var array - */ - private static $_createCredentialSql = - "CREATE TABLE `d3wa_usercredentials` ( - `OXID` char(32) NOT NULL, - `OXUSERID` char(32) NOT NULL, - `OXSHOPID` int(11) NOT NULL, - `NAME` varchar(100) NOT NULL, - `CREDENTIALID` char(128) NOT NULL, - `CREDENTIAL` varchar(2000) NOT NULL, - `OXTIMESTAMP` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), - PRIMARY KEY (`OXID`), - KEY `CREDENTIALID_IDX` (`CREDENTIALID`), - KEY `SHOPUSER_IDX` (`OXUSERID`,`OXSHOPID`) USING BTREE - ) ENGINE=InnoDB COMMENT='WebAuthn Credentials';"; - /** * Execute action on activate event + * @codeCoverageIgnore * @return void * @throws DatabaseConnectionException * @throws DatabaseErrorException @@ -63,158 +33,18 @@ class Events return; } - self::setupModule(); - - self::regenerateViews(); - - self::clearCache(); - - self::seoUrl(); - } - - public static function onDeactivate() - { - } - - /** - * Execute the sql at the first time of the module installation. - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - private static function setupModule() - { - if (!self::tableExists('d3wa_usercredentials')) { - self::executeSQL(self::$_createCredentialSql); - } - } - - /** - * Check if table exists - * - * @param string $sTableName table name - * - * @return bool - */ - protected static function tableExists(string $sTableName): bool - { - $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); - - return $oDbMetaDataHandler->tableExists($sTableName); - } - - /** - * Executes given sql statement. - * - * @param string $sSQL Sql to execute. - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - private static function executeSQL($sSQL) - { - DatabaseProvider::getDb()->execute($sSQL); - } - - /** - * Check if field exists in table - * - * @param string $sFieldName field name - * @param string $sTableName table name - * - * @return bool - */ - protected static function fieldExists(string $sFieldName, string $sTableName): bool - { - $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); - - return $oDbMetaDataHandler->fieldExists($sFieldName, $sTableName); - } - - /** - * Regenerate views for changed tables - */ - protected static function regenerateViews() - { - $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class ); - $oDbMetaDataHandler->updateViews(); - } - - /** - * clear cache - */ - private static function clearCache() - { - /** @var UtilsView $oUtilsView */ - $oUtilsView = Registry::getUtilsView(); - $sSmartyDir = $oUtilsView->getSmartyDir(); - - if ($sSmartyDir && is_readable($sSmartyDir)) { - foreach (glob($sSmartyDir . '*') as $sFile) { - if (!is_dir($sFile)) { - @unlink($sFile); - } - } - } + $actions = oxNew(Actions::class); + $actions->setupModule(); + $actions->regenerateViews(); + $actions->clearCache(); + $actions->seoUrl(); } /** + * @codeCoverageIgnore * @return void */ - private static function seoUrl() + public static function onDeactivate(): void { - try { - if (!self::hasSeoUrl()) { - self::createSeoUrl(); - } - } catch (Exception|NotFoundExceptionInterface|DoctrineDriverException|ContainerExceptionInterface $e) { - Registry::getUtilsView()->addErrorToDisplay('error wile creating SEO URLs: '.$e->getMessage()); - } - } - - /** - * @return bool - * @throws DoctrineDriverException - * @throws DoctrineException - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface - */ - private static function hasSeoUrl(): bool - { - /** @var QueryBuilder $qb */ - $qb = ContainerFactory::getInstance()->getContainer()->get(QueryBuilderFactoryInterface::class)->create(); - $qb->select('1') - ->from('oxseo') - ->where( - $qb->expr()->and( - $qb->expr()->eq( - 'oxstdurl', - $qb->createNamedParameter('index.php?cl=d3_account_webauthn') - ), - $qb->expr()->eq( - 'oxshopid', - $qb->createNamedParameter(Registry::getConfig()->getShopId()) - ), - $qb->expr()->eq( - 'oxlang', - $qb->createNamedParameter('1') - ) - ) - ) - ->setMaxResults(1); - return (bool) $qb->execute()->fetchOne(); - } - - /** - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - private static function createSeoUrl() - { - $query = "INSERT INTO `oxseo` (`OXOBJECTID`, `OXIDENT`, `OXSHOPID`, `OXLANG`, `OXSTDURL`, `OXSEOURL`, `OXTYPE`, `OXFIXED`, `OXEXPIRED`, `OXPARAMS`, `OXTIMESTAMP`) VALUES - ('ff57646b47249ee33c6b672741ac371a', 'bd3b6183c9a2f94682f4c62e714e4d6b', 1, 1, 'index.php?cl=d3_account_webauthn', 'en/key-authentication/', 'static', 0, 0, '', NOW()), - ('ff57646b47249ee33c6b672741ac371a', '94d0d3ec07f10e8838a71e54084be885', 1, 0, 'index.php?cl=d3_account_webauthn', 'sicherheitsschluessel/', 'static', 0, 0, '', NOW());"; - - DatabaseProvider::getDb()->execute($query); } } \ No newline at end of file diff --git a/src/tests/unit/Modules/Application/Component/UserComponentWebauthnTest.php b/src/tests/unit/Modules/Application/Component/UserComponentWebauthnTest.php index 1b7b7a2..9546111 100644 --- a/src/tests/unit/Modules/Application/Component/UserComponentWebauthnTest.php +++ b/src/tests/unit/Modules/Application/Component/UserComponentWebauthnTest.php @@ -16,13 +16,21 @@ declare(strict_types=1); namespace D3\Webauthn\tests\unit\Modules\Application\Component; use D3\TestingTools\Development\CanAccessRestricted; -use D3\Webauthn\Application\Model\Exceptions\WebauthnException; use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; use D3\Webauthn\Application\Model\Exceptions\WebauthnLoginErrorException; +use D3\Webauthn\Application\Model\Webauthn; +use D3\Webauthn\Application\Model\WebauthnConf; use D3\Webauthn\Application\Model\WebauthnLogin; +use D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent; +use D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent_parent; use OxidEsales\Eshop\Application\Component\UserComponent; +use OxidEsales\Eshop\Application\Controller\Admin\LoginController; +use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Controller\BaseController; use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\Request; use OxidEsales\Eshop\Core\Session; +use OxidEsales\Eshop\Core\Utils; use OxidEsales\Eshop\Core\UtilsView; use OxidEsales\TestingLibrary\UnitTestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -32,6 +40,273 @@ class UserComponentWebauthnTest extends UnitTestCase { use CanAccessRestricted; + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent::login + */ + public function canLogin() + { + /** @var d3_webauthn_UserComponent|MockObject $sut */ + $sut = $this->getMockBuilder(UserComponent::class) + ->onlyMethods(['d3CallMockableFunction', 'd3WebauthnLogin']) + ->getMock(); + $sut->expects($this->once())->method('d3CallMockableFunction')->with( + $this->identicalTo([d3_webauthn_UserComponent_parent::class, 'login']) + ); + $sut->expects($this->once())->method('d3WebauthnLogin'); + + $this->callMethod( + $sut, + 'login' + ); + } + + /** + * @test + * @param $canUseWebauthn + * @param $loggedin + * @param $setVariableCount + * @return void + * @throws ReflectionException + * @dataProvider canCheckloginDataProvider + * @covers \D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent::d3WebauthnLogin + */ + public function canWebauthnLogin($canUseWebauthn, $loggedin, $setVariableCount, $doRedirect) + { + /** @var Utils|MockObject $utilsMock */ + $utilsMock = $this->getMockBuilder(Utils::class) + ->onlyMethods(['redirect']) + ->getMock(); + $utilsMock->expects($this->exactly((int) $doRedirect))->method('redirect'); + + /** @var BaseController|MockObject $baseControllerMock */ + $baseControllerMock = $this->getMockBuilder(BaseController::class) + ->addMethods(['getNavigationParams']) + ->getMock(); + $baseControllerMock->method('getNavigationParams')->willReturn(['empty']); + + /** @var Request|MockObject $requestMock */ + $requestMock = $this->getMockBuilder(Request::class) + ->onlyMethods(['getRequestParameter']) + ->getMock(); + $requestMock->method('getRequestParameter')->willReturnMap([ + ['lgn_usr', 'myUserName'] + ]); + + /** @var User|MockObject $userMock */ + $userMock = $this->getMockBuilder(User::class) + ->onlyMethods(['d3GetLoginUserId']) + ->getMock(); + $userMock->method('d3GetLoginUserId')->willReturn('myUserId'); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['setVariable', 'getVariable']) + ->getMock(); + $sessionMock->expects($this->exactly($setVariableCount))->method('setVariable'); + $sessionMock->method('getVariable')->with(WebauthnConf::WEBAUTHN_ADMIN_SESSION_LOGINUSER) + ->willReturn('myUserName'); + + /** @var d3_webauthn_UserComponent|MockObject $sut */ + $sut = $this->getMockBuilder(UserComponent::class) + ->onlyMethods(['d3CanUseWebauthn', 'd3CallMockableFunction', 'd3HasWebauthnButNotLoggedin', + 'd3GetMockableOxNewObject', 'd3GetMockableRegistryObject', 'getParent' + ]) + ->getMock(); + $sut->method('d3CanUseWebauthn')->willReturn($canUseWebauthn); + $sut->method('d3CallMockableFunction')->willReturn('parentReturn'); + $sut->method('d3HasWebauthnButNotLoggedin')->willReturn($loggedin); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($userMock) { + $args = func_get_args(); + switch ($args[0]) { + case User::class: + return $userMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($utilsMock, $requestMock, $sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Utils::class: + return $utilsMock; + case Request::class: + return $requestMock; + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->method('getParent')->willReturn($baseControllerMock); + + $this->callMethod( + $sut, + 'd3WebauthnLogin' + ); + } + + /** + * @return array + */ + public function canCheckloginDataProvider(): array + { + return [ + 'can not use webauthn' => [false, false, 0, false], + 'already logged in' => [true, false, 0, false], + 'passed' => [true, true, 4, true], + ]; + } + + /** + * @test + * @param $username + * @param $userId + * @param $hasWebauthnLogin + * @param $usedPassword + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canUseWebauthnDataProvider + * @covers \D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent::d3CanUseWebauthn + */ + public function canUseWebauthn($username, $userId, $hasWebauthnLogin, $usedPassword, $expected) + { + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['hasVariable']) + ->getMock(); + $sessionMock->method('hasVariable')->with(WebauthnConf::WEBAUTHN_SESSION_AUTH) + ->willReturn($hasWebauthnLogin); + + /** @var Request|MockObject $requestMock */ + $requestMock = $this->getMockBuilder(Request::class) + ->onlyMethods(['getRequestParameter']) + ->getMock(); + $requestMock->method('getRequestParameter')->with('lgn_pwd')->willReturn($usedPassword); + + /** @var d3_webauthn_UserComponent|MockObject $sut */ + $sut = $this->getMockBuilder(UserComponent::class) + ->onlyMethods(['d3GetMockableRegistryObject']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($requestMock, $sessionMock) { + $args = func_get_args(); + switch ($args[0]) { + case Request::class: + return $requestMock; + case Session::class: + return $sessionMock; + default: + return Registry::get($args[0]); + } + } + ); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'd3CanUseWebauthn', + [$username, $userId] + ) + ); + } + + /** + * @return array + */ + public function canUseWebauthnDataProvider(): array + { + return [ + 'no username' => [null, 'myUserId', false, null, false], + 'no userid' => ['username', null, false, null, false], + 'existing webauthn login' => ['username', 'myUserId', true, null, false], + 'used password' => ['username', 'myUserId', false, 'myPassword', false], + 'passed' => ['username', 'myUserId', false, null, true], + ]; + } + + /** + * @test + * @param $webauthnActive + * @param $hasAuth + * @param $expected + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Modules\Application\Component\d3_webauthn_UserComponent::d3HasWebauthnButNotLoggedin + * @dataProvider canHasWebauthnButNotLoggedinDataProvider + */ + public function canHasWebauthnButNotLoggedin($webauthnActive, $hasAuth, $expected) + { + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->with(WebauthnConf::WEBAUTHN_SESSION_AUTH) + ->willReturn($hasAuth); + + /** @var Webauthn|MockObject $webauthnMock */ + $webauthnMock = $this->getMockBuilder(Webauthn::class) + ->onlyMethods(['isActive']) + ->getMock(); + $webauthnMock->method('isActive')->willReturn($webauthnActive); + + /** @var UserComponent|MockObject $sut */ + $sut = $this->getMockBuilder(UserComponent::class) + ->onlyMethods(['d3GetMockableOxNewObject', 'd3GetMockableRegistryObject']) + ->getMock(); + $sut->method('d3GetMockableOxNewObject')->willReturnCallback( + function () use ($webauthnMock) { + $args = func_get_args(); + switch ($args[0]) { + case Webauthn::class: + return $webauthnMock; + default: + return call_user_func_array("oxNew", $args); + } + } + ); + $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]); + } + } + ); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'd3HasWebauthnButNotLoggedin', + ['userId'] + ) + ); + } + + /** + * @return array + */ + public function canHasWebauthnButNotLoggedinDataProvider(): array + { + return [ + 'webauthn not active' => [false, false, false], + 'has webauthn auth' => [true, true, false], + 'passed' => [true, false, true], + ]; + } + /** * @test * @return void diff --git a/src/tests/unit/Modules/Application/Model/UserWebauthnTest.php b/src/tests/unit/Modules/Application/Model/UserWebauthnTest.php index 08b5471..deb518c 100644 --- a/src/tests/unit/Modules/Application/Model/UserWebauthnTest.php +++ b/src/tests/unit/Modules/Application/Model/UserWebauthnTest.php @@ -16,7 +16,13 @@ declare(strict_types=1); namespace D3\Webauthn\tests\unit\Modules\Application\Model; use D3\TestingTools\Development\CanAccessRestricted; +use D3\Webauthn\Application\Model\WebauthnConf; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; +use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn_parent; +use Exception; use OxidEsales\Eshop\Application\Model\User; +use OxidEsales\Eshop\Core\Config; +use OxidEsales\Eshop\Core\Exception\UserException; use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Session; use OxidEsales\TestingLibrary\UnitTestCase; @@ -27,6 +33,34 @@ class UserWebauthnTest extends UnitTestCase { use CanAccessRestricted; + protected $userId = 'userIdFixture'; + + public function setUp(): void + { + parent::setUp(); + + /** @var d3_User_Webauthn $user */ + $user = oxNew(User::class); + $user->setId($this->userId); + $user->assign([ + 'oxusername' => 'userNameFixture', + 'oxshopid' => '15', + 'oxrights' => 'user', + ]); + $user->save(); + } + + public function tearDown(): void + { + parent::tearDown(); + + try { + /** @var d3_User_Webauthn $user */ + $user = oxNew(User::class); + $user->delete($this->userId); + } catch (Exception $e) {} + } + /** * @test * @return void @@ -34,6 +68,27 @@ class UserWebauthnTest extends UnitTestCase * @covers \D3\Webauthn\Modules\Application\Model\d3_User_Webauthn::logout */ public function canLogout() + { + /** @var User|MockObject $sut */ + $sut = $this->getMockBuilder(User::class) + ->onlyMethods(['d3CallMockableFunction', 'd3WebauthnLogout']) + ->getMock(); + $sut->expects($this->once())->method('d3CallMockableFunction')->willReturn(true); + $sut->expects($this->once())->method('d3WebauthnLogout'); + + $this->callMethod( + $sut, + 'logout' + ); + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Modules\Application\Model\d3_User_Webauthn::d3WebauthnLogout + */ + public function canWebauthnLogout() { /** @var Session|MockObject $sessionMock */ $sessionMock = $this->getMockBuilder(Session::class) @@ -43,9 +98,8 @@ class UserWebauthnTest extends UnitTestCase /** @var User|MockObject $sut */ $sut = $this->getMockBuilder(User::class) - ->onlyMethods(['d3CallMockableFunction', 'd3GetMockableRegistryObject']) + ->onlyMethods(['d3GetMockableRegistryObject']) ->getMock(); - $sut->method('d3CallMockableFunction')->willReturn(true); $sut->method('d3GetMockableRegistryObject')->willReturnCallback( function () use ($sessionMock) { $args = func_get_args(); @@ -60,7 +114,165 @@ class UserWebauthnTest extends UnitTestCase $this->callMethod( $sut, - 'logout' + 'd3WebauthnLogout' ); } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Modules\Application\Model\d3_User_Webauthn::login + */ + public function canLogin() + { + /** @var User|MockObject $sut */ + $sut = $this->getMockBuilder(User::class) + ->onlyMethods(['d3CallMockableFunction', 'd3WebauthnLogin']) + ->getMock(); + $sut->expects($this->once())->method('d3CallMockableFunction')->with($this->identicalTo( + [d3_User_Webauthn_parent::class, 'login'] + ))->willReturn(true); + $sut->expects($this->once())->method('d3WebauthnLogin')->willReturn(true); + + $this->callMethod( + $sut, + 'login', + ['myUserName', 'myPassword'] + ); + } + + /** + * @test + * @return void + * @throws ReflectionException + * @covers \D3\Webauthn\Modules\Application\Model\d3_User_Webauthn::d3WebauthnLogin + * @dataProvider canWebauthnLoginDataProvider + */ + public function canWebauthnLogin($authInSession, $userNameArgument, $userNameInSession, $canLoad, $userIsLoadable, $expected) + { + /** @var Config|MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->onlyMethods(['getShopId']) + ->getMock(); + $configMock->method('getShopId')->willReturn(1); + + /** @var Session|MockObject $sessionMock */ + $sessionMock = $this->getMockBuilder(Session::class) + ->onlyMethods(['getVariable']) + ->getMock(); + $sessionMock->method('getVariable')->willReturnMap([ + [WebauthnConf::WEBAUTHN_SESSION_AUTH, $authInSession], + [WebauthnConf::WEBAUTHN_SESSION_LOGINUSER, $userNameInSession] + ]); + + /** @var User|MockObject $sut */ + $sut = $this->getMockBuilder(User::class) + ->onlyMethods(['d3GetMockableRegistryObject', 'load']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($sessionMock, $configMock) { + $args = func_get_args(); + switch ($args[0]) { + case Session::class: + return $sessionMock; + case Config::class: + return $configMock; + default: + return Registry::get($args[0]); + } + } + ); + $sut->expects($this->exactly((int) ($canLoad)))->method('load')->will( + $userIsLoadable ? + $this->returnValue(true) : + $this->throwException(oxNew(UserException::class)) + ); + + if (!$userIsLoadable) { + $this->expectException(UserException::class); + } + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'd3WebauthnLogin', + [$userNameArgument] + ) + ); + } + + /** + * @return array[] + */ + public function canWebauthnLoginDataProvider(): array + { + return [ + 'has no session auth' => [null, 'userArgument', null, false, true, 'userArgument'], + 'different username' => ['sessionAuth', 'userArgument', 'sessionArgument', false, true, 'userArgument'], + 'identical username' => ['sessionAuth', 'myUserName', 'myUserName', true, true, 'myUserName'], + 'user not loadable' => ['sessionAuth', 'myUserName', 'myUserName', true, false, 'userSession'], + ]; + } + + /** + * @test + * @param $userName + * @param $shopId + * @param $rights + * @param $expected + * @return void + * @throws ReflectionException + * @dataProvider canGetLoginUserIdDataProvider + * @covers \D3\Webauthn\Modules\Application\Model\d3_User_Webauthn::d3GetLoginUserId + */ + public function canGetLoginUserId($userName, $shopId, $rights, $expected) + { + /** @var Config|MockObject $configMock */ + $configMock = $this->getMockBuilder(Config::class) + ->onlyMethods(['getShopId']) + ->getMock(); + $configMock->method('getShopId')->willReturn($shopId); + + /** @var User|MockObject $sut */ + $sut = $this->getMockBuilder(User::class) + ->onlyMethods(['d3GetMockableRegistryObject']) + ->getMock(); + $sut->method('d3GetMockableRegistryObject')->willReturnCallback( + function () use ($configMock) { + $args = func_get_args(); + switch ($args[0]) { + case Config::class: + return $configMock; + default: + return Registry::get($args[0]); + } + } + ); + + $this->assertSame( + $expected, + $this->callMethod( + $sut, + 'd3GetLoginUserId', + [$userName, $rights] + ) + ); + } + + /** + * @return array[] + */ + public function canGetLoginUserIdDataProvider(): array + { + return [ + 'username not set' => [null, '15', 'user', null], + 'user is loadable' => ['userNameFixture', '15', 'user', $this->userId], + 'wrong shop id' => ['userNameFixture', '13', 'user', null], + 'wrong rights' => ['userNameFixture', '15', 'foobar', null], + 'no rights check' => ['userNameFixture', '15', null, $this->userId], + 'user not loadable' => ['unknown', '15', '20', null], + ]; + } } \ No newline at end of file