diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8ccdf51 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,98 @@ +name: CI + +on: + pull_request: ~ + +jobs: + run: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: + - '8.1' + - '8.2' + - '8.3' + coverage: ['none'] + symfony-versions: + - '4.4.*' + - '5.4.*' + - '6.0.*' + - '6.2.*' + - '7.0.*' + exclude: + - php: '8.1' + symfony-versions: '7.0.*' + include: + - php: '7.4' + symfony-versions: '^4.4' + coverage: 'none' + - php: '7.4' + symfony-versions: '^5.4' + coverage: 'none' + - php: '8.0' + symfony-versions: '^4.4' + coverage: 'none' + - php: '8.0' + symfony-versions: '^5.4' + coverage: 'none' + - php: '8.2' + coverage: 'xdebug' + symfony-versions: '^7.0' + description: 'Log Code Coverage' + + name: PHP ${{ matrix.php }} Symfony ${{ matrix.symfony-versions }} ${{ matrix.description }} + steps: + - name: Checkout + uses: actions/checkout@v2 + + - uses: actions/cache@v4 + with: + path: ~/.composer/cache/files + key: ${{ matrix.php }}-${{ matrix.symfony-versions }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: ${{ matrix.coverage }} + + - name: Add PHPUnit matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Set composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache composer + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer-${{ hashFiles('composer.json') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-${{ matrix.symfony-versions }}-composer + + - name: Update Symfony version + if: matrix.symfony-versions != '' + run: | + composer require symfony/config:${{ matrix.symfony-versions }} --no-update --no-scripts + composer require symfony/dependency-injection:${{ matrix.symfony-versions }} --no-update --no-scripts + composer require symfony/http-kernel:${{ matrix.symfony-versions }} --no-update --no-scripts + + - name: Install dependencies + run: composer install + + - name: Run PHPUnit tests + run: composer phpunit + if: matrix.coverage == 'none' + + - name: PHPUnit tests and Log Code coverage + run: vendor/bin/phpunit --coverage-clover=coverage.xml + if: matrix.coverage == 'xdebug' + + - name: Run codecov + uses: codecov/codecov-action@v4.0.1 + if: matrix.coverage == 'xdebug' + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: './coverage.xml' + fail_ci_if_error: true diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..248d5d0 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,24 @@ +on: + pull_request: + push: + branches: [ main, develop ] + +jobs: + security-checker: + name: Security checker + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Install dependencies + run: composer install --no-progress --no-interaction --prefer-dist + + - name: Download local-php-security-checker + run: curl -s -L -o local-php-security-checker https://github.com/fabpot/local-php-security-checker/releases/download/v1.0.0/local-php-security-checker_1.0.0_linux_amd64 + + - name: Run local-php-security-checker + run: chmod +x local-php-security-checker && ./local-php-security-checker diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml new file mode 100644 index 0000000..c54cc67 --- /dev/null +++ b/.github/workflows/static-analysis.yaml @@ -0,0 +1,55 @@ +name: Code style and static analysis + +on: + pull_request: + push: + branches: [ main, develop ] + +jobs: + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Install dependencies + run: composer install --no-progress --no-interaction --prefer-dist + + - name: Run script + run: composer code-style + + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Install dependencies + run: composer install --no-progress --no-interaction --prefer-dist + + - name: Run script + run: composer phpstan + + composer-validate: + name: Composer validate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + + - name: Install dependencies + run: composer install --no-progress --no-interaction --prefer-dist + + - name: Run script + run: composer composer-validate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d073d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea/ +/vendor/ +/composer.lock +.DS_Store +/.phpunit.result.cache diff --git a/.infrastructure/docker/Dockerfile b/.infrastructure/docker/Dockerfile new file mode 100644 index 0000000..22e32ad --- /dev/null +++ b/.infrastructure/docker/Dockerfile @@ -0,0 +1,10 @@ +ARG from_image + +FROM $from_image + +ENV XDEBUG_MODE=off + +RUN apk add --no-cache bash linux-headers $PHPIZE_DEPS \ + && pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && curl --silent https://getcomposer.org/composer-stable.phar -o /usr/bin/composer && chmod a+x /usr/bin/composer diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..f96e33e --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +yozhef@macpaw.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7aab1b6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 MacPaw Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 25a9e6d..d47c4bc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ -# behat-orm-context -Behat ORM context Bundle +# Behat ORM Context Bundle + +| Version | Build Status | Code Coverage | +|:---------:|:---------------------------------------------------------:|:------------------------------------------------------------------------:| +| `master` | [![CI][master Build Status Image]][master Build Status] | [![Coverage Status][master Code Coverage Image]][master Code Coverage] | +| `develop` | [![CI][develop Build Status Image]][develop Build Status] | [![Coverage Status][develop Code Coverage Image]][develop Code Coverage] | + +Behat context for testing Doctrine ORM integration. + +## Description + +This bundle provides a Behat context for testing your application's interaction with a database using Doctrine ORM. + +## Installation + +See the [installation instructions](docs/install.md). + +## Features + +The bundle provides several Behat step definitions for ORM testing: + +* [See X entities in repository](docs/ORMContext/see-count-in-repository.md) - Check if the count of entities in the repository matches expected +* [See entity with ID](docs/ORMContext/see-entity-in-repository-with-id.md) - Check if an entity with a specific ID exists +* [See entity with properties](docs/ORMContext/see-entity-in-repository-with-properties.md) - Check if an entity with specific properties exists + +## License + +This bundle is released under the MIT license. See the included [LICENSE](LICENSE) file for more information. + +[master Build Status]: https://github.com/macpaw/behat-orm-context/actions?query=workflow%3ACI+branch%3Amaster +[master Build Status Image]: https://github.com/macpaw/behat-orm-context/workflows/CI/badge.svg?branch=master +[develop Build Status]: https://github.com/macpaw/behat-orm-context/actions?query=workflow%3ACI+branch%3Adevelop +[develop Build Status Image]: https://github.com/macpaw/behat-orm-context/workflows/CI/badge.svg?branch=develop +[master Code Coverage]: https://codecov.io/gh/macpaw/behat-orm-context/branch/master +[master Code Coverage Image]: https://img.shields.io/codecov/c/github/macpaw/behat-orm-context/master?logo=codecov +[develop Code Coverage]: https://codecov.io/gh/macpaw/behat-orm-context/branch/develop +[develop Code Coverage Image]: https://img.shields.io/codecov/c/github/macpaw/behat-orm-context/develop?logo=codecov + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8d26322 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.x.x | :white_check_mark: | +| 1.x.x | :white_check_mark: | + +## Reporting a Bug + +Report security bugs by emailing the lead maintainer at yozhef@macpaw.com or create [Issues](https://github.com/MacPaw/BehatMessengerContext/issues) + +## Reporting a Vulnerability + +Please report (suspected) security vulnerabilities to +**[yozhef@macpaw.com](mailto:yozhef@macpaw.com)**. You will receive a response from +us within 48 hours. If the issue is confirmed, we will release a patch as soon +as possible depending on complexity but historically within a few days. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8967c9e --- /dev/null +++ b/composer.json @@ -0,0 +1,79 @@ +{ + "name": "macpaw/behat-orm-context", + "description": "Behat Context for testing Symfony ORM", + "type": "symfony-bundle", + "license": "MIT", + "keywords": [ + "MacPaw", + "symfony", + "behat", + "BDD", + "Context", + "ORM" + ], + "authors": [ + { + "name": "Vladislav Hanziuk", + "email": "ganzyukv@macpaw.com", + "homepage": "https://macpaw.com/", + "role": "Software Engineer" + }, + { + "name": "Serhii Donii", + "email": "serhii.donii@macpaw.com", + "homepage": "https://macpaw.com/", + "role": "Software Engineer" + }, + { + "name": "Yozhef Hisem", + "email": "hisemjo@gmail.com", + "homepage": "https://macpaw.com/", + "role": "Software Engineer" + } + ], + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "behat/behat": "^3.0", + "symfony/config": "^4.4 || ^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^4.4 || ^5.4.34 || ^6.0 || ^7.0.2", + "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0 || ^7.0", + "macpaw/similar-arrays": "^1.0", + "doctrine/orm": "^2.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^7.2", + "squizlabs/php_codesniffer": "^3.12" + }, + "autoload": { + "psr-4": { + "BehatOrmContext\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "BehatOrmContext\\Tests\\": "tests" + } + }, + "scripts": { + "composer-validate": "composer validate", + "phpstan": "./vendor/bin/phpstan analyse -l max", + "code-style": "./vendor/bin/phpcs", + "code-style-fix": "./vendor/bin/phpcbf", + "phpunit": "./vendor/bin/phpunit", + "phpunit-html-coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html=coverage", + "dev-checks": [ + "composer validate", + "@phpstan", + "@code-style", + "@phpunit" + ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..cf844e5 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,14 @@ +version: '3.9' + +services: + php80: + build: + context: .infrastructure + dockerfile: docker/Dockerfile + args: + from_image: php:8.0-fpm-alpine + working_dir: /app + environment: + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/vendor/bin" + volumes: + - ./:/app diff --git a/docs/ORMContext/see-count-in-repository.md b/docs/ORMContext/see-count-in-repository.md new file mode 100644 index 0000000..0db7c27 --- /dev/null +++ b/docs/ORMContext/see-count-in-repository.md @@ -0,0 +1,26 @@ +### I See X Entities in Repository + +#### Step Definition: + +This step checks if the database contains exactly the specified number of entities for a given entity class. + +#### Gherkin Example: + +```gherkin +And I see 5 entities "App\Entity\User" +``` + +#### Description: + +This step allows you to verify that a specific number of entities exist in the database. It executes a count query on the database for the specified entity class and verifies that the result matches the expected count. + +#### Parameters: + +- `count`: The expected number of entities +- `entityClass`: The fully-qualified class name of the entity to check + +#### Use Cases: + +- Verifying that data setup was successful +- Checking that operations have created/deleted the expected number of records +- Validating database state after data manipulation steps \ No newline at end of file diff --git a/docs/ORMContext/see-entity-in-repository-with-id.md b/docs/ORMContext/see-entity-in-repository-with-id.md new file mode 100644 index 0000000..2b88155 --- /dev/null +++ b/docs/ORMContext/see-entity-in-repository-with-id.md @@ -0,0 +1,26 @@ +### I See Entity with Specific ID + +#### Step Definition: + +This step checks if the database contains an entity of a specific type with the given ID. + +#### Gherkin Example: + +```gherkin +And I see entity "App\Entity\Product" with id "abc123" +``` + +#### Description: + +This step allows you to verify that a specific entity exists in the database by its ID. It performs a count query with a condition on the ID field and expects exactly one matching entity. + +#### Parameters: + +- `entityClass`: The fully-qualified class name of the entity to check +- `id`: The ID value to match against the entity's ID field + +#### Use Cases: + +- Verifying that a specific record exists after creation +- Confirming entity persistence during a multi-step process +- Checking that an entity with a specific identifier is still present \ No newline at end of file diff --git a/docs/ORMContext/see-entity-in-repository-with-properties.md b/docs/ORMContext/see-entity-in-repository-with-properties.md new file mode 100644 index 0000000..1187bb2 --- /dev/null +++ b/docs/ORMContext/see-entity-in-repository-with-properties.md @@ -0,0 +1,40 @@ +### I See Entity with Specific Properties + +#### Step Definition: + +This step checks if the database contains an entity of a specific type with the given properties. + +#### Gherkin Example: + +```gherkin +Then I see entity "App\Entity\User" with properties: + """ + { + "email": "user@example.com", + "firstName": "John", + "lastName": "Doe", + "active": true + } + """ +``` + +#### Description: + +This step allows you to verify that a specific entity exists in the database by matching multiple property values. It performs a count query with conditions on each specified property and expects exactly one matching entity. + +#### Parameters: + +- `entityClass`: The fully-qualified class name of the entity to check +- `properties`: A JSON string containing key-value pairs representing entity properties to match + +#### Use Cases: + +- Verifying that an entity with specific field values exists +- Checking that entity properties match expected values after operations +- Validating complex entity state with multiple property conditions +- Testing business logic that modifies entity properties + +#### Notes: + +- Properties with `null` values are queried using `IS NULL` condition +- All other properties are matched using equality \ No newline at end of file diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..ee3e510 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,70 @@ +# Installation + +## Step 1: Download the Bundle + +Open a command console, enter your project directory and execute: + +### Applications that use Symfony Flex + +```bash + composer require --dev macpaw/behat-orm-context +``` + +### Applications that don't use Symfony Flex + +Open a command console, enter your project directory and execute the following command to download the latest stable +version of this bundle: + +```bash + composer require --dev macpaw/behat-orm-context +``` + +This command requires you to have Composer installed globally, as explained +in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + +Then, enable the bundle by adding it to the list of registered bundles +in the `app/AppKernel.php` file of your project: + +```php + ['test' => true], + ); + + // ... + } + + // ... +} +``` + +## Step 2: Configure Behat + +Go to `behat.yml`: + +```yaml +# ... +contexts: + - BehatOrmContext\Context\OrmContext +# ... +``` + +## Configuration + +By default, the bundle has the following configuration: + +```yaml +behat_orm_context: + # Currently no specific configuration options are available +``` + +You can override it manually in your `config/packages/test/behat_orm_context.yaml`. \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..a3ac003 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,45 @@ + + + The coding standard of Behat ORM Context package + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + src/ + tests/ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..fbb2748 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + excludes_analyse: + paths: + - src + level: max + checkExplicitMixed: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..9aacc0e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + tests + + + + + ./src + + + + + + diff --git a/src/BehatOrmContextBundle.php b/src/BehatOrmContextBundle.php new file mode 100644 index 0000000..4529143 --- /dev/null +++ b/src/BehatOrmContextBundle.php @@ -0,0 +1,11 @@ +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 + { + $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/BehatOrmContextExtension.php b/src/DependencyInjection/BehatOrmContextExtension.php new file mode 100644 index 0000000..5cfb72c --- /dev/null +++ b/src/DependencyInjection/BehatOrmContextExtension.php @@ -0,0 +1,24 @@ +> $configs + * + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container): void + { + $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('orm_context.xml'); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..c0f9c7b --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,16 @@ + + + + + + diff --git a/tests/DependencyInjection/BehatOrmContextExtensionTest.php b/tests/DependencyInjection/BehatOrmContextExtensionTest.php new file mode 100644 index 0000000..2fb15dd --- /dev/null +++ b/tests/DependencyInjection/BehatOrmContextExtensionTest.php @@ -0,0 +1,35 @@ +load([], $container); + + self::assertInstanceOf(Extension::class, $extension); + + self::assertTrue($container->has(OrmContext::class)); + } + + public function testOrmContextIsCorrectlyDefined(): void + { + $extension = new BehatOrmContextExtension(); + $container = new ContainerBuilder(); + $extension->load([], $container); + + $definition = $container->getDefinition(OrmContext::class); + self::assertSame(OrmContext::class, $definition->getClass()); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..298dded --- /dev/null +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,25 @@ +processConfiguration($configuration, []); + + self::assertSame([], $configs); + } +} diff --git a/tests/Unit/Context/ORMContextTest.php b/tests/Unit/Context/ORMContextTest.php new file mode 100644 index 0000000..f721a9d --- /dev/null +++ b/tests/Unit/Context/ORMContextTest.php @@ -0,0 +1,177 @@ +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); + } +}