From de9d500d85fd4ea00f0aa8db7f471a2cbd74da10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D?= Date: Sun, 20 Nov 2022 11:46:11 +0100 Subject: [PATCH] chore: Create AccountToTagRepository tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A RepositoryInterface is added to expose transactionAware method to the service. Signed-off-by: Rubén D --- .../In/AccountToTagRepositoryInterface.php | 18 +-- .../Account/Services/AccountService.php | 8 +- .../Repositories/RepositoryInterface.php | 44 ++++++ .../Repositories/AccountToTagRepository.php | 101 +++++------- .../Common/Repositories/Repository.php | 3 +- .../AccountToTagRepositoryTest.php | 149 ++++++++++++++++++ tests/SP/UnitaryTestCase.php | 17 +- 7 files changed, 262 insertions(+), 78 deletions(-) create mode 100644 lib/SP/Domain/Common/Repositories/RepositoryInterface.php create mode 100644 tests/SP/Infrastructure/Account/Repositories/AccountToTagRepositoryTest.php diff --git a/lib/SP/Domain/Account/In/AccountToTagRepositoryInterface.php b/lib/SP/Domain/Account/In/AccountToTagRepositoryInterface.php index 25bb539b..a56ba2bc 100644 --- a/lib/SP/Domain/Account/In/AccountToTagRepositoryInterface.php +++ b/lib/SP/Domain/Account/In/AccountToTagRepositoryInterface.php @@ -28,6 +28,7 @@ namespace SP\Domain\Account\In; use SP\Core\Exceptions\ConstraintException; use SP\Core\Exceptions\QueryException; use SP\Domain\Account\Services\AccountRequest; +use SP\Domain\Common\Repositories\RepositoryInterface; use SP\Infrastructure\Database\QueryResult; /** @@ -35,7 +36,7 @@ use SP\Infrastructure\Database\QueryResult; * * @package SP\Infrastructure\Account\Repositories */ -interface AccountToTagRepositoryInterface +interface AccountToTagRepositoryInterface extends RepositoryInterface { /** * Devolver las etiquetas de una cuenta @@ -48,33 +49,24 @@ interface AccountToTagRepositoryInterface */ public function getTagsByAccountId(int $id): QueryResult; - /** - * @param AccountRequest $accountRequest - * - * @throws ConstraintException - * @throws QueryException - */ - public function update(AccountRequest $accountRequest): void; - /** * Eliminar las etiquetas de una cuenta * * @param int $id * - * @return int + * @return bool * @throws ConstraintException * @throws QueryException */ - public function deleteByAccountId(int $id): int; + public function deleteByAccountId(int $id): bool; /** * Actualizar las etiquetas de una cuenta * * @param AccountRequest $accountRequest * - * @return int * @throws ConstraintException * @throws QueryException */ - public function add(AccountRequest $accountRequest): int; + public function add(AccountRequest $accountRequest): void; } \ No newline at end of file diff --git a/lib/SP/Domain/Account/Services/AccountService.php b/lib/SP/Domain/Account/Services/AccountService.php index 91da40dc..547cd38b 100644 --- a/lib/SP/Domain/Account/Services/AccountService.php +++ b/lib/SP/Domain/Account/Services/AccountService.php @@ -478,6 +478,7 @@ final class AccountService extends Service implements AccountServiceInterface * * @throws QueryException * @throws ConstraintException + * @throws \SP\Domain\Common\Services\ServiceException */ private function updateItems(AccountRequest $accountRequest): void { @@ -517,7 +518,12 @@ final class AccountService extends Service implements AccountServiceInterface if ($accountRequest->tags !== null) { if (count($accountRequest->tags) > 0) { - $this->accountToTagRepository->update($accountRequest); + $this->accountToTagRepository->transactionAware( + function () use ($accountRequest) { + $this->accountToTagRepository->deleteByAccountId($accountRequest->id); + $this->accountToTagRepository->add($accountRequest); + } + ); } else { $this->accountToTagRepository->deleteByAccountId($accountRequest->id); } diff --git a/lib/SP/Domain/Common/Repositories/RepositoryInterface.php b/lib/SP/Domain/Common/Repositories/RepositoryInterface.php new file mode 100644 index 00000000..760bafac --- /dev/null +++ b/lib/SP/Domain/Common/Repositories/RepositoryInterface.php @@ -0,0 +1,44 @@ +. + */ + +namespace SP\Domain\Common\Repositories; + +use Closure; + +/** + * Interface RepositoryInterface + */ +interface RepositoryInterface +{ + /** + * Bubbles a Closure in a database transaction + * + * @param \Closure $closure + * + * @return mixed + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \Exception + */ + public function transactionAware(Closure $closure): mixed; +} \ No newline at end of file diff --git a/lib/SP/Infrastructure/Account/Repositories/AccountToTagRepository.php b/lib/SP/Infrastructure/Account/Repositories/AccountToTagRepository.php index f12bcb10..ef881172 100644 --- a/lib/SP/Infrastructure/Account/Repositories/AccountToTagRepository.php +++ b/lib/SP/Infrastructure/Account/Repositories/AccountToTagRepository.php @@ -24,15 +24,13 @@ namespace SP\Infrastructure\Account\Repositories; -use SP\Core\Exceptions\ConstraintException; -use SP\Core\Exceptions\QueryException; -use SP\DataModel\ItemData; use SP\Domain\Account\In\AccountToTagRepositoryInterface; use SP\Domain\Account\Services\AccountRequest; use SP\Infrastructure\Common\Repositories\Repository; use SP\Infrastructure\Common\Repositories\RepositoryItemTrait; use SP\Infrastructure\Database\QueryData; use SP\Infrastructure\Database\QueryResult; +use function SP\__u; /** * Class AccountToTagRepository @@ -49,36 +47,22 @@ final class AccountToTagRepository extends Repository implements AccountToTagRep * @param int $id * * @return QueryResult - * @throws ConstraintException - * @throws QueryException */ public function getTagsByAccountId(int $id): QueryResult { - $query = /** @lang SQL */ - 'SELECT T.id, T.name - FROM AccountToTag AT - INNER JOIN Tag T ON AT.tagId = T.id - WHERE AT.accountId = ? - ORDER BY T.name'; + $query = $this->queryFactory + ->newSelect() + ->cols([ + 'Tag.id', + 'Tag.name', + ]) + ->from('AccountToTag') + ->join('INNER', 'Tag', 'Tag.id == AccountToTag.tagId') + ->where('AccountToTag.accountId = :accountId') + ->bindValues(['accountId' => $id]) + ->orderBy(['Tag.name ASC']); - $queryData = new QueryData(); - $queryData->setQuery($query); - $queryData->addParam($id); - $queryData->setMapClassName(ItemData::class); - - return $this->db->doSelect($queryData); - } - - /** - * @param AccountRequest $accountRequest - * - * @throws ConstraintException - * @throws QueryException - */ - public function update(AccountRequest $accountRequest): void - { - $this->deleteByAccountId($accountRequest->id); - $this->add($accountRequest); + return $this->db->doSelect(QueryData::build($query)); } /** @@ -86,18 +70,23 @@ final class AccountToTagRepository extends Repository implements AccountToTagRep * * @param int $id * - * @return int - * @throws ConstraintException - * @throws QueryException + * @return bool + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException */ - public function deleteByAccountId(int $id): int + public function deleteByAccountId(int $id): bool { - $queryData = new QueryData(); - $queryData->setQuery('DELETE FROM AccountToTag WHERE accountId = ?'); - $queryData->addParam($id); - $queryData->setOnErrorMessage(__u('Error while removing the account\'s tags')); + $query = $this->queryFactory + ->newDelete() + ->from('AccountToTag') + ->where('accountId = :accountId') + ->bindValues([ + 'accountId' => $id, + ]); - return $this->db->doQuery($queryData)->getAffectedNumRows(); + $queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while removing the account\'s tags')); + + return $this->db->doQuery($queryData)->getAffectedNumRows() === 1; } /** @@ -105,28 +94,24 @@ final class AccountToTagRepository extends Repository implements AccountToTagRep * * @param AccountRequest $accountRequest * - * @return int - * @throws ConstraintException - * @throws QueryException + * @return void + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException */ - public function add(AccountRequest $accountRequest): int + public function add(AccountRequest $accountRequest): void { - $query = /** @lang SQL */ - 'INSERT INTO AccountToTag (accountId, tagId) VALUES '.$this->buildParamsFromArray( - $accountRequest->tags, - '(?,?)' - ); - - $queryData = new QueryData(); - $queryData->setQuery($query); - $queryData->setOnErrorMessage(__u('Error while adding the account\'s tags')); - foreach ($accountRequest->tags as $tag) { - $queryData->addParam($accountRequest->id); - $queryData->addParam($tag); + $query = $this->queryFactory + ->newInsert() + ->into('AccountToTag') + ->cols([ + 'accountId' => $accountRequest->id, + 'tagId' => $tag, + ]); + + $queryData = QueryData::build($query)->setOnErrorMessage(__u('Error while adding the account\'s tags')); + + $this->db->doQuery($queryData); } - - return $this->db->doQuery($queryData)->getAffectedNumRows(); } - -} \ No newline at end of file +} diff --git a/lib/SP/Infrastructure/Common/Repositories/Repository.php b/lib/SP/Infrastructure/Common/Repositories/Repository.php index fb3f76aa..33960454 100644 --- a/lib/SP/Infrastructure/Common/Repositories/Repository.php +++ b/lib/SP/Infrastructure/Common/Repositories/Repository.php @@ -31,6 +31,7 @@ use SP\Core\Context\ContextInterface; use SP\Core\Events\Event; use SP\Core\Events\EventDispatcherInterface; use SP\Core\Events\EventMessage; +use SP\Domain\Common\Repositories\RepositoryInterface; use SP\Domain\Common\Services\ServiceException; use SP\Infrastructure\Database\DatabaseInterface; use function SP\__u; @@ -41,7 +42,7 @@ use function SP\logger; * * @package SP\Infrastructure\Common\Repositories */ -abstract class Repository +abstract class Repository implements RepositoryInterface { protected ContextInterface $context; protected DatabaseInterface $db; diff --git a/tests/SP/Infrastructure/Account/Repositories/AccountToTagRepositoryTest.php b/tests/SP/Infrastructure/Account/Repositories/AccountToTagRepositoryTest.php new file mode 100644 index 00000000..5a57bc2e --- /dev/null +++ b/tests/SP/Infrastructure/Account/Repositories/AccountToTagRepositoryTest.php @@ -0,0 +1,149 @@ +. + */ + +namespace SP\Tests\Infrastructure\Account\Repositories; + +use Aura\SqlQuery\QueryFactory; +use PHPUnit\Framework\Constraint\Callback; +use PHPUnit\Framework\MockObject\MockObject; +use SP\Domain\Account\Services\AccountRequest; +use SP\Domain\Common\Out\SimpleModel; +use SP\Infrastructure\Account\Repositories\AccountToTagRepository; +use SP\Infrastructure\Database\DatabaseInterface; +use SP\Infrastructure\Database\QueryData; +use SP\Infrastructure\Database\QueryResult; +use SP\Tests\UnitaryTestCase; + +/** + * Class AccountToTagRepositoryTest + */ +class AccountToTagRepositoryTest extends UnitaryTestCase +{ + private MockObject|DatabaseInterface $database; + private AccountToTagRepository $accountToTagRepository; + + public function testGetTagsByAccountId() + { + $id = self::$faker->randomNumber(); + + $callback = new Callback( + static function (QueryData $arg) use ($id) { + $query = $arg->getQuery(); + + return $query->getBindValues()['accountId'] === $id + && $arg->getMapClassName() === SimpleModel::class + && !empty($query->getStatement()); + } + ); + + $this->database + ->expects(self::once()) + ->method('doSelect') + ->with($callback) + ->willReturn(new QueryResult()); + + $this->accountToTagRepository->getTagsByAccountId($id); + } + + /** + * @throws \SP\Core\Exceptions\QueryException + * @throws \SP\Core\Exceptions\ConstraintException + */ + public function testDeleteByAccountId() + { + $accountId = self::$faker->randomNumber(); + + $expected = new QueryResult(); + $expected->setAffectedNumRows(1); + + $callback = new Callback( + static function (QueryData $arg) use ($accountId) { + $query = $arg->getQuery(); + $params = $query->getBindValues(); + + return $params['accountId'] === $accountId + && !empty($query->getStatement()); + } + ); + + $this->database + ->expects(self::once()) + ->method('doQuery') + ->with($callback) + ->willReturn($expected); + + $this->assertTrue($this->accountToTagRepository->deleteByAccountId($accountId)); + } + + /** + * @throws \SP\Core\Exceptions\ConstraintException + * @throws \SP\Core\Exceptions\QueryException + */ + public function testAdd() + { + $accountRequest = new AccountRequest(); + $accountRequest->id = self::$faker->randomNumber(); + $accountRequest->tags = self::getRandomNumbers(10); + + $callbacks = array_map( + function ($tag) use ($accountRequest) { + return [ + new Callback( + static function (QueryData $arg) use ($accountRequest, $tag) { + $query = $arg->getQuery(); + $params = $query->getBindValues(); + + return $params['accountId'] === $accountRequest->id + && $params['tagId'] === $tag + && !empty($query->getStatement()); + } + ), + ]; + }, + $accountRequest->tags + ); + + $this->database + ->expects(self::exactly(count($accountRequest->tags))) + ->method('doQuery') + ->withConsecutive(...$callbacks); + + $this->accountToTagRepository->add($accountRequest); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->database = $this->createMock(DatabaseInterface::class); + $queryFactory = new QueryFactory('mysql'); + + $this->accountToTagRepository = new AccountToTagRepository( + $this->database, + $this->context, + $this->application->getEventDispatcher(), + $queryFactory, + ); + } +} diff --git a/tests/SP/UnitaryTestCase.php b/tests/SP/UnitaryTestCase.php index aa4b0874..6187a29d 100644 --- a/tests/SP/UnitaryTestCase.php +++ b/tests/SP/UnitaryTestCase.php @@ -24,7 +24,6 @@ namespace SP\Tests; - use DG\BypassFinals; use Faker\Factory; use Faker\Generator; @@ -33,6 +32,7 @@ use SP\Core\Application; use SP\Core\Context\ContextInterface; use SP\Core\Context\StatelessContext; use SP\Core\Events\EventDispatcher; +use SP\Domain\Config\ConfigInterface; use SP\Domain\Config\Services\ConfigBackupService; use SP\Domain\Config\Services\ConfigFileService; use SP\Domain\User\Services\UserLoginResponse; @@ -44,13 +44,15 @@ use SP\Infrastructure\File\XmlHandler; */ abstract class UnitaryTestCase extends TestCase { - protected static Generator $faker; - protected ConfigFileService $config; - protected Application $application; - protected ContextInterface $context; + protected static Generator $faker; + protected ConfigInterface $config; + protected Application $application; + protected ContextInterface $context; public static function setUpBeforeClass(): void { + defined('APP_ROOT') || die(); + BypassFinals::enable(); BypassFinals::setWhitelist([APP_ROOT.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'*']); @@ -97,4 +99,9 @@ abstract class UnitaryTestCase extends TestCase return new Application($config, $this->createStub(EventDispatcher::class), $this->context); } + + public static function getRandomNumbers(int $count): array + { + return array_map(static fn() => self::$faker->randomNumber(), range(0, $count - 1)); + } } \ No newline at end of file