chore: Refactor upgrade subsystem

Use attributes to detect and load upgrade handlers.

Signed-off-by: Rubén D <nuxsmin@syspass.org>
This commit is contained in:
Rubén D
2024-05-03 22:24:13 +02:00
parent 0304221fae
commit 10dedaa03f
20 changed files with 526 additions and 490 deletions

View File

@@ -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 {

View File

@@ -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;
};
}
}

View File

@@ -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<T> $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());
}
}
}

View File

@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
/*
/**
* sysPass
*
* @author nuxsmin
@@ -24,25 +22,19 @@ declare(strict_types=1);
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/*
* sysPass
@@ -25,6 +26,8 @@ declare(strict_types=1);
namespace SP\Domain\Common\Models;
use SP\Domain\Core\Exceptions\SPException;
/**
* Interface HydratableModel
*/
@@ -33,18 +36,20 @@ interface HydratableModel
/**
* Deserialize the hydratable property and returns the object.
*
* @template T
* @param class-string<T> $class
* @template THydrate
* @param class-string<THydrate> $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;
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/*
* sysPass
@@ -28,7 +29,6 @@ namespace SP\Domain\Common\Models;
use ReflectionClass;
use SP\Domain\Common\Adapters\Serde;
use SP\Domain\Common\Attributes\Hydratable;
use SP\Domain\Core\Exceptions\SPException;
/**
* Trait SerializedModel
@@ -36,30 +36,34 @@ use SP\Domain\Core\Exceptions\SPException;
trait SerializedModel
{
/**
* @template THydrate
* @param class-string<THydrate> $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;
);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@@ -1,5 +1,5 @@
<?php
/*
/**
* sysPass
*
* @author nuxsmin

View File

@@ -0,0 +1,37 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
/*
/**
* sysPass
*
* @author nuxsmin
@@ -32,13 +33,16 @@ use SP\Domain\Config\Ports\ConfigDataInterface;
*/
interface UpgradeService
{
/**
* Check if it needs to be upgraded
*/
public static function needsUpgrade(string $version): bool;
/**
* Performs the upgrading process
*/
public function upgrade(string $version, ConfigDataInterface $configData);
/**
* Register an upgrade class to run upgrade tasks.
*
* @param string $class
* @return void
*/
public function registerUpgradeHandler(string $class): void;
}

View File

@@ -1,6 +1,7 @@
<?php
declare(strict_types=1);
/*
/**
* sysPass
*
* @author nuxsmin
@@ -25,28 +26,44 @@ declare(strict_types=1);
namespace SP\Domain\Upgrade\Services;
use Psr\Container\ContainerInterface;
use ReflectionAttribute;
use ReflectionClass;
use SP\Core\Application;
use SP\Core\Events\Event;
use SP\Core\Events\EventMessage;
use SP\Domain\Common\Attributes\UpgradeVersion;
use SP\Domain\Common\Providers\Version;
use SP\Domain\Common\Services\Service;
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\Ports\UpgradeService;
use SP\Infrastructure\File\FileException;
use Throwable;
use function SP\__u;
use function SP\logger;
/**
* Class UpgradeBase
* Class Upgrade
*/
abstract class UpgradeBase extends Service implements UpgradeService
final class Upgrade extends Service implements UpgradeService
{
protected ?ConfigDataInterface $configData = null;
public function __construct(Application $application, FileHandlerProvider $fileHandlerProvider)
{
/**
* @var array<string> $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<UpgradeHandlerService>
* @throws ServiceException
*/
private function getTargetUpgradeHandlers(string $version): iterable
{
try {
foreach ($this->upgradeHandlers as $class) {
$reflection = new ReflectionClass($class);
/** @var ReflectionAttribute<UpgradeVersion> $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;
}
}

View File

@@ -1,5 +1,5 @@
<?php
/*
/**
* sysPass
*
* @author nuxsmin
@@ -30,10 +30,12 @@ use Exception;
use SP\Core\Application;
use SP\Core\Events\Event;
use SP\Core\Events\EventMessage;
use SP\Domain\Common\Attributes\UpgradeVersion;
use SP\Domain\Common\Services\Service;
use SP\Domain\Config\Ports\ConfigDataInterface;
use SP\Domain\Database\Ports\DatabaseInterface;
use SP\Domain\Log\Ports\FileHandlerProvider;
use SP\Domain\Upgrade\Ports\UpgradeHandlerService;
use SP\Infrastructure\Database\MysqlFileParser;
use SP\Infrastructure\File\FileException;
use SP\Infrastructure\File\FileHandler;
use SP\Infrastructure\File\FileSystem;
@@ -45,44 +47,32 @@ use function SP\processException;
/**
* Class UpgradeDatabase
*/
final class UpgradeDatabase extends UpgradeBase
#[UpgradeVersion('400.24210101')]
final class UpgradeDatabase extends Service implements UpgradeHandlerService
{
public function __construct(
Application $application,
FileHandlerProvider $fileHandlerProvider,
private readonly DatabaseInterface $database,
) {
parent::__construct($application, $fileHandlerProvider);
}
protected static function getUpgrades(): array
{
return [
'400.24210101',
];
}
protected function commitVersion(string $version): void
{
$this->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());

View File

@@ -1,6 +1,6 @@
<?php
declare(strict_types=1);
/*
/**
* sysPass
*
* @author nuxsmin

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/*
* sysPass
@@ -88,7 +89,7 @@ class FileSystem
* @param string $file
* @param class-string<T>|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));
}
/**

View File

@@ -1,101 +0,0 @@
<?php
declare(strict_types=1);
/*
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -1,87 +0,0 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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 <http://www.gnu.org/licenses/>.
*
*/
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);
}
}

View File

@@ -1,87 +0,0 @@
<?php
/*
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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 <http://www.gnu.org/licenses/>.
*
*/
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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,217 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link https://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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -1,5 +1,5 @@
<?php
/*
/**
* sysPass
*
* @author nuxsmin
@@ -24,38 +24,21 @@
declare(strict_types=1);
namespace SP\Domain\Upgrade\Services;
namespace SP\Tests\Stubs;
use SP\Core\Application;
use SP\Domain\Log\Ports\FileHandlerProvider;
use SP\Domain\Upgrade\Ports\UpgradeConfigService;
use SP\Domain\Common\Attributes\UpgradeVersion;
use SP\Domain\Config\Ports\ConfigDataInterface;
use SP\Domain\Upgrade\Ports\UpgradeHandlerService;
/**
* Class UpgradeConfig
* Class UpgradeHandlerStub
*/
final class UpgradeConfig extends UpgradeBase implements UpgradeConfigService
#[UpgradeVersion('400.00000002')]
#[UpgradeVersion('400.00000001')]
#[UpgradeVersion('400.00000000')]
class UpgradeHandlerStub implements UpgradeHandlerService
{
public function __construct(
Application $application,
FileHandlerProvider $fileLogHandlerProvider
) {
parent::__construct($application, $fileLogHandlerProvider);
$this->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;
}