From 35fa0f395a09922103b0aeb6ebcc7009cf126411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D?= Date: Sat, 20 Jan 2024 09:24:22 +0100 Subject: [PATCH] chore(tests): UT for TemporaryMasterPass service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rubén D --- .../ConfigEncryption/SaveTempController.php | 8 +- .../ConfigManager/IndexController.php | 10 +- app/modules/web/Init.php | 12 +- lib/SP/Core/Definitions/DomainDefinitions.php | 12 +- lib/SP/Domain/Auth/Services/Login.php | 22 +- ...Interface.php => SecureSessionService.php} | 4 +- ...ace.php => TemporaryMasterPassService.php} | 4 +- ...reSessionService.php => SecureSession.php} | 4 +- ...assService.php => TemporaryMasterPass.php} | 53 ++- .../Services/SecureSessionServiceTest.php | 12 +- .../Services/TemporaryMasterPassTest.php | 410 ++++++++++++++++++ 11 files changed, 479 insertions(+), 72 deletions(-) rename lib/SP/Domain/Crypt/Ports/{SecureSessionServiceInterface.php => SecureSessionService.php} (92%) rename lib/SP/Domain/Crypt/Ports/{TemporaryMasterPassServiceInterface.php => TemporaryMasterPassService.php} (95%) rename lib/SP/Domain/Crypt/Services/{SecureSessionService.php => SecureSession.php} (96%) rename lib/SP/Domain/Crypt/Services/{TemporaryMasterPassService.php => TemporaryMasterPass.php} (86%) create mode 100644 tests/SPT/Domain/Crypt/Services/TemporaryMasterPassTest.php diff --git a/app/modules/web/Controllers/ConfigEncryption/SaveTempController.php b/app/modules/web/Controllers/ConfigEncryption/SaveTempController.php index 2051791e..1d629ada 100644 --- a/app/modules/web/Controllers/ConfigEncryption/SaveTempController.php +++ b/app/modules/web/Controllers/ConfigEncryption/SaveTempController.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -31,7 +31,7 @@ use SP\Core\Events\Event; use SP\Domain\Core\Acl\AclActionsInterface; use SP\Domain\Core\Acl\UnauthorizedPageException; use SP\Domain\Core\Exceptions\SessionTimeout; -use SP\Domain\Crypt\Ports\TemporaryMasterPassServiceInterface; +use SP\Domain\Crypt\Ports\TemporaryMasterPassService; use SP\Http\JsonMessage; use SP\Modules\Web\Controllers\SimpleControllerBase; use SP\Modules\Web\Controllers\Traits\JsonTrait; @@ -46,12 +46,12 @@ final class SaveTempController extends SimpleControllerBase { use JsonTrait; - private TemporaryMasterPassServiceInterface $temporaryMasterPassService; + private TemporaryMasterPassService $temporaryMasterPassService; public function __construct( Application $application, SimpleControllerHelper $simpleControllerHelper, - TemporaryMasterPassServiceInterface $temporaryMasterPassService + TemporaryMasterPassService $temporaryMasterPassService ) { parent::__construct($application, $simpleControllerHelper); diff --git a/app/modules/web/Controllers/ConfigManager/IndexController.php b/app/modules/web/Controllers/ConfigManager/IndexController.php index 00c90a7a..515d0cd4 100644 --- a/app/modules/web/Controllers/ConfigManager/IndexController.php +++ b/app/modules/web/Controllers/ConfigManager/IndexController.php @@ -44,7 +44,7 @@ use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Core\File\MimeType; use SP\Domain\Core\File\MimeTypesService; -use SP\Domain\Crypt\Services\TemporaryMasterPassService; +use SP\Domain\Crypt\Services\TemporaryMasterPass; use SP\Domain\Export\Services\BackupFiles; use SP\Domain\Export\Services\XmlExportService; use SP\Domain\Task\Services\Task; @@ -425,17 +425,17 @@ final class IndexController extends ControllerBase $template->assign( 'tempMasterPassTime', - $this->configService->getByParam(TemporaryMasterPassService::PARAM_TIME, 0) + $this->configService->getByParam(TemporaryMasterPass::PARAM_TIME, 0) ); $template->assign( 'tempMasterMaxTime', - $this->configService->getByParam(TemporaryMasterPassService::PARAM_MAX_TIME, 0) + $this->configService->getByParam(TemporaryMasterPass::PARAM_MAX_TIME, 0) ); $tempMasterAttempts = sprintf( '%d/%d', - $this->configService->getByParam(TemporaryMasterPassService::PARAM_ATTEMPTS, 0), - TemporaryMasterPassService::MAX_ATTEMPTS + $this->configService->getByParam(TemporaryMasterPass::PARAM_ATTEMPTS, 0), + TemporaryMasterPass::MAX_ATTEMPTS ); $template->assign('tempMasterAttempts', $tempMasterAttempts); diff --git a/app/modules/web/Init.php b/app/modules/web/Init.php index 068631ed..3b7ef468 100644 --- a/app/modules/web/Init.php +++ b/app/modules/web/Init.php @@ -49,8 +49,8 @@ use SP\Domain\Core\Exceptions\NoSuchPropertyException; use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Core\LanguageInterface; -use SP\Domain\Crypt\Ports\SecureSessionServiceInterface; -use SP\Domain\Crypt\Services\SecureSessionService; +use SP\Domain\Crypt\Ports\SecureSessionService; +use SP\Domain\Crypt\Services\SecureSession; use SP\Domain\Http\RequestInterface; use SP\Domain\ItemPreset\Ports\ItemPresetInterface; use SP\Domain\ItemPreset\Services\ItemPresetService; @@ -137,9 +137,9 @@ final class Init extends HttpModuleBase private Csrf $csrf; - private Language $language; - private SecureSessionService $secureSessionService; - private PluginManager $pluginManager; + private Language $language; + private SecureSession $secureSessionService; + private PluginManager $pluginManager; private ItemPresetService $itemPresetService; private DatabaseUtil $databaseUtil; private UserProfileService $userProfileService; @@ -152,7 +152,7 @@ final class Init extends HttpModuleBase Klein $router, CsrfInterface $csrf, LanguageInterface $language, - SecureSessionServiceInterface $secureSessionService, + SecureSessionService $secureSessionService, PluginManager $pluginManager, ItemPresetService $itemPresetService, DatabaseUtil $databaseUtil, diff --git a/lib/SP/Core/Definitions/DomainDefinitions.php b/lib/SP/Core/Definitions/DomainDefinitions.php index acdb4904..7358af9d 100644 --- a/lib/SP/Core/Definitions/DomainDefinitions.php +++ b/lib/SP/Core/Definitions/DomainDefinitions.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -32,8 +32,8 @@ use SP\Domain\Account\Ports\AccountSearchDataBuilder; use SP\Domain\Account\Services\Builders\AccountSearchData; use SP\Domain\Config\Ports\ConfigDataInterface; use SP\Domain\Core\Crypt\CryptInterface; -use SP\Domain\Crypt\Ports\SecureSessionServiceInterface; -use SP\Domain\Crypt\Services\SecureSessionService; +use SP\Domain\Crypt\Ports\SecureSessionService; +use SP\Domain\Crypt\Services\SecureSession; use SP\Infrastructure\File\FileCache; use function DI\autowire; @@ -103,16 +103,16 @@ final class DomainDefinitions 'SP\Domain\Plugin\Ports\*RepositoryInterface' => autowire( 'SP\Infrastructure\Plugin\Repositories\*Repository' ), - SecureSessionServiceInterface::class => factory( + SecureSessionService::class => factory( static function (ContainerInterface $c) { $fileCache = new FileCache( - SecureSessionService::getFileNameFrom( + SecureSession::getFileNameFrom( $c->get(UuidCookie::class), $c->get(ConfigDataInterface::class)->getPasswordSalt() ) ); - return new SecureSessionService( + return new SecureSession( $c->get(Application::class), $c->get(CryptInterface::class), $fileCache, diff --git a/lib/SP/Domain/Auth/Services/Login.php b/lib/SP/Domain/Auth/Services/Login.php index 736ed7b8..d1dcc2cb 100644 --- a/lib/SP/Domain/Auth/Services/Login.php +++ b/lib/SP/Domain/Auth/Services/Login.php @@ -42,7 +42,7 @@ use SP\Domain\Core\Exceptions\InvalidArgumentException; use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Core\LanguageInterface; -use SP\Domain\Crypt\Ports\TemporaryMasterPassServiceInterface; +use SP\Domain\Crypt\Ports\TemporaryMasterPassService; use SP\Domain\Http\RequestInterface; use SP\Domain\Security\Ports\TrackServiceInterface; use SP\Domain\User\Ports\UserPassRecoverServiceInterface; @@ -86,16 +86,16 @@ final class Login extends Service implements LoginService * @throws InvalidArgumentException */ public function __construct( - Application $application, - private readonly AuthProviderInterface $authProvider, - private readonly LanguageInterface $language, - private readonly TrackServiceInterface $trackService, - private readonly RequestInterface $request, - private readonly UserServiceInterface $userService, - private readonly UserPassRecoverServiceInterface $userPassRecoverService, - private readonly TemporaryMasterPassServiceInterface $temporaryMasterPassService, - private readonly UserPassServiceInterface $userPassService, - private readonly UserProfileServiceInterface $userProfileService + Application $application, + private readonly AuthProviderInterface $authProvider, + private readonly LanguageInterface $language, + private readonly TrackServiceInterface $trackService, + private readonly RequestInterface $request, + private readonly UserServiceInterface $userService, + private readonly UserPassRecoverServiceInterface $userPassRecoverService, + private readonly TemporaryMasterPassService $temporaryMasterPassService, + private readonly UserPassServiceInterface $userPassService, + private readonly UserProfileServiceInterface $userProfileService ) { parent::__construct($application); diff --git a/lib/SP/Domain/Crypt/Ports/SecureSessionServiceInterface.php b/lib/SP/Domain/Crypt/Ports/SecureSessionService.php similarity index 92% rename from lib/SP/Domain/Crypt/Ports/SecureSessionServiceInterface.php rename to lib/SP/Domain/Crypt/Ports/SecureSessionService.php index 71b24e54..b390e001 100644 --- a/lib/SP/Domain/Crypt/Ports/SecureSessionServiceInterface.php +++ b/lib/SP/Domain/Crypt/Ports/SecureSessionService.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -33,7 +33,7 @@ use SP\Domain\Core\Crypt\UuidCookieInterface; * * @package SP\Domain\Crypt\Services */ -interface SecureSessionServiceInterface +interface SecureSessionService { /** * Returns an unique filename from a browser cookie diff --git a/lib/SP/Domain/Crypt/Ports/TemporaryMasterPassServiceInterface.php b/lib/SP/Domain/Crypt/Ports/TemporaryMasterPassService.php similarity index 95% rename from lib/SP/Domain/Crypt/Ports/TemporaryMasterPassServiceInterface.php rename to lib/SP/Domain/Crypt/Ports/TemporaryMasterPassService.php index 48c7346e..2e2e7e50 100644 --- a/lib/SP/Domain/Crypt/Ports/TemporaryMasterPassServiceInterface.php +++ b/lib/SP/Domain/Crypt/Ports/TemporaryMasterPassService.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -36,7 +36,7 @@ use SP\Infrastructure\Common\Repositories\NoSuchItemException; * * @package SP\Domain\Crypt\Services */ -interface TemporaryMasterPassServiceInterface +interface TemporaryMasterPassService { /** * Crea una clave temporal para encriptar la clave maestra y guardarla. diff --git a/lib/SP/Domain/Crypt/Services/SecureSessionService.php b/lib/SP/Domain/Crypt/Services/SecureSession.php similarity index 96% rename from lib/SP/Domain/Crypt/Services/SecureSessionService.php rename to lib/SP/Domain/Crypt/Services/SecureSession.php index 21184972..16bff410 100644 --- a/lib/SP/Domain/Crypt/Services/SecureSessionService.php +++ b/lib/SP/Domain/Crypt/Services/SecureSession.php @@ -33,7 +33,7 @@ use SP\Domain\Common\Services\ServiceException; use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Crypt\RequestBasedPasswordInterface; use SP\Domain\Core\Crypt\UuidCookieInterface; -use SP\Domain\Crypt\Ports\SecureSessionServiceInterface; +use SP\Domain\Crypt\Ports\SecureSessionService; use SP\Domain\Storage\Ports\FileCacheService; use SP\Infrastructure\File\FileException; @@ -45,7 +45,7 @@ use function SP\processException; * * @package SP\Domain\Crypt\Services */ -final class SecureSessionService extends Service implements SecureSessionServiceInterface +final class SecureSession extends Service implements SecureSessionService { private const CACHE_EXPIRE_TIME = 86400; private const CACHE_PATH = CACHE_PATH . DIRECTORY_SEPARATOR . 'secure_session'; diff --git a/lib/SP/Domain/Crypt/Services/TemporaryMasterPassService.php b/lib/SP/Domain/Crypt/Services/TemporaryMasterPass.php similarity index 86% rename from lib/SP/Domain/Crypt/Services/TemporaryMasterPassService.php rename to lib/SP/Domain/Crypt/Services/TemporaryMasterPass.php index 175e7d23..2a469230 100644 --- a/lib/SP/Domain/Crypt/Services/TemporaryMasterPassService.php +++ b/lib/SP/Domain/Crypt/Services/TemporaryMasterPass.php @@ -24,10 +24,8 @@ namespace SP\Domain\Crypt\Services; -use Defuse\Crypto\Exception\CryptoException; use Exception; use SP\Core\Application; -use SP\Core\Crypt\Crypt; use SP\Core\Crypt\Hash; use SP\Core\Events\Event; use SP\Core\Events\EventMessage; @@ -37,21 +35,26 @@ use SP\Domain\Common\Services\Service; use SP\Domain\Common\Services\ServiceException; use SP\Domain\Config\Ports\ConfigService; use SP\Domain\Core\AppInfoInterface; +use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Exceptions\ConstraintException; +use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\Core\Exceptions\QueryException; -use SP\Domain\Crypt\Ports\TemporaryMasterPassServiceInterface; +use SP\Domain\Crypt\Ports\TemporaryMasterPassService; use SP\Domain\Notification\Ports\MailServiceInterface; use SP\Domain\User\Ports\UserServiceInterface; use SP\Infrastructure\Common\Repositories\NoSuchItemException; use SP\Util\PasswordUtil; +use function SP\__; +use function SP\__u; +use function SP\processException; + /** * Class TemporaryMasterPassService * * @package SP\Domain\Crypt\Services */ -final class TemporaryMasterPassService extends Service - implements TemporaryMasterPassServiceInterface +final class TemporaryMasterPass extends Service implements TemporaryMasterPassService { /** * Número máximo de intentos @@ -67,29 +70,23 @@ final class TemporaryMasterPassService extends Service public const PARAM_MAX_TIME = 'tempmaster_maxtime'; public const PARAM_ATTEMPTS = 'tempmaster_attempts'; - private ConfigService $configService; - private UserServiceInterface $userService; - private MailServiceInterface $mailService; - private ?int $maxTime = null; + private ?int $maxTime = null; public function __construct( - Application $application, - ConfigService $configService, - UserServiceInterface $userService, - MailServiceInterface $mailService + Application $application, + private readonly ConfigService $configService, + private readonly UserServiceInterface $userService, + private readonly MailServiceInterface $mailService, + private readonly CryptInterface $crypt, ) { parent::__construct($application); - - $this->configService = $configService; - $this->userService = $userService; - $this->mailService = $mailService; } /** * Crea una clave temporal para encriptar la clave maestra y guardarla. * - * @param int $maxTime El tiempo máximo de validez de la clave + * @param int $maxTime El tiempo máximo de validez de la clave * * @return string * @throws ServiceException @@ -101,12 +98,12 @@ final class TemporaryMasterPassService extends Service // Encriptar la clave maestra con hash aleatorio generado $randomKey = PasswordUtil::generateRandomBytes(32); - $secureKey = Crypt::makeSecuredKey($randomKey); + $secureKey = $this->crypt->makeSecuredKey($randomKey); $configRequest = new ConfigRequest(); $configRequest->add( self::PARAM_PASS, - Crypt::encrypt($this->getMasterKeyFromContext(), $secureKey, $randomKey) + $this->crypt->encrypt($this->getMasterKeyFromContext(), $secureKey, $randomKey) ); $configRequest->add(self::PARAM_KEY, $secureKey); $configRequest->add(self::PARAM_HASH, Hash::hashKey($randomKey)); @@ -122,8 +119,8 @@ final class TemporaryMasterPassService extends Service $this->eventDispatcher->notify( 'create.tempMasterPassword', new Event( - $this, EventMessage::factory() - ->addDescription(__u('Generate temporary password')) + $this, + EventMessage::factory()->addDescription(__u('Generate temporary password')) ) ); @@ -138,7 +135,7 @@ final class TemporaryMasterPassService extends Service /** * Comprueba si la clave temporal es válida * - * @param string $pass clave a comprobar + * @param string $pass clave a comprobar * * @return bool * @throws ServiceException @@ -186,7 +183,7 @@ final class TemporaryMasterPassService extends Service } return $isValid; - } catch (NoSuchItemException $e) { + } catch (NoSuchItemException) { return false; } catch (Exception $e) { processException($e); @@ -213,8 +210,8 @@ final class TemporaryMasterPassService extends Service $this->eventDispatcher->notify( 'expire.tempMasterPassword', new Event( - $this, EventMessage::factory() - ->addDescription(__u('Temporary password expired')) + $this, + EventMessage::factory()->addDescription(__u('Temporary password expired')) ) ); } @@ -283,11 +280,11 @@ final class TemporaryMasterPassService extends Service * @return string con la clave maestra desencriptada * @throws NoSuchItemException * @throws ServiceException - * @throws CryptoException + * @throws CryptException */ public function getUsingKey(string $key): string { - return Crypt::decrypt( + return $this->crypt->decrypt( $this->configService->getByParam(self::PARAM_PASS), $this->configService->getByParam(self::PARAM_KEY), $key diff --git a/tests/SPT/Domain/Crypt/Services/SecureSessionServiceTest.php b/tests/SPT/Domain/Crypt/Services/SecureSessionServiceTest.php index 00ab12ad..abd1d444 100644 --- a/tests/SPT/Domain/Crypt/Services/SecureSessionServiceTest.php +++ b/tests/SPT/Domain/Crypt/Services/SecureSessionServiceTest.php @@ -36,7 +36,7 @@ use SP\Domain\Common\Services\ServiceException; use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Crypt\RequestBasedPasswordInterface; use SP\Domain\Core\Exceptions\CryptException; -use SP\Domain\Crypt\Services\SecureSessionService; +use SP\Domain\Crypt\Services\SecureSession; use SP\Domain\Storage\Ports\FileCacheService; use SP\Infrastructure\File\FileException; use SPT\UnitaryTestCase; @@ -48,7 +48,7 @@ use SPT\UnitaryTestCase; */ class SecureSessionServiceTest extends UnitaryTestCase { - private SecureSessionService $secureSessionService; + private SecureSession $secureSessionService; private RequestBasedPasswordInterface|MockObject $requestBasedPassword; private CryptInterface|MockObject $crypt; private FileCacheService|MockObject $fileCache; @@ -150,7 +150,7 @@ class SecureSessionServiceTest extends UnitaryTestCase $uuidCookie->method('create') ->willReturn(uniqid('', true)); - $this->assertNotEmpty(SecureSessionService::getFileNameFrom($uuidCookie, self::$faker->password)); + $this->assertNotEmpty(SecureSession::getFileNameFrom($uuidCookie, self::$faker->password)); } /** @@ -166,7 +166,7 @@ class SecureSessionServiceTest extends UnitaryTestCase $this->expectException(ServiceException::class); $this->expectExceptionMessage('Unable to get UUID for filename'); - SecureSessionService::getFileNameFrom($uuidCookie, self::$faker->password); + SecureSession::getFileNameFrom($uuidCookie, self::$faker->password); } /** @@ -182,7 +182,7 @@ class SecureSessionServiceTest extends UnitaryTestCase $this->expectException(ServiceException::class); $this->expectExceptionMessage('Unable to get UUID for filename'); - SecureSessionService::getFileNameFrom($uuidCookie, self::$faker->password); + SecureSession::getFileNameFrom($uuidCookie, self::$faker->password); } /** @@ -198,7 +198,7 @@ class SecureSessionServiceTest extends UnitaryTestCase $this->requestBasedPassword = $this->createMock(RequestBasedPasswordInterface::class); $this->fileCache = $this->createMock(FileCacheService::class); - $this->secureSessionService = new SecureSessionService( + $this->secureSessionService = new SecureSession( $this->application, $this->crypt, $this->fileCache, diff --git a/tests/SPT/Domain/Crypt/Services/TemporaryMasterPassTest.php b/tests/SPT/Domain/Crypt/Services/TemporaryMasterPassTest.php new file mode 100644 index 00000000..73a16fed --- /dev/null +++ b/tests/SPT/Domain/Crypt/Services/TemporaryMasterPassTest.php @@ -0,0 +1,410 @@ +. + */ + +namespace SPT\Domain\Crypt\Services; + +use PHPMailer\PHPMailer\Exception; +use PHPUnit\Framework\Constraint\Callback; +use PHPUnit\Framework\MockObject\MockObject; +use RuntimeException; +use SP\Core\Context\ContextException; +use SP\DataModel\Dto\ConfigRequest; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Config\Ports\ConfigService; +use SP\Domain\Core\Context\ContextInterface; +use SP\Domain\Core\Crypt\CryptInterface; +use SP\Domain\Core\Exceptions\ConstraintException; +use SP\Domain\Core\Exceptions\CryptException; +use SP\Domain\Core\Exceptions\QueryException; +use SP\Domain\Crypt\Services\TemporaryMasterPass; +use SP\Domain\Notification\Ports\MailServiceInterface; +use SP\Domain\User\Ports\UserServiceInterface; +use SP\Infrastructure\Common\Repositories\NoSuchItemException; +use SPT\UnitaryTestCase; + +/** + * Class TemporaryMasterPassTest + * + * @group unitary + */ +class TemporaryMasterPassTest extends UnitaryTestCase +{ + + private ConfigService|MockObject $configService; + private UserServiceInterface|MockObject $userService; + private MailServiceInterface|MockObject $mailService; + private CryptInterface|MockObject $crypt; + private TemporaryMasterPass $temporaryMasterPass; + + /** + * @throws Exception + * @throws ServiceException + * @throws ConstraintException + * @throws QueryException + */ + public function testSendByEmailForAllUsers() + { + $key = self::$faker->sha1(); + $emails = array_map(static fn() => self::$faker->email(), range(0, 4)); + + $this->userService + ->expects(self::once()) + ->method('getUserEmailForAll') + ->willReturn(array_map(static fn($email) => (object)['email' => $email], $emails)); + + $this->mailService + ->expects(self::once()) + ->method('sendBatch') + ->with(self::anything(), $emails, self::anything()); + + $this->temporaryMasterPass->sendByEmailForAllUsers($key); + } + + /** + * @throws CryptException + * @throws NoSuchItemException + * @throws ServiceException + */ + public function testGetUsingKey() + { + $masterPass = self::$faker->password(); + $masterKey = self::$faker->sha1(); + $key = self::$faker->sha1(); + + $this->configService + ->expects(self::exactly(2)) + ->method('getByParam') + ->with(...self::withConsecutive(['tempmaster_pass'], ['tempmaster_passkey'])) + ->willReturn($masterPass, $masterKey); + + $this->crypt + ->expects(self::once()) + ->method('decrypt') + ->with($masterPass, $masterKey, $key) + ->willReturn('test'); + + self::assertEquals('test', $this->temporaryMasterPass->getUsingKey($key)); + } + + /** + * @throws Exception + * @throws ServiceException + * @throws ConstraintException + * @throws QueryException + */ + public function testSendByEmailForGroup() + { + $groupId = self::$faker->randomNumber(); + $key = self::$faker->sha1(); + $emails = array_map(static fn() => self::$faker->email(), range(0, 4)); + + $this->userService + ->expects(self::once()) + ->method('getUserEmailForGroup') + ->with($groupId) + ->willReturn(array_map(static fn($email) => (object)['email' => $email], $emails)); + + $this->mailService + ->expects(self::once()) + ->method('sendBatch') + ->with(self::anything(), $emails, self::anything()); + + $this->temporaryMasterPass->sendByEmailForGroup($groupId, $key); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPass() + { + $now = time(); + $pass = self::$faker->password(); + $hash = password_hash($pass, PASSWORD_BCRYPT); + + $this->configService + ->expects(self::exactly(4)) + ->method('getByParam') + ->with( + ... + self::withConsecutive( + ['tempmaster_maxtime'], + ['tempmaster_passtime'], + ['tempmaster_attempts'], + ['tempmaster_passhash'] + ) + ) + ->willReturn( + (string)($now + 3600), + (string)$now, + (string)self::$faker->numberBetween(0, 49), + $hash + ); + + $this->configService + ->expects(self::never()) + ->method('save'); + + self::assertTrue($this->temporaryMasterPass->checkTempMasterPass($pass)); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPassWithZeroMaxTime() + { + $pass = self::$faker->password(); + + $this->configService + ->expects(self::once()) + ->method('getByParam') + ->with('tempmaster_maxtime') + ->willReturn('0'); + + $this->configService + ->expects(self::never()) + ->method('save'); + + self::assertFalse($this->temporaryMasterPass->checkTempMasterPass($pass)); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPassWithWrongKey() + { + $now = time(); + $pass = self::$faker->password(); + $hash = password_hash(self::$faker->sha1(), PASSWORD_BCRYPT); + $attempts = self::$faker->numberBetween(0, 49); + + $this->configService + ->expects(self::exactly(4)) + ->method('getByParam') + ->with( + ... + self::withConsecutive( + ['tempmaster_maxtime'], + ['tempmaster_passtime'], + ['tempmaster_attempts'], + ['tempmaster_passhash'] + ) + ) + ->willReturn( + (string)$now, + (string)($now + 3600), + (string)$attempts, + $hash + ); + + $this->configService + ->expects(self::once()) + ->method('save') + ->with('tempmaster_attempts', $attempts + 1); + + self::assertFalse($this->temporaryMasterPass->checkTempMasterPass($pass)); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPassWithMaxAttempts() + { + $now = time(); + $pass = self::$faker->password(); + $hash = password_hash(self::$faker->sha1(), PASSWORD_BCRYPT); + $attempts = self::$faker->numberBetween(0, 49); + + $this->configService + ->expects(self::exactly(3)) + ->method('getByParam') + ->with( + ... + self::withConsecutive( + ['tempmaster_maxtime'], + ['tempmaster_passtime'], + ['tempmaster_attempts'] + ) + ) + ->willReturn( + (string)$now, + (string)($now + 3600), + '50' + ); + + $configRequest = new ConfigRequest(); + $configRequest->add('tempmaster_pass', ''); + $configRequest->add('tempmaster_passkey', ''); + $configRequest->add('tempmaster_passhash', ''); + $configRequest->add('tempmaster_passtime', 0); + $configRequest->add('tempmaster_maxtime', 0); + $configRequest->add('tempmaster_attempts', 0); + + $this->configService + ->expects(self::once()) + ->method('saveBatch') + ->with($configRequest); + + self::assertFalse($this->temporaryMasterPass->checkTempMasterPass($pass)); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPassWithNoConfigItem() + { + $pass = self::$faker->password(); + + $this->configService + ->expects(self::once()) + ->method('getByParam') + ->willThrowException(NoSuchItemException::error('test')); + + $this->configService + ->expects(self::never()) + ->method('save'); + + self::assertFalse($this->temporaryMasterPass->checkTempMasterPass($pass)); + } + + /** + * @throws ServiceException + */ + public function testCheckTempMasterPassWithError() + { + $pass = self::$faker->password(); + + $this->configService + ->expects(self::once()) + ->method('getByParam') + ->willThrowException(new RuntimeException('test')); + + $this->configService + ->expects(self::never()) + ->method('save'); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Error while checking the temporary password'); + + $this->temporaryMasterPass->checkTempMasterPass($pass); + } + + /** + * @throws ServiceException + * @throws ContextException + */ + public function testCreate() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'test_master_pass'); + + $this->crypt + ->expects(self::once()) + ->method('makeSecuredKey') + ->willReturn('super_secure_key'); + + $this->crypt + ->expects(self::once()) + ->method('encrypt') + ->with('test_master_pass', self::anything(), self::anything()) + ->willReturn('super_secret'); + + $this->configService + ->expects(self::once()) + ->method('saveBatch') + ->with( + new Callback(static function (ConfigRequest $configRequest) { + return $configRequest->get('tempmaster_pass') === 'super_secret' + && $configRequest->get('tempmaster_passkey') === 'super_secure_key' + && !empty($configRequest->get('tempmaster_passhash')) + && !empty($configRequest->get('tempmaster_passtime')) + && $configRequest->get('tempmaster_maxtime') <= time() + 3600 + && $configRequest->get('tempmaster_maxtime') > $configRequest->get('tempmaster_passtime') + && $configRequest->get('tempmaster_attempts') === '0'; + }) + ); + + self::assertNotEmpty($this->temporaryMasterPass->create(3600)); + } + + /** + * @throws ServiceException + * @throws ContextException + */ + public function testCreateWithError() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'test_master_pass'); + + $this->crypt + ->expects(self::once()) + ->method('makeSecuredKey') + ->willReturn('super_secure_key'); + + $this->crypt + ->expects(self::once()) + ->method('encrypt') + ->with('test_master_pass', self::anything(), self::anything()) + ->willReturn('super_secret'); + + $this->configService + ->expects(self::once()) + ->method('saveBatch') + ->with( + new Callback(static function (ConfigRequest $configRequest) { + return $configRequest->get('tempmaster_pass') === 'super_secret' + && $configRequest->get('tempmaster_passkey') === 'super_secure_key' + && !empty($configRequest->get('tempmaster_passhash')) + && !empty($configRequest->get('tempmaster_passtime')) + && $configRequest->get('tempmaster_maxtime') <= time() + 3600 + && $configRequest->get('tempmaster_maxtime') > $configRequest->get('tempmaster_passtime') + && $configRequest->get('tempmaster_attempts') === '0'; + }) + ) + ->willThrowException(new RuntimeException('test')); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Error while generating the temporary password'); + + $this->temporaryMasterPass->create(3600); + } + + + protected function setUp(): void + { + parent::setUp(); + + $this->configService = $this->createMock(ConfigService::class); + $this->userService = $this->createMock(UserServiceInterface::class); + $this->mailService = $this->createMock(MailServiceInterface::class); + $this->crypt = $this->createMock(CryptInterface::class); + + $this->temporaryMasterPass = new TemporaryMasterPass( + $this->application, + $this->configService, + $this->userService, + $this->mailService, + $this->crypt + ); + } + + +}