From 9b3fea6983d52320877ff3d5b62667754ee6bc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D?= Date: Mon, 16 May 2022 11:27:32 +0200 Subject: [PATCH] chore: Rework tests for Installer, no more infra dependencies. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows to test without any infrastructure dependency. Final classes are sorted out by using the `BypassFinals` library. Signed-off-by: Rubén D --- lib/SP/Config/Config.php | 48 +- .../Install/DatabaseSetupInterface.php | 9 +- lib/SP/Services/Install/InstallData.php | 11 + lib/SP/Services/Install/Installer.php | 216 +++---- lib/SP/Services/Install/MySQL.php | 125 ++-- tests/SP/BypassFinalHook.php | 36 ++ tests/SP/Services/Install/InstallerTest.php | 564 +++++++++++------- tests/SP/UnitaryTestCase.php | 84 +++ tests/phpunit.xml | 3 + 9 files changed, 681 insertions(+), 415 deletions(-) create mode 100644 tests/SP/BypassFinalHook.php create mode 100644 tests/SP/UnitaryTestCase.php diff --git a/lib/SP/Config/Config.php b/lib/SP/Config/Config.php index d5833639..58575fd5 100644 --- a/lib/SP/Config/Config.php +++ b/lib/SP/Config/Config.php @@ -26,7 +26,6 @@ namespace SP\Config; use Defuse\Crypto\Exception\EnvironmentIsBrokenException; use Exception; -use Psr\Container\ContainerInterface; use SP\Core\AppInfoInterface; use SP\Core\Context\ContextInterface; use SP\Core\Exceptions\ConfigException; @@ -47,29 +46,28 @@ final class Config /** * Cache file name */ - public const CONFIG_CACHE_FILE = CACHE_PATH . DIRECTORY_SEPARATOR . 'config.cache'; - private static int $timeUpdated; - private ContextInterface $context; - private bool $configLoaded = false; - private ?ConfigDataInterface $configData = null; + public const CONFIG_CACHE_FILE = CACHE_PATH.DIRECTORY_SEPARATOR.'config.cache'; + private static int $timeUpdated; + private ContextInterface $context; + private bool $configLoaded = false; + private ?ConfigDataInterface $configData = null; private XmlFileStorageInterface $fileStorage; - private FileCacheInterface $fileCache; - private ContainerInterface $dic; + private FileCacheInterface $fileCache; + private ConfigBackupService $configBackupService; /** * @throws ConfigException */ public function __construct( XmlFileStorageInterface $fileStorage, - FileCacheInterface $fileCache, - ContextInterface $context, - ContainerInterface $dic - ) - { + FileCacheInterface $fileCache, + ContextInterface $context, + ConfigBackupService $configBackupService + ) { $this->fileCache = $fileCache; $this->fileStorage = $fileStorage; $this->context = $context; - $this->dic = $dic; + $this->configBackupService = $configBackupService; $this->initialize(); } @@ -89,6 +87,7 @@ final class Config if ($this->configData->count() === 0) { $this->fileCache->delete(); $this->initialize(); + return; } @@ -117,11 +116,13 @@ final class Config } catch (Exception $e) { processException($e); - throw new ConfigException($e->getMessage(), + throw new ConfigException( + $e->getMessage(), SPException::CRITICAL, null, $e->getCode(), - $e); + $e + ); } } } @@ -153,7 +154,7 @@ final class Config $configData = new ConfigData(); foreach ($items as $item => $value) { - $methodName = 'set' . ucfirst($item); + $methodName = 'set'.ucfirst($item); if (method_exists($configData, $methodName)) { $configData->$methodName($value); @@ -167,15 +168,18 @@ final class Config /** * Guardar la configuración * - * @throws FileException + * @param \SP\Config\ConfigDataInterface $configData + * @param bool|null $backup + * + * @return \SP\Config\Config + * @throws \SP\Storage\File\FileException */ public function saveConfig( ConfigDataInterface $configData, - ?bool $backup = true - ): Config - { + ?bool $backup = true + ): Config { if ($backup) { - $this->dic->get(ConfigBackupService::class)->backup($configData); + $this->configBackupService->backup($configData); } $configSaver = $this->context->getUserData()->getLogin() diff --git a/lib/SP/Services/Install/DatabaseSetupInterface.php b/lib/SP/Services/Install/DatabaseSetupInterface.php index 3f692948..ceb7482f 100644 --- a/lib/SP/Services/Install/DatabaseSetupInterface.php +++ b/lib/SP/Services/Install/DatabaseSetupInterface.php @@ -41,17 +41,14 @@ interface DatabaseSetupInterface */ public function connectDatabase(); - /** - * @return mixed - */ - public function setupDbUser(); + public function setupDbUser(): array; /** * Crear el usuario para conectar con la base de datos. * Esta función crea el usuario para conectar con la base de datos. * - * @param string $user - * @param string $pass + * @param string $user + * @param string $pass */ public function createDBUser(string $user, string $pass); diff --git a/lib/SP/Services/Install/InstallData.php b/lib/SP/Services/Install/InstallData.php index 0db55c66..edff5c92 100644 --- a/lib/SP/Services/Install/InstallData.php +++ b/lib/SP/Services/Install/InstallData.php @@ -46,6 +46,7 @@ final class InstallData private ?string $dbAuthHost = null; private ?string $dbAuthHostDns = null; private string $siteLang = 'en_US'; + private string $backendType = 'mysql'; public function getDbUser(): ?string { @@ -196,4 +197,14 @@ final class InstallData { $this->dbSocket = $dbSocket; } + + public function getBackendType(): string + { + return $this->backendType; + } + + public function setBackendType(string $backendType): void + { + $this->backendType = $backendType; + } } \ No newline at end of file diff --git a/lib/SP/Services/Install/Installer.php b/lib/SP/Services/Install/Installer.php index 7a4f70c1..c17dd830 100644 --- a/lib/SP/Services/Install/Installer.php +++ b/lib/SP/Services/Install/Installer.php @@ -26,11 +26,10 @@ namespace SP\Services\Install; use Exception; -use Psr\Container\ContainerExceptionInterface; -use Psr\Container\NotFoundExceptionInterface; +use SP\Config\Config; +use SP\Config\ConfigData as ConfigSettings; use SP\Config\ConfigDataInterface; use SP\Core\Crypt\Hash; -use SP\Core\Events\EventDispatcher; use SP\Core\Exceptions\ConstraintException; use SP\Core\Exceptions\InvalidArgumentException; use SP\Core\Exceptions\QueryException; @@ -42,38 +41,56 @@ use SP\DataModel\UserGroupData; use SP\DataModel\UserProfileData; use SP\Http\Request; use SP\Services\Config\ConfigService; -use SP\Services\Service; use SP\Services\User\UserService; use SP\Services\UserGroup\UserGroupService; use SP\Services\UserProfile\UserProfileService; -use SP\Storage\Database\Database; -use SP\Storage\Database\DBStorageInterface; use SP\Util\VersionUtil; defined('APP_ROOT') || die(); /** - * Esta clase es la encargada de instalar sysPass. + * Installer class */ -final class Installer extends Service +final class Installer { /** * sysPass' version and build number */ - public const VERSION = [3, 2, 2]; + public const VERSION = [3, 2, 2]; public const VERSION_TEXT = '3.2'; - public const BUILD = 21031301; + public const BUILD = 21031301; + + private DatabaseSetupInterface $databaseSetup; + private Request $request; + private Config $config; + private UserService $userService; + private UserGroupService $userGroupService; + private UserProfileService $userProfileService; + private ConfigService $configService; + private ?InstallData $installData = null; + + public function __construct( + DatabaseSetupInterface $databaseSetup, + Request $request, + Config $config, + UserService $userService, + UserGroupService $userGroupService, + UserProfileService $userProfileService, + ConfigService $configService + ) { + $this->databaseSetup = $databaseSetup; + $this->request = $request; + $this->config = $config; + $this->userService = $userService; + $this->userGroupService = $userGroupService; + $this->userProfileService = $userProfileService; + $this->configService = $configService; + } - private ?DatabaseSetupInterface $dbs = null; - private ?Request $request = null; - private ?InstallData $installData = null; - private ?ConfigDataInterface $configData = null; /** * @throws InvalidArgumentException * @throws SPException - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ public function run(InstallData $installData): Installer { @@ -94,35 +111,40 @@ final class Installer extends Service throw new InvalidArgumentException( __u('Please, enter the admin username'), SPException::ERROR, - __u('Admin user to log into the application')); + __u('Admin user to log into the application') + ); } if (empty($this->installData->getAdminPass())) { throw new InvalidArgumentException( __u('Please, enter the admin\'s password'), SPException::ERROR, - __u('Application administrator\'s password')); + __u('Application administrator\'s password') + ); } if (empty($this->installData->getMasterPassword())) { throw new InvalidArgumentException( __u('Please, enter the Master Password'), SPException::ERROR, - __u('Master password to encrypt the passwords')); + __u('Master password to encrypt the passwords') + ); } if (strlen($this->installData->getMasterPassword()) < 11) { throw new InvalidArgumentException( __u('Master password too short'), SPException::CRITICAL, - __u('The Master Password length need to be at least 11 characters')); + __u('The Master Password length need to be at least 11 characters') + ); } if (empty($this->installData->getDbAdminUser())) { throw new InvalidArgumentException( __u('Please, enter the database user'), SPException::CRITICAL, - __u('An user with database administrative rights')); + __u('An user with database administrative rights') + ); } if (IS_TESTING @@ -130,58 +152,55 @@ final class Installer extends Service throw new InvalidArgumentException( __u('Please, enter the database password'), SPException::ERROR, - __u('Database administrator\'s password')); + __u('Database administrator\'s password') + ); } if (empty($this->installData->getDbName())) { throw new InvalidArgumentException( __u('Please, enter the database name'), SPException::ERROR, - __u('Application database name. eg. syspass')); + __u('Application database name. eg. syspass') + ); } if (substr_count($this->installData->getDbName(), '.') >= 1) { throw new InvalidArgumentException( __u('Database name cannot contain "."'), SPException::CRITICAL, - __u('Please, remove dots in database name')); + __u('Please, remove dots in database name') + ); } if (empty($this->installData->getDbHost())) { throw new InvalidArgumentException( __u('Please, enter the database server'), SPException::ERROR, - __u('Server where the database will be installed')); + __u('Server where the database will be installed') + ); } } /** - * Iniciar instalación. - * * @throws SPException - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface * @throws ConstraintException * @throws QueryException */ private function install(): void { $this->setupDbHost(); - $this->setupConfig(); - $this->setupDb(); - $this->updateConnectionData(); + $configData = $this->setupConfig(); + + $this->setupDb($configData); $this->saveMasterPassword(); $this->createAdminAccount(); - $version = VersionUtil::getVersionStringNormalized(); + $this->configService->create(new ConfigData('version', VersionUtil::getVersionStringNormalized())); - $this->dic->get(ConfigService::class) - ->create(new ConfigData('version', $version)); + $configData->setInstalled(true); - $this->configData->setInstalled(true); - - $this->config->saveConfig($this->configData, false); + $this->config->saveConfig($configData, false); } /** @@ -190,9 +209,10 @@ final class Installer extends Service private function setupDbHost(): void { if (preg_match( - '/^(?:(?P.*):(?P\d{1,5}))|^(?:unix:(?P.*))/', + '/^(?P.*):(?P\d{1,5})|^unix:(?P.*)/', $this->installData->getDbHost(), - $match) + $match + ) ) { if (!empty($match['socket'])) { $this->installData->setDbSocket($match['socket']); @@ -204,9 +224,10 @@ final class Installer extends Service $this->installData->setDbPort(3306); } - if (strpos('localhost', $this->installData->getDbHost()) === false - && strpos('127.0.0.1', $this->installData->getDbHost()) === false + if (strpos($this->installData->getDbHost(), 'localhost') === false + && strpos($this->installData->getDbHost(), '127.0.0.1') === false ) { + // Use real IP address when unitary testing, because no HTTP request is performed if (defined('SELF_IP_ADDRESS')) { $address = SELF_IP_ADDRESS; } else { @@ -226,7 +247,6 @@ final class Installer extends Service $this->installData->setDbAuthHostDns($dnsHostname); } } - } else { $this->installData->setDbAuthHost('localhost'); } @@ -235,89 +255,61 @@ final class Installer extends Service /** * Setup sysPass config data */ - private function setupConfig(): void + private function setupConfig(): ConfigDataInterface { - // Sets version and remove upgrade key - $this->configData->setConfigVersion(VersionUtil::getVersionStringNormalized()); - $this->configData->setDatabaseVersion(VersionUtil::getVersionStringNormalized()); - $this->configData->setUpgradeKey(null); + $configData = new ConfigSettings(); + $configData->setConfigVersion(VersionUtil::getVersionStringNormalized()) + ->setDatabaseVersion(VersionUtil::getVersionStringNormalized()) + ->setUpgradeKey(null) + ->setDbHost($this->installData->getDbHost()) + ->setDbSocket($this->installData->getDbSocket()) + ->setDbPort($this->installData->getDbPort()) + ->setDbName($this->installData->getDbName()) + ->setSiteLang($this->installData->getSiteLang()); - // Set DB connection info - $this->configData->setDbHost($this->installData->getDbHost()); - $this->configData->setDbSocket($this->installData->getDbSocket()); - $this->configData->setDbPort($this->installData->getDbPort()); - $this->configData->setDbName($this->installData->getDbName()); + $this->config->updateConfig($configData); - // Set site config - $this->configData->setSiteLang($this->installData->getSiteLang()); - - $this->config->updateConfig($this->configData); + return $configData; } /** - * @throws SPException + * @param ConfigDataInterface $configData */ - private function setupDb(string $type = 'mysql'): void + private function setupDb(ConfigDataInterface $configData): void { - if ($type === 'mysql') { - $this->dbs = new MySQL($this->installData, $this->configData); - } - - // Si no es modo hosting se crea un hash para la clave y un usuario con prefijo "sp_" para la DB if ($this->installData->isHostingMode()) { - // Guardar el usuario/clave de conexión a la BD - $this->configData->setDbUser($this->installData->getDbAdminUser()); - $this->configData->setDbPass($this->installData->getDbAdminPass()); + // Save DB connection user and pass + $configData->setDbUser($this->installData->getDbAdminUser()); + $configData->setDbPass($this->installData->getDbAdminPass()); } else { - $this->dbs->setupDbUser(); + [$user, $pass] = $this->databaseSetup->setupDbUser(); + + $configData->setDbUser($user); + $configData->setDbPass($pass); } - $this->dbs->createDatabase(); - $this->dbs->createDBStructure(); - $this->dbs->checkConnection(); - } + $this->config->updateConfig($configData); - /** - * Setup database connection for sysPass. - * - * Updates the database storage interface in the dependency container - */ - private function updateConnectionData(): void - { - // Ugly things... - $this->dic->set( - DBStorageInterface::class, - $this->dbs->createDbHandlerFromInstaller() - ); - $this->dic->set( - Database::class, - new Database( - $this->dic->get(DBStorageInterface::class), - $this->dic->get(EventDispatcher::class) - ) - ); + $this->databaseSetup->createDatabase(); + $this->databaseSetup->createDBStructure(); + $this->databaseSetup->checkConnection(); } /** * Saves the master password metadata * - * @throws SPException + * @throws \SP\Core\Exceptions\SPException */ private function saveMasterPassword(): void { try { - // This service needs to be called after a successful database setup, since - // DI container stores the definition on its first call, so it would contain - // an incomplete database setup - $configService = $this->dic->get(ConfigService::class); - - $configService->create( + $this->configService->create( new ConfigData( 'masterPwd', Hash::hashKey($this->installData->getMasterPassword()) ) ); - $configService->create( + $this->configService->create( new ConfigData( 'lastupdatempass', time() @@ -326,7 +318,7 @@ final class Installer extends Service } catch (Exception $e) { processException($e); - $this->dbs->rollback(); + $this->databaseSetup->rollback(); throw new SPException( $e->getMessage(), @@ -339,12 +331,7 @@ final class Installer extends Service } /** - * Crear el usuario admin de sysPass. - * Esta función crea el grupo, perfil y usuario 'admin' para utilizar sysPass. - * * @throws SPException - * @throws ContainerExceptionInterface - * @throws NotFoundExceptionInterface */ private function createAdminAccount(): void { @@ -357,19 +344,14 @@ final class Installer extends Service $userProfileData->setName('Admin'); $userProfileData->setProfile(new ProfileData()); - $userService = $this->dic->get(UserService::class); - $userGroupService = $this->dic->get(UserGroupService::class); - $userProfileService = $this->dic->get(UserProfileService::class); - - // Datos del usuario $userData = new UserData(); - $userData->setUserGroupId($userGroupService->create($userGroupData)); - $userData->setUserProfileId($userProfileService->create($userProfileData)); + $userData->setUserGroupId($this->userGroupService->create($userGroupData)); + $userData->setUserProfileId($this->userProfileService->create($userProfileData)); $userData->setLogin($this->installData->getAdminLogin()); $userData->setName('sysPass Admin'); $userData->setIsAdminApp(1); - $id = $userService->createWithMasterPass( + $id = $this->userService->createWithMasterPass( $userData, $this->installData->getAdminPass(), $this->installData->getMasterPassword() @@ -381,7 +363,7 @@ final class Installer extends Service } catch (Exception $e) { processException($e); - $this->dbs->rollback(); + $this->databaseSetup->rollback(); throw new SPException( $e->getMessage(), @@ -392,10 +374,4 @@ final class Installer extends Service ); } } - - protected function initialize(): void - { - $this->configData = $this->config->getConfigData(); - $this->request = $this->dic->get(Request::class); - } } \ No newline at end of file diff --git a/lib/SP/Services/Install/MySQL.php b/lib/SP/Services/Install/MySQL.php index 3d08cb33..b5f49d1d 100644 --- a/lib/SP/Services/Install/MySQL.php +++ b/lib/SP/Services/Install/MySQL.php @@ -43,9 +43,10 @@ use SP\Util\PasswordUtil; */ final class MySQL implements DatabaseSetupInterface { - protected InstallData $installData; - protected ConfigDataInterface $configData; - protected ?MySQLHandler $mysqlHandler = null; + private InstallData $installData; + + private ?MySQLHandler $mysqlHandler = null; + private ConfigDataInterface $configData; /** * MySQL constructor. @@ -53,10 +54,9 @@ final class MySQL implements DatabaseSetupInterface * @throws SPException */ public function __construct( - InstallData $installData, + InstallData $installData, ConfigDataInterface $configData - ) - { + ) { $this->installData = $installData; $this->configData = $configData; @@ -98,8 +98,9 @@ final class MySQL implements DatabaseSetupInterface /** * @throws SPException + * @throws \Exception */ - public function setupDbUser() + public function setupDbUser(): array { $user = substr(uniqid('sp_', true), 0, 16); $pass = PasswordUtil::randomPassword(); @@ -112,7 +113,7 @@ final class MySQL implements DatabaseSetupInterface $sth->execute([ $user, $this->installData->getDbAuthHost(), - $this->installData->getDbAuthHostDns() + $this->installData->getDbAuthHostDns(), ]); // Si no existe el usuario, se intenta crear @@ -131,9 +132,7 @@ final class MySQL implements DatabaseSetupInterface ); } - // Guardar el nuevo usuario/clave de conexión a la BD - $this->configData->setDbUser($user); - $this->configData->setDbPass($pass); + return [$user, $pass]; } /** @@ -156,20 +155,24 @@ final class MySQL implements DatabaseSetupInterface $dbc = $this->mysqlHandler->getConnectionSimple(); $dbc->exec( - sprintf($query, + sprintf( + $query, $dbc->quote($user), $dbc->quote($this->installData->getDbAuthHost()), - $dbc->quote($pass)) + $dbc->quote($pass) + ) ); if (!empty($this->installData->getDbAuthHostDns()) && $this->installData->getDbAuthHost() !== $this->installData->getDbAuthHostDns() ) { $dbc->exec( - sprintf($query, + sprintf( + $query, $dbc->quote($user), $this->installData->getDbAuthHostDns(), - $dbc->quote($pass)) + $dbc->quote($pass) + ) ); } @@ -207,10 +210,12 @@ final class MySQL implements DatabaseSetupInterface try { $dbc = $this->mysqlHandler->getConnectionSimple(); - $dbc->exec(sprintf( - 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', - $this->installData->getDbName() - )); + $dbc->exec( + sprintf( + 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', + $this->installData->getDbName() + ) + ); } catch (PDOException $e) { throw new SPException( sprintf(__('Error while creating the DB (\'%s\')'), $e->getMessage()), @@ -224,21 +229,25 @@ final class MySQL implements DatabaseSetupInterface try { $query = 'GRANT ALL PRIVILEGES ON `%s`.* TO %s@%s'; - $dbc->exec(sprintf( + $dbc->exec( + sprintf( $query, $this->installData->getDbName(), $dbc->quote($this->configData->getDbUser()), - $dbc->quote($this->installData->getDbAuthHost())) + $dbc->quote($this->installData->getDbAuthHost()) + ) ); if (!empty($this->installData->getDbAuthHostDns()) && $this->installData->getDbAuthHost() !== $this->installData->getDbAuthHostDns() ) { - $dbc->exec(sprintf( + $dbc->exec( + sprintf( $query, $this->installData->getDbName(), $dbc->quote($this->configData->getDbUser()), - $dbc->quote($this->installData->getDbAuthHostDns())) + $dbc->quote($this->installData->getDbAuthHostDns()) + ) ); } @@ -260,10 +269,12 @@ final class MySQL implements DatabaseSetupInterface try { // Commprobar si existe al seleccionarla $this->mysqlHandler->getConnectionSimple() - ->exec(sprintf( - 'USE `%s`', - $this->installData->getDbName() - )); + ->exec( + sprintf( + 'USE `%s`', + $this->installData->getDbName() + ) + ); } catch (PDOException $e) { throw new SPException( __u('The database does not exist'), @@ -297,29 +308,37 @@ final class MySQL implements DatabaseSetupInterface if ($this->installData->isHostingMode()) { foreach (DatabaseUtil::TABLES as $table) { - $dbc->exec(sprintf( - 'DROP TABLE IF EXISTS `%s`.`%s`', - $this->installData->getDbName(), - $table - )); + $dbc->exec( + sprintf( + 'DROP TABLE IF EXISTS `%s`.`%s`', + $this->installData->getDbName(), + $table + ) + ); } } else { - $dbc->exec(sprintf( - 'DROP DATABASE IF EXISTS `%s`', - $this->installData->getDbName() - )); - $dbc->exec(sprintf( - 'DROP USER IF EXISTS %s@%s', - $dbc->quote($this->configData->getDbUser()), - $dbc->quote($this->installData->getDbAuthHost()) - )); - - if ($this->installData->getDbAuthHost() !== $this->installData->getDbAuthHostDns()) { - $dbc->exec(sprintf( + $dbc->exec( + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ) + ); + $dbc->exec( + sprintf( 'DROP USER IF EXISTS %s@%s', $dbc->quote($this->configData->getDbUser()), - $dbc->quote($this->installData->getDbAuthHostDns()) - )); + $dbc->quote($this->installData->getDbAuthHost()) + ) + ); + + if ($this->installData->getDbAuthHost() !== $this->installData->getDbAuthHostDns()) { + $dbc->exec( + sprintf( + 'DROP USER IF EXISTS %s@%s', + $dbc->quote($this->configData->getDbUser()), + $dbc->quote($this->installData->getDbAuthHostDns()) + ) + ); } } @@ -338,9 +357,15 @@ final class MySQL implements DatabaseSetupInterface $dbc->exec(sprintf('USE `%s`', $this->installData->getDbName())); } catch (PDOException $e) { throw new SPException( - sprintf(__('Error while selecting \'%s\' database (%s)'), $this->installData->getDbName(), $e->getMessage()), + sprintf( + __('Error while selecting \'%s\' database (%s)'), + $this->installData->getDbName(), + $e->getMessage() + ), SPException::CRITICAL, - __u('Unable to use the database to create the structure. Please check the permissions and it does not exist.'), + __u( + 'Unable to use the database to create the structure. Please check the permissions and it does not exist.' + ), $e->getCode(), $e ); @@ -349,8 +374,8 @@ final class MySQL implements DatabaseSetupInterface try { $parser = new MySQLFileParser( new FileHandler( - SQL_PATH . - DIRECTORY_SEPARATOR . + SQL_PATH. + DIRECTORY_SEPARATOR. 'dbstructure.sql' ) ); diff --git a/tests/SP/BypassFinalHook.php b/tests/SP/BypassFinalHook.php new file mode 100644 index 00000000..19e380f3 --- /dev/null +++ b/tests/SP/BypassFinalHook.php @@ -0,0 +1,36 @@ +. + */ + +namespace SP\Tests; + +use DG\BypassFinals; +use PHPUnit\Runner\BeforeTestHook; + +final class BypassFinalHook implements BeforeTestHook +{ + public function executeBeforeTest(string $test): void + { + BypassFinals::enable(); + } +} \ No newline at end of file diff --git a/tests/SP/Services/Install/InstallerTest.php b/tests/SP/Services/Install/InstallerTest.php index 8c625c97..8e656b25 100644 --- a/tests/SP/Services/Install/InstallerTest.php +++ b/tests/SP/Services/Install/InstallerTest.php @@ -1,10 +1,10 @@ . + * along with sysPass. If not, see . */ namespace SP\Tests\Services\Install; -use Defuse\Crypto\Exception\EnvironmentIsBrokenException; -use DI\Container; -use DI\DependencyException; -use DI\NotFoundException; -use PHPUnit\Framework\TestCase; -use SP\Config\Config; -use SP\Core\Context\ContextException; use SP\Core\Exceptions\InvalidArgumentException; use SP\Core\Exceptions\SPException; -use SP\Services\Crypt\MasterPassService; +use SP\Http\Request; +use SP\Services\Config\ConfigService; use SP\Services\Install\InstallData; use SP\Services\Install\Installer; -use SP\Storage\Database\DBStorageInterface; -use SP\Tests\DatabaseUtil; -use SP\Util\PasswordUtil; -use function SP\Tests\getResource; -use function SP\Tests\recreateDir; -use function SP\Tests\saveResource; -use function SP\Tests\setupContext; +use SP\Services\Install\MySQL; +use SP\Services\User\UserService; +use SP\Services\UserGroup\UserGroupService; +use SP\Services\UserProfile\UserProfileService; +use SP\Tests\UnitaryTestCase; +use SP\Util\VersionUtil; + +define('APP_MODULE', 'web-test'); /** * Class InstallerTest * * @package SP\Tests\Services\Install */ -class InstallerTest extends TestCase +class InstallerTest extends UnitaryTestCase { - const DB_NAME = 'syspass-test-install'; - - private static $currentConfig; - /** - * @var Container + * @var \PHPUnit\Framework\MockObject\MockObject|\SP\Services\Install\MySQL */ - private static $dic; - + private $mysqlSetup; /** - * @throws ContextException + * @var \PHPUnit\Framework\MockObject\MockObject|\SP\Services\User\UserService */ - public static function setUpBeforeClass(): void - { - self::$dic = setupContext(); - - self::$currentConfig = getResource('config', 'config.xml'); - } - + private $userService; /** - * This method is called after the last test of this test class is run. + * @var \PHPUnit\Framework\MockObject\Stub|\SP\Http\Request */ - public static function tearDownAfterClass(): void - { - saveResource('config', 'config.xml', self::$currentConfig); - recreateDir(CACHE_PATH); - } + private $request; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\SP\Services\Config\ConfigService + */ + private $configService; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\SP\Services\UserGroup\UserGroupService + */ + private $userGroupService; + /** + * @var \PHPUnit\Framework\MockObject\MockObject|\SP\Services\UserProfile\UserProfileService + */ + private $userProfileService; /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException * @throws InvalidArgumentException * @throws SPException */ - public function testRun() + public function testRunIsSuccessful(): void { - $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost(getenv('DB_SERVER')); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $expectedDbSetup = [self::$faker->userName, self::$faker->password]; + + $this->mysqlSetup->expects($this->once())->method('setupDbUser')->willReturn($expectedDbSetup); + $this->mysqlSetup->expects($this->once())->method('createDatabase'); + $this->mysqlSetup->expects($this->once())->method('createDBStructure'); + $this->mysqlSetup->expects($this->once())->method('checkConnection'); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(1); + $this->configService->expects($this->exactly(3))->method('create'); + $this->userGroupService->expects($this->once())->method('create'); + $this->userProfileService->expects($this->once())->method('create'); + + $params = $this->getInstallData(); + + $installer = $this->getDefaultInstaller(); - $installer = self::$dic->get(Installer::class); $installer->run($params); - $configData = self::$dic->get(Config::class)->getConfigData(); + $configData = $this->config->getConfigData(); $this->assertEquals($params->getDbName(), $configData->getDbName()); $this->assertEquals($params->getDbHost(), $configData->getDbHost()); $this->assertEquals(3306, $configData->getDbPort()); - $this->assertTrue(preg_match('/sp_\w+/', $configData->getDbUser()) === 1); - $this->assertNotEmpty($configData->getDbPass()); + $this->assertEquals($expectedDbSetup[0], $configData->getDbUser()); + $this->assertEquals($expectedDbSetup[1], $configData->getDbPass()); $this->assertEquals($params->getSiteLang(), $configData->getSiteLang()); - - $this->assertTrue(self::$dic->get(MasterPassService::class)->checkMasterPassword($params->getMasterPassword())); - - DatabaseUtil::dropDatabase(self::DB_NAME); - - DatabaseUtil::dropUser($configData->getDbUser(), $params->getDbAuthHost()); - - if ($params->getDbAuthHostDns()) { - DatabaseUtil::dropUser($configData->getDbUser(), $params->getDbAuthHostDns()); - } + $this->assertEquals(VersionUtil::getVersionStringNormalized(), $configData->getConfigVersion()); + $this->assertEquals(VersionUtil::getVersionStringNormalized(), $configData->getDatabaseVersion()); + $this->assertEquals(SELF_IP_ADDRESS, $params->getDbAuthHost()); + $this->assertNull($configData->getUpgradeKey()); + $this->assertNull($configData->getDbSocket()); } /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException + * @return \SP\Services\Install\InstallData */ - public function testFailDbHostName() + private function getInstallData(): InstallData { $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost('fail'); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $params->setDbAdminUser(self::$faker->userName); + $params->setDbAdminPass(self::$faker->password); + $params->setDbName(self::$faker->colorName); + $params->setDbHost(self::$faker->domainName); + $params->setAdminLogin(self::$faker->userName); + $params->setAdminPass(self::$faker->password); + $params->setMasterPassword(self::$faker->password(11)); + $params->setSiteLang(self::$faker->languageCode); - $installer = self::$dic->get(Installer::class); + return $params; + } - $this->expectException(SPException::class); - $this->expectExceptionCode(2002); + /** + * @return \SP\Services\Install\Installer + */ + private function getDefaultInstaller(): Installer + { + return new Installer( + $this->mysqlSetup, + $this->request, + $this->config, + $this->userService, + $this->userGroupService, + $this->userProfileService, + $this->configService + ); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSocketIsUsedForDBConnection(): void + { + $expectedDbSetup = [self::$faker->userName, self::$faker->password]; + $dbSocket = 'unix:/path/to/socket'; + + $this->mysqlSetup->expects($this->once())->method('setupDbUser')->willReturn($expectedDbSetup); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(1); + + $params = $this->getInstallData(); + $params->setDbHost($dbSocket); + + $installer = $this->getDefaultInstaller(); $installer->run($params); + + $configData = $this->config->getConfigData(); + + $this->assertEquals(str_replace('unix:', '', $dbSocket), $configData->getDbSocket()); + $this->assertEquals($dbSocket, $configData->getDbHost()); + $this->assertEquals(0, $configData->getDbPort()); } /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException */ - public function testFailDbHostIp() + public function testLocalhostIsUsedForDBConnection(): void { - $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost('192.168.0.1'); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $expectedDbSetup = [self::$faker->userName, self::$faker->password]; - $installer = self::$dic->get(Installer::class); + $this->mysqlSetup->expects($this->once())->method('setupDbUser')->willReturn($expectedDbSetup); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(1); - $this->expectException(SPException::class); - $this->expectExceptionCode(2002); + $params = $this->getInstallData(); + $params->setDbHost('localhost'); + + $installer = $this->getDefaultInstaller(); $installer->run($params); + + $this->assertEquals($params->getDbHost(), $params->getDbAuthHost()); } /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException */ - public function testFailDbHostPort() + public function testHostAndPortAreUsedForDBConnection(): void { - $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost(getenv('DB_SERVER') . ':3307'); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $expectedDbSetup = [self::$faker->userName, self::$faker->password]; - $installer = self::$dic->get(Installer::class); + $this->mysqlSetup->expects($this->once())->method('setupDbUser')->willReturn($expectedDbSetup); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(1); - $this->expectException(SPException::class); - $this->expectExceptionCode(2002); + $params = $this->getInstallData(); + $params->setDbHost('host:3307'); + + $installer = $this->getDefaultInstaller(); $installer->run($params); + + $this->assertEquals(SELF_IP_ADDRESS, $params->getDbAuthHost()); + $this->assertEquals('host', $params->getDbHost()); + $this->assertEquals(3307, $params->getDbPort()); } /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException */ - public function testFailDbUser() + public function testHostingModeIsUsed(): void { - $params = new InstallData(); - $params->setDbAdminUser('toor'); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost(getenv('DB_SERVER')); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $this->mysqlSetup->expects($this->never())->method('setupDbUser'); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(1); - $installer = self::$dic->get(Installer::class); - - $this->expectException(SPException::class); - $this->expectExceptionCode(1045); - - $installer->run($params); - } - - /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException - */ - public function testFailDbPass() - { - $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass('test'); - $params->setDbName(self::DB_NAME); - $params->setDbHost(getenv('DB_SERVER')); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); - - $installer = self::$dic->get(Installer::class); - - $this->expectException(SPException::class); - $this->expectExceptionCode(1045); - - $installer->run($params); - } - - /** - * @throws DependencyException - * @throws NotFoundException - * @throws EnvironmentIsBrokenException - * @throws InvalidArgumentException - * @throws SPException - */ - public function testHostingMode() - { - $pass = PasswordUtil::randomPassword(); - $host = getenv('DB_SERVER'); - - DatabaseUtil::dropDatabase(self::DB_NAME); - DatabaseUtil::createDatabase(self::DB_NAME); - DatabaseUtil::createUser('syspass_user', $pass, self::DB_NAME, $host); - - $params = new InstallData(); - $params->setDbAdminUser('syspass_user'); - $params->setDbAdminPass($pass); - $params->setDbName(self::DB_NAME); - $params->setDbHost($host); - $params->setAdminLogin('admin'); - $params->setAdminPass('syspass_admin'); - $params->setMasterPassword('00123456789'); - $params->setSiteLang('en_US'); + $params = $this->getInstallData(); $params->setHostingMode(true); - $installer = self::$dic->get(Installer::class); + $installer = $this->getDefaultInstaller(); + $installer->run($params); - $databaseUtil = new \SP\Storage\Database\DatabaseUtil(self::$dic->get(DBStorageInterface::class)); + $configData = $this->config->getConfigData(); - $this->assertTrue($databaseUtil->checkDatabaseTables(self::DB_NAME)); - - $configData = self::$dic->get(Config::class)->getConfigData(); - - $this->assertEquals($params->getDbName(), $configData->getDbName()); - $this->assertEquals($params->getDbHost(), $configData->getDbHost()); - $this->assertEquals(3306, $configData->getDbPort()); - $this->assertNotEmpty($configData->getDbPass()); - $this->assertEquals($params->getSiteLang(), $configData->getSiteLang()); - - $this->assertTrue(self::$dic->get(MasterPassService::class)->checkMasterPassword($params->getMasterPassword())); - - DatabaseUtil::dropDatabase(self::DB_NAME); - DatabaseUtil::dropUser('syspass_user', SELF_IP_ADDRESS); - DatabaseUtil::dropUser('syspass_user', SELF_HOSTNAME); - DatabaseUtil::dropUser('syspass_user', $host); + $this->assertEquals($params->getDbAdminUser(), $configData->getDbUser()); + $this->assertEquals($params->getDbAdminPass(), $configData->getDbPass()); } - protected function tearDown(): void + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + */ + public function testAdminUserIsNotCreated(): void { - @unlink(CONFIG_FILE); + $this->mysqlSetup->expects($this->once())->method('rollback'); + $this->userService->expects($this->once())->method('createWithMasterPass')->willReturn(0); + + $params = $this->getInstallData(); + $params->setHostingMode(true); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(SPException::class); + $this->expectExceptionMessage('Error while creating \'admin\' user'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + */ + public function testConfigIsNotSaved(): void + { + $this->configService->method('create')->willThrowException(new \Exception('Create exception')); + $this->mysqlSetup->expects($this->once())->method('rollback'); + $this->userService->expects($this->never())->method('createWithMasterPass'); + + $params = $this->getInstallData(); + $params->setHostingMode(true); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(SPException::class); + $this->expectExceptionMessage('Create exception'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testAdminLoginIsNotBlank(): void + { + $params = $this->getInstallData(); + $params->setAdminLogin(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the admin username'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testAdminPassIsNotBlank(): void + { + $params = $this->getInstallData(); + $params->setAdminPass(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the admin\'s password'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testMasterPasswordIsNotBlank(): void + { + $params = $this->getInstallData(); + $params->setMasterPassword(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the Master Password'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testMasterPasswordLengthIsWrong(): void + { + $params = $this->getInstallData(); + $params->setMasterPassword(self::$faker->password(1, 10)); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Master password too short'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testDbAdminUserIsWrong(): void + { + $params = $this->getInstallData(); + $params->setDbAdminUser(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the database user'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testDbAdminPassIsWrong(): void + { + $params = $this->getInstallData(); + $params->setDbAdminPass(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the database password'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testDbNameIsBlank(): void + { + $params = $this->getInstallData(); + $params->setDbName(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the database name'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testDbNameIsWrong(): void + { + $params = $this->getInstallData(); + $params->setDbName('test.db'); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Database name cannot contain "."'); + + $installer->run($params); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Exceptions\SPException + **/ + public function testDbHostIsBlank(): void + { + $params = $this->getInstallData(); + $params->setDbHost(''); + + $installer = $this->getDefaultInstaller(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Please, enter the database server'); + + $installer->run($params); + } + + protected function setUp(): void + { + $this->mysqlSetup = $this->createMock(MySQL::class); + $this->userService = $this->createMock(UserService::class); + $this->request = $this->createStub(Request::class); + $this->configService = $this->createMock(ConfigService::class); + $this->userGroupService = $this->createMock(UserGroupService::class); + $this->userProfileService = $this->createMock(UserProfileService::class); + + parent::setUp(); } } diff --git a/tests/SP/UnitaryTestCase.php b/tests/SP/UnitaryTestCase.php new file mode 100644 index 00000000..11e1e5e8 --- /dev/null +++ b/tests/SP/UnitaryTestCase.php @@ -0,0 +1,84 @@ +. + */ + +namespace SP\Tests; + + +use Faker\Factory; +use Faker\Generator; +use PHPUnit\Framework\TestCase; +use SP\Config\Config; +use SP\Core\Context\StatelessContext; +use SP\Services\Config\ConfigBackupService; +use SP\Services\User\UserLoginResponse; +use SP\Storage\File\FileCache; +use SP\Storage\File\XmlHandler; + +/** + * A class to test using a mocked Dependency Injection Container + */ +abstract class UnitaryTestCase extends TestCase +{ + protected static Generator $faker; + protected Config $config; + + public static function setUpBeforeClass(): void + { + self::$faker = Factory::create(); + + parent::setUpBeforeClass(); + } + + /** + * @throws \SP\Core\Exceptions\ConfigException + * @throws \SP\Core\Context\ContextException + */ + protected function setUp(): void + { + $this->config = $this->getConfig(); + + parent::setUp(); + } + + /** + * @throws \SP\Core\Exceptions\ConfigException + * @throws \SP\Core\Context\ContextException + */ + private function getConfig(): Config + { + $userLogin = new UserLoginResponse(); + $userLogin->setLogin(self::$faker->userName); + + $context = new StatelessContext(); + $context->initialize(); + $context->setUserData($userLogin); + + return new Config( + $this->createStub(XmlHandler::class), + $this->createStub(FileCache::class), + $context, + $this->createStub(ConfigBackupService::class) + ); + } +} \ No newline at end of file diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 5a832d1f..ea2fb099 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -10,6 +10,9 @@ processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> + + + ../lib/SP