diff --git a/app/modules/api/Bootstrap.php b/app/modules/api/Bootstrap.php index 1511c41d..69675787 100644 --- a/app/modules/api/Bootstrap.php +++ b/app/modules/api/Bootstrap.php @@ -35,6 +35,7 @@ use SP\Domain\Api\Ports\ApiRequestService; use SP\Domain\Api\Services\JsonRpcResponse; use SP\Domain\Core\Bootstrap\BootstrapInterface; use SP\Domain\Core\Bootstrap\ModuleInterface; +use SP\Domain\Http\Code; use function SP\logger; use function SP\processException; @@ -73,18 +74,17 @@ final class Bootstrap extends BootstrapBase try { logger('API route'); - $apiRequest = $this->createObjectFor(ApiRequestService::class); + $response->headers()->set('Content-type', 'application/json; charset=utf-8'); + $apiRequest = $this->buildInstanceFor(ApiRequestService::class); [$controllerName, $actionName] = explode('/', $apiRequest->getMethod()); - $controllerClass = self::getClassFor($controllerName, $actionName); - $method = $actionName . 'Action'; if (!method_exists($controllerClass, $method)) { logger($controllerClass . '::' . $method); - $response->headers()->set('Content-type', 'application/json; charset=utf-8'); + $response->code(Code::NOT_FOUND->value); return $response->body( JsonRpcResponse::getResponseError( @@ -103,11 +103,11 @@ final class Bootstrap extends BootstrapBase logger('Routing call: ' . $controllerClass . '::' . $method); - return call_user_func([$this->createObjectFor($controllerClass), $method]); + return call_user_func([$this->buildInstanceFor($controllerClass), $method]); } catch (Exception $e) { processException($e); - $response->headers()->set('Content-type', 'application/json; charset=utf-8'); + $response->code(Code::INTERNAL_SERVER_ERROR->value); return $response->body(JsonRpcResponse::getResponseException($e, 0)); } finally { diff --git a/app/modules/web/Bootstrap.php b/app/modules/web/Bootstrap.php index c4f3ea21..2a6cd591 100644 --- a/app/modules/web/Bootstrap.php +++ b/app/modules/web/Bootstrap.php @@ -30,13 +30,13 @@ use Klein\Request; use Klein\Response; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; -use RuntimeException; use SP\Core\Bootstrap\BootstrapBase; use SP\Core\Bootstrap\RouteContext; use SP\Domain\Common\Providers\Filter; use SP\Domain\Core\Bootstrap\BootstrapInterface; use SP\Domain\Core\Bootstrap\ModuleInterface; use SP\Domain\Core\Exceptions\SessionTimeout; +use SP\Domain\Http\Code; use function SP\__; use function SP\logger; @@ -87,9 +87,10 @@ final class Bootstrap extends BootstrapBase if (!method_exists($controllerClass, $routeContextData->getMethodName())) { logger($controllerClass . '::' . $routeContextData->getMethodName()); - $response->code(404); + $response->code(Code::NOT_FOUND->value); + $response->append(self::OOPS_MESSAGE); - throw new RuntimeException(self::OOPS_MESSAGE); + return $response; } $this->context->setTrasientKey(self::CONTEXT_ACTION_NAME, $routeContextData->getActionName()); @@ -109,10 +110,8 @@ final class Bootstrap extends BootstrapBase ) ); - $controller = $this->createObjectFor($controllerClass); - return call_user_func_array( - [$controller, $routeContextData->getMethodName()], + [$this->buildInstanceFor($controllerClass), $routeContextData->getMethodName()], $routeContextData->getMethodParams() ); } catch (SessionTimeout) { @@ -125,7 +124,12 @@ final class Bootstrap extends BootstrapBase if (DEBUG) { echo $e->getTraceAsString(); } + + $response->code(Code::INTERNAL_SERVER_ERROR->value); + $response->append($e->getMessage()); } + + return $response; }; } } diff --git a/lib/SP/Core/Bootstrap/BootstrapBase.php b/lib/SP/Core/Bootstrap/BootstrapBase.php index 259fd4d7..6bf0ff42 100644 --- a/lib/SP/Core/Bootstrap/BootstrapBase.php +++ b/lib/SP/Core/Bootstrap/BootstrapBase.php @@ -46,8 +46,6 @@ use SP\Domain\Core\Exceptions\CheckException; use SP\Domain\Core\Exceptions\ConfigException; use SP\Domain\Core\Exceptions\InitializationException; use SP\Domain\Http\Ports\RequestService; -use SP\Domain\Upgrade\Services\UpgradeConfig; -use SP\Infrastructure\File\FileException; use Symfony\Component\Debug\Debug; use Throwable; @@ -61,9 +59,9 @@ use function SP\processException; */ abstract class BootstrapBase implements BootstrapInterface { - public const CONTEXT_ACTION_NAME = "_actionName"; + public const CONTEXT_ACTION_NAME = '_actionName'; - protected const OOPS_MESSAGE = "Oops, it looks like this content does not exist..."; + protected const OOPS_MESSAGE = 'Oops, it looks like this content does not exist...'; public static mixed $LOCK; public static bool $checkPhpVersion = false; @@ -146,7 +144,6 @@ abstract class BootstrapBase implements BootstrapInterface /** * @throws CheckException * @throws ConfigException - * @throws FileException * @throws InitializationException */ final protected function initializeCommon(): void @@ -239,23 +236,22 @@ abstract class BootstrapBase implements BootstrapInterface */ private function initConfig(): void { - UpgradeConfig::needsUpgrade($this->configData->getConfigVersion()); - ConfigUtil::checkConfigDir(); } /** - * @deprecated - * FIXME: delete + * @template T of object + * @param class-string $class + * @return T&object */ - final protected function createObjectFor(string $class): object + final protected function buildInstanceFor(string $class): object { try { return $this->container->get($class); } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) { processException($e); - throw new RuntimeException($e); + throw new RuntimeException($e->getMessage()); } } } diff --git a/lib/SP/Domain/Upgrade/Services/UpgradeApp.php b/lib/SP/Domain/Common/Attributes/UpgradeVersion.php similarity index 68% rename from lib/SP/Domain/Upgrade/Services/UpgradeApp.php rename to lib/SP/Domain/Common/Attributes/UpgradeVersion.php index 52d3c092..778efef8 100644 --- a/lib/SP/Domain/Upgrade/Services/UpgradeApp.php +++ b/lib/SP/Domain/Common/Attributes/UpgradeVersion.php @@ -1,7 +1,5 @@ . */ -namespace SP\Domain\Upgrade\Services; +declare(strict_types=1); + +namespace SP\Domain\Common\Attributes; + +use Attribute; /** - * Class UpgradeApp + * Class UpgradeHandler */ -final class UpgradeApp extends UpgradeBase +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] +final class UpgradeVersion { - protected static function getUpgrades(): array + public function __construct(public readonly string $version) { - return []; - } - - protected function applyUpgrade(string $version): bool - { - return true; - } - - protected function commitVersion(string $version): void - { - $this->configData->setAppVersion($version); } } diff --git a/lib/SP/Domain/Common/Models/HydratableModel.php b/lib/SP/Domain/Common/Models/HydratableModel.php index 8feb2b7b..33d31252 100644 --- a/lib/SP/Domain/Common/Models/HydratableModel.php +++ b/lib/SP/Domain/Common/Models/HydratableModel.php @@ -1,4 +1,5 @@ $class + * @template THydrate + * @param class-string $class * - * @return T|null + * @return THydrate|null + * @throws SPException */ public function hydrate(string $class): ?object; /** * Serialize the object in the hydratable property - * @param object $object * - * @return static A new instance of the model with the serialized property + * @param object $object + * @return static|null A new instance of the model with the serialized property or null if the property + * couldn't be serialized */ - public function dehydrate(object $object): static; + public function dehydrate(object $object): static|null; } diff --git a/lib/SP/Domain/Common/Models/SerializedModel.php b/lib/SP/Domain/Common/Models/SerializedModel.php index 0bfa44cb..fc5250bd 100644 --- a/lib/SP/Domain/Common/Models/SerializedModel.php +++ b/lib/SP/Domain/Common/Models/SerializedModel.php @@ -1,4 +1,5 @@ $class - * - * @return THydrate|null - * @throws SPException + * @inheritDoc */ public function hydrate(string $class): ?object + { + return $this->parseAttribute( + function (Hydratable $hydratable) use ($class) { + $valid = array_filter( + $hydratable->getTargetClass(), + static fn(string $targetClass) => is_a($class, $targetClass, true) + ); + + $property = $this->{$hydratable->getSourceProperty()}; + + if (count($valid) > 0 && $property !== null) { + return Serde::deserialize($property, $class) ?: null; + } + + return null; + } + ); + } + + private function parseAttribute(callable $callback): mixed { $reflectionClass = new ReflectionClass($this); foreach ($reflectionClass->getAttributes(Hydratable::class) as $attribute) { - /** @var Hydratable $instance */ - $instance = $attribute->newInstance(); - - $valid = array_filter( - $instance->getTargetClass(), - static fn(string $targetClass) => is_a($class, $targetClass, true) - ); - - $property = $this->{$instance->getSourceProperty()}; - - if (count($valid) > 0 && $property !== null) { - return Serde::deserialize($property, $class) ?: null; - } + return $callback($attribute->newInstance()); } return null; @@ -68,24 +72,21 @@ trait SerializedModel /** * @inheritDoc */ - public function dehydrate(object $object): static + public function dehydrate(object $object): static|null { - $reflectionClass = new ReflectionClass($this); + return $this->parseAttribute( + function (Hydratable $hydratable) use ($object) { + $valid = array_filter( + $hydratable->getTargetClass(), + static fn(string $targetClass) => is_a($object, $targetClass) + ); - foreach ($reflectionClass->getAttributes(Hydratable::class) as $attribute) { - /** @var Hydratable $instance */ - $instance = $attribute->newInstance(); + if (count($valid) > 0) { + return $this->mutate([$hydratable->getSourceProperty() => Serde::serialize($object)]); + } - $valid = array_filter( - $instance->getTargetClass(), - static fn(string $targetClass) => is_a($object, $targetClass, true) - ); - - if (count($valid) > 0) { - return $this->mutate([$instance->getSourceProperty() => Serde::serialize($object)]); + return $this; } - } - - return $this; + ); } } diff --git a/lib/SP/Domain/Http/Code.php b/lib/SP/Domain/Http/Code.php new file mode 100644 index 00000000..62c6fe2f --- /dev/null +++ b/lib/SP/Domain/Http/Code.php @@ -0,0 +1,44 @@ +. + */ + +namespace SP\Domain\Http; + +/** + * Enum Code + */ +enum Code: int +{ + case INTERNAL_SERVER_ERROR = 500; + case SERVICE_UNAVALIABLE = 503; + case BAD_REQUEST = 400; + case UNAUTHORIZED = 401; + case FORBIDDEN = 403; + case NOT_FOUND = 404; + case OK = 200; + case CREATED = 201; + case NO_CONTENT = 204; + case MOVED_PERMANENTLY = 301; + case FOUND = 302; + case NOT_MODIFIED = 304; +} diff --git a/lib/SP/Domain/Upgrade/Ports/UpgradeConfigService.php b/lib/SP/Domain/Upgrade/Ports/UpgradeConfigService.php index b27cc649..4e2b7055 100644 --- a/lib/SP/Domain/Upgrade/Ports/UpgradeConfigService.php +++ b/lib/SP/Domain/Upgrade/Ports/UpgradeConfigService.php @@ -1,5 +1,5 @@ . + */ + +declare(strict_types=1); + +namespace SP\Domain\Upgrade\Ports; + +use SP\Domain\Config\Ports\ConfigDataInterface; + +/** + * Interface UpgradeHandler + */ +interface UpgradeHandlerService +{ + public function apply(string $version, ConfigDataInterface $configData): bool; +} diff --git a/lib/SP/Domain/Upgrade/Ports/UpgradeService.php b/lib/SP/Domain/Upgrade/Ports/UpgradeService.php index 140066e5..0277eb40 100644 --- a/lib/SP/Domain/Upgrade/Ports/UpgradeService.php +++ b/lib/SP/Domain/Upgrade/Ports/UpgradeService.php @@ -1,6 +1,7 @@ $upgradeHandlers + */ + private array $upgradeHandlers = []; + + public function __construct( + Application $application, + FileHandlerProvider $fileHandlerProvider, + private readonly ContainerInterface $container + ) { parent::__construct($application); $this->eventDispatcher->attach($fileHandlerProvider); @@ -54,20 +71,11 @@ abstract class UpgradeBase extends Service implements UpgradeService /** * @inheritDoc - */ - final public static function needsUpgrade(string $version): bool - { - return !empty($version) && Version::checkVersion($version, static::getUpgrades()); - } - - abstract protected static function getUpgrades(): array; - - /** - * @inheritDoc - * @throws UpgradeException * @throws FileException + * @throws ServiceException + * @throws UpgradeException */ - final public function upgrade(string $version, ConfigDataInterface $configData): void + public function upgrade(string $version, ConfigDataInterface $configData): void { $this->configData = $configData; @@ -81,22 +89,15 @@ abstract class UpgradeBase extends Service implements UpgradeService ) ); - $upgradeVersions = array_filter( - static::getUpgrades(), - static fn(string $appVersion) => Version::checkVersion($version, $appVersion) - ); - - foreach ($upgradeVersions as $upgradeVersion) { - if ($this->applyUpgrade($upgradeVersion) === false) { + foreach ($this->getTargetUpgradeHandlers($version) as $upgradeHandler) { + if (!$upgradeHandler->apply($version, $configData)) { throw UpgradeException::critical( __u('Error while applying the update'), __u('Please, check the event log for more details') ); } - logger('Upgrade: ' . $upgradeVersion); - - $this->commitVersion($upgradeVersion); + logger('Upgrade: ' . $upgradeHandler::class); $this->config->save($configData, false); } @@ -110,7 +111,45 @@ abstract class UpgradeBase extends Service implements UpgradeService ); } - abstract protected function applyUpgrade(string $version): bool; + /** + * @return iterable + * @throws ServiceException + */ + private function getTargetUpgradeHandlers(string $version): iterable + { + try { + foreach ($this->upgradeHandlers as $class) { + $reflection = new ReflectionClass($class); + /** @var ReflectionAttribute $attribute */ + foreach ($reflection->getAttributes(UpgradeVersion::class) as $attribute) { + $instance = $attribute->newInstance(); - abstract protected function commitVersion(string $version): void; + if (Version::checkVersion($version, $instance->version)) { + yield $this->container->get($class); + } + } + } + } catch (Throwable $e) { + throw ServiceException::from($e); + } + } + + /** + * @throws ServiceException + * @throws InvalidClassException + */ + public function registerUpgradeHandler(string $class): void + { + if (!class_exists($class) || !is_subclass_of($class, UpgradeHandlerService::class)) { + throw InvalidClassException::error('Class does not either exist or implement UpgradeService class'); + } + + $hash = sha1($class); + + if (array_key_exists($hash, $this->upgradeHandlers)) { + throw ServiceException::error('Class already registered'); + } + + $this->upgradeHandlers[$hash] = $class; + } } diff --git a/lib/SP/Domain/Upgrade/Services/UpgradeDatabase.php b/lib/SP/Domain/Upgrade/Services/UpgradeDatabase.php index e59d409d..1fa6b87b 100644 --- a/lib/SP/Domain/Upgrade/Services/UpgradeDatabase.php +++ b/lib/SP/Domain/Upgrade/Services/UpgradeDatabase.php @@ -1,5 +1,5 @@ configData->setDatabaseVersion($version); + parent::__construct($application); } /** * @throws UpgradeException */ - protected function applyUpgrade(string $version): bool + public function apply(string $version, ConfigDataInterface $configData): bool { $count = 0; foreach ($this->getQueriesFromFile($version) as $query) { $count++; - try { - $this->eventDispatcher->notify( - 'upgrade.db.process', - new Event($this, EventMessage::factory()->addDetail(__u('Version'), $version)) - ); + $this->eventDispatcher->notify( + 'upgrade.db.process', + new Event($this, EventMessage::factory()->addDetail(__u('Version'), $version)) + ); + try { $this->database->runQueryRaw($query); } catch (Exception $e) { processException($e); @@ -109,6 +99,8 @@ final class UpgradeDatabase extends UpgradeBase throw UpgradeException::error(__u('Update file does not contain data'), $version); } + $configData->setDatabaseVersion($version); + $this->eventDispatcher->notify( 'upgrade.db.process', new Event( @@ -123,13 +115,13 @@ final class UpgradeDatabase extends UpgradeBase /** * @throws UpgradeException */ - private function getQueriesFromFile(string $filename): iterable + private function getQueriesFromFile(string $version): iterable { - $fileName = FileSystem::buildPath(SQL_PATH, str_replace('.', '', $filename) . '.sql'); + $filename = FileSystem::buildPath(SQL_PATH, str_replace('.', '', $version) . '.sql'); try { - return (new MysqlFileParser(new FileHandler($fileName)))->parse('$$'); - } catch (FileException $e) { + return (new MysqlFileParser(new FileHandler($filename)))->parse('$$'); + } catch (Exception $e) { processException($e); throw UpgradeException::error($e->getMessage()); diff --git a/lib/SP/Domain/Upgrade/Services/UpgradeException.php b/lib/SP/Domain/Upgrade/Services/UpgradeException.php index 50e47298..6a9bcf84 100644 --- a/lib/SP/Domain/Upgrade/Services/UpgradeException.php +++ b/lib/SP/Domain/Upgrade/Services/UpgradeException.php @@ -1,6 +1,6 @@ |null $class * - * @return null|T + * @return mixed|T * @throws FileException * @throws InvalidClassException */ @@ -98,13 +99,13 @@ class FileSystem $out = require $file; if ($class && class_exists($class) && !$out instanceof $class) { - throw new InvalidClassException(__u('Invalid class for loaded file data')); + throw InvalidClassException::error(__u('Invalid class for loaded file data')); } return $out; - } else { - throw new FileException(sprintf(__('File not found: %s'), $file)); } + + throw FileException::error(sprintf(__('File not found: %s'), $file)); } /** diff --git a/tests/SP/Domain/Config/Services/UpgradeConfigTest.php b/tests/SP/Domain/Config/Services/UpgradeConfigTest.php deleted file mode 100644 index 44136150..00000000 --- a/tests/SP/Domain/Config/Services/UpgradeConfigTest.php +++ /dev/null @@ -1,101 +0,0 @@ -. - */ - -namespace SP\Tests\Domain\Config\Services; - -use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\MockObject\Exception; -use PHPUnit\Framework\MockObject\MockObject; -use SP\Core\Application; -use SP\Domain\Config\Ports\ConfigDataInterface; -use SP\Domain\Config\Ports\ConfigFileService; -use SP\Domain\Log\Ports\FileHandlerProvider; -use SP\Domain\Upgrade\Services\UpgradeConfig; -use SP\Domain\Upgrade\Services\UpgradeException; -use SP\Infrastructure\File\FileException; -use SP\Tests\UnitaryTestCase; - -/** - * Class UpgradeConfigTest - * - */ -#[Group('unitary')] -class UpgradeConfigTest extends UnitaryTestCase -{ - private FileHandlerProvider|MockObject $fileLogHandlerProvider; - - public static function versionDataProvider(): array - { - return [ - ['320.20062801', false], - ['340.00000000', false] - ]; - } - - /** - * @throws Exception - * @throws FileException - * @throws UpgradeException - */ - public function testUpgrade() - { - $version = '200.00000000'; - $configData = $this->createMock(ConfigDataInterface::class); - $configFileService = $this->createMock(ConfigFileService::class); - $application = new Application( - $configFileService, - $this->application->getEventDispatcher(), - $this->application->getContext() - ); - - $configData->expects(self::never()) - ->method('setConfigVersion') - ->with(self::anything()); - - $configFileService->expects(self::never()) - ->method('save') - ->with($configData, false); - - $upgradeConfig = new UpgradeConfig($application, $this->fileLogHandlerProvider); - $upgradeConfig->upgrade($version, $configData); - } - - /** - * @return void - */ - #[DataProvider('versionDataProvider')] - public function testNeedsUpgrade(string $version, bool $expected) - { - $this->assertEquals($expected, UpgradeConfig::needsUpgrade($version)); - } - - protected function setUp(): void - { - parent::setUp(); - - $this->fileLogHandlerProvider = $this->createMock(FileHandlerProvider::class); - } -} diff --git a/tests/SP/Domain/Upgrade/Services/UpgradeAppTest.php b/tests/SP/Domain/Upgrade/Services/UpgradeAppTest.php deleted file mode 100644 index 191b95f5..00000000 --- a/tests/SP/Domain/Upgrade/Services/UpgradeAppTest.php +++ /dev/null @@ -1,87 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * sysPass - * - * @author nuxsmin - * @link http://syspass.org - * @copyright 2012-2024 Rubén Domínguez nuxsmin@$syspass.org - * - * This file is part of sysPass. - * - * sysPass is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * sysPass is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with sysPass. If not, see . - * - */ - -namespace SP\Tests\Domain\Upgrade\Services; - -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\MockObject\Exception; -use SP\Domain\Config\Ports\ConfigDataInterface; -use SP\Domain\Log\Ports\FileHandlerProvider; -use SP\Domain\Upgrade\Services\UpgradeApp; -use SP\Domain\Upgrade\Services\UpgradeException; -use SP\Infrastructure\File\FileException; -use SP\Tests\UnitaryTestCase; - -/** - * Class UpgradeAppTest - */ -#[Group('unitary')] -class UpgradeAppTest extends UnitaryTestCase -{ - /** - * @throws Exception - * @throws UpgradeException - * @throws FileException - */ - public function testUpgrade() - { - $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); - $configData = $this->createMock(ConfigDataInterface::class); - - $configData->expects($this->never()) - ->method('setAppVersion'); - - $this->config->expects($this->never()) - ->method('save'); - - $upgradeApp = new UpgradeApp($this->application, $fileHandlerProvider); - $upgradeApp->upgrade('123', $configData); - } -} diff --git a/tests/SP/Domain/Upgrade/Services/UpgradeConfigTest.php b/tests/SP/Domain/Upgrade/Services/UpgradeConfigTest.php deleted file mode 100644 index b23c7557..00000000 --- a/tests/SP/Domain/Upgrade/Services/UpgradeConfigTest.php +++ /dev/null @@ -1,87 +0,0 @@ -. - */ - -declare(strict_types=1); - -/** - * sysPass - * - * @author nuxsmin - * @link http://syspass.org - * @copyright 2012-2024 Rubén Domínguez nuxsmin@$syspass.org - * - * This file is part of sysPass. - * - * sysPass is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * sysPass is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with sysPass. If not, see . - * - */ - -namespace SP\Tests\Domain\Upgrade\Services; - -use PHPUnit\Framework\Attributes\Group; -use PHPUnit\Framework\MockObject\Exception; -use SP\Domain\Config\Ports\ConfigDataInterface; -use SP\Domain\Log\Ports\FileHandlerProvider; -use SP\Domain\Upgrade\Services\UpgradeConfig; -use SP\Domain\Upgrade\Services\UpgradeException; -use SP\Infrastructure\File\FileException; -use SP\Tests\UnitaryTestCase; - -/** - * Class UpgradeConfigTest - */ -#[Group('unitary')] -class UpgradeConfigTest extends UnitaryTestCase -{ - /** - * @throws Exception - * @throws UpgradeException - * @throws FileException - */ - public function testUpgrade() - { - $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); - $configData = $this->createMock(ConfigDataInterface::class); - - $configData->expects($this->never()) - ->method('setConfigVersion'); - - $this->config->expects($this->never()) - ->method('save'); - - $upgradeConfig = new UpgradeConfig($this->application, $fileHandlerProvider); - $upgradeConfig->upgrade('123', $configData); - } -} diff --git a/tests/SP/Domain/Upgrade/Services/UpgradeDatabaseTest.php b/tests/SP/Domain/Upgrade/Services/UpgradeDatabaseTest.php index 218d05b9..b7110a1a 100644 --- a/tests/SP/Domain/Upgrade/Services/UpgradeDatabaseTest.php +++ b/tests/SP/Domain/Upgrade/Services/UpgradeDatabaseTest.php @@ -28,13 +28,12 @@ namespace SP\Tests\Domain\Upgrade\Services; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; use SP\Domain\Config\Ports\ConfigDataInterface; use SP\Domain\Database\Ports\DatabaseInterface; -use SP\Domain\Log\Ports\FileHandlerProvider; use SP\Domain\Upgrade\Services\UpgradeDatabase; use SP\Domain\Upgrade\Services\UpgradeException; -use SP\Infrastructure\File\FileException; use SP\Tests\UnitaryTestCase; /** @@ -43,83 +42,80 @@ use SP\Tests\UnitaryTestCase; #[Group('unitary')] class UpgradeDatabaseTest extends UnitaryTestCase { + private UpgradeDatabase $upgradeDatabase; + private DatabaseInterface|MockObject $database; + /** * @throws Exception * @throws UpgradeException - * @throws FileException */ public function testUpgrade() { - $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); - $database = $this->createMock(DatabaseInterface::class); $configData = $this->createMock(ConfigDataInterface::class); - $database->expects($this->exactly(2)) - ->method('runQueryRaw') - ->with( - ... - self::withConsecutive( - ['alter table CustomFieldData drop column id'], - ['alter table CustomFieldData add primary key (moduleId, itemId, definitionId)'] - ) - ); + $this->database->expects($this->exactly(2)) + ->method('runQueryRaw') + ->with( + ... + self::withConsecutive( + ['alter table CustomFieldData drop column id'], + ['alter table CustomFieldData add primary key (moduleId, itemId, definitionId)'] + ) + ); $configData->expects($this->once()) ->method('setDatabaseVersion') ->with('400.24210101'); - $this->config->expects($this->once()) - ->method('save') - ->with($configData); - - $upgradeDatabase = new UpgradeDatabase($this->application, $fileHandlerProvider, $database); - $upgradeDatabase->upgrade('400.00000000', $configData); + $this->upgradeDatabase->apply('400.24210101', $configData); } /** * @throws Exception * @throws UpgradeException - * @throws FileException */ public function testUpgradeWithException() { - $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); - $database = $this->createMock(DatabaseInterface::class); $configData = $this->createMock(ConfigDataInterface::class); - $database->expects($this->once()) - ->method('runQueryRaw') - ->willThrowException(new RuntimeException('test')); + $this->database->expects($this->once()) + ->method('runQueryRaw') + ->willThrowException(new RuntimeException('test')); $configData->expects($this->never()) ->method('setDatabaseVersion'); - $upgradeDatabase = new UpgradeDatabase($this->application, $fileHandlerProvider, $database); - $this->expectException(UpgradeException::class); $this->expectExceptionMessage('Error while updating the database'); - $upgradeDatabase->upgrade('400.00000000', $configData); + $this->upgradeDatabase->apply('400.24210101', $configData); } /** * @throws Exception * @throws UpgradeException - * @throws FileException */ - public function testUpgradeWithNoUpgrades() + public function testUpgradeWithFileException() { - $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); - $database = $this->createMock(DatabaseInterface::class); $configData = $this->createMock(ConfigDataInterface::class); - $database->expects($this->never()) - ->method('runQueryRaw'); + $this->database->expects($this->never()) + ->method('runQueryRaw'); $configData->expects($this->never()) ->method('setDatabaseVersion'); - $upgradeDatabase = new UpgradeDatabase($this->application, $fileHandlerProvider, $database); - $upgradeDatabase->upgrade('400.24210101', $configData); + $this->expectException(UpgradeException::class); + $this->expectExceptionMessage('Failed to open stream: No such file or directory'); + + $this->upgradeDatabase->apply('400.00000000', $configData); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->database = $this->createMock(DatabaseInterface::class); + $this->upgradeDatabase = new UpgradeDatabase($this->application, $this->database); } } diff --git a/tests/SP/Domain/Upgrade/Services/UpgradeTest.php b/tests/SP/Domain/Upgrade/Services/UpgradeTest.php new file mode 100644 index 00000000..44faeb49 --- /dev/null +++ b/tests/SP/Domain/Upgrade/Services/UpgradeTest.php @@ -0,0 +1,217 @@ +. + */ + +declare(strict_types=1); + +namespace SP\Tests\Domain\Upgrade\Services; + +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerInterface; +use RuntimeException; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Config\Ports\ConfigDataInterface; +use SP\Domain\Core\Exceptions\InvalidClassException; +use SP\Domain\Log\Ports\FileHandlerProvider; +use SP\Domain\Upgrade\Ports\UpgradeHandlerService; +use SP\Domain\Upgrade\Services\Upgrade; +use SP\Domain\Upgrade\Services\UpgradeException; +use SP\Infrastructure\File\FileException; +use SP\Tests\Stubs\UpgradeHandlerStub; +use SP\Tests\UnitaryTestCase; +use stdClass; + +/** + * Class UpgradeTest + */ +#[Group('unitary')] +class UpgradeTest extends UnitaryTestCase +{ + + private MockObject|ContainerInterface $container; + private Upgrade $upgrade; + + /** + * @throws ServiceException + * @throws InvalidClassException + * @throws Exception + */ + public function testRegisterUpgradeHandler() + { + $handler = $this->createMock(UpgradeHandlerService::class); + + $this->upgrade->registerUpgradeHandler($handler::class); + + $this->expectNotToPerformAssertions(); + } + + /** + * @throws ServiceException + * @throws InvalidClassException + */ + public function testRegisterUpgradeHandlerWithClassException() + { + $this->expectException(InvalidClassException::class); + $this->expectExceptionMessage('Class does not either exist or implement UpgradeService class'); + + $this->upgrade->registerUpgradeHandler(stdClass::class); + } + + /** + * @throws ServiceException + * @throws InvalidClassException + * @throws Exception + */ + public function testRegisterUpgradeHandlerWithDuplicate() + { + $handler = $this->createMock(UpgradeHandlerService::class); + + $this->upgrade->registerUpgradeHandler($handler::class); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Class already registered'); + + $this->upgrade->registerUpgradeHandler($handler::class); + } + + /** + * @throws Exception + * @throws ServiceException + * @throws FileException + * @throws UpgradeException + */ + public function testUpgrade() + { + $configData = $this->createMock(ConfigDataInterface::class); + $this->config->expects($this->never()) + ->method('save'); + + $this->upgrade->upgrade('400.00000000', $configData); + } + + /** + * @throws Exception + * @throws ServiceException + * @throws FileException + * @throws UpgradeException + * @throws InvalidClassException + */ + public function testUpgradeWithHandler() + { + $configData = $this->createMock(ConfigDataInterface::class); + $handler = $this->createMock(UpgradeHandlerService::class); + $handler->expects($this->exactly(2)) + ->method('apply') + ->with('400.00000000', $configData) + ->willReturn(true); + + $this->container + ->expects($this->exactly(2)) + ->method('get') + ->with(UpgradeHandlerStub::class) + ->willReturn($handler); + + $this->config->expects($this->exactly(2)) + ->method('save') + ->with($configData, false); + + $this->upgrade->registerUpgradeHandler(UpgradeHandlerStub::class); + $this->upgrade->upgrade('400.00000000', $configData); + } + + /** + * @throws Exception + * @throws ServiceException + * @throws FileException + * @throws UpgradeException + * @throws InvalidClassException + */ + public function testUpgradeWithHandlerWithFailedApply() + { + $configData = $this->createMock(ConfigDataInterface::class); + $handler = $this->createMock(UpgradeHandlerService::class); + $handler->expects($this->once()) + ->method('apply') + ->with('400.00000000', $configData) + ->willReturn(false); + + $this->container + ->expects($this->once()) + ->method('get') + ->with(UpgradeHandlerStub::class) + ->willReturn($handler); + + $this->config->expects($this->never()) + ->method('save'); + + $this->upgrade->registerUpgradeHandler(UpgradeHandlerStub::class); + + $this->expectException(UpgradeException::class); + $this->expectExceptionMessage('Error while applying the update'); + + $this->upgrade->upgrade('400.00000000', $configData); + } + + /** + * @throws Exception + * @throws ServiceException + * @throws FileException + * @throws UpgradeException + * @throws InvalidClassException + */ + public function testUpgradeWithException() + { + $configData = $this->createMock(ConfigDataInterface::class); + $handler = $this->createMock(UpgradeHandlerService::class); + $handler->expects($this->never()) + ->method('apply'); + + $this->container + ->expects($this->once()) + ->method('get') + ->with(UpgradeHandlerStub::class) + ->willThrowException(new RuntimeException('test')); + + $this->config->expects($this->never()) + ->method('save'); + + $this->upgrade->registerUpgradeHandler(UpgradeHandlerStub::class); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('test'); + + $this->upgrade->upgrade('400.00000000', $configData); + } + + protected function setUp(): void + { + parent::setUp(); + + $fileHandlerProvider = $this->createMock(FileHandlerProvider::class); + $this->container = $this->createMock(ContainerInterface::class); + + $this->upgrade = new Upgrade($this->application, $fileHandlerProvider, $this->container); + } +} diff --git a/lib/SP/Domain/Upgrade/Services/UpgradeConfig.php b/tests/SP/Stubs/UpgradeHandlerStub.php similarity index 52% rename from lib/SP/Domain/Upgrade/Services/UpgradeConfig.php rename to tests/SP/Stubs/UpgradeHandlerStub.php index 1ecb955f..1adf5c4c 100644 --- a/lib/SP/Domain/Upgrade/Services/UpgradeConfig.php +++ b/tests/SP/Stubs/UpgradeHandlerStub.php @@ -1,5 +1,5 @@ eventDispatcher->attach($fileLogHandlerProvider); - } - - - protected static function getUpgrades(): array - { - return []; - } - - protected function commitVersion(string $version): void - { - $this->configData->setConfigVersion($version); - } - - protected function applyUpgrade(string $version): bool + public function apply(string $version, ConfigDataInterface $configData): bool { return true; }