diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile index 33dd796..82be215 100644 --- a/.docker/php/Dockerfile +++ b/.docker/php/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.4-fpm-alpine +FROM php:8.5-fpm-alpine ARG UID ARG GID @@ -13,8 +13,7 @@ RUN apk update && apk add \ bash \ icu-dev \ && docker-php-ext-configure intl \ - && docker-php-ext-install intl opcache \ - && docker-php-ext-enable opcache + && docker-php-ext-install intl RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \ && sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index a07ce8b..f4e4f5b 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -15,11 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 with: - php-version: '8.4' + php-version: '8.5' coverage: none tools: composer:v2 - name: Install Composer dependencies (locked) @@ -32,11 +32,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 with: - php-version: '8.4' + php-version: '8.5' coverage: none tools: composer:v2 - name: Install Composer dependencies (locked) @@ -49,11 +49,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 with: - php-version: '8.4' + php-version: '8.5' coverage: none tools: composer:v2 - name: Install Composer dependencies (locked) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0e07db0..1e0baed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,13 +23,14 @@ jobs: - '8.2' - '8.3' - '8.4' - dependencies: [highest] + - '8.5' + dependencies: [highest, lowest] allowed-to-fail: [false] symfony-require: [''] variant: [normal] include: - - php-version: '8.1' - dependencies: highest + - php-version: '8.2' + dependencies: lowest allowed-to-fail: false symfony-require: 6.4.* variant: symfony/symfony:"6.4.*" @@ -41,8 +42,8 @@ jobs: - php-version: '8.2' dependencies: highest allowed-to-fail: false - symfony-require: 7.3.* - variant: symfony/symfony:"7.3.*" + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" - php-version: '8.3' dependencies: highest allowed-to-fail: false @@ -51,8 +52,8 @@ jobs: - php-version: '8.3' dependencies: highest allowed-to-fail: false - symfony-require: 7.3.* - variant: symfony/symfony:"7.3.*" + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" - php-version: '8.4' dependencies: highest allowed-to-fail: false @@ -61,12 +62,31 @@ jobs: - php-version: '8.4' dependencies: highest allowed-to-fail: false - symfony-require: 7.3.* - variant: symfony/symfony:"7.3.*" - + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.4' + dependencies: highest + allowed-to-fail: false + symfony-require: 8.* + variant: symfony/symfony:"8.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 8.* + variant: symfony/symfony:"8.*" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 with: diff --git a/.gitignore b/.gitignore index ca08796..f9e680f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .phpunit.cache .php-cs-fixer.cache coverage-report +GEMINI.md diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 273e26d..9222ff1 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -24,9 +24,8 @@ return (new PhpCsFixer\Config()) ->setRules([ - '@PHP71Migration' => true, - '@PHP82Migration' => true, - '@PHPUnit75Migration:risky' => true, + '@PHP8x2Migration' => true, + '@PHPUnit7x5Migration:risky' => true, '@Symfony' => true, '@Symfony:risky' => true, '@DoctrineAnnotation' => true, @@ -35,7 +34,7 @@ 'header_comment' => ['header' => $fileHeaderComment], 'modernize_strpos' => true, 'get_class_to_class_keyword' => true, - 'phpdoc_to_comment' => ['ignored_tags' => ['var']], // Fix issue on initializeStatement method $params variable + 'phpdoc_to_comment' => ['ignored_tags' => ['var']], // Fix an issue on initializeStatement method $params variable ]) ->setRiskyAllowed(true) ->setFinder( diff --git a/composer.json b/composer.json index 10e1643..b3462c7 100644 --- a/composer.json +++ b/composer.json @@ -49,21 +49,20 @@ } }, "require": { - "php": ">=8.1", - "cleverage/process-bundle": "^4.0", - "doctrine/common": "^3.0", - "doctrine/dbal": "^2.9 || ^3.0", - "doctrine/doctrine-bundle": "^2.5", - "doctrine/doctrine-migrations-bundle": "^3.2", - "doctrine/orm": "^2.9 || ^3.0" + "php": ">=8.2", + "cleverage/process-bundle": "^5.0", + "doctrine/common": "^3.5", + "doctrine/doctrine-bundle": "^2.18 || ^3.1", + "doctrine/doctrine-migrations-bundle": "^3.7 || ^4", + "doctrine/orm": "^2.20 || ^3.5" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", "phpstan/extension-installer": "*", "phpstan/phpstan": "*", - "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-doctrine": "*", "phpstan/phpstan-symfony": "*", - "phpunit/phpunit": "<10.0", + "phpunit/phpunit": "*", "rector/rector": "*", "roave/security-advisories": "dev-latest", "symfony/test-pack": "^1.1" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 766495c..c3e7947 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,27 +1,22 @@ + failOnWarning="true"> tests - - + src - - + + \ No newline at end of file diff --git a/rector.php b/rector.php index 9f9d327..8725ef8 100644 --- a/rector.php +++ b/rector.php @@ -8,18 +8,18 @@ use Rector\ValueObject\PhpVersion; return RectorConfig::configure() - ->withPhpVersion(PhpVersion::PHP_84) + ->withPhpVersion(PhpVersion::PHP_85) ->withPaths([ __DIR__.'/src', __DIR__.'/tests', ]) - ->withPhpSets(php81: true) - // here we can define, what prepared sets of rules will be applied + ->withPhpSets(php82: true) + // here we can define what prepared sets of rules will be applied ->withComposerBased(doctrine: true) ->withPreparedSets(deadCode: true, codeQuality: true, doctrineCodeQuality: true, symfonyCodeQuality: true) ->withAttributesSets(symfony: true, doctrine: true) ->withSets([ - LevelSetList::UP_TO_PHP_81, + LevelSetList::UP_TO_PHP_82, SymfonySetList::SYMFONY_64, SymfonySetList::SYMFONY_CODE_QUALITY, SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, diff --git a/src/Task/Database/DatabaseReaderTask.php b/src/Task/Database/DatabaseReaderTask.php index 639f2c0..9e1e430 100644 --- a/src/Task/Database/DatabaseReaderTask.php +++ b/src/Task/Database/DatabaseReaderTask.php @@ -17,7 +17,9 @@ use CleverAge\ProcessBundle\Model\FinalizableTaskInterface; use CleverAge\ProcessBundle\Model\IterableTaskInterface; use CleverAge\ProcessBundle\Model\ProcessState; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Result; use Doctrine\DBAL\Types\Type; use Doctrine\Persistence\ManagerRegistry; @@ -37,7 +39,7 @@ * 'offset': ?int, * 'input_as_params': bool, * 'params': array, - * 'types': array|array + * 'types': array|string, ArrayParameterType|ParameterType|Type|string> * } */ class DatabaseReaderTask extends AbstractConfigurableTask implements IterableTaskInterface, FinalizableTaskInterface diff --git a/src/Task/Database/DatabaseUpdaterTask.php b/src/Task/Database/DatabaseUpdaterTask.php index 1d6df62..65aebd4 100644 --- a/src/Task/Database/DatabaseUpdaterTask.php +++ b/src/Task/Database/DatabaseUpdaterTask.php @@ -15,8 +15,10 @@ use CleverAge\ProcessBundle\Model\AbstractConfigurableTask; use CleverAge\ProcessBundle\Model\ProcessState; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Exception; +use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Types\Type; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; @@ -31,7 +33,7 @@ * 'sql': string, * 'input_as_params': bool, * 'params': array, - * 'types': array|array + * 'types': array|string, ArrayParameterType|ParameterType|Type|string> * } */ class DatabaseUpdaterTask extends AbstractConfigurableTask diff --git a/src/Task/EntityManager/DoctrineBatchWriterTask.php b/src/Task/EntityManager/DoctrineBatchWriterTask.php index f26bdf5..0fcd49f 100644 --- a/src/Task/EntityManager/DoctrineBatchWriterTask.php +++ b/src/Task/EntityManager/DoctrineBatchWriterTask.php @@ -72,7 +72,7 @@ protected function writeBatch(ProcessState $state): void throw new \UnexpectedValueException("No manager found for class {$class}"); } $entityManager->persist($entity); - $entityManagers->attach($entityManager); + $entityManagers->offsetSet($entityManager); } foreach ($entityManagers as $entityManager) { diff --git a/tests/Task/Database/DatabaseReaderTaskTest.php b/tests/Task/Database/DatabaseReaderTaskTest.php new file mode 100644 index 0000000..82b7c98 --- /dev/null +++ b/tests/Task/Database/DatabaseReaderTaskTest.php @@ -0,0 +1,477 @@ +logger = $this->createStub(LoggerInterface::class); + $this->doctrine = $this->createStub(ManagerRegistry::class); + } + + public function testExecute(): void + { + $state = $this->createMock(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $resultData = ['id' => 1, 'name' => 'test']; + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData, false); + + $connection = $this->createStub(Connection::class); + $connection->method('executeQuery')->willReturn($result); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $state->expects($this->once())->method('setOutput')->with($resultData); + + $task->initialize($state); + $task->execute($state); + } + + public function testExecuteWithPagination(): void + { + $state = $this->createMock(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => 2, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $resultData1 = ['id' => 1, 'name' => 'test1']; + $resultData2 = ['id' => 2, 'name' => 'test2']; + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData1, $resultData2, false); + + $connection = $this->createStub(Connection::class); + $connection->method('executeQuery')->willReturn($result); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + $state->expects($this->once())->method('setOutput')->with([$resultData1, $resultData2]); + + $task->initialize($state); + $task->execute($state); + } + + public function testFinalize(): void + { + $task = new DatabaseReaderTask($this->logger, $this->doctrine); + + $state = $this->createStub(ProcessState::class); + + $result = $this->createMock(Result::class); + $result->expects($this->once())->method('free'); + + $reflection = new \ReflectionClass(DatabaseReaderTask::class); + $statementProperty = $reflection->getProperty('statement'); + $statementProperty->setValue($task, $result); + + $task->finalize($state); + } + + public function testExecuteWithEmptyResult(): void + { + $logger = $this->createMock(LoggerInterface::class); // Use createMock to assert on logger + $state = $this->createMock(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturn(false); + + $connection = $this->createStub(Connection::class); + $connection->method('executeQuery')->willReturn($result); + + $task = new class($logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + $logger->expects($this->once())->method('log')->with(LogLevel::WARNING, 'Empty resultset for query', ['options' => $options]); + $state->expects($this->once())->method('setSkipped')->with(true); + + $task->initialize($state); + $task->execute($state); + } + + public function testExecuteWithInputAsParams(): void + { + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturn(['id' => 1]); + + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table WHERE id = :id', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => true, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $resultData = ['id' => 1, 'name' => 'test']; + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData, false); + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('executeQuery')->with($options['sql'], ['id' => 1], $options['types'])->willReturn($result); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + $state->expects($this->once())->method('setOutput')->with($resultData); + + $task->initialize($state); + $task->execute($state); + } + + public function testNext(): void + { + $state = $this->createMock(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $resultData1 = ['id' => 1, 'name' => 'test1']; + $resultData2 = ['id' => 2, 'name' => 'test2']; + $result = $this->createStub(Result::class); + $result->method('fetchAssociative')->willReturnOnConsecutiveCalls($resultData1, $resultData2, false, false); + + $connection = $this->createStub(Connection::class); + $connection->method('executeQuery')->willReturn($result); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $task->initialize($state); + + $state->expects($this->once())->method('setOutput')->with($resultData1); + $task->execute($state); + + // The statement is now initialized, we can test next() + $this->assertTrue($task->next($state)); + $this->assertFalse($task->next($state)); + } + + public function testInitializeStatementWithSql(): void + { + $state = $this->createStub(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('executeQuery')->with($options['sql'], [], [])->willReturn($this->createStub(Result::class)); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $this->callInitializeStatement($task, $state); + } + + public function testInitializeStatementWithoutSql(): void + { + $state = $this->createStub(ProcessState::class); + $options = [ + 'table' => 'my_table', + 'sql' => null, + 'limit' => 10, + 'offset' => 5, + 'paginate' => null, + 'input_as_params' => false, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $qb = $this->createMock(QueryBuilder::class); + $qb->expects($this->once())->method('select')->with('tbl.*')->willReturnSelf(); + $qb->expects($this->once())->method('from')->with('my_table', 'tbl')->willReturnSelf(); + $qb->expects($this->once())->method('setMaxResults')->with(10)->willReturnSelf(); + $qb->expects($this->once())->method('setFirstResult')->with(5)->willReturnSelf(); + $qb->expects($this->once())->method('getSQL')->willReturn('SELECT tbl.* FROM my_table LIMIT 10 OFFSET 5'); + + $connection = $this->createMock(Connection::class); + $connection->method('createQueryBuilder')->willReturn($qb); + $connection->expects($this->once())->method('executeQuery')->with('SELECT tbl.* FROM my_table LIMIT 10 OFFSET 5', [], [])->willReturn($this->createStub(Result::class)); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $this->callInitializeStatement($task, $state); + } + + public function testInitializeStatementWithInvalidParams(): void + { + $this->expectException(\UnexpectedValueException::class); + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn('not an array'); + + $options = [ + 'table' => 'my_table', + 'sql' => 'SELECT * FROM my_table', + 'limit' => null, + 'offset' => null, + 'paginate' => null, + 'input_as_params' => true, + 'params' => [], + 'types' => [], + 'empty_log_level' => LogLevel::WARNING, + 'connection' => null, + ]; + + $connection = $this->createStub(Connection::class); + + $task = new class($this->logger, $this->doctrine, $options, $connection) extends DatabaseReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $this->callInitializeStatement($task, $state); + } + + private function callInitializeStatement(DatabaseReaderTask $task, ProcessState $state): Result + { + $reflection = new \ReflectionClass(DatabaseReaderTask::class); + $method = $reflection->getMethod('initializeStatement'); + + /** @var Result $result */ + $result = $method->invoke($task, $state); + + return $result; + } +} diff --git a/tests/Task/Database/DatabaseUpdaterTaskTest.php b/tests/Task/Database/DatabaseUpdaterTaskTest.php new file mode 100644 index 0000000..748d3d6 --- /dev/null +++ b/tests/Task/Database/DatabaseUpdaterTaskTest.php @@ -0,0 +1,168 @@ +createStub(ManagerRegistry::class); + $logger = $this->createStub(LoggerInterface::class); + $state = $this->createMock(ProcessState::class); + $options = [ + 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id', + 'input_as_params' => false, + 'params' => ['id' => 1, 'name' => 'test'], + 'types' => [], + 'connection' => null, + ]; + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('executeStatement') + ->with($options['sql'], $options['params'], $options['types']) + ->willReturn(1); + + $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask { + /** + * @param array $testOptions + */ + public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($doctrine, $logger); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $state->expects($this->once())->method('setOutput')->with(1); + + $task->initialize($state); + $task->execute($state); + } + + public function testExecuteWithInputAsParams(): void + { + $doctrine = $this->createStub(ManagerRegistry::class); + $logger = $this->createStub(LoggerInterface::class); + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturn(['id' => 1, 'name' => 'test']); + + $options = [ + 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id', + 'input_as_params' => true, + 'params' => [], + 'types' => [], + 'connection' => null, + ]; + + $connection = $this->createMock(Connection::class); + $connection->expects($this->once())->method('executeStatement') + ->with($options['sql'], ['id' => 1, 'name' => 'test'], $options['types']) + ->willReturn(1); + + $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask { + /** + * @param array $testOptions + */ + public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($doctrine, $logger); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $state->expects($this->once())->method('setOutput')->with(1); + + $task->initialize($state); + $task->execute($state); + } + + public function testExecuteWithInvalidParams(): void + { + $this->expectException(\UnexpectedValueException::class); + + $doctrine = $this->createStub(ManagerRegistry::class); + $logger = $this->createStub(LoggerInterface::class); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn('not an array'); + + $options = [ + 'sql' => 'UPDATE my_table SET name = :name WHERE id = :id', + 'input_as_params' => true, + 'params' => [], + 'types' => [], + 'connection' => null, + ]; + + $connection = $this->createStub(Connection::class); + + $task = new class($doctrine, $logger, $options, $connection) extends DatabaseUpdaterTask { + /** + * @param array $testOptions + */ + public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger, private readonly array $testOptions, private readonly Connection $testConnection) + { + parent::__construct($doctrine, $logger); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + + protected function getConnection(?ProcessState $state = null): Connection + { + return $this->testConnection; + } + }; + + $task->initialize($state); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php new file mode 100644 index 0000000..d3d3e3a --- /dev/null +++ b/tests/Task/EntityManager/AbstractDoctrineQueryTaskTest.php @@ -0,0 +1,39 @@ +expectException(\UnexpectedValueException::class); + + $task = $this->createStub(AbstractDoctrineQueryTask::class); + $repository = $this->createStub(EntityRepository::class); + $repository->method('createQueryBuilder')->willReturn(new QueryBuilder($this->createStub(EntityManagerInterface::class))); + + $reflection = new \ReflectionClass(AbstractDoctrineQueryTask::class); + $method = $reflection->getMethod('getQueryBuilder'); + + $method->invoke($task, $repository, ['e.field; DROP TABLE dummy;' => 'value'], []); + } +} diff --git a/tests/Task/EntityManager/ClearEntityManagerTaskTest.php b/tests/Task/EntityManager/ClearEntityManagerTaskTest.php new file mode 100644 index 0000000..b343ca2 --- /dev/null +++ b/tests/Task/EntityManager/ClearEntityManagerTaskTest.php @@ -0,0 +1,52 @@ +createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('clear'); + + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry->expects($this->once()) + ->method('getManager') + ->willReturn($entityManager); + + $task = $this->getMockBuilder(ClearEntityManagerTask::class) + ->setConstructorArgs([$managerRegistry]) + ->onlyMethods(['getOption']) // Mock only the getOption method + ->getMock(); + + // Ensure getOption returns null for 'entity_manager', mimicking default behavior + $task->expects($this->once()) + ->method('getOption') + ->with(self::anything(), 'entity_manager') + ->willReturn(null); + + $processState = $this->createStub(ProcessState::class); + + $task->execute($processState); + } +} diff --git a/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php b/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php new file mode 100644 index 0000000..7a13d4a --- /dev/null +++ b/tests/Task/EntityManager/DoctrineBatchWriterTaskTest.php @@ -0,0 +1,210 @@ + $options + */ + protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineBatchWriterTask + { + if (!$managerRegistry instanceof ManagerRegistry) { + $managerRegistry = $this->createStub(ManagerRegistry::class); + } + $task = new DoctrineBatchWriterTask($managerRegistry); + + $state = $this->createStub(ProcessState::class); + $state->method('getContextualizedOptions')->willReturn($options); + $task->initialize($state); + + return $task; + } + + public function testExecuteAddsEntityToBatchAndSkipsWhenBatchCountNotReached(): void + { + $entity1 = new \stdClass(); + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturn($entity1); + + $task = $this->getTask(['batch_count' => 2]); + + $state->expects($this->once())->method('setSkipped')->with(true); + $task->execute($state); + + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $this->assertCount(1, (array) $batchProperty->getValue($task)); + $this->assertContains($entity1, (array) $batchProperty->getValue($task)); + } + + public function testExecuteFlushesBatchWhenBatchCountReached(): void + { + $entity1 = new \stdClass(); + $entity2 = new \stdClass(); + + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturnOnConsecutiveCalls($entity1, $entity2); + $state->expects($this->once())->method('setOutput')->with([$entity1, $entity2]); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->exactly(2))->method('persist'); // Called for entity1 and entity2 + $entityManager->expects($this->once())->method('flush'); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = $this->getTask(['batch_count' => 2], $managerRegistry); + + // Simulate filling the batch + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + // Add first entity + $task->execute($state); // batch has 1 entity, not flushed + $this->assertCount(1, (array) $batchProperty->getValue($task)); + $this->assertContains($entity1, (array) $batchProperty->getValue($task)); + + // Add second entity, should trigger flush + $task->execute($state); // batch has 2 entities, should be flushed + $this->assertCount(0, (array) $batchProperty->getValue($task)); // Batch should be empty after flush + } + + public function testFlushCallsWriteBatch(): void + { + $state = $this->createStub(ProcessState::class); + $task = $this->getMockBuilder(DoctrineBatchWriterTask::class) + ->disableOriginalConstructor() + ->onlyMethods(['writeBatch']) + ->getMock(); + + $task->expects($this->once())->method('writeBatch')->with($state); + $task->flush($state); + } + + public function testWriteBatchWithEmptyBatchSkipsState(): void + { + $state = $this->createMock(ProcessState::class); + $state->expects($this->once())->method('setSkipped')->with(true); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $task = new DoctrineBatchWriterTask($managerRegistry); + + // Ensure batch is empty + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $batchProperty->setValue($task, []); + + // Call writeBatch directly + $writeBatchMethod = $reflection->getMethod('writeBatch'); + $writeBatchMethod->invoke($task, $state); + } + + public function testWriteBatchPersistsAndFlushesEntities(): void + { + $entity1 = new \stdClass(); + $entity2 = new \stdClass(); + $state = $this->createMock(ProcessState::class); // Use mock to set expectation on setSkipped + $state->expects($this->never())->method('setSkipped'); // Should not be skipped if batch is not empty + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->exactly(2))->method('persist'); // Called for entity1 and entity2 + $entityManager->expects($this->once())->method('flush'); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = new DoctrineBatchWriterTask($managerRegistry); + + // Manually set batch + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $batchProperty->setValue($task, [$entity1, $entity2]); + + // Call writeBatch directly + $writeBatchMethod = $reflection->getMethod('writeBatch'); + $writeBatchMethod->invoke($task, $state); + } + + public function testWriteBatchClearsBatchAfterFlushing(): void + { + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + + $entityManager = $this->createStub(EntityManagerInterface::class); + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = new DoctrineBatchWriterTask($managerRegistry); + + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $batchProperty->setValue($task, [$entity]); // Add an entity to the batch + + // Call writeBatch directly + $writeBatchMethod = $reflection->getMethod('writeBatch'); + $writeBatchMethod->invoke($task, $state); + + $this->assertCount(0, (array) $batchProperty->getValue($task)); // Batch should be empty + } + + public function testWriteBatchSetsOutput(): void + { + $entity = new \stdClass(); + $state = $this->createMock(ProcessState::class); // Use mock to set expectation on setOutput + $state->expects($this->once())->method('setOutput')->with([$entity]); + + $entityManager = $this->createStub(EntityManagerInterface::class); + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = new DoctrineBatchWriterTask($managerRegistry); + + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $batchProperty->setValue($task, [$entity]); // Add an entity to the batch + + // Call writeBatch directly + $writeBatchMethod = $reflection->getMethod('writeBatch'); + $writeBatchMethod->invoke($task, $state); + } + + public function testWriteBatchThrowsExceptionWhenNoManagerFound(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); // Simulate no manager found + + $task = new DoctrineBatchWriterTask($managerRegistry); + + $reflection = new \ReflectionClass(DoctrineBatchWriterTask::class); + $batchProperty = $reflection->getProperty('batch'); + $batchProperty->setValue($task, [$entity]); // Add an entity to the batch + + // Call writeBatch directly + $writeBatchMethod = $reflection->getMethod('writeBatch'); + $writeBatchMethod->invoke($task, $state); + } +} diff --git a/tests/Task/EntityManager/DoctrineCleanerTaskTest.php b/tests/Task/EntityManager/DoctrineCleanerTaskTest.php new file mode 100644 index 0000000..09d3cec --- /dev/null +++ b/tests/Task/EntityManager/DoctrineCleanerTaskTest.php @@ -0,0 +1,69 @@ +createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once())->method('clear'); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = new DoctrineCleanerTask($managerRegistry); + $task->execute($state); + } + + public function testExecuteWithNullInput(): void + { + $this->expectException(\RuntimeException::class); + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn(null); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + + $task = new DoctrineCleanerTask($managerRegistry); + $task->execute($state); + } + + public function testExecuteWithNoManager(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); + + $task = new DoctrineCleanerTask($managerRegistry); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/DoctrineDetacherTaskTest.php b/tests/Task/EntityManager/DoctrineDetacherTaskTest.php new file mode 100644 index 0000000..0770d69 --- /dev/null +++ b/tests/Task/EntityManager/DoctrineDetacherTaskTest.php @@ -0,0 +1,84 @@ + $options + */ + protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineDetacherTask + { + if (!$managerRegistry instanceof ManagerRegistry) { + $managerRegistry = $this->createStub(ManagerRegistry::class); + } + $task = new DoctrineDetacherTask($managerRegistry); + + $state = $this->createStub(ProcessState::class); + $state->method('getContextualizedOptions')->willReturn($options); + $task->initialize($state); + + return $task; + } + + public function testExecuteDetachesEntity(): void + { + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once())->method('detach')->with($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } + + public function testExecuteThrowsExceptionOnNullInput(): void + { + $this->expectException(\RuntimeException::class); + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn(null); + + $task = $this->getTask(); + $task->execute($state); + } + + public function testExecuteThrowsExceptionWhenNoManagerFound(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/DoctrineReaderTaskTest.php b/tests/Task/EntityManager/DoctrineReaderTaskTest.php new file mode 100644 index 0000000..267a323 --- /dev/null +++ b/tests/Task/EntityManager/DoctrineReaderTaskTest.php @@ -0,0 +1,260 @@ +createStub(LoggerInterface::class); + $doctrine = $this->createStub(ManagerRegistry::class); + $state = $this->createMock(ProcessState::class); + $options = [ + 'class_name' => 'App\Entity\MyEntity', + 'criteria' => ['id' => 1], + 'order_by' => ['createdAt' => 'DESC'], + 'limit' => 10, + 'offset' => 0, + 'empty_log_level' => LogLevel::WARNING, + ]; + + $entity = new \stdClass(); + $query = $this->createStub(Query::class); + $query->method('toIterable')->willReturn(new \ArrayIterator([$entity])); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('getQuery')->willReturn($query); + $qb->method('select')->willReturn($qb); + $qb->method('from')->willReturn($qb); + $qb->method('andWhere')->willReturn($qb); + $qb->method('orderBy')->willReturn($qb); + $qb->method('setFirstResult')->willReturn($qb); + $qb->method('setMaxResults')->willReturn($qb); + $qb->method('setParameter')->willReturn($qb); + + $repository = $this->createStub(EntityRepository::class); + $repository->method('createQueryBuilder')->willReturn($qb); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repository); + + $doctrine->method('getManagerForClass')->willReturn($em); + + $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + }; + + $state->expects($this->once())->method('setOutput')->with($entity); + $state->expects($this->never())->method('setSkipped'); + + $task->initialize($state); + $task->execute($state); + } + + public function testExecuteEmpty(): void + { + $logger = $this->createMock(LoggerInterface::class); + $doctrine = $this->createStub(ManagerRegistry::class); + $state = $this->createMock(ProcessState::class); + $options = [ + 'class_name' => 'App\Entity\MyEntity', + 'criteria' => ['id' => 1], + 'order_by' => [], + 'limit' => null, + 'offset' => null, + 'empty_log_level' => LogLevel::WARNING, + ]; + + $query = $this->createStub(Query::class); + $query->method('toIterable')->willReturn(new \ArrayIterator([])); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('getQuery')->willReturn($query); + $qb->method('select')->willReturn($qb); + $qb->method('from')->willReturn($qb); + $qb->method('andWhere')->willReturn($qb); + $qb->method('setParameter')->willReturn($qb); + + $repository = $this->createStub(EntityRepository::class); + $repository->method('createQueryBuilder')->willReturn($qb); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repository); + + $doctrine->method('getManagerForClass')->willReturn($em); + + $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + }; + + $logger->expects(self::once())->method('log')->with(LogLevel::WARNING, 'Empty resultset for query'); + $state->expects($this->once())->method('setSkipped')->with(true); + + $task->initialize($state); + $task->execute($state); + } + + public function testNext(): void + { + $logger = $this->createStub(LoggerInterface::class); + $doctrine = $this->createStub(ManagerRegistry::class); + $state = $this->createMock(ProcessState::class); + $options = [ + 'class_name' => 'App\Entity\MyEntity', + 'criteria' => [], + 'order_by' => [], + 'limit' => null, + 'offset' => null, + 'empty_log_level' => LogLevel::WARNING, + ]; + + $entity1 = new \stdClass(); + $entity2 = new \stdClass(); + $query = $this->createStub(Query::class); + $query->method('toIterable')->willReturn(new \ArrayIterator([$entity1, $entity2])); + + $qb = $this->createStub(QueryBuilder::class); + $qb->method('getQuery')->willReturn($query); + $qb->method('select')->willReturn($qb); + $qb->method('from')->willReturn($qb); + + $repository = $this->createStub(EntityRepository::class); + $repository->method('createQueryBuilder')->willReturn($qb); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($repository); + + $doctrine->method('getManagerForClass')->willReturn($em); + + $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + }; + + $task->initialize($state); + + $call = 0; + $state->expects($this->exactly(2))->method('setOutput') + ->with($this->callback(function ($output) use (&$call, $entity1, $entity2) { + if (0 === $call) { + $this->assertSame($entity1, $output); + } + if (1 === $call) { + $this->assertSame($entity2, $output); + } + ++$call; + + return true; + })); + + $task->execute($state); + $this->assertTrue($task->next($state)); + $task->execute($state); + $this->assertFalse($task->next($state)); + } + + public function testExecuteThrowsExceptionWhenNoManagerFound(): void + { + $this->expectException(\UnexpectedValueException::class); + + $logger = $this->createStub(LoggerInterface::class); + $doctrine = $this->createStub(ManagerRegistry::class); + $state = $this->createStub(ProcessState::class); + $options = [ + 'class_name' => 'App\Entity\MyEntity', + 'criteria' => [], + 'order_by' => [], + 'limit' => null, + 'offset' => null, + 'empty_log_level' => LogLevel::WARNING, + ]; + + $doctrine->method('getManagerForClass')->willReturn(null); + + $task = new class($logger, $doctrine, $options) extends DoctrineReaderTask { + /** + * @param array $testOptions + */ + public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine, private readonly array $testOptions) + { + parent::__construct($logger, $doctrine); + } + + /** + * @return array + */ + protected function getOptions(?ProcessState $state = null): array + { + return $this->testOptions; + } + }; + + $task->initialize($state); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/DoctrineRefresherTaskTest.php b/tests/Task/EntityManager/DoctrineRefresherTaskTest.php new file mode 100644 index 0000000..3d68093 --- /dev/null +++ b/tests/Task/EntityManager/DoctrineRefresherTaskTest.php @@ -0,0 +1,85 @@ + $options + */ + protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineRefresherTask + { + if (!$managerRegistry instanceof ManagerRegistry) { + $managerRegistry = $this->createStub(ManagerRegistry::class); + } + $task = new DoctrineRefresherTask($managerRegistry); + + $state = $this->createStub(ProcessState::class); + $state->method('getContextualizedOptions')->willReturn($options); + $task->initialize($state); + + return $task; + } + + public function testExecuteRefreshesEntity(): void + { + $entity = new \stdClass(); + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturn($entity); + $state->expects($this->once())->method('setOutput')->with($entity); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once())->method('refresh')->with($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } + + public function testExecuteThrowsExceptionOnNullInput(): void + { + $this->expectException(\RuntimeException::class); + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn(null); + + $task = $this->getTask(); + $task->execute($state); + } + + public function testExecuteThrowsExceptionWhenNoManagerFound(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/DoctrineRemoverTaskTest.php b/tests/Task/EntityManager/DoctrineRemoverTaskTest.php new file mode 100644 index 0000000..7d689b8 --- /dev/null +++ b/tests/Task/EntityManager/DoctrineRemoverTaskTest.php @@ -0,0 +1,70 @@ +createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once())->method('remove')->with($entity); + $entityManager->expects($this->once())->method('flush'); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = new DoctrineRemoverTask($managerRegistry); + $task->execute($state); + } + + public function testExecuteWithNullInput(): void + { + $this->expectException(\TypeError::class); // ClassUtils::getClass expects an object, null will cause a TypeError + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn(null); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + + $task = new DoctrineRemoverTask($managerRegistry); + $task->execute($state); + } + + public function testExecuteWithNoManager(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); + + $task = new DoctrineRemoverTask($managerRegistry); + $task->execute($state); + } +} diff --git a/tests/Task/EntityManager/DoctrineWriterTaskTest.php b/tests/Task/EntityManager/DoctrineWriterTaskTest.php new file mode 100644 index 0000000..aa8ea13 --- /dev/null +++ b/tests/Task/EntityManager/DoctrineWriterTaskTest.php @@ -0,0 +1,86 @@ + $options + */ + protected function getTask(array $options = [], ?ManagerRegistry $managerRegistry = null): DoctrineWriterTask + { + if (!$managerRegistry instanceof ManagerRegistry) { + $managerRegistry = $this->createStub(ManagerRegistry::class); + } + $task = new DoctrineWriterTask($managerRegistry); + + $state = $this->createStub(ProcessState::class); + $state->method('getContextualizedOptions')->willReturn($options); + $task->initialize($state); + + return $task; + } + + public function testExecuteWritesAndFlushesEntity(): void + { + $entity = new \stdClass(); + $state = $this->createMock(ProcessState::class); + $state->method('getInput')->willReturn($entity); + $state->expects($this->once())->method('setOutput')->with($entity); + + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once())->method('persist')->with($entity); + $entityManager->expects($this->once())->method('flush'); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn($entityManager); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } + + public function testExecuteThrowsExceptionOnNullInput(): void + { + $this->expectException(\RuntimeException::class); + + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn(null); + + $task = $this->getTask(); + $task->execute($state); + } + + public function testExecuteThrowsExceptionWhenNoManagerFound(): void + { + $this->expectException(\UnexpectedValueException::class); + + $entity = new \stdClass(); + $state = $this->createStub(ProcessState::class); + $state->method('getInput')->willReturn($entity); + + $managerRegistry = $this->createStub(ManagerRegistry::class); + $managerRegistry->method('getManagerForClass')->willReturn(null); + + $task = $this->getTask([], $managerRegistry); + $task->execute($state); + } +}