From cc25090260f86130bb4023923d28846caffe26f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D?= Date: Sat, 30 Mar 2024 11:52:12 +0100 Subject: [PATCH] chore(tests): UT for MysqlHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rubén D --- .../Domain/Install/Services/MysqlService.php | 26 +- .../Database/DatabaseConnectionData.php | 143 ++++------ .../Database/DbStorageHandler.php | 18 +- .../Infrastructure/Database/MysqlHandler.php | 193 ++++++-------- lib/SP/Infrastructure/Database/PDOWrapper.php | 60 +++++ .../Database/MysqlHandlerTest.php | 249 ++++++++++++++++++ 6 files changed, 459 insertions(+), 230 deletions(-) create mode 100644 lib/SP/Infrastructure/Database/PDOWrapper.php create mode 100644 tests/SPT/Infrastructure/Database/MysqlHandlerTest.php diff --git a/lib/SP/Domain/Install/Services/MysqlService.php b/lib/SP/Domain/Install/Services/MysqlService.php index 0eb6f34c..08aa2b8d 100644 --- a/lib/SP/Domain/Install/Services/MysqlService.php +++ b/lib/SP/Domain/Install/Services/MysqlService.php @@ -28,6 +28,7 @@ use Exception; use PDOException; use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Install\Adapters\InstallData; +use SP\Infrastructure\Database\DatabaseException; use SP\Infrastructure\Database\DatabaseFileInterface; use SP\Infrastructure\Database\DatabaseUtil; use SP\Infrastructure\Database\DbStorageHandler; @@ -44,7 +45,7 @@ use function SP\processException; * * @package SP\Domain\Install\Services */ -final class MysqlService implements DatabaseSetupInterface +final readonly class MysqlService implements DatabaseSetupInterface { /** @@ -52,10 +53,10 @@ final class MysqlService implements DatabaseSetupInterface * */ public function __construct( - private readonly DbStorageHandler $dbStorage, - private readonly InstallData $installData, - private readonly DatabaseFileInterface $databaseFile, - private readonly DatabaseUtil $databaseUtil + private DbStorageHandler $dbStorage, + private InstallData $installData, + private DatabaseFileInterface $databaseFile, + private DatabaseUtil $databaseUtil ) { } @@ -259,17 +260,24 @@ final class MysqlService implements DatabaseSetupInterface } } + /** + * @throws DatabaseException + */ public function checkDatabaseExists(): bool { - $sth = $this->dbStorage->getConnectionSimple() - ->prepare( - 'SELECT COUNT(*) FROM information_schema.schemata WHERE `schema_name` = ? LIMIT 1' - ); + $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 DatabaseException + */ public function rollback(?string $dbUser = null): void { $dbc = $this->dbStorage->getConnectionSimple(); diff --git a/lib/SP/Infrastructure/Database/DatabaseConnectionData.php b/lib/SP/Infrastructure/Database/DatabaseConnectionData.php index eb54a804..c7430821 100644 --- a/lib/SP/Infrastructure/Database/DatabaseConnectionData.php +++ b/lib/SP/Infrastructure/Database/DatabaseConnectionData.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. * @@ -28,10 +28,8 @@ use SP\Domain\Config\Ports\ConfigDataInterface; /** * Class DatabaseConnectionData - * - * @package SP\Infrastructure\Database */ -final class DatabaseConnectionData +class DatabaseConnectionData { private ?string $dbHost = null; private ?string $dbSocket = null; @@ -42,83 +40,25 @@ final class DatabaseConnectionData public static function getFromConfig(ConfigDataInterface $configData): DatabaseConnectionData { - return (new self()) - ->setDbHost($configData->getDbHost() ?? '') - ->setDbName($configData->getDbName() ?? '') - ->setDbUser($configData->getDbUser() ?? '') - ->setDbPass($configData->getDbPass() ?? '') - ->setDbPort($configData->getDbPort() ?? 0) - ->setDbSocket($configData->getDbSocket() ?? ''); + $self = new self(); + self::setup($configData, $self); + + return $self; } - public function refreshFromConfig(ConfigDataInterface $configData): DatabaseConnectionData + /** + * @param ConfigDataInterface $configData + * @param DatabaseConnectionData $self + * @return void + */ + private static function setup(ConfigDataInterface $configData, DatabaseConnectionData $self): void { - return $this->setDbHost($configData->getDbHost()) - ->setDbName($configData->getDbName()) - ->setDbUser($configData->getDbUser()) - ->setDbPass($configData->getDbPass()) - ->setDbPort($configData->getDbPort()) - ->setDbSocket($configData->getDbSocket()); - } - - public function getDbHost(): ?string - { - return $this->dbHost; - } - - public function setDbHost(string $dbHost): DatabaseConnectionData - { - $this->dbHost = $dbHost; - - return $this; - } - - public function getDbName(): ?string - { - return $this->dbName; - } - - public function setDbName(string $dbName): DatabaseConnectionData - { - $this->dbName = $dbName; - - return $this; - } - - public function getDbUser(): ?string - { - return $this->dbUser; - } - - public function setDbUser(string $dbUser): DatabaseConnectionData - { - $this->dbUser = $dbUser; - - return $this; - } - - public function getDbPass(): ?string - { - return $this->dbPass; - } - - public function setDbPass(string $dbPass): DatabaseConnectionData - { - $this->dbPass = $dbPass; - - return $this; - } - - public function getDbPort(): ?int - { - return $this->dbPort; - } - - public function setDbPort(int $dbPort): DatabaseConnectionData - { - $this->dbPort = $dbPort; - - return $this; + $self->dbSocket = $configData->getDbSocket(); + $self->dbHost = $configData->getDbHost(); + $self->dbPort = $configData->getDbPort(); + $self->dbName = $configData->getDbName(); + $self->dbUser = $configData->getDbUser(); + $self->dbPass = $configData->getDbPass(); } public function getDbSocket(): ?string @@ -126,21 +66,48 @@ final class DatabaseConnectionData return $this->dbSocket; } - public function setDbSocket(?string $dbSocket): DatabaseConnectionData + public function getDbHost(): ?string { - $this->dbSocket = $dbSocket; + return $this->dbHost; + } - return $this; + public function getDbPort(): ?int + { + return $this->dbPort; + } + + public function getDbName(): ?string + { + return $this->dbName; + } + + public function getDbUser(): ?string + { + return $this->dbUser; + } + + public function getDbPass(): ?string + { + return $this->dbPass; } public static function getFromEnvironment(): DatabaseConnectionData { - return (new self()) - ->setDbHost(getenv('DB_SERVER')) - ->setDbName(getenv('DB_NAME')) - ->setDbUser(getenv('DB_USER')) - ->setDbPass(getenv('DB_PASS')) - ->setDbPort((int)getenv('DB_PORT')) - ->setDbSocket(getenv('DB_SOCKET')); + $self = new self(); + $self->dbSocket = getenv('DB_SOCKET'); + $self->dbHost = getenv('DB_SERVER'); + $self->dbPort = (int)getenv('DB_PORT'); + $self->dbName = getenv('DB_NAME'); + $self->dbUser = getenv('DB_USER'); + $self->dbPass = getenv('DB_PASS'); + + return $self; + } + + public function refreshFromConfig(ConfigDataInterface $configData): DatabaseConnectionData + { + self::setup($configData, $this); + + return $this; } } diff --git a/lib/SP/Infrastructure/Database/DbStorageHandler.php b/lib/SP/Infrastructure/Database/DbStorageHandler.php index eedce1ff..3169ee9e 100644 --- a/lib/SP/Infrastructure/Database/DbStorageHandler.php +++ b/lib/SP/Infrastructure/Database/DbStorageHandler.php @@ -37,6 +37,7 @@ interface DbStorageHandler * Obtener una conexión PDO * * @return PDO + * @throws DatabaseException */ public function getConnection(): PDO; @@ -44,25 +45,10 @@ interface DbStorageHandler * Obtener una conexión PDO sin seleccionar la BD * * @return PDO + * @throws DatabaseException */ public function getConnectionSimple(): PDO; - /** - * Devolcer el estado de la BD - * - * @return int - */ - public function getDbStatus(): int; - - /** - * @return string - */ - public function getConnectionUri(): string; - - /** - * @return string|null - */ - public function getDatabaseName(): ?string; public function getDriver(): DbStorageDriver; } diff --git a/lib/SP/Infrastructure/Database/MysqlHandler.php b/lib/SP/Infrastructure/Database/MysqlHandler.php index 4be30abe..2abbe655 100644 --- a/lib/SP/Infrastructure/Database/MysqlHandler.php +++ b/lib/SP/Infrastructure/Database/MysqlHandler.php @@ -24,166 +24,125 @@ namespace SP\Infrastructure\Database; -use Exception; use PDO; -use SP\Domain\Core\Exceptions\SPException; + +use function SP\__u; /** * Class MySQLHandler - * - * Esta clase se encarga de crear las conexiones a la BD */ final class MysqlHandler implements DbStorageHandler { - public const STATUS_OK = 0; - public const STATUS_KO = 1; - public const PDO_OPTS = [ + private const PDO_OPTS = [ PDO::ATTR_EMULATE_PREPARES => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::MYSQL_ATTR_FOUND_ROWS => true, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, ]; - private ?PDO $db = null; - private int $dbStatus = self::STATUS_KO; - private DatabaseConnectionData $connectionData; + private ?PDO $pdo = null; + private readonly string $connectionUri; - public function __construct(DatabaseConnectionData $connectionData) - { - $this->connectionData = $connectionData; + public function __construct( + private readonly DatabaseConnectionData $connectionData, + private readonly PDOWrapper $PDOWrapper + ) { + $this->connectionUri = $this->getConnectionUri(); } - /** - * Devuelve el estado de conexión a la BBDD - * - * OK -> 0 - * KO -> 1 - */ - public function getDbStatus(): int - { - return $this->dbStatus; - } - - /** - * Realizar la conexión con la BBDD. - * Esta función utiliza PDO para conectar con la base de datos. - * - * @throws DatabaseException - */ - public function getConnection(): PDO - { - if (!$this->db) { - if (null === $this->connectionData->getDbUser() - || null === $this->connectionData->getDbPass() - || null === $this->connectionData->getDbName() - || (null === $this->connectionData->getDbHost() - && null === $this->connectionData->getDbSocket()) - ) { - throw new DatabaseException( - __u('Unable to connect to DB'), - SPException::CRITICAL, - __u('Please, check the connection parameters') - ); - } - - try { - $this->db = new PDO( - $this->getConnectionUri(), - $this->connectionData->getDbUser(), - $this->connectionData->getDbPass(), - self::PDO_OPTS - ); - - // Set prepared statement emulation depending on server version - $serverVersion = $this->db->getAttribute(PDO::ATTR_SERVER_VERSION); - $this->db->setAttribute( - PDO::ATTR_EMULATE_PREPARES, - version_compare($serverVersion, '5.1.17', '<') - ); - - $this->dbStatus = self::STATUS_OK; - } catch (Exception $e) { - throw new DatabaseException( - __u('Unable to connect to DB'), - SPException::CRITICAL, - sprintf('Error %s: %s', $e->getCode(), $e->getMessage()), - $e->getCode(), - $e - ); - } - } - - return $this->db; - } - - public function getConnectionUri(): string + private function getConnectionUri(): string { $dsn = ['charset=utf8']; if (empty($this->connectionData->getDbSocket())) { - $dsn[] = 'host=' . $this->connectionData->getDbHost(); + $dsn[] = sprintf('host=%s', $this->connectionData->getDbHost()); if (null !== $this->connectionData->getDbPort()) { - $dsn[] = 'port=' . $this->connectionData->getDbPort(); + $dsn[] = sprintf('port=%s', $this->connectionData->getDbPort()); } } else { - $dsn[] = 'unix_socket=' . $this->connectionData->getDbSocket(); + $dsn[] = sprintf('unix_socket=%s', $this->connectionData->getDbSocket()); } if (!empty($this->connectionData->getDbName())) { - $dsn[] = 'dbname=' . $this->connectionData->getDbName(); + $dsn[] = sprintf('dbname=%s', $this->connectionData->getDbName()); } - return 'mysql:' . implode(';', $dsn); + return sprintf('mysql:%s', implode(';', $dsn)); } /** - * Obtener una conexión PDO sin seleccionar la BD + * Set up a database connection with the given connection data. + * This method will only set ATTR_EMULATE_PREPARES and ATTR_ERRMODE options. * * @throws DatabaseException */ public function getConnectionSimple(): PDO { - if (!$this->db) { - if (null === $this->connectionData->getDbHost() - && null === $this->connectionData->getDbSocket() - ) { - throw new DatabaseException( - __u('Unable to connect to DB'), - SPException::CRITICAL, - __u('Please, check the connection parameters') - ); - } + if (!$this->pdo) { + $this->checkConnectionData(); - try { - $opts = [ - PDO::ATTR_EMULATE_PREPARES => true, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ]; + $opts = [ + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ]; - $this->db = new PDO( - $this->getConnectionUri(), - $this->connectionData->getDbUser(), - $this->connectionData->getDbPass(), - $opts - ); - $this->dbStatus = self::STATUS_OK; - } catch (Exception $e) { - throw new DatabaseException( - __u('Unable to connect to DB'), - SPException::CRITICAL, - sprintf('Error %s: %s', $e->getCode(), $e->getMessage()), - $e->getCode(), - $e - ); - } + $this->pdo = $this->PDOWrapper->build($this->connectionUri, $this->connectionData, $opts); } - return $this->db; + return $this->pdo; } - public function getDatabaseName(): ?string + /** + * @param bool $checkName + * @return void + * @throws DatabaseException + */ + private function checkConnectionData(bool $checkName = false): void { - return $this->connectionData->getDbName(); + $nameIsNotPresent = $checkName && null === $this->connectionData->getDbName(); + + if ($nameIsNotPresent + || null === $this->connectionData->getDbUser() + || null === $this->connectionData->getDbPass() + || (null === $this->connectionData->getDbHost() + && null === $this->connectionData->getDbSocket()) + ) { + throw DatabaseException::critical( + __u('Unable to connect to DB'), + __u('Please, check the connection parameters') + ); + } + } + + /** + * @return DbStorageDriver + */ + public function getDriver(): DbStorageDriver + { + return DbStorageDriver::mysql; + } + + /** + * Set up a database connection with the given connection data + * + * @throws DatabaseException + */ + public function getConnection(): PDO + { + if (!$this->pdo) { + $this->checkConnectionData(true); + + $this->pdo = $this->PDOWrapper->build($this->connectionUri, $this->connectionData, self::PDO_OPTS); + + // Set prepared statement emulation depending on server version + $serverVersion = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION); + $this->pdo->setAttribute( + PDO::ATTR_EMULATE_PREPARES, + version_compare($serverVersion, '5.1.17', '<') + ); + } + + return $this->pdo; } } diff --git a/lib/SP/Infrastructure/Database/PDOWrapper.php b/lib/SP/Infrastructure/Database/PDOWrapper.php new file mode 100644 index 00000000..fe09d782 --- /dev/null +++ b/lib/SP/Infrastructure/Database/PDOWrapper.php @@ -0,0 +1,60 @@ +. + */ + +namespace SP\Infrastructure\Database; + +use Exception; +use PDO; + +use function SP\__u; + +/** + * Class PDOWrapper + */ +class PDOWrapper +{ + /** + * Build a PDO object with the given connection data and options + * + * @throws DatabaseException + */ + public function build(string $connectionUri, DatabaseConnectionData $connectionData, array $opts): PDO + { + try { + return new PDO( + $connectionUri, + $connectionData->getDbUser(), + $connectionData->getDbPass(), + $opts + ); + } catch (Exception $e) { + throw DatabaseException::critical( + __u('Unable to connect to DB'), + sprintf('Error %s: %s', $e->getCode(), $e->getMessage()), + $e->getCode(), + $e + ); + } + } +} diff --git a/tests/SPT/Infrastructure/Database/MysqlHandlerTest.php b/tests/SPT/Infrastructure/Database/MysqlHandlerTest.php new file mode 100644 index 00000000..54998c9d --- /dev/null +++ b/tests/SPT/Infrastructure/Database/MysqlHandlerTest.php @@ -0,0 +1,249 @@ +. + */ + +namespace SPT\Infrastructure\Database; + +use PDO; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\MockObject\Exception; +use SP\Infrastructure\Database\DatabaseConnectionData; +use SP\Infrastructure\Database\DatabaseException; +use SP\Infrastructure\Database\DbStorageDriver; +use SP\Infrastructure\Database\MysqlHandler; +use SP\Infrastructure\Database\PDOWrapper; +use SPT\UnitaryTestCase; + +/** + * Class MysqlHandlerTest + */ +#[Group('unitary')] +class MysqlHandlerTest extends UnitaryTestCase +{ + + public static function getConnectionDataProvider(): array + { + return [ + ['socket', 'localhost', null, 'a_user', 'a_password'], + ['socket', 'localhost', 'test', null, 'a_password'], + ['socket', 'localhost', 'test', 'a_user', null], + [null, null, 'test', 'a_user', 'a_password'], + ]; + } + + public static function getConnectionSimpleDataProvider(): array + { + return [ + ['socket', 'localhost', null, 'a_password'], + ['socket', 'localhost', 'a_user', null], + [null, null, 'a_user', 'a_password'], + ]; + } + + /** + * @throws Exception + * @throws DatabaseException + */ + #[DataProvider('getConnectionDataProvider')] + public function testGetConnectionWithException( + ?string $socket, + ?string $host, + ?string $name, + ?string $user, + ?string $pass + ) { + $connectionData = $this->createStub(DatabaseConnectionData::class); + $pdoWrapper = $this->createMock(PDOWrapper::class); + + $connectionData->method('getDbSocket') + ->willReturn($socket); + + $connectionData->method('getDbHost') + ->willReturn($host); + + $connectionData->method('getDbName') + ->willReturn($name); + + $connectionData->method('getDbUser') + ->willReturn($user); + + $connectionData->method('getDbPass') + ->willReturn($pass); + + $pdoWrapper->expects($this->never()) + ->method('build'); + + $mysqlHandler = new MysqlHandler($connectionData, $pdoWrapper); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Unable to connect to DB'); + + $mysqlHandler->getConnection(); + } + + /** + * @throws Exception + * @throws DatabaseException + */ + public function testGetConnection() + { + $pdoOptions = [ + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::MYSQL_ATTR_FOUND_ROWS => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ, + ]; + + $connectionData = $this->createStub(DatabaseConnectionData::class); + $pdoWrapper = $this->createMock(PDOWrapper::class); + $pdo = $this->createMock(PDO::class); + + $connectionData->method('getDbHost') + ->willReturn('localhost'); + + $connectionData->method('getDbPort') + ->willReturn(3306); + + $connectionData->method('getDbName') + ->willReturn('test'); + + $connectionData->method('getDbUser') + ->willReturn('a_user'); + + $connectionData->method('getDbPass') + ->willReturn('a_password'); + + $dsn = 'mysql:charset=utf8;host=localhost;port=3306;dbname=test'; + + $pdoWrapper->expects($this->once()) + ->method('build') + ->with($dsn, $connectionData, $pdoOptions) + ->willReturn($pdo); + + $pdo->expects($this->once()) + ->method('getAttribute') + ->with(PDO::ATTR_SERVER_VERSION) + ->willReturn('5.1.17'); + + $pdo->expects($this->once()) + ->method('setAttribute') + ->with(PDO::ATTR_EMULATE_PREPARES, false); + + $mysqlHandler = new MysqlHandler($connectionData, $pdoWrapper); + + $out = $mysqlHandler->getConnection(); + + $this->assertEquals($pdo, $out); + } + + /** + * @throws DatabaseException + * @throws Exception + */ + public function testGetConnectionSimple() + { + $pdoOptions = [ + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION + ]; + + $connectionData = $this->createStub(DatabaseConnectionData::class); + $pdoWrapper = $this->createMock(PDOWrapper::class); + $pdo = $this->createMock(PDO::class); + + $connectionData->method('getDbHost') + ->willReturn('localhost'); + + $connectionData->method('getDbPort') + ->willReturn(3306); + + $connectionData->method('getDbUser') + ->willReturn('a_user'); + + $connectionData->method('getDbPass') + ->willReturn('a_password'); + + $dsn = 'mysql:charset=utf8;host=localhost;port=3306'; + + $pdoWrapper->expects($this->once()) + ->method('build') + ->with($dsn, $connectionData, $pdoOptions) + ->willReturn($pdo); + + $mysqlHandler = new MysqlHandler($connectionData, $pdoWrapper); + + $out = $mysqlHandler->getConnectionSimple(); + + $this->assertEquals($pdo, $out); + } + + /** + * @throws DatabaseException + * @throws Exception + */ + #[DataProvider('getConnectionSimpleDataProvider')] + public function testGetConnectionSimpleWithException( + ?string $socket, + ?string $host, + ?string $user, + ?string $pass + ) { + $connectionData = $this->createStub(DatabaseConnectionData::class); + $pdoWrapper = $this->createMock(PDOWrapper::class); + + $connectionData->method('getDbSocket') + ->willReturn($socket); + + $connectionData->method('getDbHost') + ->willReturn($host); + + $connectionData->method('getDbUser') + ->willReturn($user); + + $connectionData->method('getDbPass') + ->willReturn($pass); + + $pdoWrapper->expects($this->never()) + ->method('build'); + + $mysqlHandler = new MysqlHandler($connectionData, $pdoWrapper); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage('Unable to connect to DB'); + + $mysqlHandler->getConnectionSimple(); + } + + /** + * @throws Exception + */ + public function testGetDriver() + { + $connectionData = $this->createStub(DatabaseConnectionData::class); + $pdoWrapper = $this->createMock(PDOWrapper::class); + $mysqlHandler = new MysqlHandler($connectionData, $pdoWrapper); + + $this->assertEquals(DbStorageDriver::mysql, $mysqlHandler->getDriver()); + } +}