From e04fd7f4eee414643512cdb5610627a76634c919 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 20 May 2025 13:46:14 +0300 Subject: [PATCH 1/2] feat(ORC-8133): remove ORMContext --- .github/workflows/ci.yaml | 5 +- README.md | 117 +---------- composer.json | 18 -- docs/examples.md | 68 ++++++ docs/install.md | 44 ++++ docs/ormcontext-migration.md | 31 --- docs/runnable-parameters.md | 42 ++++ docs/steps.md | 197 ++++++++++++++++++ src/Context/ORMContext.php | 106 ---------- .../BehatApiContextExtension.php | 16 -- src/DependencyInjection/Configuration.php | 10 - src/Resources/config/api_context.xml | 1 + src/Resources/config/orm_context.xml | 10 - .../BehatApiContextExtensionTest.php | 32 +-- .../DependencyInjection/ConfigurationTest.php | 11 +- .../Fixtures/empty_bundle_config.yaml | 1 - .../Fixtures/filled_bundle_config.yaml | 1 - .../Fixtures/with_orm_context.yaml | 4 - ...th_orm_context_without_reset_managers.yaml | 3 - .../Fixtures/without_orm_context.yaml | 4 - tests/Unit/Context/DB/ORMContextTest.php | 177 ---------------- 21 files changed, 365 insertions(+), 533 deletions(-) create mode 100644 docs/examples.md create mode 100644 docs/install.md delete mode 100644 docs/ormcontext-migration.md create mode 100644 docs/runnable-parameters.md create mode 100644 docs/steps.md delete mode 100644 src/Context/ORMContext.php delete mode 100644 src/Resources/config/orm_context.xml delete mode 100644 tests/DependencyInjection/Fixtures/with_orm_context.yaml delete mode 100644 tests/DependencyInjection/Fixtures/with_orm_context_without_reset_managers.yaml delete mode 100644 tests/DependencyInjection/Fixtures/without_orm_context.yaml delete mode 100644 tests/Unit/Context/DB/ORMContextTest.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a0765ca..b5ef5dd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -62,7 +62,7 @@ jobs: - name: Set composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - name: Cache composer uses: actions/cache@v4 @@ -83,9 +83,6 @@ jobs: - name: Install dependencies run: composer install - - name: Add doctrine/orm - run: composer require --no-progress --no-interaction --prefer-dist doctrine/orm:^2.0 - - name: Run PHPUnit tests run: composer phpunit if: matrix.coverage == 'none' diff --git a/README.md b/README.md index da5d20e..64a4071 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,22 @@ # Behat Api Context Bundle -| Version | Build Status | Code Coverage | Latest Release | -|:---------:|:---------------------------------------------------------:|:------------------------------------------------------------------------:|:-----------------:| -| `master` | [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] | ![Latest Release] | +| Version | Build Status | Code Coverage | Latest Release | +| :-------: | :-------------------------------------------------------: | :----------------------------------------------------------------------: | :---------------: | +| `master` | [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] | ![Latest Release] | | `develop` | [![CI][develop Build Status Image]][develop Build Status] | [![Coverage Status][develop Code Coverage Image]][develop Code Coverage] | - | -## ⚠️ Deprecation Notice - -> The `ORMContext` has been **deprecated** and **removed** from this package. -> Please use the standalone package [`macpaw/behat-orm-context`](https://github.com/macpaw/behat-orm-context) instead. - ---- - -## Installation - -### Step 1: Install the Bundle - -Run the following command in your project directory to install the bundle as a development dependency: - -```bash - composer require --dev macpaw/behat-api-context -``` - -> If you are using Symfony Flex, the bundle will be registered automatically. -> Otherwise, follow Step 2 to register the bundle manually. -### Step 2: Register the Bundle - -If your project does **not** use Symfony Flex or the bundle does not provide a recipe, manually register it in `config/bundles.php`: - -```php - ['test' => true], -]; -``` - -> ℹ️ The bundle should only be enabled in the `test` environment. - -### Step 3: Configure Behat - -Update your `behat.yml`: - -```yaml -default: - suites: - default: - contexts: - - BehatApiContext\Context\ApiContext - - BehatApiContext\Context\ORMContext -``` - -> If you also want to use `ORMContext`, install [macpaw/behat-orm-context](https://github.com/macpaw/behat-orm-context) and follow its setup instructions. - -> 📄 **Migration Notice:** `OrmContext` will be removed from `behat-api-context` in the next major release. -> Please migrate to [`behat-orm-context`](https://github.com/macpaw/behat-orm-context) to avoid test failures. -> See the full [ORMContext Migration Plan](./docs/ormcontext-migration.md) for step-by-step instructions. - - --- -## Configuration - -By default, the bundle provides the following configuration: -> This bundle does not yet include a Symfony recipe to automatically create the configuration file. -> If you need a specific configuration, you have to add it manually. -> [Recipe in progress](https://github.com/MacPaw/BehatRedisContext/issues/2) - -```yaml -behat_api_context: - kernel_reset_managers: [] -``` - -You can also add your own reset manager by overriding the configuration manually in `config/packages/test/behat_api_context.yaml`: - -```yaml -behat_api_context: - kernel_reset_managers: - - BehatApiContext\Service\ResetManager\DoctrineResetManager -``` +Behat API Context provides a set of Behat steps for testing RESTful APIs with support for dynamic request parameters, context persistence, and Symfony integration. --- -## Usage - -### Runnable request parameters - -Main use case when tests need to use the current date. -Instead of static data in some `.feature` file like this: - -```gherkin -""" -{ - "dateTo": 1680360081, - "dateFrom": 1680532881 -} -""" -``` - -You can use dynamic expressions: - -```gherkin -""" -{ - "dateTo": "<(new DateTimeImmutable())->add(new DateInterval('P6D'))->getTimestamp()>", - "dateFrom": "<(new DateTimeImmutable())->add(new DateInterval('P2D'))->getTimestamp()>" -} -""" -``` +## 📄 Documentation -#### To achieve this, several conditions must be met: -- Runnable code must be a string and placed inside `<>`. -- Do not add `return` keyword at the beginning, otherwise a `RuntimeException` will be thrown. -- Do not add a semicolon (`;`) at the end of the expression, otherwise a `RuntimeException` will be thrown. -- Avoid code that returns `null`, otherwise a `RuntimeException` will be thrown. +- [Installation & Configuration](docs/install.md) +- [Available Steps](docs/steps.md) +- [Usage Examples](docs/examples.md) +- [Runnable Parameters](docs/runnable-parameters.md) --- diff --git a/composer.json b/composer.json index 5f9cfb3..4f892e8 100644 --- a/composer.json +++ b/composer.json @@ -42,9 +42,6 @@ "slevomat/coding-standard": "^7.0", "squizlabs/php_codesniffer": "^3.6" }, - "suggest": { - "doctrine/orm": "^2.0" - }, "autoload": { "psr-4": { "BehatApiContext\\": "src" @@ -55,12 +52,6 @@ "BehatApiContext\\Tests\\": "tests" } }, - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - }, - "deprecated": "The ORMContext has been removed from this package. Please use macpaw/behat-orm-context instead." - }, "scripts": { "composer-validate": "composer validate", "phpstan": "./vendor/bin/phpstan analyse -l max", @@ -73,15 +64,6 @@ "@phpstan", "@code-style", "@phpunit" - ], - "post-install-cmd": [ - "@show-deprecation" - ], - "post-update-cmd": [ - "@show-deprecation" - ], - "show-deprecation": [ - "@php -r \"echo '[DEPRECATED] ORMContext has been removed from this package. Use macpaw/behat-orm-context instead.';\"" ] }, "config": { diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..eec8c87 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,68 @@ +# Usage Examples + +Real-world `.feature` examples using the API Context. + +--- + +## 🔐 Example: Successful Login + +```gherkin +Feature: Login API + + Scenario: Successful login returns token + Given the request contains params: + """ + { + "email": "test@example.com", + "password": "securepassword" + } + """ + When I send "POST" request to "api_v1_sign_in" route + Then response status code should be 200 + And response should be JSON with variable fields "token": + """ + { + "token": "abc123" + } + """ +``` + +--- + +## ❌ Example: Invalid Credentials + +```gherkin +Scenario: Login with wrong password + Given the request contains params: + """ + { + "email": "test@example.com", + "password": "wrongpassword" + } + """ + When I send "POST" request to "api_v1_sign_in" route + Then response status code should be 401 + And response should be JSON: + """ + { + "error": "Invalid credentials" + } + """ +``` + +--- + +## 🕓 Example: Relative Dates in Request + +```gherkin +Scenario: Create report with dynamic date range + Given the request contains params: + """ + { + "dateFrom": "<(new DateTimeImmutable('-7 days'))->format('Y-m-d')>", + "dateTo": "<(new DateTimeImmutable())->format('Y-m-d')>" + } + """ + When I send a "POST" request to "api_v1_generate_reports" + Then response status code should be 201 +``` diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..93f75dc --- /dev/null +++ b/docs/install.md @@ -0,0 +1,44 @@ +# Installation & Configuration + +## 1. Install the package + +```bash +composer require --dev macpaw/behat-api-context +``` + +> If you're using Symfony Flex, the bundle will be registered automatically. + +## 2. Register the bundle manually (if not using Flex) + +```php +// config/bundles.php +return [ + BehatApiContext\BehatApiContextBundle::class => ['test' => true], +]; +``` + +## 3. Configure Behat +By default, the bundle provides the following configuration: +> This bundle does not yet include a Symfony recipe to automatically create the configuration file. +> If you need a specific configuration, you have to add it manually. +> [Recipe in progress](https://github.com/MacPaw/BehatRedisContext/issues/2) + +```yaml +# behat.yml +default: + suites: + default: + contexts: + - BehatApiContext\Context\ApiContext +``` + +## 4. Optional configuration +You can also add your own reset manager by overriding the configuration manually in `config/packages/behat_api_context.yaml`: + +```yaml +# config/packages/behat_api_context.yaml +when@test: + behat_api_context: + kernel_reset_managers: + - BehatApiContext\Service\ResetManager\DoctrineResetManager +``` diff --git a/docs/ormcontext-migration.md b/docs/ormcontext-migration.md deleted file mode 100644 index 126fb65..0000000 --- a/docs/ormcontext-migration.md +++ /dev/null @@ -1,31 +0,0 @@ -## Migration plan for using ORMContext - -### Step 1: 📦 Add `behat-orm-context` to `composer.json` -```bash -composer require --dev macpaw/behat-orm-context -``` - -### Step 2: 🧩 Register `OrmContext` in `behat.yml` -```yaml -default: - suites: - default: - contexts: - - BehatOrmContext\Context\OrmContext - -``` -> If you previously used `BehatApiContext\Context\OrmContext`, replace it with `BehatOrmContext\Context\OrmContext` - -### Step 3: 🧪 Verify Scenarios -1. Run Behat tests: -```bash - vendor/bin/behat -``` - -2. Ensure that all steps previously relying on OrmContext still work correctly after the migration. - -### 🚨 Backward Compatibility Notice -> `OrmContext` will be removed from `behat-api-context` in a future major release. If you don't migrate, your tests will break after updating dependencies. - -### 🔔 Recommendation -Perform the migration before the next major release of `behat-api-context` to avoid CI/CD disruptions and unexpected test failures in your production pipeline. \ No newline at end of file diff --git a/docs/runnable-parameters.md b/docs/runnable-parameters.md new file mode 100644 index 0000000..c6bb57d --- /dev/null +++ b/docs/runnable-parameters.md @@ -0,0 +1,42 @@ +# Runnable Parameters + +Behat API Context supports inline PHP expressions in request payloads using angle brackets `<>`. + +## ✨ Example + +```gherkin +Given the request contains params: +""" +{ + "timestamp": "<(new DateTimeImmutable())->getTimestamp()>", + "uuid": "" +} +""" +``` + +## ✅ Rules + +- Expressions must be wrapped in `<>` +- Do **not** use `return` statements +- Do **not** end expressions with a semicolon +- Expressions **must not** return `null` + +## 💡 Common Use Cases + +| Use Case | Example | +|---------------|--------------------------------------------------------| +| Timestamps | `<(new DateTimeImmutable())->getTimestamp()>` | +| UUIDs | `` | +| Relative Time | `<(new DateTimeImmutable('+1 day'))->format('Y-m-d')>` | +| Random value | `` | + +## 🔥 Pro Tip + +You can mix static and dynamic parameters: + +```json +{ + "start_date": "2024-01-01", + "end_date": "<(new DateTimeImmutable('+7 days'))->format('Y-m-d')>" +} +``` diff --git a/docs/steps.md b/docs/steps.md new file mode 100644 index 0000000..7272023 --- /dev/null +++ b/docs/steps.md @@ -0,0 +1,197 @@ +# API Context Behat Steps Documentation + +## Table of Contents + +* [Introduction](#introduction) +* [🧪Step: `Given the ":headerName" request header contains ":value"`](#step-given-the-headername-request-header-contains-value) +* [🧪Step: `Given the ":headerName" request header contains multiline value`](#step-given-the-headername-request-header-contains-multiline-value) +* [🧪Step: `Given the request ip is ":ip"`](#step-given-the-request-ip-is-ip) +* [🧪Step: `Given the request contains params`](#step-given-the-request-contains-params) +* [🧪Step: `When I send ":method" request to ":route" route`](#step-when-i-send-method-request-to-route-route) +* [🧪Step: `Then response status code should be :httpStatus`](#step-then-response-status-code-should-be-httpstatus) +* [🧪Step: `Then response is JSON`](#step-then-response-is-json) +* [🧪Step: `Then response should be empty`](#step-then-response-should-be-empty) +* [🧪Step: `Then response should be JSON`](#step-then-response-should-be-json) +* [🧪Step: `When I save ":paramPath" param from json response as ":valueKey"`](#step-when-i-save-parampath-param-from-json-response-as-valuekey) +* [🧪Step: `Then response should be JSON with variable fields ":variableFields"`](#step-then-response-should-be-json-with-variable-fields-variablefields) +* [🧪Step: `Then the ":headerName" response headers contains ":headerValue"`](#step-then-the-headername-response-headers-contains-headervalue) +* [📝Notes](#-notes) + +--- + +## Introduction + +This document describes the Behat step definitions used in the `ApiContext` class for testing HTTP API endpoints. Each step allows configuring the HTTP request, sending it, and asserting various properties of the response. + +--- + +### 🧪Step: `Given the ":headerName" request header contains ":value"` + +Set or replace the specified HTTP request header with the given value. Supports variable substitutions from saved context variables. + +```gherkin +Given the "Authorization" request header contains "Bearer abc123" +``` + +--- + +### 🧪Step: `Given the ":headerName" request header contains multiline value` + +Set or replace the specified HTTP request header with a multiline value block. + +```gherkin +Given the "Authorization" request header contains multiline value: + """ + Bearer + {{token}} + UserId={{user_id}} + """ +``` + +--- + +### 🧪Step: `Given the request ip is ":ip"` + +Set the client IP address for the request by modifying the `REMOTE_ADDR` server parameter. + +```gherkin +Given the request ip is "192.168.1.1" +``` + +--- + +### 🧪Step: `Given the request contains params` + +Add parameters to the request payload or query string. Supports embedded PHP expressions wrapped in `< >` that will be evaluated. Also saves parameters for reuse. +> See [Runnable Parameters](runnable-parameters.md) for more details on how expressions work. + +```gherkin +Given the request contains params: + """ + { + "user_id": "", + "active": true + } + """ +``` + +--- + +### 🧪Step: `When I send ":method" request to ":route" route` + +Sends an HTTP request with the specified method (`GET`, `POST`, `PUT`, `PATCH`) to the Symfony route named `:route`. Uses previously configured headers and parameters. + +```gherkin +When I send "POST" request to "api_login" route +``` + +--- + +### 🧪Step: `Then response status code should be :httpStatus` + +Asserts that the HTTP response status matches the expected status code. + +```gherkin +Then response status code should be 200 +``` + +--- + +### 🧪Step: `Then response is JSON` + +Asserts that the response body contains valid, non-empty JSON. + +```gherkin +Then response is JSON +``` + { + "name": "status", + "result": true, + "message": "up", + "params": [] + } +--- + +### 🧪Step: `Then response should be empty` + +Asserts that the response body is empty. + +```gherkin +Then response should be empty +``` +--- + +### 🧪Step: `Then response should be JSON` + +Compare the actual JSON response to the expected JSON block for equality. + +```gherkin +Then response should be JSON: + """ + { + "success": true, + "data": { + "id": 123 + } + } + """ +``` + +--- + +### 🧪Step: `When I save ":paramPath" param from json response as ":valueKey"` + +Extracts a value from the JSON response at the dot-notated path and saves it in the context for later use. + +```gherkin +When I save "data.id" param from json response as "userId" +``` + +--- + +### 🧪Step: `Then response should be JSON with variable fields ":variableFields"` + +Compares the actual JSON response to the expected JSON while ignoring differences in specified fields that may vary (e.g., timestamps, UUIDs, etc.). + +You can also use regular expressions to match the values of these variable fields. To do so, prefix the expected value with a ~ (tilde). This indicates that the value should match the given regex pattern rather than be compared literally. + +For example: + +To match any numeric timestamp (e.g., Unix timestamp), use: +`~^\\d+$` + +To match a UUID value, use: +`~^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}$` + +```gherkin +Then response should be JSON with variable fields "id, createdAt, updatedAt": + """ + { + "user": { + "id": "~^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}$", + "name": "John", + "createdAt": "~^\\d+$", + "updatedAt": "~^\\d+$" + } + } + """ +``` +This allows flexibility in matching dynamic values while still validating the structure and correctness of the response. + +--- + +### 🧪Step: `Then the ":headerName" response headers contains ":headerValue"` + +Asserts that the response contains the specified header with a value that includes the given substring. + +```gherkin +Then the "Content-Type" response headers contains "application/json" +``` + +--- + +### 📝 Notes +- ✅ Variable substitutions ({{variable}}) are supported in headers and body. +- ✅ PHP expressions (, , etc.) are dynamically evaluated. +- ✅ Saved values can be reused across steps for chaining and correlation. +- ✅ Requests are sent using Symfony route names, not raw URLs. diff --git a/src/Context/ORMContext.php b/src/Context/ORMContext.php deleted file mode 100644 index b1d1e0c..0000000 --- a/src/Context/ORMContext.php +++ /dev/null @@ -1,106 +0,0 @@ -manager = $manager; - } - - /** - * @And I see :count entities :entityClass - */ - public function andISeeInRepository(int $count, string $entityClass): void - { - $this->seeInRepository($count, $entityClass); - } - - /** - * @Then I see :count entities :entityClass - */ - public function thenISeeInRepository(int $count, string $entityClass): void - { - $this->seeInRepository($count, $entityClass); - } - - /** - * @And I see entity :entity with id :id - */ - public function andISeeEntityInRepositoryWithId(string $entityClass, string $id): void - { - $this->seeInRepository(1, $entityClass, ['id' => $id]); - } - - /** - * @Then I see entity :entity with id :id - */ - public function thenISeeEntityInRepositoryWithId(string $entityClass, string $id): void - { - $this->seeInRepository(1, $entityClass, ['id' => $id]); - } - - /** - * @Then I see entity :entity with properties: - */ - public function andISeeEntityInRepositoryWithProperties(string $entityClass, PyStringNode $string): void - { - $expectedProperties = json_decode(trim($string->getRaw()), true, 512, JSON_THROW_ON_ERROR); - $this->seeInRepository(1, $entityClass, $expectedProperties); - } - - /** - * @param array $params - * - * @throws NonUniqueResultException - * @throws NoResultException - */ - private function seeInRepository(int $count, string $entityClass, ?array $params = null): void - { - /** @var class-string $entityClass */ - $query = $this->manager->createQueryBuilder() - ->from($entityClass, 'e') - ->select('count(e)'); - - if (null !== $params) { - foreach ($params as $columnName => $columnValue) { - if ($columnValue === null) { - $query->andWhere(sprintf('e.%s IS NULL', $columnName)); - } else { - $query->andWhere(sprintf('e.%s = :%s', $columnName, $columnName)) - ->setParameter($columnName, $columnValue); - } - } - } - - $realCount = $query->getQuery() - ->getSingleScalarResult(); - - if ($count !== $realCount) { - throw new RuntimeException( - sprintf('Real count is %d, not %d', $realCount, $count), - ); - } - } -} diff --git a/src/DependencyInjection/BehatApiContextExtension.php b/src/DependencyInjection/BehatApiContextExtension.php index c63385c..8592916 100644 --- a/src/DependencyInjection/BehatApiContextExtension.php +++ b/src/DependencyInjection/BehatApiContextExtension.php @@ -25,7 +25,6 @@ public function load(array $configs, ContainerBuilder $container): void $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $this->loadApiContext($config, $loader, $container); - $this->loadOrmContext($config, $loader, $container); } /** @@ -37,21 +36,6 @@ private function loadApiContext( ContainerBuilder $container ): void { $this->safeLoad($loader, 'api_context.xml'); - } - - /** - * @param array $config - */ - private function loadOrmContext( - array $config, - XmlFileLoader $loader, - ContainerBuilder $container - ): void { - if (!($config['use_orm_context'] ?? true)) { - return; - } - - $this->safeLoad($loader, 'orm_context.xml'); $this->configureKernelResetManagers( $config, diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 0b91bb5..0429c95 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -4,7 +4,6 @@ namespace BehatApiContext\DependencyInjection; -use Composer\InstalledVersions; use Symfony\Component\Config\Definition\Builder\NodeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -27,15 +26,6 @@ private function addKernelResetManagersSection(NodeBuilder $builder): void ->arrayNode('kernel_reset_managers') ->scalarPrototype()->end() ->end() - - ->booleanNode('use_orm_context') - ->defaultValue($this->checkOrmContextDefValue()) - ->end() ->end(); } - - private function checkOrmContextDefValue(): bool - { - return InstalledVersions::isInstalled('doctrine/orm'); - } } diff --git a/src/Resources/config/api_context.xml b/src/Resources/config/api_context.xml index d934710..0a6ade0 100644 --- a/src/Resources/config/api_context.xml +++ b/src/Resources/config/api_context.xml @@ -5,5 +5,6 @@ + diff --git a/src/Resources/config/orm_context.xml b/src/Resources/config/orm_context.xml deleted file mode 100644 index bbad664..0000000 --- a/src/Resources/config/orm_context.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - diff --git a/tests/DependencyInjection/BehatApiContextExtensionTest.php b/tests/DependencyInjection/BehatApiContextExtensionTest.php index f29629a..8563142 100644 --- a/tests/DependencyInjection/BehatApiContextExtensionTest.php +++ b/tests/DependencyInjection/BehatApiContextExtensionTest.php @@ -5,7 +5,6 @@ namespace BehatApiContext\Tests\DependencyInjection; use BehatApiContext\Context\ApiContext; -use BehatApiContext\Context\ORMContext; use BehatApiContext\DependencyInjection\BehatApiContextExtension; use BehatApiContext\Service\ResetManager\DoctrineResetManager; use PHPUnit\Framework\TestCase; @@ -25,45 +24,18 @@ public function testWithEmptyConfig(): void public function testWithFilledConfig(): void { $container = $this->createContainerFromFixture('filled_bundle_config'); - self::assertFalse($container->hasDefinition(ORMContext::class)); $apiContextDefinition = $container->getDefinition(ApiContext::class); - self::assertCount(0, $apiContextDefinition->getMethodCalls()); - } - - public function testWithOrmContextEnabled(): void - { - $container = $this->createContainerFromFixture('with_orm_context'); - - self::assertTrue($container->hasDefinition(ORMContext::class)); - - $ormContextDefinition = $container->getDefinition(ApiContext::class); $doctrineResetManagerDefinition = $container->getDefinition(DoctrineResetManager::class); - $methodCalls = $ormContextDefinition->getMethodCalls(); + $methodCalls = $apiContextDefinition->getMethodCalls(); $this->assertDefinitionMethodCall( $methodCalls[0], 'addKernelResetManager', [$doctrineResetManagerDefinition] ); - } - - public function testWithOrmContextDisabled(): void - { - $container = $this->createContainerFromFixture('without_orm_context'); - self::assertFalse($container->hasDefinition(ORMContext::class)); - } - - public function testWithOrmContextButWithoutResetManagers(): void - { - $container = $this->createContainerFromFixture('with_orm_context_without_reset_managers'); - - self::assertTrue($container->hasDefinition(ORMContext::class)); - - $ormContextDefinition = $container->getDefinition(ORMContext::class); - $methodCalls = $ormContextDefinition->getMethodCalls(); - self::assertCount(0, $methodCalls); + self::assertCount(1, $apiContextDefinition->getMethodCalls()); } private function createContainerFromFixture(string $fixtureFile): ContainerBuilder diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 2db5a5b..3b8fbff 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -16,7 +16,6 @@ public function testProcessConfigurationWithEmptyConfiguration(): void { $expectedBundleDefaultConfig = [ 'kernel_reset_managers' => [], - 'use_orm_context' => true, ]; $this->assertSame($expectedBundleDefaultConfig, $this->processConfiguration([])); @@ -27,13 +26,11 @@ public function testProcessConfigurationWithDefaultConfiguration(): void $config = [ 'behat_api_context' => [ 'kernel_reset_managers' => [], - 'use_orm_context' => true, ] ]; $expectedBundleDefaultConfig = [ 'kernel_reset_managers' => [], - 'use_orm_context' => true, ]; $this->assertSame($expectedBundleDefaultConfig, $this->processConfiguration($config)); @@ -46,7 +43,6 @@ public function testProcessConfigurationWithFilledConfiguration(): void 'kernel_reset_managers' => [ DoctrineResetManager::class ], - 'use_orm_context' => true, ] ]; @@ -54,13 +50,12 @@ public function testProcessConfigurationWithFilledConfiguration(): void 'kernel_reset_managers' => [ DoctrineResetManager::class ], - 'use_orm_context' => true, ]; $this->assertSame($expectedBundleDefaultConfig, $this->processConfiguration($config)); } - public function testProcessConfigurationWithoutDoctrineOrm(): void + public function testProcessConfiguration(): void { $configuration = new class extends Configuration { public function getConfigTreeBuilder(): TreeBuilder @@ -73,9 +68,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarPrototype() ->end() ->end() - ->booleanNode('use_orm_context') - ->defaultValue(false) - ->end() ->end(); return $treeBuilder; @@ -84,7 +76,6 @@ public function getConfigTreeBuilder(): TreeBuilder $expected = [ 'kernel_reset_managers' => [], - 'use_orm_context' => false, ]; $this->assertSame($expected, $this->processConfiguration([], $configuration)); diff --git a/tests/DependencyInjection/Fixtures/empty_bundle_config.yaml b/tests/DependencyInjection/Fixtures/empty_bundle_config.yaml index ce4e21c..0fd1c23 100644 --- a/tests/DependencyInjection/Fixtures/empty_bundle_config.yaml +++ b/tests/DependencyInjection/Fixtures/empty_bundle_config.yaml @@ -1,2 +1 @@ behat_api_context: - use_orm_context: false diff --git a/tests/DependencyInjection/Fixtures/filled_bundle_config.yaml b/tests/DependencyInjection/Fixtures/filled_bundle_config.yaml index e47cc7d..05f3bc6 100644 --- a/tests/DependencyInjection/Fixtures/filled_bundle_config.yaml +++ b/tests/DependencyInjection/Fixtures/filled_bundle_config.yaml @@ -1,4 +1,3 @@ behat_api_context: - use_orm_context: false kernel_reset_managers: - BehatApiContext\Service\ResetManager\DoctrineResetManager diff --git a/tests/DependencyInjection/Fixtures/with_orm_context.yaml b/tests/DependencyInjection/Fixtures/with_orm_context.yaml deleted file mode 100644 index 16f38aa..0000000 --- a/tests/DependencyInjection/Fixtures/with_orm_context.yaml +++ /dev/null @@ -1,4 +0,0 @@ -behat_api_context: - use_orm_context: true - kernel_reset_managers: - - BehatApiContext\Service\ResetManager\DoctrineResetManager diff --git a/tests/DependencyInjection/Fixtures/with_orm_context_without_reset_managers.yaml b/tests/DependencyInjection/Fixtures/with_orm_context_without_reset_managers.yaml deleted file mode 100644 index cd0e618..0000000 --- a/tests/DependencyInjection/Fixtures/with_orm_context_without_reset_managers.yaml +++ /dev/null @@ -1,3 +0,0 @@ -behat_api_context: - use_orm_context: true - kernel_reset_managers: [] diff --git a/tests/DependencyInjection/Fixtures/without_orm_context.yaml b/tests/DependencyInjection/Fixtures/without_orm_context.yaml deleted file mode 100644 index e47cc7d..0000000 --- a/tests/DependencyInjection/Fixtures/without_orm_context.yaml +++ /dev/null @@ -1,4 +0,0 @@ -behat_api_context: - use_orm_context: false - kernel_reset_managers: - - BehatApiContext\Service\ResetManager\DoctrineResetManager diff --git a/tests/Unit/Context/DB/ORMContextTest.php b/tests/Unit/Context/DB/ORMContextTest.php deleted file mode 100644 index 9c85778..0000000 --- a/tests/Unit/Context/DB/ORMContextTest.php +++ /dev/null @@ -1,177 +0,0 @@ -createContext('App\Entity\SomeEntity', self::COUNT); - $context->andISeeInRepository(self::COUNT, 'App\Entity\SomeEntity'); - } - - public function testAndISeeCountInRepositoryFailed(): void - { - $context = $this->createContext('App\Entity\SomeEntity', self::COUNT); - self::expectException(RuntimeException::class); - $context->andISeeInRepository(self::COUNT + 1, 'App\Entity\SomeEntity'); - } - - public function testThenISeeCountInRepository(): void - { - $context = $this->createContext('App\Entity\SomeEntity', self::COUNT); - $context->thenISeeInRepository(self::COUNT, 'App\Entity\SomeEntity'); - } - - public function testThenISeeCountInRepositoryFailed(): void - { - $context = $this->createContext('App\Entity\SomeEntity', self::COUNT); - self::expectException(RuntimeException::class); - $context->thenISeeInRepository(self::COUNT + 1, 'App\Entity\SomeEntity'); - } - - public function testThenISeeCountInRepositoryWithId(): void - { - $context = $this->createContext( - 'App\Entity\SomeEntity', - 1, - ['id' => self::UUID], - ); - $context->thenISeeEntityInRepositoryWithId( - 'App\Entity\SomeEntity', - self::UUID, - ); - } - - public function testThenISeeCountInRepositoryWithIdFailed(): void - { - $context = $this->createContext( - 'App\Entity\SomeEntity', - 1, - ['id' => self::UUID], - ); - $context->andISeeEntityInRepositoryWithId( - 'App\Entity\SomeEntity', - self::UUID, - ); - } - - public function testThenISeeEntityInRepositoryWithProperties(): void - { - $context = $this->createContext( - 'App\Entity\SomeEntity', - 1, - [ - 'id' => self::UUID, - 'someProperty' => 'someValue', - 'otherProperty' => 'otherValue', - ], - ); - $context->andISeeEntityInRepositoryWithProperties( - 'App\Entity\SomeEntity', - new PyStringNode([ - <<<'PSN' - { - "id": "e809639f-011a-4ae0-9ae3-8fcb460fe950", - "someProperty": "someValue", - "otherProperty": "otherValue" - } - PSN - ], 1), - ); - } - - public function testThenISeeEntityInRepositoryWithPropertyNull(): void - { - $context = $this->createContext( - 'App\Entity\SomeEntity', - 1, - [ - 'id' => self::UUID, - 'someProperty' => null, - ], - ); - $context->andISeeEntityInRepositoryWithProperties( - 'App\Entity\SomeEntity', - new PyStringNode([ - <<<'PSN' - { - "id": "e809639f-011a-4ae0-9ae3-8fcb460fe950", - "someProperty": null - } - PSN - ], 1), - ); - } - - private function createContext( - string $entityName, - int $count = 1, - ?array $properties = null - ): ORMContext { - $queryMock = $this->getMockBuilder(Query::class) - ->disableOriginalConstructor() - ->getMock(); - - $queryMock->expects(self::once()) - ->method('getSingleScalarResult') - ->willReturn($count); - - $entityManagerMock = $this->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $queryBuilderMock = $this->getMockBuilder(QueryBuilder::class) - ->disableOriginalConstructor() - ->getMock(); - - $queryBuilderMock->expects(self::once()) - ->method('from') - ->with( - $entityName, - 'e', - )->willReturn($queryBuilderMock); - - if (null !== $properties) { - foreach ($properties as $name => $value) { - $queryBuilderMock->expects(self::exactly(count($properties))) - ->method('andWhere') - ->willReturnSelf(); - $setParametersCount = count(array_filter($properties, function ($value) { - return !is_null($value); - })); - $queryBuilderMock->expects(self::exactly($setParametersCount)) - ->method('setParameter') - ->willReturnSelf(); - } - } - - $queryBuilderMock->expects(self::once()) - ->method('select') - ->with('count(e)') - ->willReturn($queryBuilderMock); - - $queryBuilderMock->expects(self::once()) - ->method('getQuery') - ->willReturn($queryMock); - - $entityManagerMock->expects(self::once()) - ->method('createQueryBuilder') - ->willReturn($queryBuilderMock); - - return new ORMContext($entityManagerMock); - } -} From 1a61be1ea5ed93b18fba37e8963c65bf78419c30 Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 20 May 2025 16:07:13 +0300 Subject: [PATCH 2/2] feat(ORC-8133): add tests for DoctrineResetManager --- .github/workflows/ci.yaml | 3 + .../ResetManager/DoctrineResetManagerTest.php | 119 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 tests/Unit/Service/ResetManager/DoctrineResetManagerTest.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5ef5dd..d0d5a4e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -83,6 +83,9 @@ jobs: - name: Install dependencies run: composer install + - name: Add doctrine/orm + run: composer require --no-progress --no-interaction --prefer-dist doctrine/orm:^2.0 + - name: Run PHPUnit tests run: composer phpunit if: matrix.coverage == 'none' diff --git a/tests/Unit/Service/ResetManager/DoctrineResetManagerTest.php b/tests/Unit/Service/ResetManager/DoctrineResetManagerTest.php new file mode 100644 index 0000000..6d2c522 --- /dev/null +++ b/tests/Unit/Service/ResetManager/DoctrineResetManagerTest.php @@ -0,0 +1,119 @@ +resetManager = new DoctrineResetManager(); + } + + public function testNeedsResetReturnsFalseForGetMethod(): void + { + $this->assertFalse($this->resetManager->needsReset(Request::METHOD_GET)); + $this->assertFalse($this->resetManager->needsReset('get')); + } + + public function testNeedsResetReturnsTrueForOtherMethods(): void + { + $this->assertTrue($this->resetManager->needsReset(Request::METHOD_POST)); + $this->assertTrue($this->resetManager->needsReset(Request::METHOD_PUT)); + $this->assertTrue($this->resetManager->needsReset('delete')); + } + + public function testResetDoesNothingIfNoEntityManagersParameter(): void + { + $kernel = $this->createMock(KernelInterface::class); + $container = $this->createMock(ContainerInterface::class); + + $kernel->expects($this->once()) + ->method('getContainer') + ->willReturn($container); + + $container->expects($this->once()) + ->method('hasParameter') + ->with('doctrine.entity_managers') + ->willReturn(false); + + $container->expects($this->never())->method('getParameter'); + $container->expects($this->never())->method('initialized'); + $container->expects($this->never())->method('get'); + + $this->resetManager->reset($kernel); + } + + public function testResetClearsEntityManagersAndClosesConnections(): void + { + $kernel = $this->createMock(KernelInterface::class); + $container = $this->createMock(ContainerInterface::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $connection = $this->createMock(Connection::class); + + $entityManagers = ['em1', 'em2']; + + $kernel->method('getContainer')->willReturn($container); + $container->method('hasParameter')->with('doctrine.entity_managers')->willReturn(true); + $container->method('getParameter')->with('doctrine.entity_managers')->willReturn($entityManagers); + + $container->method('initialized') + ->willReturnMap([ + ['em1', true], + ['em2', false], + ]); + + $container->method('get')->willReturnCallback(function ($id) use ($entityManager) { + if ($id === 'em1') { + return $entityManager; + } + return null; + }); + + $entityManager->expects($this->once())->method('clear'); + + $entityManager->method('getConnection')->willReturn($connection); + + $connection->method('isConnected')->willReturn(true); + $connection->expects($this->once())->method('close'); + + $this->resetManager->reset($kernel); + } + + public function testResetDoesNotCloseConnectionIfNotConnected(): void + { + $kernel = $this->createMock(KernelInterface::class); + $container = $this->createMock(ContainerInterface::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $connection = $this->createMock(Connection::class); + + $entityManagers = ['em1']; + + $kernel->method('getContainer')->willReturn($container); + $container->method('hasParameter')->with('doctrine.entity_managers')->willReturn(true); + $container->method('getParameter')->with('doctrine.entity_managers')->willReturn($entityManagers); + + $container->method('initialized')->with('em1')->willReturn(true); + $container->method('get')->with('em1')->willReturn($entityManager); + + $entityManager->expects($this->once())->method('clear'); + + $entityManager->method('getConnection')->willReturn($connection); + + $connection->method('isConnected')->willReturn(false); + $connection->expects($this->never())->method('close'); + + $this->resetManager->reset($kernel); + } +}