diff --git a/composer.json b/composer.json index 08560522..cd487e7f 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "fzaninotto/faker": "1.9.x-dev", "fabpot/goutte": "^v3.2", "nikic/php-parser": "^v4.1", - "dg/bypass-finals": "^v1.3" + "dg/bypass-finals": "^v1.3", + "jimbojsb/pseudo": "^0.5" }, "suggest": { "syspass/plugin-authenticator": "^v2.2", diff --git a/composer.lock b/composer.lock index efbe95d6..30a89455 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c621fdf745779bfd4d70c03e177361c7", + "content-hash": "6fd1ed6990aa4d5a83ef253a9b84f18e", "packages": [ { "name": "ademarre/binary-to-text-php", @@ -3974,6 +3974,49 @@ "abandoned": true, "time": "2020-12-11T09:59:14+00:00" }, + { + "name": "jimbojsb/pseudo", + "version": "0.5", + "source": { + "type": "git", + "url": "https://github.com/jimbojsb/pseudo.git", + "reference": "d75d639baf6b5f0a770d0691607ffb7385ba3c10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jimbojsb/pseudo/zipball/d75d639baf6b5f0a770d0691607ffb7385ba3c10", + "reference": "d75d639baf6b5f0a770d0691607ffb7385ba3c10", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^5.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/php-sql-parser.php" + ], + "psr-0": { + "Pseudo": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Butts", + "email": "josh@joshbutts.com" + } + ], + "description": "PDO/MySQL Connection Mocking", + "support": { + "issues": "https://github.com/jimbojsb/pseudo/issues", + "source": "https://github.com/jimbojsb/pseudo/tree/0.5" + }, + "time": "2021-09-27T03:12:35+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.11.0", diff --git a/lib/SP/Services/Install/DatabaseSetupInterface.php b/lib/SP/Services/Install/DatabaseSetupInterface.php index ceb7482f..b25f2cfb 100644 --- a/lib/SP/Services/Install/DatabaseSetupInterface.php +++ b/lib/SP/Services/Install/DatabaseSetupInterface.php @@ -24,8 +24,6 @@ namespace SP\Services\Install; -use SP\Storage\Database\DBStorageInterface; - /** * Interface DatabaseInterface * @@ -60,7 +58,7 @@ interface DatabaseSetupInterface /** * @return mixed */ - public function checkDatabaseExist(); + public function checkDatabaseExists(); /** * Deshacer la instalación en caso de fallo. @@ -78,14 +76,4 @@ interface DatabaseSetupInterface * Comprobar la conexión a la BBDD */ public function checkConnection(); - - /** - * @return DBStorageInterface - */ - public function getDbHandler(): DBStorageInterface; - - /** - * @return DBStorageInterface - */ - public function createDbHandlerFromInstaller(): DBStorageInterface; } \ No newline at end of file diff --git a/lib/SP/Services/Install/InstallData.php b/lib/SP/Services/Install/InstallData.php index edff5c92..76a854c6 100644 --- a/lib/SP/Services/Install/InstallData.php +++ b/lib/SP/Services/Install/InstallData.php @@ -31,42 +31,22 @@ namespace SP\Services\Install; */ final class InstallData { - private ?string $dbUser = null; - private ?string $dbAdminUser = null; - private ?string $dbPass = null; - private ?string $dbAdminPass = null; - private string $dbName = 'syspass'; - private string $dbHost = 'localhost'; - private ?string $dbSocket = null; - private int $dbPort = 0; - private ?string $adminLogin = null; - private ?string $adminPass = null; + public const BACKEND_MYSQL = 'mysql'; + + private ?string $dbAdminUser = null; + private ?string $dbAdminPass = null; + private string $dbName = 'syspass'; + private string $dbHost = 'localhost'; + private ?string $dbSocket = null; + private int $dbPort = 0; + private ?string $adminLogin = null; + private ?string $adminPass = null; private ?string $masterPassword = null; - private bool $hostingMode = false; - private ?string $dbAuthHost = null; - private ?string $dbAuthHostDns = null; - private string $siteLang = 'en_US'; - private string $backendType = 'mysql'; - - public function getDbUser(): ?string - { - return $this->dbUser; - } - - public function setDbUser(string $dbUser): void - { - $this->dbUser = $dbUser; - } - - public function getDbPass(): ?string - { - return $this->dbPass; - } - - public function setDbPass(string $dbPass): void - { - $this->dbPass = $dbPass; - } + private bool $hostingMode = false; + private ?string $dbAuthHost = null; + private ?string $dbAuthHostDns = null; + private string $siteLang = 'en_US'; + private string $backendType = self::BACKEND_MYSQL; public function getDbName(): string { diff --git a/lib/SP/Services/Install/Installer.php b/lib/SP/Services/Install/Installer.php index 379ae743..8cc43542 100644 --- a/lib/SP/Services/Install/Installer.php +++ b/lib/SP/Services/Install/Installer.php @@ -45,7 +45,10 @@ use SP\Services\User\UserService; use SP\Services\UserGroup\UserGroupService; use SP\Services\UserProfile\UserProfileService; use SP\Storage\Database\DatabaseConnectionData; +use SP\Storage\Database\DatabaseUtil; +use SP\Storage\Database\MySQLFileParser; use SP\Storage\Database\MySQLHandler; +use SP\Storage\File\FileHandler; use SP\Util\VersionUtil; defined('APP_ROOT') || die(); @@ -103,9 +106,22 @@ final class Installer ->setDbUser($installData->getDbAdminUser()) ->setDbPass($installData->getDbAdminPass()); - return new MySQL(new MySQLHandler($connectionData), $installData, $configData); - } + if ($installData->getBackendType() === 'mysql') { + $parser = new MySQLFileParser( + new FileHandler( + SQL_PATH. + DIRECTORY_SEPARATOR. + 'dbstructure.sql' + ) + ); + $mySQLHandler = new MySQLHandler($connectionData); + + return new MySQL($mySQLHandler, $installData, $configData, $parser, new DatabaseUtil($mySQLHandler)); + } + + throw new SPException(__u('Unimplemented'), SPException::ERROR, __u('Wrong backend type')); + } /** * @throws InvalidArgumentException diff --git a/lib/SP/Services/Install/MySQL.php b/lib/SP/Services/Install/MySQL.php index b5f49d1d..f6850c7c 100644 --- a/lib/SP/Services/Install/MySQL.php +++ b/lib/SP/Services/Install/MySQL.php @@ -27,13 +27,10 @@ namespace SP\Services\Install; use PDOException; use SP\Config\ConfigDataInterface; use SP\Core\Exceptions\SPException; -use SP\Storage\Database\DatabaseConnectionData; +use SP\Storage\Database\DatabaseFileInterface; use SP\Storage\Database\DatabaseUtil; use SP\Storage\Database\DBStorageInterface; -use SP\Storage\Database\MySQLFileParser; -use SP\Storage\Database\MySQLHandler; use SP\Storage\File\FileException; -use SP\Storage\File\FileHandler; use SP\Util\PasswordUtil; /** @@ -43,24 +40,28 @@ use SP\Util\PasswordUtil; */ final class MySQL implements DatabaseSetupInterface { - private InstallData $installData; - - private ?MySQLHandler $mysqlHandler = null; - private ConfigDataInterface $configData; + private InstallData $installData; + private DBStorageInterface $DBStorage; + private ConfigDataInterface $configData; + private DatabaseFileInterface $databaseFile; + private DatabaseUtil $databaseUtil; /** * MySQL constructor. * - * @throws SPException */ public function __construct( + DBStorageInterface $DBStorage, InstallData $installData, - ConfigDataInterface $configData + ConfigDataInterface $configData, + DatabaseFileInterface $databaseFile, + DatabaseUtil $databaseUtil ) { $this->installData = $installData; $this->configData = $configData; - - $this->connectDatabase(); + $this->DBStorage = $DBStorage; + $this->databaseFile = $databaseFile; + $this->databaseUtil = $databaseUtil; } /** @@ -74,15 +75,7 @@ final class MySQL implements DatabaseSetupInterface public function connectDatabase(): void { try { - $dbc = (new DatabaseConnectionData()) - ->setDbHost($this->installData->getDbHost()) - ->setDbPort($this->installData->getDbPort()) - ->setDbSocket($this->installData->getDbSocket()) - ->setDbUser($this->installData->getDbAdminUser()) - ->setDbPass($this->installData->getDbAdminPass()); - - $this->mysqlHandler = new MySQLHandler($dbc); - $this->mysqlHandler->getConnectionSimple(); + $this->DBStorage->getConnectionSimple(); } catch (SPException $e) { processException($e); @@ -107,7 +100,7 @@ final class MySQL implements DatabaseSetupInterface try { // Comprobar si el usuario proporcionado existe - $sth = $this->mysqlHandler->getConnectionSimple() + $sth = $this->DBStorage->getConnectionSimple() ->prepare('SELECT COUNT(*) FROM mysql.user WHERE `user` = ? AND (`host` = ? OR `host` = ?)'); $sth->execute([ @@ -152,7 +145,7 @@ final class MySQL implements DatabaseSetupInterface try { $query = 'CREATE USER %s@%s IDENTIFIED BY %s'; - $dbc = $this->mysqlHandler->getConnectionSimple(); + $dbc = $this->DBStorage->getConnectionSimple(); $dbc->exec( sprintf( @@ -199,7 +192,7 @@ final class MySQL implements DatabaseSetupInterface { if (!$this->installData->isHostingMode()) { - if ($this->checkDatabaseExist()) { + if ($this->checkDatabaseExists()) { throw new SPException( __u('The database already exists'), SPException::ERROR, @@ -208,7 +201,7 @@ final class MySQL implements DatabaseSetupInterface } try { - $dbc = $this->mysqlHandler->getConnectionSimple(); + $dbc = $this->DBStorage->getConnectionSimple(); $dbc->exec( sprintf( @@ -266,45 +259,22 @@ final class MySQL implements DatabaseSetupInterface ); } } else { - try { - // Commprobar si existe al seleccionarla - $this->mysqlHandler->getConnectionSimple() - ->exec( - sprintf( - 'USE `%s`', - $this->installData->getDbName() - ) - ); - } catch (PDOException $e) { - throw new SPException( - __u('The database does not exist'), - SPException::ERROR, - __u('You need to create it and assign the needed permissions'), - $e->getCode(), - $e - ); - } + $this->checkDatabase(__u('You need to create it and assign the needed permissions')); } } - /** - * @throws SPException - */ - public function checkDatabaseExist(): bool + public function checkDatabaseExists(): bool { - $sth = $this->mysqlHandler->getConnectionSimple() + $sth = $this->DBStorage->getConnectionSimple() ->prepare('SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1'); $sth->execute([$this->installData->getDbName()]); return (int)$sth->fetchColumn() === 1; } - /** - * @throws SPException - */ public function rollback(): void { - $dbc = $this->mysqlHandler->getConnectionSimple(); + $dbc = $this->DBStorage->getConnectionSimple(); if ($this->installData->isHostingMode()) { foreach (DatabaseUtil::TABLES as $table) { @@ -346,15 +316,14 @@ final class MySQL implements DatabaseSetupInterface } /** - * @throws SPException + * @throws \SP\Core\Exceptions\SPException */ - public function createDBStructure(): void + private function checkDatabase(string $exceptionHint): void { try { - $dbc = $this->mysqlHandler->getConnectionSimple(); - - // Usar la base de datos de sysPass - $dbc->exec(sprintf('USE `%s`', $this->installData->getDbName())); + $this->DBStorage + ->getConnectionSimple() + ->exec(sprintf('USE `%s`', $this->installData->getDbName())); } catch (PDOException $e) { throw new SPException( sprintf( @@ -363,24 +332,28 @@ final class MySQL implements DatabaseSetupInterface $e->getMessage() ), SPException::CRITICAL, - __u( - 'Unable to use the database to create the structure. Please check the permissions and it does not exist.' - ), + $exceptionHint, $e->getCode(), $e ); } + } + + /** + * @throws SPException + */ + public function createDBStructure(): void + { + $this->checkDatabase( + __u( + 'Unable to use the database to create the structure. Please check the permissions and it does not exist.' + ) + ); try { - $parser = new MySQLFileParser( - new FileHandler( - SQL_PATH. - DIRECTORY_SEPARATOR. - 'dbstructure.sql' - ) - ); + $dbc = $this->DBStorage->getConnectionSimple(); - foreach ($parser->parse() as $query) { + foreach ($this->databaseFile->parse() as $query) { $dbc->exec($query); } } catch (PDOException $e) { @@ -417,9 +390,7 @@ final class MySQL implements DatabaseSetupInterface */ public function checkConnection(): void { - $databaseUtil = new DatabaseUtil($this->mysqlHandler); - - if (!$databaseUtil->checkDatabaseTables($this->installData->getDbName())) { + if (!$this->databaseUtil->checkDatabaseTables($this->installData->getDbName())) { $this->rollback(); throw new SPException( @@ -429,14 +400,4 @@ final class MySQL implements DatabaseSetupInterface ); } } - - public function getDbHandler(): DBStorageInterface - { - return $this->mysqlHandler; - } - - public function createDbHandlerFromInstaller(): DBStorageInterface - { - return new MySQLHandler(DatabaseConnectionData::getFromConfig($this->configData)); - } } \ No newline at end of file diff --git a/lib/SP/Storage/Database/MySQLFileParser.php b/lib/SP/Storage/Database/MySQLFileParser.php index 1aa6a7dc..bf967fce 100644 --- a/lib/SP/Storage/Database/MySQLFileParser.php +++ b/lib/SP/Storage/Database/MySQLFileParser.php @@ -54,7 +54,7 @@ final class MySQLFileParser implements DatabaseFileInterface $this->fileHandler->checkIsReadable(); - $handle = $this->fileHandler->open('rb'); + $handle = $this->fileHandler->open(); while (($buffer = fgets($handle)) !== false) { $buffer = trim($buffer); @@ -69,22 +69,24 @@ final class MySQLFileParser implements DatabaseFileInterface // Checks if line is an SQL statement wrapped by a comment if (preg_match('#^(?/\*!\d+.*\*/)#', $buffer, $matches)) { if (!$end) { - $query .= $matches['stmt'] . PHP_EOL; + $query .= $matches['stmt'].PHP_EOL; } else { - $queries[] = $query . $matches['stmt']; + $queries[] = $query.$matches['stmt']; $query = ''; } - } else if (!$end) { - $query .= $buffer . PHP_EOL; - } else if (strpos($buffer, 'DELIMITER') === false) { - $queries[] = $query . - trim(substr_replace( - $buffer, - '', - $length - $delimiterLength), - $delimiterLength - ); + } elseif (!$end) { + $query .= $buffer.PHP_EOL; + } elseif (strpos($buffer, 'DELIMITER') === false) { + $queries[] = $query. + trim( + substr_replace( + $buffer, + '', + $length - $delimiterLength + ), + $delimiterLength + ); $query = ''; } diff --git a/tests/SP/Services/Install/InstallerTest.php b/tests/SP/Services/Install/InstallerTest.php index 3b7b902d..b186dd20 100644 --- a/tests/SP/Services/Install/InstallerTest.php +++ b/tests/SP/Services/Install/InstallerTest.php @@ -428,6 +428,30 @@ class InstallerTest extends UnitaryTestCase $installer->run($this->databaseSetup, $params); } + /** + * @throws \SP\Core\Exceptions\SPException + * + * @doesNotPerformAssertions + */ + public function testGetDatabaseSetupIsSuccessful() + { + Installer::getDatabaseSetup($this->getInstallData(), $this->config->getConfigData()); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetDatabaseSetupIsNotSuccessfulWithWrongBackend() + { + $installData = $this->getInstallData(); + $installData->setBackendType('test'); + + $this->expectException(SPException::class); + $this->expectExceptionMessage('Unimplemented'); + + Installer::getDatabaseSetup($installData, $this->config->getConfigData()); + } + /** * @noinspection ClassMockingCorrectnessInspection * @noinspection PhpUnitInvalidMockingEntityInspection diff --git a/tests/SP/Services/Install/MySQLTest.php b/tests/SP/Services/Install/MySQLTest.php index 994544bc..bc39b395 100644 --- a/tests/SP/Services/Install/MySQLTest.php +++ b/tests/SP/Services/Install/MySQLTest.php @@ -24,191 +24,764 @@ namespace SP\Tests\Services\Install; -use PHPUnit\Framework\TestCase; -use SP\Config\ConfigData; +use PDOException; +use SP\Config\ConfigDataInterface; use SP\Core\Exceptions\SPException; use SP\Services\Install\InstallData; use SP\Services\Install\MySQL; -use SP\Storage\Database\DatabaseException; -use SP\Storage\Database\MySQLHandler; -use SP\Tests\DatabaseUtil; -use SP\Util\PasswordUtil; +use SP\Storage\Database\DatabaseFileInterface; +use SP\Storage\Database\DatabaseUtil; +use SP\Storage\Database\DBStorageInterface; +use SP\Storage\File\FileException; +use SP\Tests\Stubs\Pdo; +use SP\Tests\UnitaryTestCase; /** * Class MySQLTest * * @package SP\Tests\Services\Install */ -class MySQLTest extends TestCase +class MySQLTest extends UnitaryTestCase { - const DB_NAME = 'syspass-test-install'; + private DBStorageInterface $DBStorage; + private MySQL $mysql; + private Pdo $pdo; + private InstallData $installData; + private ConfigDataInterface $configData; + private DatabaseFileInterface $databaseFile; + private DatabaseUtil $databaseUtil; /** - * @throws SPException + * @throws \SP\Core\Exceptions\SPException */ - public function testCheckDatabaseNotExist() + public function testConnectDatabaseIsSuccessful(): void { - DatabaseUtil::dropDatabase(self::DB_NAME); + $this->DBStorage->expects(self::once())->method('getConnectionSimple'); - $mysql = new MySQL($this->getParams(), new ConfigData()); - - $this->assertFalse($mysql->checkDatabaseExist()); + $this->mysql->connectDatabase(); } /** - * @return InstallData + * @throws \SP\Core\Exceptions\SPException */ - private function getParams() + public function testConnectDatabaseIsNotSuccessful(): void + { + $this->DBStorage->expects(self::once()) + ->method('getConnectionSimple') + ->willThrowException( + new SPException('test') + ); + + $this->expectException(SPException::class); + $this->expectExceptionMessage('Unable to connect to DB'); + + $this->mysql->connectDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupUserIsSuccessful(): void + { + $this->pdo->mock('SELECT COUNT(*) FROM mysql.user WHERE `user` = ? AND (`host` = ? OR `host` = ?)', []); + + [$user, $pass] = $this->mysql->setupDbUser(); + + $this->assertSame(preg_match('/sp_\w+/', $user), 1); + $this->assertNotEmpty($pass); + $this->assertEquals(16, strlen($pass)); + + } + + public function testSetupUserIsNotSuccessful(): void + { + $this->pdo->mock('SELECT COUNT(*) FROM mysql.user WHERE `user` = ? AND (`host` = ? OR `host` = ?)', []); + + $pdoException = new PDOException('test'); + + $this->DBStorage->expects(self::once()) + ->method('getConnectionSimple') + ->willThrowException($pdoException); + + $this->expectException(SPException::class); + $this->expectExceptionMessageMatches('/Unable to check the sysPass user \(sp_\w+\)/'); + + $this->mysql->setupDbUser(); + } + + public function testCheckDatabaseDoesNotExist(): void + { + $this->pdo->mock('SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', [], []); + + $this->assertFalse($this->mysql->checkDatabaseExists()); + } + + public function testCheckDatabaseExists(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [[1]], + [$this->installData->getDbName()] + ); + + $this->assertTrue($this->mysql->checkDatabaseExists()); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsSuccessful(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [], + [$this->installData->getDbName()] + ); + + $this->configData->setDbUser(self::$faker->userName); + + $execArguments = [ + [ + sprintf( + 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'GRANT ALL PRIVILEGES ON `%s`.* TO %s@%s', + $this->installData->getDbName(), + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + ['FLUSH PRIVILEGES'], + ]; + + $this->pdo->expects(self::exactly(3)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->createDatabase(); + } + + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsSuccessfulInHostingMode(): void + { + $this->installData->setHostingMode(true); + + $this->pdo->expects(self::once()) + ->method('exec') + ->with( + sprintf( + 'USE `%s`', + $this->installData->getDbName() + ) + ); + + $this->mysql->createDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsNotSuccessfulWithDuplicateDatabase(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [[1]], + [$this->installData->getDbName()] + ); + + $this->expectException(SPException::class); + $this->expectExceptionMessage('The database already exists'); + + $this->mysql->createDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsSuccessfulWithDns(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [], + [$this->installData->getDbName()] + ); + + $this->configData->setDbUser(self::$faker->userName); + $this->installData->setDbAuthHostDns(self::$faker->domainName); + + $execArguments = [ + [ + sprintf( + 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'GRANT ALL PRIVILEGES ON `%s`.* TO %s@%s', + $this->installData->getDbName(), + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + [ + sprintf( + 'GRANT ALL PRIVILEGES ON `%s`.* TO %s@%s', + $this->installData->getDbName(), + $this->configData->getDbUser(), + $this->installData->getDbAuthHostDns() + ), + ], + ['FLUSH PRIVILEGES'], + ]; + + $this->pdo->expects(self::exactly(4)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->createDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsNotSuccessfulWithCreateError(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [], + [$this->installData->getDbName()] + ); + + $pdoException = new PDOException('test'); + + $this->pdo->method('exec') + ->with( + sprintf( + 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', + $this->installData->getDbName() + ), + ) + ->willThrowException($pdoException); + + $this->expectException(SPException::class); + $this->expectExceptionMessage(sprintf(__('Error while creating the DB (\'%s\')'), $pdoException->getMessage())); + + $this->mysql->createDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsNotSuccessfulWithPermissionError(): void + { + $this->pdo->mock( + 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1', + [], + [$this->installData->getDbName()] + ); + + $this->configData->setDbUser(self::$faker->userName); + + $matcher = $this->any(); + $execArguments = [ + [ + sprintf( + 'CREATE SCHEMA `%s` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'GRANT ALL PRIVILEGES ON `%s`.* TO %s@%s', + $this->installData->getDbName(), + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + ]; + $pdoException = new PDOException('test'); + + $this->pdo->expects($matcher) + ->method('exec') + ->withConsecutive(...$execArguments) + ->willReturnCallback(function () use ($matcher, $pdoException) { + if ($matcher->getInvocationCount() === 3) { + throw $pdoException; + } + }); + + $this->expectException(SPException::class); + $this->expectExceptionMessage( + sprintf(__('Error while setting the database permissions (\'%s\')'), $pdoException->getMessage()) + ); + + $this->mysql->createDatabase(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDatabaseIsNotSuccessfulInHostingMode(): void + { + $this->installData->setHostingMode(true); + + $pdoException = new PDOException('test'); + + $this->pdo->expects(self::once()) + ->method('exec') + ->with( + sprintf( + 'USE `%s`', + $this->installData->getDbName() + ) + )->willThrowException($pdoException); + + $this->expectException(SPException::class); + $this->expectExceptionMessage( + sprintf( + __('Error while selecting \'%s\' database (%s)'), + $this->installData->getDbName(), + $pdoException->getMessage() + ) + ); + + $this->mysql->createDatabase(); + } + + public function testRollbackIsSuccessful(): void + { + $this->configData->setDbUser(self::$faker->userName); + + $execArguments = [ + [ + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHostDns() + ), + ], + ]; + + $this->pdo->expects(self::exactly(3)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->rollback(); + } + + public function testRollbackIsSuccessfulWithSameDnsHost(): void + { + $this->configData->setDbUser(self::$faker->userName); + $this->installData->setDbAuthHost('localhost'); + $this->installData->setDbAuthHostDns('localhost'); + + $execArguments = [ + [ + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + ]; + + $this->pdo->expects(self::exactly(2)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->rollback(); + } + + public function testRollbackIsSuccessfulWithHostingMode(): void + { + $this->installData->setHostingMode(true); + + $dropRegex = '/DROP TABLE IF EXISTS `'.$this->installData->getDbName().'`\.`\w+`/'; + $this->pdo->expects(self::exactly(count(DatabaseUtil::TABLES))) + ->method('exec') + ->with($this->callback(fn($arg) => preg_match($dropRegex, $arg) > 0)); + + $this->mysql->rollback(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBStructureIsSuccessful(): void + { + $execArguments = [ + [ + sprintf('USE `%s`', $this->installData->getDbName()), + ], + [ + 'DROP TABLE IF EXISTS `Account`;', + ], + ]; + + $this->pdo->expects(self::exactly(2)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->databaseFile->expects(self::once()) + ->method('parse') + ->willReturn(['DROP TABLE IF EXISTS `Account`;']); + + $this->mysql->createDBStructure(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBStructureIsNotSuccessfulWithUseDatabaseError(): void + { + $pdoException = new PDOException("test"); + + $this->pdo->expects(self::once()) + ->method('exec') + ->with(sprintf('USE `%s`', $this->installData->getDbName())) + ->willThrowException($pdoException); + + $this->expectException(SPException::class); + $this->expectExceptionMessage( + sprintf( + __('Error while selecting \'%s\' database (%s)'), + $this->installData->getDbName(), + $pdoException->getMessage() + ) + ); + + $this->mysql->createDBStructure(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBStructureIsNotSuccessfulWithCreateSchemaError(): void + { + $pdoException = new PDOException("test"); + $execArguments = [ + [ + sprintf('USE `%s`', $this->installData->getDbName()), + ], + [ + 'DROP TABLE IF EXISTS `Account`;', + ], + [ + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHostDns() + ), + ], + ]; + $matcher = $this->exactly(5); + + $this->pdo->expects($matcher) + ->method('exec') + ->withConsecutive(...$execArguments) + ->willReturnCallback(function () use ($matcher, $pdoException) { + if ($matcher->getInvocationCount() === 2) { + throw $pdoException; + } + }); + + $this->databaseFile->expects(self::once()) + ->method('parse') + ->willReturn(['DROP TABLE IF EXISTS `Account`;']); + + $this->expectException(SPException::class); + $this->expectExceptionMessage( + sprintf(__('Error while creating the DB (\'%s\')'), $pdoException->getMessage()) + ); + + $this->mysql->createDBStructure(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBStructureIsNotSuccessfulWithParseSchemaError(): void + { + $fileException = new FileException("test"); + $execArguments = [ + [ + sprintf('USE `%s`', $this->installData->getDbName()), + ], + [ + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHostDns() + ), + ], + ]; + $matcher = $this->exactly(4); + + $this->pdo->expects($matcher) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->databaseFile->expects(self::once()) + ->method('parse') + ->willThrowException($fileException); + + $this->expectException(SPException::class); + $this->expectExceptionMessage( + sprintf(__('Error while creating the DB (\'%s\')'), $fileException->getMessage()) + ); + + $this->mysql->createDBStructure(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCheckConnectionIsSuccessful(): void + { + $this->databaseUtil->expects(self::once()) + ->method('checkDatabaseTables') + ->with($this->installData->getDbName()) + ->willReturn(true); + + $this->mysql->checkConnection(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCheckConnectionIsNotSuccessful(): void + { + $this->databaseUtil->expects(self::once()) + ->method('checkDatabaseTables') + ->with($this->installData->getDbName()) + ->willReturn(false); + + $execArguments = [ + [ + sprintf( + 'DROP DATABASE IF EXISTS `%s`', + $this->installData->getDbName() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHost() + ), + ], + [ + sprintf( + 'DROP USER IF EXISTS %s@%s', + $this->configData->getDbUser(), + $this->installData->getDbAuthHostDns() + ), + ], + ]; + + $matcher = $this->exactly(3); + + $this->pdo->expects($matcher) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->expectException(SPException::class); + $this->expectExceptionMessage(__u('Error while checking the database')); + + $this->mysql->checkConnection(); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBUserIsSuccessful(): void + { + $user = self::$faker->userName; + $pass = self::$faker->password; + + $execArguments = [ + [ + sprintf( + 'CREATE USER %s@%s IDENTIFIED BY %s', + $user, + $this->installData->getDbAuthHost(), + $pass + ), + ], + [ + 'FLUSH PRIVILEGES', + ], + ]; + + $this->pdo->expects(self::exactly(2)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->createDBUser($user, $pass); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBUserIsSuccessfulWithDns(): void + { + $this->installData->setDbAuthHostDns(self::$faker->domainName); + + $user = self::$faker->userName; + $pass = self::$faker->password; + + $execArguments = [ + [ + sprintf( + 'CREATE USER %s@%s IDENTIFIED BY %s', + $user, + $this->installData->getDbAuthHost(), + $pass + ), + ], + [ + sprintf( + 'CREATE USER %s@%s IDENTIFIED BY %s', + $user, + $this->installData->getDbAuthHostDns(), + $pass + ), + ], + [ + 'FLUSH PRIVILEGES', + ], + ]; + + $this->pdo->expects(self::exactly(3)) + ->method('exec') + ->withConsecutive(...$execArguments); + + $this->mysql->createDBUser($user, $pass); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBUserIsSuccessfulWithHostingMode(): void + { + $this->installData->setHostingMode(true); + + $user = self::$faker->userName; + $pass = self::$faker->password; + + $this->pdo->expects(self::exactly(0))->method('exec'); + + $this->mysql->createDBUser($user, $pass); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testCreateDBUserIsNotSuccessful(): void + { + $user = self::$faker->userName; + $pass = self::$faker->password; + + $this->pdo->expects(self::once()) + ->method('exec') + ->willThrowException(new PDOException('test')); + + $this->expectException(SPException::class); + $this->expectExceptionMessage(sprintf(__u('Error while creating the MySQL connection user \'%s\''), $user)); + + $this->mysql->createDBUser($user, $pass); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->pdo = $this->getMockBuilder(Pdo::class)->enableProxyingToOriginalMethods()->getMock(); + + $this->DBStorage = $this->createMock(DBStorageInterface::class); + $this->DBStorage->method('getConnectionSimple')->willReturn($this->pdo); + $this->databaseFile = $this->createMock(DatabaseFileInterface::class); + + $this->installData = $this->getInstallData(); + $this->configData = $this->config->getConfigData(); + $this->databaseUtil = $this->createMock(DatabaseUtil::class); + $this->mysql = new MySQL( + $this->DBStorage, $this->installData, $this->configData, $this->databaseFile, $this->databaseUtil + ); + } + + /** + * @return \SP\Services\Install\InstallData + */ + private function getInstallData(): InstallData { $params = new InstallData(); - $params->setDbAdminUser(getenv('DB_USER')); - $params->setDbAdminPass(getenv('DB_PASS')); - $params->setDbName(self::DB_NAME); - $params->setDbHost(getenv('DB_SERVER')); - $params->setDbAuthHost(SELF_IP_ADDRESS); - - // Long hostname returned by CI/CD tests - if (strlen(SELF_HOSTNAME) < 60) { - $params->setDbAuthHostDns(SELF_HOSTNAME); - } else { - $params->setDbAuthHostDns('localhost'); - } - - $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); + $params->setDbAuthHost(self::$faker->ipv4); return $params; } - - /** - * @throws DatabaseException - * @throws SPException - */ - public function testCheckDatabaseExist() - { - DatabaseUtil::createDatabase(self::DB_NAME); - - $mysql = new MySQL($this->getParams(), new ConfigData()); - - $this->assertTrue($mysql->checkDatabaseExist()); - - DatabaseUtil::dropDatabase(self::DB_NAME); - } - - /** - * @throws SPException - */ - public function testSetupDbUser() - { - $configData = new ConfigData(); - - $mysql = new MySQL($this->getParams(), $configData); - $mysql->setupDbUser(); - - $this->assertTrue(preg_match('/sp_\w+/', $configData->getDbUser()) === 1); - $this->assertNotEmpty($configData->getDbPass()); - - DatabaseUtil::dropUser($configData->getDbUser(), SELF_IP_ADDRESS); - DatabaseUtil::dropUser($configData->getDbUser(), SELF_HOSTNAME); - } - - /** - * @throws SPException - */ - public function testCreateDatabase() - { - $configData = new ConfigData(); - - $mysql = new MySQL($this->getParams(), $configData); - $mysql->setupDbUser(); - $mysql->createDatabase(); - - $this->assertTrue($mysql->checkDatabaseExist()); - - DatabaseUtil::dropDatabase(self::DB_NAME); - DatabaseUtil::dropUser($configData->getDbUser(), SELF_IP_ADDRESS); - DatabaseUtil::dropUser($configData->getDbUser(), SELF_HOSTNAME); - } - - /** - * @throws SPException - */ - public function testCheckConnection() - { - $configData = new ConfigData(); - - $mysql = new MySQL($this->getParams(), $configData); - $mysql->setupDbUser(); - $mysql->createDatabase(); - $mysql->createDBStructure(); - $mysql->checkConnection(); - - // Previous steps did not fail then true... - $this->assertTrue(true); - - DatabaseUtil::dropDatabase(self::DB_NAME); - DatabaseUtil::dropUser($configData->getDbUser(), SELF_IP_ADDRESS); - DatabaseUtil::dropUser($configData->getDbUser(), SELF_HOSTNAME); - } - - /** - * @throws SPException - */ - public function testConnectDatabase() - { - $mysql = new MySQL($this->getParams(), new ConfigData()); - $mysql->connectDatabase(); - - $this->assertInstanceOf(MySQLHandler::class, $mysql->getDbHandler()); - } - - /** - * @throws SPException - */ - public function testCreateDBUser() - { - $mysql = new MySQL($this->getParams(), new ConfigData()); - $mysql->createDBUser('test', PasswordUtil::randomPassword()); - - $num = (int)$mysql->getDbHandler() - ->getConnectionSimple() - ->query('SELECT COUNT(*) FROM mysql.user WHERE `User` = \'test\'') - ->fetchColumn(0); - - $this->assertEquals(2, $num); - - DatabaseUtil::dropUser('test', SELF_IP_ADDRESS); - DatabaseUtil::dropUser('test', SELF_HOSTNAME); - } - - /** - * @throws SPException - */ - public function testRollback() - { - $mysql = new MySQL($this->getParams(), new ConfigData()); - $mysql->setupDbUser(); - $mysql->createDatabase(); - $mysql->createDBStructure(); - $mysql->rollback(); - - $this->assertFalse($mysql->checkDatabaseExist()); - } - - /** - * @throws SPException - */ - public function testCreateDBStructure() - { - $mysql = new MySQL($this->getParams(), new ConfigData()); - $mysql->setupDbUser(); - $mysql->createDatabase(); - $mysql->createDBStructure(); - - $this->assertTrue($mysql->checkDatabaseExist()); - - $mysql->rollback(); - } } diff --git a/tests/SP/Stubs/Pdo.php b/tests/SP/Stubs/Pdo.php new file mode 100644 index 00000000..a5d21e80 --- /dev/null +++ b/tests/SP/Stubs/Pdo.php @@ -0,0 +1,53 @@ +. + */ + +namespace SP\Tests\Stubs; + +/** + * A PDO stub that overrides some unimplementd methods from \Pseudo\Pdo + */ +class Pdo extends \Pseudo\Pdo +{ + /** + * (PHP 5 >= 5.1.0, PHP 7, PECL pdo >= 0.2.1)
+ * Quotes a string for use in a query. + * + * @link https://php.net/manual/en/pdo.quote.php + * + * @param string $string

+ * The string to be quoted. + *

+ * @param int $type [optional]

+ * Provides a data type hint for drivers that have alternate quoting styles. + *

+ * + * @return string|false a quoted string that is theoretically safe to pass into an + * SQL statement. Returns FALSE if the driver does not support quoting in + * this way. + */ + public function quote($string, $parameter_type = self::PARAM_STR) + { + return $string; + } +} \ No newline at end of file