diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 021767ec54b..62d9ce3f8ea 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -447,94 +447,8 @@ jobs:
cd $(composer ${{matrix.component}} --cwd)
./vendor/bin/phpunit --fail-on-deprecation --display-deprecations --log-junit "/tmp/build/logs/phpunit/junit.xml"
- behat:
- name: Behat (PHP ${{ matrix.php }} ${{ matrix.shard }})
- runs-on: ubuntu-latest
- timeout-minutes: 20
- strategy:
- matrix:
- php: ${{ fromJSON(github.event_name == 'pull_request' && '["8.2","8.5"]' || '["8.2","8.3","8.4","8.5"]') }}
- shard:
- - graphql-doctrine
- include:
- - php: '8.5'
- shard: graphql-doctrine
- coverage: true
- fail-fast: false
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: pecl, composer:2.9.8
- extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite
- coverage: pcov
- ini-values: memory_limit=-1
- - name: Get composer cache directory
- id: composercache
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- - name: Cache dependencies
- uses: actions/cache@v5
- with:
- path: ${{ steps.composercache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Update project dependencies
- run: |
- composer global require soyuka/pmu
- composer global config allow-plugins.soyuka/pmu true --no-interaction
- composer global link .
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Resolve shard paths
- id: shard
- run: |
- case "${{ matrix.shard }}" in
- graphql-doctrine) paths="features/graphql features/doctrine" ;;
- esac
- echo "paths=$paths" >> $GITHUB_OUTPUT
- - name: Run Behat tests (PHP ${{ matrix.php }} ${{ matrix.shard }})
- run: |
- mkdir -p build/logs/behat
- vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --no-interaction ${{ matrix.coverage && '--profile=default-coverage' || '--profile=default' }} ${{ steps.shard.outputs.paths }}
- - name: Merge code coverage reports
- if: matrix.coverage
- run: |
- wget -qO /usr/local/bin/phpcov https://phar.phpunit.de/phpcov-12.phar
- chmod +x /usr/local/bin/phpcov
- mkdir -p build/coverage
- phpcov merge --clover build/logs/behat/clover.xml build/coverage
- - name: Upload test artifacts
- if: always()
- uses: actions/upload-artifact@v6
- with:
- name: behat-logs-php${{ matrix.php }}-shard${{ matrix.shard }}
- path: build/logs/behat
- continue-on-error: true
- - name: Upload coverage results to Codecov
- if: matrix.coverage
- uses: codecov/codecov-action@v5
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- directory: build/logs/behat
- name: behat-php${{ matrix.php }}-shard${{ matrix.shard }}
- flags: behat
- fail_ci_if_error: true
- continue-on-error: true
- - name: Upload coverage results to Coveralls
- if: matrix.coverage
- env:
- COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
- export PATH="$PATH:$HOME/.composer/vendor/bin"
- php-coveralls --coverage_clover=build/logs/behat/clover.xml
- continue-on-error: true
-
postgresql:
- name: PHPUnit + Behat (PHP ${{ matrix.php }}) (PostgreSQL)
+ name: PHPUnit (PHP ${{ matrix.php }}) (PostgreSQL)
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
@@ -581,14 +495,9 @@ jobs:
run: tests/Fixtures/app/console cache:clear --ansi
- name: Run PHPUnit tests
run: vendor/bin/phpunit
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- run: |
- vendor/bin/behat --out=std --format=progress --profile=postgres --no-interaction -vv
mysql:
- name: PHPUnit + Behat (PHP ${{ matrix.php }}) (MySQL)
+ name: PHPUnit (PHP ${{ matrix.php }}) (MySQL)
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
@@ -636,13 +545,9 @@ jobs:
run: tests/Fixtures/app/console cache:clear --ansi
- name: Run PHPUnit tests
run: vendor/bin/phpunit
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags '~@!mysql'
mongodb:
- name: PHPUnit + Behat (PHP ${{ matrix.php }}) (MongoDB)
+ name: PHPUnit (PHP ${{ matrix.php }}) (MongoDB)
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
@@ -692,33 +597,20 @@ jobs:
run: tests/Fixtures/app/console cache:clear --ansi
- name: Run PHPUnit tests
run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --exclude-group=orm
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- run: |
- mkdir -p build/logs/behat
- vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mongodb-coverage --no-interaction
- - name: Merge code coverage reports
- run: |
- wget -qO /usr/local/bin/phpcov https://phar.phpunit.de/phpcov-12.phar
- chmod +x /usr/local/bin/phpcov
- mkdir -p build/coverage
- phpcov merge --clover build/logs/behat/clover.xml build/coverage
- continue-on-error: true
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v6
with:
- name: behat-logs-php${{ matrix.php }}
- path: build/logs/behat
+ name: phpunit-logs-php${{ matrix.php }}-mongodb
+ path: build/logs/phpunit
continue-on-error: true
- name: Upload coverage results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: build/logs/behat
- name: behat-php${{ matrix.php }}
- flags: behat
+ directory: build/logs/phpunit
+ name: phpunit-php${{ matrix.php }}-mongodb
+ flags: phpunit
fail_ci_if_error: true
continue-on-error: true
- name: Upload coverage results to Coveralls
@@ -727,11 +619,11 @@ jobs:
run: |
composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
export PATH="$PATH:$HOME/.composer/vendor/bin"
- php-coveralls --coverage_clover=build/logs/behat/clover.xml
+ php-coveralls --coverage_clover=build/logs/phpunit/clover.xml
continue-on-error: true
mercure:
- name: PHPUnit + Behat (PHP ${{ matrix.php }}) (Mercure)
+ name: PHPUnit (PHP ${{ matrix.php }}) (Mercure)
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
@@ -785,31 +677,20 @@ jobs:
run: tests/Fixtures/app/console cache:clear --ansi
- name: Run PHPUnit tests
run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure
- - name: Run Behat tests
- run: |
- mkdir -p build/logs/behat
- vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=mercure-coverage --no-interaction
- - name: Merge code coverage reports
- run: |
- wget -qO /usr/local/bin/phpcov https://phar.phpunit.de/phpcov-12.phar
- chmod +x /usr/local/bin/phpcov
- mkdir -p build/coverage
- phpcov merge --clover build/logs/behat/clover.xml build/coverage
- continue-on-error: true
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@v6
with:
- name: behat-logs-php${{ matrix.php }}
- path: build/logs/behat
+ name: phpunit-logs-php${{ matrix.php }}-mercure
+ path: build/logs/phpunit
continue-on-error: true
- name: Upload coverage results to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: build/logs/behat
- name: behat-php${{ matrix.php }}
- flags: behat
+ directory: build/logs/phpunit
+ name: phpunit-php${{ matrix.php }}-mercure
+ flags: phpunit
fail_ci_if_error: true
continue-on-error: true
- name: Upload coverage results to Coveralls
@@ -818,7 +699,7 @@ jobs:
run: |
composer global require --prefer-dist --no-interaction --no-progress --ansi php-coveralls/php-coveralls
export PATH="$PATH:$HOME/.composer/vendor/bin"
- php-coveralls --coverage_clover=build/logs/behat/clover.xml
+ php-coveralls --coverage_clover=build/logs/phpunit/clover.xml
continue-on-error: true
elasticsearch:
@@ -1029,50 +910,6 @@ jobs:
- name: Run PHPUnit tests
run: vendor/bin/phpunit --fail-on-deprecation
- behat-symfony-next:
- name: Behat (PHP ${{ matrix.php }}) (Symfony dev)
- runs-on: ubuntu-latest
- timeout-minutes: 20
- strategy:
- matrix:
- php:
- - '8.5'
- fail-fast: false
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: pecl, composer:2.9.8
- extensions: intl, bcmath, curl, openssl, mbstring
- coverage: none
- ini-values: memory_limit=-1
- - name: Get composer cache directory
- id: composercache
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- - name: Allow unstable project dependencies
- run: composer config minimum-stability dev
- - name: Cache dependencies
- uses: actions/cache@v5
- with:
- path: ${{ steps.composercache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Remove cache
- run: rm -Rf tests/Fixtures/app/var/cache/*
- - name: Update project dependencies
- run: |
- composer global require soyuka/pmu
- composer global config allow-plugins.soyuka/pmu true --no-interaction
- composer global link .
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction
-
- # remove once behat can be installed with symfony 8.1
phpunit-symfony-edge:
name: PHPUnit (PHP ${{ matrix.php }}) (Symfony 8.1)
runs-on: ubuntu-latest
@@ -1099,8 +936,6 @@ jobs:
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Allow unstable project dependencies
run: composer config minimum-stability dev
- - name: Drop Behat dev dependencies (incompatible with Symfony 8.1)
- run: composer remove --no-update --no-interaction --dev behat/behat behat/mink soyuka/contexts friends-of-behat/symfony-extension friends-of-behat/mink-browserkit-driver friends-of-behat/mink-extension
- name: Force Symfony 8.1 dev for framework-bundle and json-streamer
run: composer require --dev --no-update --no-interaction "symfony/framework-bundle:8.1.x-dev" "symfony/json-streamer:8.1.x-dev"
- name: Cache dependencies
@@ -1121,59 +956,6 @@ jobs:
- name: Run PHPUnit tests
run: vendor/bin/phpunit
- windows-behat:
- name: Windows Behat (PHP ${{ matrix.php }}) (SQLite)
- runs-on: windows-latest
- timeout-minutes: 20
- strategy:
- matrix:
- php:
- - '8.5'
- fail-fast: false
- env:
- APP_ENV: sqlite
- DATABASE_URL: sqlite:///%kernel.project_dir%/var/data.db
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Setup PHP with pre-release PECL extension
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: pecl, composer:2.9.8
- extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, fileinfo
- coverage: none
- ini-values: memory_limit=-1
- - name: Get composer cache directory
- id: composercache
- shell: bash
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- - name: Cache dependencies
- uses: actions/cache@v5
- with:
- path: ${{ steps.composercache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Keep windows path
- id: get-cwd
- shell: bash
- run: |
- cwd=$(php -r 'echo(str_replace("\\", "\\\\", $_SERVER["argv"][1]));' '${{ github.workspace }}')
- echo cwd=$cwd >> $GITHUB_OUTPUT
- - name: Update project dependencies
- shell: bash
- run: |
- php -m
- composer global require soyuka/pmu
- composer global config allow-plugins.soyuka/pmu true --no-interaction
- composer global link . --working-directory='${{ steps.get-cwd.outputs.cwd }}'
- - name: Clear test app cache
- shell: bash
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- shell: bash
- run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction
-
phpunit-symfony-lowest:
name: PHPUnit (PHP ${{ matrix.php }}) (Symfony lowest)
runs-on: ubuntu-latest
@@ -1218,48 +1000,6 @@ jobs:
env:
SYMFONY_DEPRECATIONS_HELPER: max[self]=0&ignoreFile=./tests/.ignored-deprecations
- behat-symfony-lowest:
- name: Behat (PHP ${{ matrix.php }}) (Symfony lowest)
- runs-on: ubuntu-latest
- timeout-minutes: 20
- strategy:
- matrix:
- php:
- - '8.5'
- fail-fast: false
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: pecl, composer:2.9.8
- extensions: intl, bcmath, curl, openssl, mbstring
- coverage: none
- ini-values: memory_limit=-1
- - name: Get composer cache directory
- id: composercache
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- - name: Cache dependencies
- uses: actions/cache@v5
- with:
- path: ${{ steps.composercache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Remove cache
- run: rm -Rf tests/Fixtures/app/var/cache/*
- - name: Update project dependencies
- run: |
- composer global require soyuka/pmu
- composer global config allow-plugins.soyuka/pmu true --no-interaction
- composer global link . --permanent
- composer update --prefer-lowest
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests
- run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags='~@disableForSymfonyLowest'
-
phpunit_listeners:
name: PHPUnit event listeners (PHP ${{ matrix.php }})
env:
@@ -1339,56 +1079,6 @@ jobs:
php-coveralls --coverage_clover=build/logs/phpunit/clover.xml
continue-on-error: true
- behat_listeners:
- name: Behat event listeners (PHP ${{ matrix.php }})
- env:
- USE_SYMFONY_LISTENERS: 1
- runs-on: ubuntu-latest
- timeout-minutes: 20
- strategy:
- matrix:
- php:
- - '8.5'
- fail-fast: false
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- tools: pecl, composer:2.9.8
- extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite
- coverage: pcov
- ini-values: memory_limit=-1
- - name: Get composer cache directory
- id: composercache
- run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- - name: Cache dependencies
- uses: actions/cache@v5
- with:
- path: ${{ steps.composercache.outputs.dir }}
- key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
- restore-keys: ${{ runner.os }}-composer-
- - name: Update project dependencies
- run: |
- composer global require soyuka/pmu
- composer global config allow-plugins.soyuka/pmu true --no-interaction
- composer global link .
- - name: Clear test app cache
- run: tests/Fixtures/app/console cache:clear --ansi
- - name: Run Behat tests (PHP 8)
- run: |
- mkdir -p build/logs/behat
- vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=symfony_listeners --no-interaction
- - name: Upload test artifacts
- if: always()
- uses: actions/upload-artifact@v6
- with:
- name: behat-logs-php${{ matrix.php }}
- path: build/logs/behat
- continue-on-error: true
-
openapi:
name: OpenAPI
runs-on: ubuntu-latest
diff --git a/AGENTS.md b/AGENTS.md
index 4f0d29e0b27..8f0adddc594 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -6,7 +6,7 @@ You are an expert Core Contributor to API Platform, a PHP framework supporting S
* Context Retrieval (VectorCode): Before writing new code or asking for clarification, ALWAYS use vectorcode if available to search for existing patterns, interfaces, or similar implementations in the codebase.
* Test-First Mandate: Your primary output should be functional tests to expose bugs or verify features. Do not fix bugs unless explicitly requested.
-* Execution Restraint: NEVER run the full test suite (Behat or PHPUnit). It is too slow. Only run specific, filtered tests relevant to the current task.
+* Execution Restraint: NEVER run the full PHPUnit test suite. It is too slow. Only run specific, filtered tests relevant to the current task.
* Fixture Isolation: Do not modify existing fixtures (tests/Fixtures/...). Always create new Entities, DTOs, or Models to prevent regression in other tests.
* Git Policy: Do not perform git commits unless explicitly asked.
@@ -26,7 +26,7 @@ When to use:
3. Testing Quick-Reference (Default/Symfony)
-For advanced configurations (Event Listeners, MongoDB, Behat tuning), refer to `tests/AGENTS.md`.
+For advanced configurations (Event Listeners, MongoDB), refer to `tests/AGENTS.md`.
Common Commands:
@@ -43,12 +43,9 @@ rm -rf tests/Fixtures/app/var/cache/test
# indefinitely. Remove them before running tests:
find src -name vendor -exec rm -rf {} +
-# PHPUnit (Preferred)
+# PHPUnit
vendor/bin/phpunit --filter testMethodName
-# Behat (Legacy)
-vendor/bin/behat features/main/crud.feature:120 --format=progress
-
#Component Testing
cd src/Metadata
composer link ../../
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 89ba9542382..5c18c931cfe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -87,7 +87,7 @@ See also the [related documentation for Symfony](https://symfony.com/doc/current
When you send a PR, just make sure that:
-* You add valid test cases (Behat and PHPUnit).
+* You add valid test cases (PHPUnit).
* Tests are green.
* You make a PR on the related documentation in the [api-platform/docs](https://github.com/api-platform/docs) repository.
* You make the PR on the same branch you based your changes on. If you see commits
@@ -123,11 +123,11 @@ Only the first commit on a Pull Request need to use a conventional commit, other
### Tests
-On `api-platform/core` there are two kinds of tests: unit (`phpunit`) and integration tests (`behat`).
+On `api-platform/core` tests are written with `phpunit` (unit tests and functional tests under `tests/Functional`).
Note that we stopped using `prophesize` for new tests since 3.2, use `phpunit` stub system.
-Both `phpunit` and `behat` are development dependencies and should be available in the `vendor` directory.
+`phpunit` is a development dependency and should be available in the `vendor` directory.
Recommendations:
@@ -157,20 +157,11 @@ Sometimes there might be an error with too many open files when generating cover
Coverage will be available in `coverage/index.html`.
-#### Behat
+To run functional tests for MongoDB:
-> [!WARNING]
-> Please **do not add new Behat tests**, use a functional test (for example: [ComputedFieldTest](https://github.com/api-platform/core/blob/04d5cff1b28b494ac2e90257a79ce6c045ba82ae/tests/Functional/Doctrine/ComputedFieldTest.php)).
+ MONGODB_URL=mongodb://localhost:27017 APP_ENV=mongodb vendor/bin/phpunit --group mongodb
-The command to launch Behat tests is:
-
- php -d memory_limit=-1 ./vendor/bin/behat --profile=default --stop-on-failure --format=progress
-
-If you want to launch Behat tests for MongoDB, the command is:
-
- MONGODB_URL=mongodb://localhost:27017 APP_ENV=mongodb php -d memory_limit=-1 ./vendor/bin/behat --profile=mongodb --stop-on-failure --format=progress
-
-To get more details about an error, replace `--format=progress` by `-vvv`. You may run a mongo instance using docker:
+You may run a mongo instance using docker:
docker run -p 27017:27017 mongo:latest
diff --git a/behat.yml.dist b/behat.yml.dist
deleted file mode 100644
index a771434c534..00000000000
--- a/behat.yml.dist
+++ /dev/null
@@ -1,219 +0,0 @@
-default:
- suites:
- default:
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure&&~@query_parameter_validator'
- extensions:
- 'FriendsOfBehat\SymfonyExtension':
- bootstrap: 'tests/Fixtures/app/bootstrap.php'
- kernel:
- environment: 'test'
- debug: true
- class: AppKernel
- path: 'tests/Fixtures/app/AppKernel.php'
- 'Behat\MinkExtension':
- base_url: 'http://example.com/'
- files_path: 'features/files'
- sessions:
- default:
- symfony: ~
- 'Behatch\Extension': ~
-
-postgres:
- suites:
- default: false
- postgres: &postgres-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure&&~@query_parameter_validator'
-
-mongodb:
- suites:
- default: false
- mongodb: &mongodb-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@mercure&&~@controller&&~@query_parameter_validator'
-
-mercure:
- suites:
- default: false
- mercure: &mercure-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '@mercure'
-
-default-coverage:
- suites:
- default: &default-coverage-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\CoverageContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
-
-mongodb-coverage:
- suites:
- default: false
- mongodb: &mongodb-coverage-suite
- <<: *mongodb-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\CoverageContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
-
-mercure-coverage:
- suites:
- default: false
- mongodb: &mercure-coverage-suite
- <<: *mercure-suite
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\CoverageContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
-
-legacy:
- suites:
- default:
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@link_security&&~@use_listener&&~@query_parameter_validator'
- extensions:
- 'FriendsOfBehat\SymfonyExtension':
- bootstrap: 'tests/Fixtures/app/bootstrap.php'
- kernel:
- environment: 'test'
- debug: true
- class: AppKernel
- path: 'tests/Fixtures/app/AppKernel.php'
- 'Behat\MinkExtension':
- base_url: 'http://example.com/'
- files_path: 'features/files'
- sessions:
- default:
- symfony: ~
- 'Behatch\Extension': ~
-
-symfony_listeners:
- suites:
- default:
- contexts:
- - 'ApiPlatform\Tests\Behat\CommandContext'
- - 'ApiPlatform\Tests\Behat\DoctrineContext'
- - 'ApiPlatform\Tests\Behat\GraphqlContext'
- - 'ApiPlatform\Tests\Behat\JsonContext'
- - 'ApiPlatform\Tests\Behat\HydraContext'
- - 'ApiPlatform\Tests\Behat\HttpCacheContext'
- - 'ApiPlatform\Tests\Behat\JsonApiContext'
- - 'ApiPlatform\Tests\Behat\JsonHalContext'
- - 'ApiPlatform\Tests\Behat\MercureContext'
- - 'ApiPlatform\Tests\Behat\XmlContext'
- - 'Behat\MinkExtension\Context\MinkContext'
- - 'behatch:context:rest'
- filters:
- tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@mercure&&~@query_parameter_validator'
- extensions:
- 'FriendsOfBehat\SymfonyExtension':
- bootstrap: 'tests/Fixtures/app/bootstrap.php'
- kernel:
- environment: 'test'
- debug: true
- class: AppKernel
- path: 'tests/Fixtures/app/AppKernel.php'
- 'Behat\MinkExtension':
- base_url: 'http://example.com/'
- files_path: 'features/files'
- sessions:
- default:
- symfony: ~
- 'Behatch\Extension': ~
diff --git a/composer.json b/composer.json
index 7ffc3c950ee..89a068313e3 100644
--- a/composer.json
+++ b/composer.json
@@ -125,16 +125,11 @@
"willdurand/negotiation": "^3.1"
},
"require-dev": {
- "behat/behat": "^3.11",
- "behat/mink": "^1.9",
"doctrine/common": "^3.2.2",
"doctrine/dbal": "^4.0",
"doctrine/doctrine-bundle": "^2.11 || ^3.1",
"doctrine/orm": "^2.17 || ^3.0",
"elasticsearch/elasticsearch": "^7.17 || ^8.4 || ^9.0",
- "friends-of-behat/mink-browserkit-driver": "^1.3.1",
- "friends-of-behat/mink-extension": "^2.2",
- "friends-of-behat/symfony-extension": "^2.1",
"friendsofphp/php-cs-fixer": "^3.93",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"illuminate/config": "^11.0 || ^12.0 || ^13.0",
@@ -160,7 +155,6 @@
"psr/log": "^1.0 || ^2.0 || ^3.0",
"ramsey/uuid": "^4.7",
"ramsey/uuid-doctrine": "^2.0",
- "soyuka/contexts": "^3.3.10",
"soyuka/pmu": "^0.2.0",
"soyuka/stubs-mongodb": "^1.0",
"symfony/asset": "^6.4 || ^7.0 || ^8.0",
diff --git a/features/doctrine/boolean_filter.feature b/features/doctrine/boolean_filter.feature
deleted file mode 100644
index e3332eea7bd..00000000000
--- a/features/doctrine/boolean_filter.feature
+++ /dev/null
@@ -1,525 +0,0 @@
-Feature: Boolean filter on collections
- In order to retrieve ordered large collections of resources
- As a client software developer
- I need to retrieve collections with boolean value
-
- @createSchema
- Scenario: Get collection by dummyBoolean true
- Given there are 15 dummy objects with dummyBoolean true
- And there are 10 dummy objects with dummyBoolean false
- When I send a "GET" request to "/dummies?dummyBoolean=true"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyBoolean=true"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 15
-
- Scenario: Get collection by dummyBoolean true
- When I send a "GET" request to "/dummies?dummyBoolean=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyBoolean=1"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 15
-
- Scenario: Get collection by dummyBoolean false
- When I send a "GET" request to "/dummies?dummyBoolean=false"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/16$"},
- {"pattern": "^/dummies/17$"},
- {"pattern": "^/dummies/18$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyBoolean=false"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 10
-
- Scenario: Get collection by dummyBoolean false
- When I send a "GET" request to "/dummies?dummyBoolean=0"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/16$"},
- {"pattern": "^/dummies/17$"},
- {"pattern": "^/dummies/18$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyBoolean=0"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 10
-
- Scenario: Get collection by embeddedDummy.dummyBoolean true
- Given there are 15 embedded dummy objects with embeddedDummy.dummyBoolean true
- And there are 10 embedded dummy objects with embeddedDummy.dummyBoolean false
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyBoolean=true"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/1$"},
- {"pattern": "^/embedded_dummies/2$"},
- {"pattern": "^/embedded_dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyBoolean=true"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 15
-
- Scenario: Get collection by embeddedDummy.dummyBoolean true
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyBoolean=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/1$"},
- {"pattern": "^/embedded_dummies/2$"},
- {"pattern": "^/embedded_dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyBoolean=1"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 15
-
- Scenario: Get collection by embeddedDummy.dummyBoolean false
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyBoolean=false"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/16$"},
- {"pattern": "^/embedded_dummies/17$"},
- {"pattern": "^/embedded_dummies/18$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyBoolean=false"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 10
-
- Scenario: Get collection by embeddedDummy.dummyBoolean false
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyBoolean=0"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/16$"},
- {"pattern": "^/embedded_dummies/17$"},
- {"pattern": "^/embedded_dummies/18$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyBoolean=0"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 10
-
- Scenario: Get collection by association with embed relatedDummy.embeddedDummy.dummyBoolean true
- Given there are 15 embedded dummy objects with relatedDummy.embeddedDummy.dummyBoolean true
- And there are 10 embedded dummy objects with relatedDummy.embeddedDummy.dummyBoolean false
- When I send a "GET" request to "/embedded_dummies?relatedDummy.embeddedDummy.dummyBoolean=true"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/26$"},
- {"pattern": "^/embedded_dummies/27$"},
- {"pattern": "^/embedded_dummies/28$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?relatedDummy.embeddedDummy\\.dummyBoolean=true"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 15
-
- Scenario: Get collection filtered by non valid properties
- When I send a "GET" request to "/dummies?unknown=0"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?unknown=0"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 25
-
- When I send a "GET" request to "/dummies?unknown=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?unknown=1"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
- And the JSON node "hydra:totalItems" should be equal to 25
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 5 convertedBoolean objects
- When I send a "GET" request to "/converted_booleans?name_converted=false"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedBoolean"},
- "@id": {"pattern": "^/converted_booleans"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_booleans/(2|4)$"},
- "@type": {"pattern": "^ConvertedBoolean"},
- "name_converted": {"type": "boolean"},
- "id": {"type": "integer", "minimum":2, "maximum": 4}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_booleans\\?name_converted=false"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_booleans\\{\\?name_converted\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {"pattern": "^name_converted$"},
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 1,
- "maxItems": 1,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
diff --git a/features/doctrine/date_filter.feature b/features/doctrine/date_filter.feature
deleted file mode 100644
index 7d8d1a906f8..00000000000
--- a/features/doctrine/date_filter.feature
+++ /dev/null
@@ -1,636 +0,0 @@
-Feature: Date filter on collections
- In order to retrieve large collections of resources filtered by date
- As a client software developer
- I need to retrieve collections filtered by date
-
- @createSchema
- Scenario: Get collection filtered by date
- Given there are 30 dummy objects with dummyDate
- When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/28$"},
- {"pattern": "^/dummies/29$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:totalItems": {"type":"number", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:totalItems": {"type":"number", "minimum": 5, "maximum": 5},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&page=1$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"]
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28T00:00:00%2B00:00"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/28$"},
- {"pattern": "^/dummies/29$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-28T00%3A00%3A00%2B00%3A00$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05Z"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:totalItems": {"type":"number", "minimum": 5, "maximum": 5},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05Z&page=1$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"]
- }
- }
- }
- """
-
- Scenario: Search for entities within a range
- # The order should not influence the search
- When I send a "GET" request to "/dummies?dummyDate[before]=2015-04-05&dummyDate[after]=2015-04-05"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies/5$"}
- },
- "required": ["@id"]
- },
- "minItems": 1,
- "maxItems": 1
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bbefore%5D=2015-04-05&dummyDate%5Bafter%5D=2015-04-05$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-05&dummyDate[before]=2015-04-05"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies/5$"}
- },
- "required": ["@id"]
- },
- "minItems": 1,
- "maxItems": 1
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-05&dummyDate%5Bbefore%5D=2015-04-05$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- Scenario: Search for entities within an impossible range
- When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-06&dummyDate[before]=2015-04-04"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "maxItems": 0
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyDate%5Bafter%5D=2015-04-06&dummyDate%5Bbefore%5D=2015-04-04$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- Scenario: Get collection filtered by association date
- Given there are 30 dummy objects with dummyDate and relatedDummy
- When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/58$"},
- {"pattern": "^/dummies/59$"},
- {"pattern": "^/dummies/60$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28&relatedDummy_dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/58$"},
- {"pattern": "^/dummies/59$"},
- {"pattern": "^/dummies/60$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28&relatedDummy_dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28T00:00:00%2B00:00"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/58$"},
- {"pattern": "^/dummies/59$"},
- {"pattern": "^/dummies/60$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28T00%3A00%3A00%2B00%3A00$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered by association date
- Given there are 2 dummy objects with dummyDate and relatedDummy
- When I send a "GET" request to "/dummies?relatedDummy.dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "maxItems": 0
- },
- "hydra:totalItems": {"type":"number", "maximum": 0},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy\\.dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered by date that is not a datetime
- Given there are 30 dummydate objects with dummyDate
- When I send a "GET" request to "/dummy_dates?dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the JSON node "hydra:totalItems" should be equal to 3
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
-
- @createSchema
- Scenario: Get collection filtered by date that is not a datetime including null after
- Given there are 3 dummydate objects with nullable dateIncludeNullAfter
- When I send a "GET" request to "/dummy_dates?dateIncludeNullAfter[after]=2015-04-02"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullAfter" should be equal to "2015-04-02T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullAfter" should be null
- When I send a "GET" request to "/dummy_dates?dateIncludeNullAfter[before]=2015-04-02"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullAfter" should be equal to "2015-04-01T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullAfter" should be equal to "2015-04-02T00:00:00+00:00"
-
- @createSchema
- Scenario: Get collection filtered by date that is not a datetime including null before
- Given there are 3 dummydate objects with nullable dateIncludeNullBefore
- When I send a "GET" request to "/dummy_dates?dateIncludeNullBefore[before]=2015-04-01"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullBefore" should be equal to "2015-04-01T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullBefore" should be null
- When I send a "GET" request to "/dummy_dates?dateIncludeNullBefore[after]=2015-04-01"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullBefore" should be equal to "2015-04-01T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullBefore" should be equal to "2015-04-02T00:00:00+00:00"
-
- @createSchema
- Scenario: Get collection filtered by date that is not a datetime including null before and after
- Given there are 3 dummydate objects with nullable dateIncludeNullBeforeAndAfter
- When I send a "GET" request to "/dummy_dates?dateIncludeNullBeforeAndAfter[before]=2015-04-01"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullBeforeAndAfter" should be equal to "2015-04-01T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullBeforeAndAfter" should be null
- When I send a "GET" request to "/dummy_dates?dateIncludeNullBeforeAndAfter[after]=2015-04-02"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:totalItems" should be equal to 2
- And the JSON node "hydra:member[0].dateIncludeNullBeforeAndAfter" should be equal to "2015-04-02T00:00:00+00:00"
- And the JSON node "hydra:member[1].dateIncludeNullBeforeAndAfter" should be null
-
- @createSchema
- Scenario: Get collection filtered by date that is an immutable date variant
- Given there are 30 dummyimmutabledate objects with dummyDate
- When I send a "GET" request to "/dummy_immutable_dates?dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the JSON node "hydra:totalItems" should be equal to 3
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
-
- @createSchema
- Scenario: Get collection filtered by embedded date
- Given there are 29 embedded dummy objects with dummyDate and embeddedDummy
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyDate[after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/28$"},
- {"pattern": "^/embedded_dummies/29$"}
- ]
- }
- },
- "required": ["@id"]
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 30 convertedDate objects
- When I send a "GET" request to "/converted_dates?name_converted[strictly_after]=2015-04-28"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedDate"},
- "@id": {"pattern": "^/converted_dates"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_dates/(29|30)$"},
- "@type": {"pattern": "^ConvertedDate"},
- "name_converted": {"type": "string"},
- "id": {"type": "integer", "minimum":29, "maximum": 30}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_dates\\?name_converted%5Bstrictly_after%5D=2015\\-04\\-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_dates\\{\\?.*name_converted\\[before\\],name_converted\\[strictly_before\\],name_converted\\[after\\],name_converted\\[strictly_after\\].*\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {"pattern": "^name_converted(\\[(strictly_)?(before|after)\\])$"},
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 4,
- "maxItems": 4,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
diff --git a/features/doctrine/eager_loading.feature b/features/doctrine/eager_loading.feature
deleted file mode 100644
index ee7e73ff6a1..00000000000
--- a/features/doctrine/eager_loading.feature
+++ /dev/null
@@ -1,99 +0,0 @@
-@!mongodb
-Feature: Eager Loading
- In order to have better performance
- As a client software developer
- The eager loading should be enabled
-
- @createSchema
- Scenario: Eager loading for a relation
- Given there is a RelatedDummy with 2 friends
- When I send a "GET" request to "/related_dummies/1"
- Then the response status code should be 200
- And the DQL should be equal to:
- """
- SELECT o, thirdLevel_a1, relatedToDummyFriend_a3, fourthLevel_a2, dummyFriend_a4
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
- LEFT JOIN o.thirdLevel thirdLevel_a1
- LEFT JOIN thirdLevel_a1.fourthLevel fourthLevel_a2
- LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a3
- LEFT JOIN relatedToDummyFriend_a3.dummyFriend dummyFriend_a4
- WHERE o.id = :id_p1
- """
-
- Scenario: Eager loading for the search filter
- Given there is a dummy object with a fourth level relation
- When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.level=3"
- Then the response status code should be 200
- And the DQL should be equal to:
- """
- SELECT o
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy o
- INNER JOIN o.relatedDummy relatedDummy_a1
- INNER JOIN relatedDummy_a1.thirdLevel thirdLevel_a2
- WHERE o IN(
- SELECT o_a3
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy o_a3
- INNER JOIN o_a3.relatedDummy relatedDummy_a4
- INNER JOIN relatedDummy_a4.thirdLevel thirdLevel_a5
- WHERE thirdLevel_a5.level = :level_p1
- )
- ORDER BY o.id ASC
- """
-
- Scenario: Eager loading for a relation and a search filter
- Given there is a RelatedDummy with 2 friends
- When I send a "GET" request to "/related_dummies?relatedToDummyFriend.dummyFriend=2"
- Then the response status code should be 200
- And the DQL should be equal to:
- """
- SELECT o, thirdLevel_a4, relatedToDummyFriend_a1, fourthLevel_a5, dummyFriend_a6
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
- INNER JOIN o.relatedToDummyFriend relatedToDummyFriend_a1
- LEFT JOIN o.thirdLevel thirdLevel_a4
- LEFT JOIN thirdLevel_a4.fourthLevel fourthLevel_a5
- INNER JOIN relatedToDummyFriend_a1.dummyFriend dummyFriend_a6
- WHERE o IN(
- SELECT o_a2
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o_a2
- INNER JOIN o_a2.relatedToDummyFriend relatedToDummyFriend_a3
- WHERE relatedToDummyFriend_a3.dummyFriend = :dummyFriend_p1
- )
- ORDER BY o.id ASC
- """
-
- Scenario: Eager loading for a relation and a property filter with multiple relations
- Given there is a dummy travel
- When I send a "GET" request to "/dummy_travels/1?properties[]=confirmed&properties[car][]=brand&properties[passenger][]=nickname"
- Then the response status code should be 200
- And the JSON node "confirmed" should be equal to "true"
- And the JSON node "car.carBrand" should be equal to "DummyBrand"
- And the JSON node "passenger.nickname" should be equal to "Tom"
- And the DQL should be equal to:
- """
- SELECT o, car_a1, passenger_a2
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel o
- LEFT JOIN o.car car_a1
- LEFT JOIN o.passenger passenger_a2
- WHERE o.id = :id_p1
- """
-
- Scenario: Eager loading for a relation with complex sub-query filter
- Given there is a RelatedDummy with 2 friends
- When I send a "GET" request to "/related_dummies?complex_sub_query_filter=1"
- Then the response status code should be 200
- And the DQL should be equal to:
- """
- SELECT o, thirdLevel_a3, relatedToDummyFriend_a5, fourthLevel_a4, dummyFriend_a6
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
- LEFT JOIN o.thirdLevel thirdLevel_a3
- LEFT JOIN thirdLevel_a3.fourthLevel fourthLevel_a4
- LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a5
- LEFT JOIN relatedToDummyFriend_a5.dummyFriend dummyFriend_a6
- WHERE o.id IN (
- SELECT related_dummy_a1.id
- FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy related_dummy_a1
- INNER JOIN related_dummy_a1.relatedToDummyFriend related_to_dummy_friend_a2
- WITH related_to_dummy_friend_a2.name = :name_p1
- )
- ORDER BY o.id ASC
- """
diff --git a/features/doctrine/exists_filter.feature b/features/doctrine/exists_filter.feature
deleted file mode 100644
index e99f2ff445a..00000000000
--- a/features/doctrine/exists_filter.feature
+++ /dev/null
@@ -1,223 +0,0 @@
-Feature: Exists filter on collections
- In order to retrieve large collections of resources
- As a client software developer
- I need to retrieve collections with properties that exist or not
-
- @createSchema
- Scenario: Get collection where a property does not exist
- Given there are 15 dummy objects with dummyBoolean true
- When I send a "GET" request to "/dummies?exists[dummyBoolean]=0"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:totalItems": {"type":"number", "maximum": 0},
- "hydra:member": {
- "type": "array",
- "maxItems": 0
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?exists%5BdummyBoolean%5D=0$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection where a property does exist
- When I send a "GET" request to "/dummies?exists[dummyBoolean]=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:totalItems": {"type":"number", "minimum": 15, "maximum": 15},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies/(1|2|3)$"}
- },
- "required": ["@id"]
- },
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?exists%5BdummyBoolean%5D=1&page=1$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Use exists filter with a empty relation collection
- Given there are 3 dummy objects having each 0 relatedDummies
- And there are 2 dummy objects having each 3 relatedDummies
- When I send a "GET" request to "/dummies?exists[relatedDummies]=0"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:totalItems": {"type":"number", "minimum": 3, "maximum": 3},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies/(1|2|3)$"}
- },
- "required": ["@id"]
- },
- "minItems": 3,
- "maxItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?exists%5BrelatedDummies%5D=0$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Use exists filter with a non empty relation collection
- When I send a "GET" request to "/dummies?exists[relatedDummies]=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:totalItems": {"type":"number", "minimum": 2, "maximum": 2},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies/(4|5)$"}
- },
- "required": ["@id"]
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?exists%5BrelatedDummies%5D=1$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 4 convertedString objects
- When I send a "GET" request to "/converted_strings?exists[name_converted]=true"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedString"},
- "@id": {"pattern": "^/converted_strings"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_strings/(1|3)$"},
- "@type": {"pattern": "^ConvertedString"},
- "name_converted": {"pattern": "^name#(1|3)$"},
- "id": {"type": "integer", "minimum":1, "maximum": 3}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_strings\\?exists%5Bname_converted%5D=true"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_strings\\{\\?exists\\[name_converted\\]\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {"pattern": "^exists\\[name_converted\\]$"},
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 1,
- "maxItems": 1,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
diff --git a/features/doctrine/handle_links.feature b/features/doctrine/handle_links.feature
deleted file mode 100644
index ebfa7b10e4f..00000000000
--- a/features/doctrine/handle_links.feature
+++ /dev/null
@@ -1,17 +0,0 @@
-Feature: Use a link handler to retrieve a resource
-
- @createSchema
- Scenario: Get collection
- Given there are a few link handled dummies
- When I send a "GET" request to "/link_handled_dummies"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "hydra:totalItems" should be equal to 1
-
- @createSchema
- Scenario: Get item
- Given there are a few link handled dummies
- When I send a "GET" request to "/link_handled_dummies/1"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "slug" should be equal to "foo"
diff --git a/features/doctrine/issue5722/subresource_without_get.feature b/features/doctrine/issue5722/subresource_without_get.feature
deleted file mode 100644
index ff54949e926..00000000000
--- a/features/doctrine/issue5722/subresource_without_get.feature
+++ /dev/null
@@ -1,8 +0,0 @@
-Feature: Get a subresource from inverse side that has no item operation
-
- @!mongodb
- @createSchema
- Scenario: Get a subresource from inverse side that has no item operation
- Given there are logs on an event
- When I send a "GET" request to "/events/03af3507-271e-4cca-8eee-6244fb06e95b/logs"
- Then the response status code should be 200
diff --git a/features/doctrine/issue6175/standard_put_entity_inheritence.feature b/features/doctrine/issue6175/standard_put_entity_inheritence.feature
deleted file mode 100644
index 07d0d7e88cb..00000000000
--- a/features/doctrine/issue6175/standard_put_entity_inheritence.feature
+++ /dev/null
@@ -1,25 +0,0 @@
-Feature: Update properties of a resource that are inherited with standard PUT operation
-
- @!mongodb
- @createSchema
- Scenario: Update properties of a resource that are inherited with standard PUT operation
- Given there is a dummy entity with a mapped superclass
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "PUT" request to "/dummy_mapped_subclasses/1" with body:
- """
- {
- "foo": "updated value"
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON should be equal to:
- """
- {
- "@context": "/contexts/DummyMappedSubclass",
- "@id": "/dummy_mapped_subclasses/1",
- "@type": "DummyMappedSubclass",
- "id": 1,
- "foo": "updated value"
- }
- """
diff --git a/features/doctrine/multiple_filter.feature b/features/doctrine/multiple_filter.feature
deleted file mode 100644
index d98e36cf264..00000000000
--- a/features/doctrine/multiple_filter.feature
+++ /dev/null
@@ -1,48 +0,0 @@
-Feature: Multiple filters on collections
- In order to retrieve large collections of filtered resources
- As a client software developer
- I need to retrieve collections filtered by multiple parameters
-
- @createSchema
- Scenario: Get collection filtered by multiple parameters
- Given there are 30 dummy objects with dummyDate and dummyBoolean true
- And there are 20 dummy objects with dummyDate and dummyBoolean false
- When I send a "GET" request to "/dummies?dummyDate[after]=2015-04-28&dummyBoolean=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/28$"},
- {"pattern": "^/dummies/29$"}
- ]
- }
- }
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyBoolean=1&dummyDate%5Bafter%5D=2015-04-28$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
diff --git a/features/doctrine/numeric_filter.feature b/features/doctrine/numeric_filter.feature
deleted file mode 100644
index ec449ae0be7..00000000000
--- a/features/doctrine/numeric_filter.feature
+++ /dev/null
@@ -1,219 +0,0 @@
-Feature: Numeric filter on collections
- In order to retrieve ordered large collections of resources
- As a client software developer
- I need to retrieve collections with numerical value
-
- @createSchema
- Scenario: Get collection by dummyPrice=9.99
- Given there are 10 dummy objects with dummyPrice
- When I send a "GET" request to "/dummies?dummyPrice=9.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/5$"},
- {"pattern": "^/dummies/9$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 3, "maximum": 3},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice=9.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection by multiple dummyPrice
- Given there are 10 dummy objects with dummyPrice
- When I send a "GET" request to "/dummies?dummyPrice[]=9.99&dummyPrice[]=12.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/5$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 6, "maximum": 6},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5B%5D=9.99&dummyPrice%5B%5D=12.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection by non-numeric dummyPrice=marty
- Given there are 10 dummy objects with dummyPrice
- When I send a "GET" request to "/dummies?dummyPrice=marty"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 20, "maximum": 20},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice=marty"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 5 convertedInteger objects
- When I send a "GET" request to "/converted_integers?name_converted[]=2&name_converted[]=3"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedInteger$"},
- "@id": {"pattern": "^/converted_integers$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers/(2|3)$"},
- "@type": {"pattern": "^ConvertedInteger$"},
- "name_converted": {"type": "integer"},
- "id": {"type": "integer", "minimum":2, "maximum": 3}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers\\?name_converted%5B%5D=2&name_converted%5B%5D=3$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_integers\\{\\?.*name_converted,name_converted\\[\\].*\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {
- "oneOf": [
- {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"},
- {"pattern": "^order\\[name_converted\\]$"}
- ]
- },
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 8,
- "maxItems": 8,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
-
diff --git a/features/doctrine/order_filter.feature b/features/doctrine/order_filter.feature
deleted file mode 100644
index 4d3d5587b97..00000000000
--- a/features/doctrine/order_filter.feature
+++ /dev/null
@@ -1,824 +0,0 @@
-Feature: Order filter on collections
- In order to retrieve ordered large collections of resources
- As a client software developer
- I need to retrieve collections ordered properties
-
- @createSchema
- Scenario: Get collection ordered in ascending order on an integer property and on which order filter has been enabled in whitelist mode
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?order[id]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bid%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered in descending order on an integer property and on which order filter has been enabled in whitelist mode
- When I send a "GET" request to "/dummies?order[id]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/30$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/29$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/28$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bid%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered in ascending order on a string property and on which order filter has been enabled in whitelist mode
- When I send a "GET" request to "/dummies?order[name]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/10$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/11$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bname%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered in descending order on a string property and on which order filter has been enabled in whitelist mode
- When I send a "GET" request to "/dummies?order[name]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/9$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/8$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/7$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bname%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered collection on several property keep the order
- # Adding 30 more data with the same name
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?order[name]=desc&order[id]=desc"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/39$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/9$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/38$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bname%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered in ascending order on an association and on which order filter has been enabled in whitelist mode
- Given there are 30 dummy objects with relatedDummy
- When I send a "GET" request to "/dummies?order[relatedDummy]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5BrelatedDummy%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered in ascending order on an embedded and on which order filter has been enabled in whitelist mode
- Given there are 30 dummy objects with embeddedDummy
- When I send a "GET" request to "/embedded_dummies?order[embeddedDummy]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/embedded_dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/embedded_dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/embedded_dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?order%5BembeddedDummy%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Get collection ordered by default configured order on a embedded string property and on which order filter has been enabled in whitelist mode with default descending order
- When I send a "GET" request to "/embedded_dummies?order[embeddedDummy.dummyName]"
- Then the response status code should be 422
-
- Scenario: Get collection ordered by a non valid properties and on which order filter has been enabled in whitelist mode
- When I send a "GET" request to "/dummies?order[alias]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Balias%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?order[alias]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Balias%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?order[unknown]=asc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bunknown%5D=asc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?order[unknown]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/3$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 3,
- "minItems": 3
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5Bunknown%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection ordered in descending order on a related property
- Given there are 2 dummy objects with relatedDummy
- When I send a "GET" request to "/dummies?order[relatedDummy.name]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/2$"
- }
- }
- },
- {
- "type": "object",
- "properties": {
- "@id": {
- "type": "string",
- "pattern": "^/dummies/1$"
- }
- }
- }
- ],
- "additionalItems": false,
- "maxItems": 2,
- "minItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?order%5BrelatedDummy.name%5D=desc"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 3 convertedInteger objects
- When I send a "GET" request to "/converted_integers?order[name_converted]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedInteger$"},
- "@id": {"pattern": "^/converted_integers$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": [
- {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers/3$"},
- "@type": {"pattern": "^ConvertedInteger$"},
- "name_converted": {"type": "integer"},
- "id": {"type": "integer", "minimum":3, "maximum": 3}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers/2$"},
- "@type": {"pattern": "^ConvertedInteger$"},
- "name_converted": {"type": "integer"},
- "id": {"type": "integer", "minimum":2, "maximum": 2}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers/1$"},
- "@type": {"pattern": "^ConvertedInteger$"},
- "name_converted": {"type": "integer"},
- "id": {"type": "integer", "minimum":1, "maximum": 1}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- }
- ],
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 3, "maximum": 3},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers\\?order%5Bname_converted%5D=desc$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_integers\\{\\?.*order\\[name_converted\\].*\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {
- "oneOf": [
- {"pattern": "^order\\[name_converted\\]$"},
- {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"}
- ]
- },
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 8,
- "maxItems": 8,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
-
- # See https://github.com/api-platform/core/pull/3673
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 3 convertedInteger objects
- When I send a "GET" request to "/converted_integers?order[]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
diff --git a/features/doctrine/range_filter.feature b/features/doctrine/range_filter.feature
deleted file mode 100644
index 9a9ec12d074..00000000000
--- a/features/doctrine/range_filter.feature
+++ /dev/null
@@ -1,506 +0,0 @@
-Feature: Range filter on collections
- In order to filter results from large collections of resources
- As a client software developer
- I need to filter collections by range
-
- @createSchema
- Scenario: Get collection filtered by range (between)
- Given there are 30 dummy objects with dummyPrice
- When I send a "GET" request to "/dummies?dummyPrice[between]=12.99..15.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"},
- {"pattern": "^/dummies/6$"},
- {"pattern": "^/dummies/7$"},
- {"pattern": "^/dummies/10$"},
- {"pattern": "^/dummies/11$"},
- {"pattern": "^/dummies/14$"},
- {"pattern": "^/dummies/15$"},
- {"pattern": "^/dummies/18$"},
- {"pattern": "^/dummies/19$"},
- {"pattern": "^/dummies/22$"},
- {"pattern": "^/dummies/23$"},
- {"pattern": "^/dummies/26$"},
- {"pattern": "^/dummies/27$"},
- {"pattern": "^/dummies/30$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 15, "maximum": 15},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bbetween%5D=12.99..15.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered by range (between the same values)
- Given there are 30 dummy objects with dummyPrice
- When I send a "GET" request to "/dummies?dummyPrice[between]=12.99..12.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/6$"},
- {"pattern": "^/dummies/10$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 8, "maximum": 8},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bbetween%5D=12.99..12.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter by range (between) with invalid format
- When I send a "GET" request to "/dummies?dummyPrice[between]=9.99..12.99..15.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "pattern": "^/dummies/([1-9]|[12][0-9]|30)$"
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 30, "maximum": 30},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bbetween%5D=9.99..12.99..15.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities by range (less than)
- When I send a "GET" request to "/dummies?dummyPrice[lt]=12.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/5$"},
- {"pattern": "^/dummies/9$"},
- {"pattern": "^/dummies/13$"},
- {"pattern": "^/dummies/17$"},
- {"pattern": "^/dummies/21$"},
- {"pattern": "^/dummies/25$"},
- {"pattern": "^/dummies/29$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 8, "maximum": 8},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Blt%5D=12.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities by range (less than or equal)
- When I send a "GET" request to "/dummies?dummyPrice[lte]=12.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/5$"},
- {"pattern": "^/dummies/6$"},
- {"pattern": "^/dummies/9$"},
- {"pattern": "^/dummies/10$"},
- {"pattern": "^/dummies/13$"},
- {"pattern": "^/dummies/14$"},
- {"pattern": "^/dummies/17$"},
- {"pattern": "^/dummies/18$"},
- {"pattern": "^/dummies/21$"},
- {"pattern": "^/dummies/22$"},
- {"pattern": "^/dummies/25$"},
- {"pattern": "^/dummies/26$"},
- {"pattern": "^/dummies/29$"},
- {"pattern": "^/dummies/30$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 16, "maximum": 16},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Blte%5D=12.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities by range (greater than)
- When I send a "GET" request to "/dummies?dummyPrice[gt]=15.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/4$"},
- {"pattern": "^/dummies/8$"},
- {"pattern": "^/dummies/12$"},
- {"pattern": "^/dummies/15$"},
- {"pattern": "^/dummies/20$"},
- {"pattern": "^/dummies/24$"},
- {"pattern": "^/dummies/28$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 7, "maximum": 7},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bgt%5D=15.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities by range (greater than or equal)
- When I send a "GET" request to "/dummies?dummyPrice[gte]=15.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/3$"},
- {"pattern": "^/dummies/4$"},
- {"pattern": "^/dummies/7$"},
- {"pattern": "^/dummies/8$"},
- {"pattern": "^/dummies/11$"},
- {"pattern": "^/dummies/12$"},
- {"pattern": "^/dummies/14$"},
- {"pattern": "^/dummies/15$"},
- {"pattern": "^/dummies/19$"},
- {"pattern": "^/dummies/20$"},
- {"pattern": "^/dummies/23$"},
- {"pattern": "^/dummies/24$"},
- {"pattern": "^/dummies/27$"},
- {"pattern": "^/dummies/28$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 14, "maximum": 14},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bgte%5D=15.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities by range (greater than and less than)
- When I send a "GET" request to "/dummies?dummyPrice[gt]=12.99&dummyPrice[lt]=19.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/3$"},
- {"pattern": "^/dummies/7$"},
- {"pattern": "^/dummies/11$"},
- {"pattern": "^/dummies/15$"},
- {"pattern": "^/dummies/19$"},
- {"pattern": "^/dummies/23$"},
- {"pattern": "^/dummies/27$"}
- ]
- }
- }
- },
- "minItems": 3,
- "maxItems": 3,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "number", "minimum": 7, "maximum": 7},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bgt%5D=12.99&dummyPrice%5Blt%5D=19.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Filter for entities within an impossible range
- When I send a "GET" request to "/dummies?dummyPrice[gt]=19.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "maxItems": 0
- },
- "hydra:totalItems": {"type": "number", "maximum": 0},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?dummyPrice%5Bgt%5D=19.99$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection filtered using a name converter
- Given there are 5 convertedInteger objects
- When I send a "GET" request to "/converted_integers?name_converted[lte]=2"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedInteger$"},
- "@id": {"pattern": "^/converted_integers$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers/(1|2)$"},
- "@type": {"pattern": "^ConvertedInteger$"},
- "name_converted": {"type": "integer"},
- "id": {"type": "integer", "minimum":1, "maximum": 2}
- },
- "required": ["@id", "@type", "name_converted", "id"],
- "additionalProperties": false
- },
- "minItems": 2,
- "maxItems": 2,
- "uniqueItems": true
- },
- "hydra:totalItems": {"type": "integer", "minimum": 2, "maximum": 2},
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_integers\\?name_converted%5Blte%5D=2$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- },
- "required": ["@id", "@type"],
- "additionalProperties": false
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_integers\\{\\?.*name_converted\\[between\\],name_converted\\[gt\\],name_converted\\[gte\\],name_converted\\[lt\\],name_converted\\[lte\\].*\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {
- "oneOf": [
- {"pattern": "^name_converted(\\[(between|gt|gte|lt|lte)?\\])?$"},
- {"pattern": "^order\\[name_converted\\]$"}
- ]
- },
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "minItems": 8,
- "maxItems": 8,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
diff --git a/features/doctrine/search_filter.feature b/features/doctrine/search_filter.feature
deleted file mode 100644
index 50718f81963..00000000000
--- a/features/doctrine/search_filter.feature
+++ /dev/null
@@ -1,1066 +0,0 @@
-Feature: Search filter on collections
- In order to get specific result from a large collections of resources
- As a client software developer
- I need to search for collections properties
-
- @createSchema
- Scenario: Test ManyToMany with filter on join table
- Given there is a RelatedDummy with 4 friends
- When I add "Accept" header equal to "application/hal+json"
- And I send a "GET" request to "/related_dummies?relatedToDummyFriend.dummyFriend=/dummy_friends/4"
- Then the response status code should be 200
- And the JSON node "_embedded.item" should have 1 element
- And the JSON node "_embedded.item[0].id" should be equal to the number 1
- And the JSON node "_embedded.item[0]._links.relatedToDummyFriend" should have 4 elements
- And the JSON node "_embedded.item[0]._embedded.relatedToDummyFriend" should have 4 elements
-
- @createSchema
- Scenario: Test #944
- Given there is a DummyCar entity with related colors
- When I send a "GET" request to "/dummy_cars?colors.prop=red"
- Then the response status code should be 200
- And the JSON should be equal to:
- """
- {
- "@context": "/contexts/DummyCar",
- "@id": "/dummy_cars",
- "@type": "hydra:Collection",
- "hydra:member": [
- {
- "@id": "/dummy_cars/1",
- "@type": "DummyCar",
- "colors": [
- {
- "@id": "/dummy_car_colors/1",
- "@type": "DummyCarColor",
- "prop": "red"
- },
- {
- "@id": "/dummy_car_colors/2",
- "@type": "DummyCarColor",
- "prop": "blue"
- }
- ],
- "secondColors": [
- {
- "@id": "/dummy_car_colors/1",
- "@type": "DummyCarColor",
- "prop": "red"
- },
- {
- "@id": "/dummy_car_colors/2",
- "@type": "DummyCarColor",
- "prop": "blue"
- }
- ],
- "thirdColors": [
- {
- "@id": "/dummy_car_colors/1",
- "@type": "DummyCarColor",
- "prop": "red"
- },
- {
- "@id": "/dummy_car_colors/2",
- "@type": "DummyCarColor",
- "prop": "blue"
- }
- ],
- "uuid": [],
- "carBrand": "DummyBrand"
- }
- ],
- "hydra:totalItems": 1,
- "hydra:view": {
- "@id": "/dummy_cars?colors.prop=red",
- "@type": "hydra:PartialCollectionView"
- },
- "hydra:search": {
- "@type": "hydra:IriTemplate",
- "hydra:template": "/dummy_cars{?availableAt[before],availableAt[strictly_before],availableAt[after],availableAt[strictly_after],canSell,foobar[],foobargroups[],foobargroups_override[],colors.prop,colors,colors[],secondColors,secondColors[],thirdColors,thirdColors[],uuid,uuid[],name,brand,brand[]}",
- "hydra:variableRepresentation": "BasicRepresentation",
- "hydra:mapping": [
- {
- "@type": "IriTemplateMapping",
- "variable": "availableAt[before]",
- "property": "availableAt",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "availableAt[strictly_before]",
- "property": "availableAt",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "availableAt[after]",
- "property": "availableAt",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "availableAt[strictly_after]",
- "property": "availableAt",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "canSell",
- "property": "canSell",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "foobar[]",
- "property": null,
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "foobargroups[]",
- "property": null,
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "foobargroups_override[]",
- "property": null,
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "colors.prop",
- "property": "colors.prop",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "colors",
- "property": "colors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "colors[]",
- "property": "colors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "secondColors",
- "property": "secondColors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "secondColors[]",
- "property": "secondColors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "thirdColors",
- "property": "thirdColors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "thirdColors[]",
- "property": "thirdColors",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "uuid",
- "property": "uuid",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "uuid[]",
- "property": "uuid",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "name",
- "property": "name",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "brand",
- "property": "brand",
- "required": false
- },
- {
- "@type": "IriTemplateMapping",
- "variable": "brand[]",
- "property": "brand",
- "required": false
- }
- ]
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by name (partial)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?name=my"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?name=my"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by name (partial)
- Given there are 30 embedded dummy objects
- When I send a "GET" request to "/embedded_dummies?embeddedDummy.dummyName=my"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/EmbeddedDummy$"},
- "@id": {"pattern": "^/embedded_dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/embedded_dummies/1$"},
- {"pattern": "^/embedded_dummies/2$"},
- {"pattern": "^/embedded_dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/embedded_dummies\\?embeddedDummy\\.dummyName=my"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by name (partial multiple values)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?name[]=2&name[]=3"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"},
- {"pattern": "^/dummies/12$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?name%5B%5D=2&name%5B%5D=3"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by name (partial case insensitive)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?dummy=somedummytest1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "dummy": {
- "pattern": "^SomeDummyTest\\d{1,2}$"
- }
- }
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by alias (start)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?alias=Ali"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?alias=Ali"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection by alias (start multiple values)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?description[]=Sma&description[]=Not"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?description%5B%5D=Sma&description%5B%5D=Not"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @sqlite
- @createSchema
- Scenario: Search collection by description (word_start)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?description=smart"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?description=smart"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- @sqlite
- Scenario: Search collection by description (word_start multiple values)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?description[]=smart&description[]=so"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?description%5B%5D=smart&description%5B%5D=so"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- # note on Postgres compared to sqlite the LIKE clause is case sensitive
- @postgres
- @createSchema
- Scenario: Search collection by description (word_start)
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?description=smart"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/4$"},
- {"pattern": "^/dummies/6$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?description=smart"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search for entities within an impossible range
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?name=MuYm"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "maxItems": 0
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?name=MuYm$"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @sqlite
- @createSchema
- Scenario: Search for entities with an existing collection route name
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?relatedDummies=dummy_cars"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array"
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummies=dummy_cars"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search related collection by name
- Given there are 3 dummy objects having each 3 relatedDummies
- When I add "Accept" header equal to "application/hal+json"
- And I send a "GET" request to "/dummies?relatedDummies.name=RelatedDummy1"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "_embedded.item" should have 3 elements
- And the JSON node "_embedded.item[0]._links.relatedDummies" should have 3 elements
- And the JSON node "_embedded.item[1]._links.relatedDummies" should have 3 elements
- And the JSON node "_embedded.item[2]._links.relatedDummies" should have 3 elements
-
- @createSchema
- Scenario: Search by related collection id
- Given there are 2 dummy objects having each 2 relatedDummies
- When I add "Accept" header equal to "application/hal+json"
- And I send a "GET" request to "/dummies?relatedDummies=3"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "totalItems" should be equal to "1"
- And the JSON node "_links.item" should have 1 element
- And the JSON node "_links.item[0].href" should be equal to "/dummies/2"
-
- @createSchema
- Scenario: Get collection by id equals 9.99 which is not possible
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?id=9.99"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?id=9.99"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection by id 10
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?id=10"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/10$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?id=10"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Get collection ordered by a non valid properties
- When I send a "GET" request to "/dummies?unknown=0"
- Given there are 30 dummy objects
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?unknown=0"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- When I send a "GET" request to "/dummies?unknown=1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/1$"},
- {"pattern": "^/dummies/2$"},
- {"pattern": "^/dummies/3$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?unknown=1"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Search at third level
- Given there is a dummy object with a fourth level relation
- When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.level=3"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/31$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy.thirdLevel.level=3"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- Scenario: Search at fourth level
- When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.level=4"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/31$"}
- ]
- }
- }
- }
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?relatedDummy.thirdLevel.fourthLevel.level=4"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Search collection on a property using a name converted
- Given there are 30 dummy objects
- When I send a "GET" request to "/dummies?name_converted=Converted 3"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/Dummy$"},
- "@id": {"pattern": "^/dummies$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/dummies/3$"},
- {"pattern": "^/dummies/30$"}
- ]
- },
- "required": ["@id"]
- }
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/dummies\\?name_converted=Converted%203"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/dummies\\{\\?.*name_converted.*}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {"pattern": "^name_converted$"},
- "property": {"pattern": "^name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "additionalItems": true,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
-
-
- @createSchema
- Scenario: Search collection on a property using a nested name converted
- Given there are 30 convertedOwner objects with convertedRelated
- When I send a "GET" request to "/converted_owners?name_converted.name_converted=Converted 3"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/ConvertedOwner$"},
- "@id": {"pattern": "^/converted_owners$"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@id": {
- "oneOf": [
- {"pattern": "^/converted_owners/3$"},
- {"pattern": "^/converted_owners/30$"}
- ]
- },
- "name_converted": {
- "oneOf": [
- {"pattern": "^/converted_relateds/3$"},
- {"pattern": "^/converted_relateds/30$"}
- ]
- },
- "required": ["@id", "name_converted"]
- }
- },
- "minItems": 2,
- "maxItems": 2
- },
- "hydra:view": {
- "type": "object",
- "properties": {
- "@id": {"pattern": "^/converted_owners\\?name_converted.name_converted=Converted%203"},
- "@type": {"pattern": "^hydra:PartialCollectionView$"}
- }
- },
- "hydra:search": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^hydra:IriTemplate$"},
- "hydra:template": {"pattern": "^/converted_owners\\{\\?.*name_converted\\.name_converted.*\\}$"},
- "hydra:variableRepresentation": {"pattern": "^BasicRepresentation$"},
- "hydra:mapping": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "@type": {"pattern": "^IriTemplateMapping$"},
- "variable": {"pattern": "^name_converted\\.name_converted"},
- "property": {"pattern": "^name_converted\\.name_converted$"},
- "required": {"type": "boolean"}
- },
- "required": ["@type", "variable", "property", "required"],
- "additionalProperties": false
- },
- "additionalItems": true,
- "uniqueItems": true
- }
- },
- "additionalProperties": false,
- "required": ["@type", "hydra:template", "hydra:variableRepresentation", "hydra:mapping"]
- },
- "additionalProperties": false,
- "required": ["@context", "@id", "@type", "hydra:member", "hydra:totalItems", "hydra:view", "hydra:search"]
- }
- }
- """
-
- @createSchema
- Scenario: Search by date (#4128)
- Given there are 3 dummydate objects with dummyDate
- When I send a "GET" request to "/dummy_dates?dummyDate=2015-04-01"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "hydra:totalItems" should be equal to 1
-
- @!mongodb
- @createSchema
- Scenario: Custom search filters can use Doctrine Expressions as join conditions
- Given there is a dummy object with 3 relatedDummies and their thirdLevel
- When I send a "GET" request to "/dummy_resource_with_custom_filter?custom=3"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "hydra:totalItems" should be equal to 1
-
- @!mongodb
- @createSchema
- Scenario: Search on nested sub-entity that doesn't use "id" as its ORM identifier
- Given there is a dummy entity with a sub entity with id "stringId" and name "someName"
- When I send a "GET" request to "/dummy_with_subresource?subEntity=/dummy_subresource/stringId"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "hydra:totalItems" should be equal to 1
-
- @!mongodb
- @createSchema
- Scenario: Filters can use UUIDs
- Given there is a group object with uuid "61817181-0ecc-42fb-a6e7-d97f2ddcb344" and 2 users
- And there is a group object with uuid "32510d53-f737-4e70-8d9d-58e292c871f8" and 1 users
- When I send a "GET" request to "/issue5735/issue5735_users?groups[]=/issue5735/groups/61817181-0ecc-42fb-a6e7-d97f2ddcb344&groups[]=/issue5735/groups/32510d53-f737-4e70-8d9d-58e292c871f8"
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "hydra:totalItems" should be equal to 3
diff --git a/features/doctrine/separated_resource.feature b/features/doctrine/separated_resource.feature
deleted file mode 100644
index 90ba193fd68..00000000000
--- a/features/doctrine/separated_resource.feature
+++ /dev/null
@@ -1,116 +0,0 @@
-Feature: Use state options to use an entity that is not a resource
- In order to work with resources and a doctrine entity
- As a client software developer
- I need to retrieve a CRUD by specifying an entity class
-
- @!mongodb
- @createSchema
- Scenario: Get collection
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_entities"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- Then the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/SeparatedEntity"},
- "@id": {"pattern": "^/separated_entities"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object"
- }
- },
- "hydra:totalItems": {"type":"number"},
- "hydra:view": {
- "type": "object"
- }
- }
- }
- """
-
- @!mongodb
- @createSchema
- Scenario: Get ordered collection
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_entities?order[value]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:member[0].value" should be equal to "5"
-
- @!mongodb
- @createSchema
- Scenario: Get item
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_entities/1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
-
- @!mongodb
- @createSchema
- Scenario: Get all EntityClassAndCustomProviderResources
- Given there are 1 separated entities
- When I send a "GET" request to "/entityClassAndCustomProviderResources"
- Then the response status code should be 200
-
- @!mongodb
- @createSchema
- Scenario: Get one EntityClassAndCustomProviderResource
- Given there are 1 separated entities
- When I send a "GET" request to "/entityClassAndCustomProviderResources/1"
- Then the response status code should be 200
-
- @mongodb
- @createSchema
- Scenario: Get collection
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_documents"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- Then the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "@context": {"pattern": "^/contexts/SeparatedDocument"},
- "@id": {"pattern": "^/separated_documents"},
- "@type": {"pattern": "^hydra:Collection$"},
- "hydra:member": {
- "type": "array",
- "items": {
- "type": "object"
- }
- },
- "hydra:totalItems": {"type":"number"},
- "hydra:view": {
- "type": "object"
- }
- }
- }
- """
-
- @mongodb
- @createSchema
- Scenario: Get ordered collection
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_documents?order[value]=desc"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
- And the JSON node "hydra:member[0].value" should be equal to "5"
-
- @mongodb
- @createSchema
- Scenario: Get item
- Given there are 5 separated entities
- When I send a "GET" request to "/separated_documents/1"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8"
diff --git a/features/graphql/authorization.feature b/features/graphql/authorization.feature
deleted file mode 100644
index f1e918b5242..00000000000
--- a/features/graphql/authorization.feature
+++ /dev/null
@@ -1,576 +0,0 @@
-Feature: Authorization checking
- In order to use the GraphQL API
- As a client software user
- I need to be authorized to access a given resource.
-
- @createSchema
- Scenario: An anonymous user tries to retrieve a secured item
- Given there are 1 SecuredDummy objects
- When I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- title
- description
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.securedDummy" should be null
-
- Scenario: An anonymous user tries to retrieve a secured collection
- Given there are 1 SecuredDummy objects
- When I send the following GraphQL request:
- """
- {
- securedDummies {
- edges {
- node {
- title
- description
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.securedDummies" should be null
-
- Scenario: An admin can retrieve a secured collection
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummies {
- edges {
- node {
- title
- description
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummies" should exist
- And the JSON node "data.securedDummies" should not be null
-
- Scenario: An anonymous user cannot retrieve a secured collection
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummies {
- edges {
- node {
- title
- description
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummies" should be null
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.securedDummies" should be null
-
- Scenario: An anonymous user tries to create a resource they are not allowed to
- When I send the following GraphQL request:
- """
- mutation {
- createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", adminOnlyProperty: "secret", clientMutationId: "auth"}) {
- securedDummy {
- title
- owner
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Only admins can create a secured dummy."
- And the JSON node "data.createSecuredDummy" should be null
-
- @createSchema
- Scenario: An admin can access a secured collection relation
- Given there are 1 SecuredDummy objects owned by admin with related dummies
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedDummies {
- edges {
- node {
- id
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedDummies" should have 1 element
-
- Scenario: An admin can access a secured relation
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedDummy {
- id
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedDummy" should exist
- And the JSON node "data.securedDummy.relatedDummy" should not be null
-
- @createSchema
- Scenario: A user can't access a secured collection relation
- Given there are 1 SecuredDummy objects owned by dunglas with related dummies
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedDummies {
- edges {
- node {
- id
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedDummies" should be null
-
- Scenario: A user can't access a secured relation
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedDummy {
- id
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedDummy" should be null
-
- Scenario: A user can't access a secured relation resource directly
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- relatedSecuredDummy(id: "/related_secured_dummies/1") {
- id
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.relatedSecuredDummy" should be null
-
- Scenario: A user can't access a secured relation resource collection directly
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- relatedSecuredDummies {
- edges {
- node {
- id
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.relatedSecuredDummies" should be null
-
- Scenario: A user can access a secured collection relation
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedSecuredDummies {
- edges {
- node {
- id
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedSecuredDummies" should have 1 element
-
- Scenario: A user can access a secured relation
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- relatedSecuredDummy {
- id
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.relatedSecuredDummy" should exist
- And the JSON node "data.securedDummy.relatedSecuredDummy" should not be null
-
- Scenario: A user can access a non-secured collection relation
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- publicRelatedSecuredDummies {
- edges {
- node {
- id
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.publicRelatedSecuredDummies" should have 1 element
-
- Scenario: A user can access a non-secured relation
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- When I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- publicRelatedSecuredDummy {
- id
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.publicRelatedSecuredDummy" should exist
- And the JSON node "data.securedDummy.publicRelatedSecuredDummy" should not be null
-
- @createSchema
- Scenario: An admin can create a secured resource
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- mutation {
- createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) {
- securedDummy {
- id
- title
- owner
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createSecuredDummy.securedDummy.owner" should be equal to "someone"
-
- Scenario: An admin can create another secured resource
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- mutation {
- createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) {
- securedDummy {
- id
- title
- owner
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createSecuredDummy.securedDummy.owner" should be equal to "dunglas"
-
- Scenario: An admin can create a secured resource with an owner-only property if they will be the owner
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- mutation {
- createSecuredDummy(input: {owner: "admin", title: "Hi", description: "Desc", adminOnlyProperty: "secret", ownerOnlyProperty: "it works"}) {
- securedDummy {
- ownerOnlyProperty
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createSecuredDummy.securedDummy.ownerOnlyProperty" should be equal to the string "it works"
- And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummies {
- edges {
- node {
- ownerOnlyProperty
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.securedDummies.edges[2].node.ownerOnlyProperty" should be equal to "it works"
-
- Scenario: An admin can't create a secured resource with an owner-only property if they won't be the owner
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- mutation {
- createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc", adminOnlyProperty: "secret", ownerOnlyProperty: "should not be set"}) {
- securedDummy {
- ownerOnlyProperty
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createSecuredDummy.securedDummy.ownerOnlyProperty" should exist
- And the JSON node "data.createSecuredDummy.securedDummy.ownerOnlyProperty" should be null
- And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/4") {
- ownerOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.securedDummy.ownerOnlyProperty" should be equal to ""
-
- Scenario: A user cannot retrieve an item they doesn't own
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/1") {
- owner
- title
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.securedDummy" should be null
-
- Scenario: A user can retrieve an item they owns
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- owner
- title
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.owner" should be equal to the string "dunglas"
-
- Scenario: An admin can see a secured admin-only property on an object they don't own
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- owner
- title
- adminOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.adminOnlyProperty" should exist
- And the JSON node "data.securedDummy.adminOnlyProperty" should not be null
-
- Scenario: A user can't see a secured admin-only property on an object they own
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- owner
- title
- adminOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.adminOnlyProperty" should be null
-
- Scenario: A user can see a secured owner-only property on an object they own
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- ownerOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.ownerOnlyProperty" should exist
- And the JSON node "data.securedDummy.ownerOnlyProperty" should not be null
-
- Scenario: A user can update a secured owner-only property on an object they own
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- mutation {
- updateSecuredDummy(input: {id: "/secured_dummies/2", ownerOnlyProperty: "updated"}) {
- securedDummy {
- ownerOnlyProperty
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateSecuredDummy.securedDummy.ownerOnlyProperty" should be equal to the string "updated"
- And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- ownerOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.securedDummy.ownerOnlyProperty" should be equal to the string "updated"
-
- Scenario: An admin can't see a secured owner-only property on an object they don't own
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- {
- securedDummy(id: "/secured_dummies/2") {
- ownerOnlyProperty
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.securedDummy.ownerOnlyProperty" should be null
-
- Scenario: A user can't assign to themself an item they doesn't own
- When I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
- And I send the following GraphQL request:
- """
- mutation {
- updateSecuredDummy(input: {id: "/secured_dummies/1", owner: "kitten"}) {
- securedDummy {
- id
- title
- owner
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.updateSecuredDummy" should be null
-
- Scenario: A user can update an item they owns and transfer it
- When I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
- And I send the following GraphQL request:
- """
- mutation {
- updateSecuredDummy(input: {id: "/secured_dummies/2", owner: "vincent"}) {
- securedDummy {
- id
- title
- owner
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateSecuredDummy.securedDummy.owner" should be equal to the string "vincent"
diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature
deleted file mode 100644
index afc2dc097ec..00000000000
--- a/features/graphql/collection.feature
+++ /dev/null
@@ -1,1109 +0,0 @@
-Feature: GraphQL collection support
-
- @createSchema
- Scenario: Retrieve a collection through a GraphQL query
- Given there are 4 dummy objects with relatedDummy and its thirdLevel
- When I send the following GraphQL request:
- """
- {
- dummies {
- ...dummyFields
- }
- }
- fragment dummyFields on DummyCursorConnection {
- edges {
- node {
- id
- name
- relatedDummy {
- name
- thirdLevel {
- id
- level
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[2].node.name" should be equal to "Dummy #3"
- And the JSON node "data.dummies.edges[2].node.relatedDummy.name" should be equal to "RelatedDummy #3"
- And the JSON node "data.dummies.edges[2].node.relatedDummy.thirdLevel.level" should be equal to 3
-
- @createSchema
- Scenario: Retrieve an nonexistent collection through a GraphQL query
- When I send the following GraphQL request:
- """
- {
- dummies {
- edges {
- node {
- name
- }
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 0 element
- And the JSON node "data.dummies.pageInfo.endCursor" should be null
- And the JSON node "data.dummies.pageInfo.startCursor" should be null
- And the JSON node "data.dummies.pageInfo.hasNextPage" should be false
- And the JSON node "data.dummies.pageInfo.hasPreviousPage" should be false
-
- @createSchema
- Scenario: Retrieve a collection with a nested collection through a GraphQL query
- Given there are 4 dummy objects having each 3 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies {
- edges {
- node {
- name
- relatedDummies {
- edges {
- node {
- name
- }
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[2].node.name" should be equal to "Dummy #3"
- And the JSON node "data.dummies.edges[2].node.relatedDummies.edges[1].node.name" should be equal to "RelatedDummy23"
-
- @createSchema
- Scenario: Retrieve a collection with a nested collection (inverse side) through a GraphQL query
- Given there is a video game with music groups
- When I send the following GraphQL request:
- """
- {
- musicGroups {
- edges {
- node {
- name
- videoGames {
- edges {
- node {
- name
- }
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.musicGroups.edges[0].node.name" should be equal to "Sum 41"
- And the JSON node "data.musicGroups.edges[0].node.videoGames.edges[0].node.name" should be equal to "Guitar Hero"
- And the JSON node "data.musicGroups.edges[1].node.name" should be equal to "Franz Ferdinand"
- And the JSON node "data.musicGroups.edges[1].node.videoGames.edges[0].node.name" should be equal to "Guitar Hero"
-
- @createSchema
- Scenario: Retrieve a collection and an item through a GraphQL query
- Given there are 3 dummy objects with dummyDate
- And there are 2 dummy group objects
- When I send the following GraphQL request:
- """
- {
- dummies {
- edges {
- node {
- name
- dummyDate
- }
- }
- }
- dummyGroup(id: "/dummy_groups/2") {
- foo
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[1].node.name" should be equal to "Dummy #2"
- And the JSON node "data.dummies.edges[1].node.dummyDate" should be equal to "2015-04-02"
- And the JSON node "data.dummyGroup.foo" should be equal to "Foo #2"
-
- @createSchema
- Scenario: Retrieve a specific number of items in a collection through a GraphQL query
- Given there are 4 dummy objects
- When I send the following GraphQL request:
- """
- {
- dummies(first: 2) {
- edges {
- node {
- name
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 2 elements
-
- @createSchema
- Scenario: Retrieve a specific number of items in a nested collection through a GraphQL query
- Given there are 2 dummy objects having each 5 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies(first: 1) {
- edges {
- node {
- name
- relatedDummies(first: 2) {
- edges {
- node {
- name
- }
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 2 elements
-
- @createSchema
- Scenario: Paginate through collections through a GraphQL query
- Given there are 4 dummy objects having each 4 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies(first: 2) {
- edges {
- node {
- name
- relatedDummies(first: 2) {
- edges {
- node {
- name
- }
- cursor
- }
- totalCount
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- cursor
- }
- totalCount
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.pageInfo.endCursor" should be equal to "MQ=="
- And the JSON node "data.dummies.pageInfo.hasNextPage" should be true
- And the JSON node "data.dummies.totalCount" should be equal to 4
- And the JSON node "data.dummies.edges[1].node.name" should be equal to "Dummy #2"
- And the JSON node "data.dummies.edges[1].cursor" should be equal to "MQ=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.pageInfo.endCursor" should be equal to "MQ=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.pageInfo.hasNextPage" should be true
- And the JSON node "data.dummies.edges[1].node.relatedDummies.totalCount" should be equal to 4
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy12"
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].cursor" should be equal to "MA=="
- When I send the following GraphQL request:
- """
- {
- dummies(first: 2, after: "MQ==") {
- edges {
- node {
- name
- relatedDummies(first: 2, after: "MA==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #3"
- And the JSON node "data.dummies.edges[0].cursor" should be equal to "Mg=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy24"
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].cursor" should be equal to "MQ=="
- When I send the following GraphQL request:
- """
- {
- dummies(first: 2, after: "Mg==") {
- edges {
- node {
- name
- relatedDummies(first: 3, after: "MQ==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.pageInfo.hasNextPage" should be false
- And the JSON node "data.dummies.pageInfo.endCursor" should be equal to "Mw=="
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #4"
- And the JSON node "data.dummies.edges[0].cursor" should be equal to "Mw=="
- And the JSON node "data.dummies.edges[0].node.relatedDummies.pageInfo.hasNextPage" should be false
- And the JSON node "data.dummies.edges[0].node.relatedDummies.pageInfo.endCursor" should be equal to "Mw=="
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 2 elements
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges[1].node.name" should be equal to "RelatedDummy44"
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges[1].cursor" should be equal to "Mw=="
- When I send the following GraphQL request:
- """
- {
- dummies(first: 2, after: "Mw==") {
- edges {
- node {
- name
- relatedDummies(first: 1, after: "MQ==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- cursor
- }
- pageInfo {
- endCursor
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 0 element
-
- @createSchema
- Scenario: Paginate backwards through collections through a GraphQL query
- Given there are 4 dummy objects having each 4 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies(last: 2) {
- edges {
- node {
- name
- relatedDummies(last: 2) {
- edges {
- node {
- name
- }
- cursor
- }
- totalCount
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- cursor
- }
- totalCount
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.pageInfo.startCursor" should be equal to "Mg=="
- And the JSON node "data.dummies.pageInfo.hasPreviousPage" should be true
- And the JSON node "data.dummies.totalCount" should be equal to 4
- And the JSON node "data.dummies.edges[1].node.name" should be equal to "Dummy #4"
- And the JSON node "data.dummies.edges[1].cursor" should be equal to "Mw=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.pageInfo.startCursor" should be equal to "Mg=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.pageInfo.hasPreviousPage" should be true
- And the JSON node "data.dummies.edges[1].node.relatedDummies.totalCount" should be equal to 4
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy34"
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].cursor" should be equal to "Mg=="
- When I send the following GraphQL request:
- """
- {
- dummies(last: 2, before: "Mw==") {
- edges {
- node {
- name
- relatedDummies(last: 2, before: "Mg==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #2"
- And the JSON node "data.dummies.edges[0].cursor" should be equal to "MQ=="
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy13"
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges[0].cursor" should be equal to "MA=="
- When I send the following GraphQL request:
- """
- {
- dummies(last: 2, before: "MQ==") {
- edges {
- node {
- name
- relatedDummies(last: 3, before: "Mg==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.pageInfo.hasPreviousPage" should be false
- And the JSON node "data.dummies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #1"
- And the JSON node "data.dummies.edges[0].cursor" should be equal to "MA=="
- And the JSON node "data.dummies.edges[0].node.relatedDummies.pageInfo.hasPreviousPage" should be false
- And the JSON node "data.dummies.edges[0].node.relatedDummies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 2 elements
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges[1].node.name" should be equal to "RelatedDummy21"
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges[1].cursor" should be equal to "MQ=="
- When I send the following GraphQL request:
- """
- {
- dummies(last: 2, before: "MA==") {
- edges {
- node {
- name
- relatedDummies(last: 1, before: "MQ==") {
- edges {
- node {
- name
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- cursor
- }
- pageInfo {
- startCursor
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 0 element
-
- @!mongodb
- @createSchema
- Scenario: Paginate through a collection through a GraphQL query with a partial pagination
- Given there are 4 of these so many objects
- When I send the following GraphQL request:
- """
- {
- soManies(first: 2) {
- edges {
- node {
- content
- }
- cursor
- }
- totalCount
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.soManies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.soManies.pageInfo.endCursor" should be equal to "MQ=="
- And the JSON node "data.soManies.pageInfo.hasNextPage" should be false
- And the JSON node "data.soManies.pageInfo.hasPreviousPage" should be false
- And the JSON node "data.soManies.totalCount" should be equal to 0
- And the JSON node "data.soManies.edges[1].node.content" should be equal to "Many #2"
- And the JSON node "data.soManies.edges[1].cursor" should be equal to "MQ=="
- When I send the following GraphQL request:
- """
- {
- soManies(first: 2, after: "MQ==") {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.soManies.pageInfo.startCursor" should be equal to "Mg=="
- And the JSON node "data.soManies.pageInfo.endCursor" should be equal to "Mw=="
- And the JSON node "data.soManies.pageInfo.hasNextPage" should be false
- And the JSON node "data.soManies.pageInfo.hasPreviousPage" should be true
- And the JSON node "data.soManies.edges[0].node.content" should be equal to "Many #3"
- And the JSON node "data.soManies.edges[0].cursor" should be equal to "Mg=="
- When I send the following GraphQL request:
- """
- {
- soManies(first: 2, after: "Mg==") {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.soManies.edges" should have 1 element
- And the JSON node "data.soManies.pageInfo.startCursor" should be equal to "Mw=="
- And the JSON node "data.soManies.pageInfo.endCursor" should be equal to "Mw=="
- And the JSON node "data.soManies.pageInfo.hasNextPage" should be false
- And the JSON node "data.soManies.pageInfo.hasPreviousPage" should be true
- And the JSON node "data.soManies.edges[0].node.content" should be equal to "Many #4"
- And the JSON node "data.soManies.edges[0].cursor" should be equal to "Mw=="
- When I send the following GraphQL request:
- """
- {
- soManies(first: 2, after: "Mw==") {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.soManies.edges" should have 0 element
- And the JSON node "data.soManies.pageInfo.startCursor" should be equal to "NA=="
- And the JSON node "data.soManies.pageInfo.endCursor" should be equal to "Mw=="
- And the JSON node "data.soManies.pageInfo.hasNextPage" should be false
- And the JSON node "data.soManies.pageInfo.hasPreviousPage" should be true
-
- @createSchema
- Scenario: Retrieve a collection with pagination disabled
- Given there are 4 foo objects with fake names
- When I send the following GraphQL request:
- """
- {
- foos {
- id
- name
- bar
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.foos[3].id" should be equal to "/foos/4"
- And the JSON node "data.foos[3].name" should be equal to "Separativeness"
- And the JSON node "data.foos[3].bar" should be equal to "Sit"
-
- Scenario: Custom collection query
- Given there are 2 dummyCustomQuery objects
- When I send the following GraphQL request:
- """
- {
- testCollectionDummyCustomQueries {
- edges {
- node {
- message
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testCollectionDummyCustomQueries": {
- "edges": [
- {
- "node": {"message": "Success!"}
- },
- {
- "node": {"message": "Success!"}
- }
- ]
- }
- }
- }
- """
-
- @createSchema
- Scenario: Custom collection query with read and serialize set to false
- Given there are 2 dummyCustomQuery objects
- When I send the following GraphQL request:
- """
- {
- testCollectionNoReadAndSerializeDummyCustomQueries {
- edges {
- node {
- message
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testCollectionNoReadAndSerializeDummyCustomQueries": {
- "edges": []
- }
- }
- }
- """
-
- @createSchema
- Scenario: Custom collection query with custom arguments
- Given there are 2 dummyCustomQuery objects
- When I send the following GraphQL request:
- """
- {
- testCollectionCustomArgumentsDummyCustomQueries(customArgumentString: "A string") {
- edges {
- node {
- message
- customArgs
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testCollectionCustomArgumentsDummyCustomQueries": {
- "edges": [
- {
- "node": {"message": "Success!", "customArgs": {"customArgumentString": "A string"}}
- },
- {
- "node": {"message": "Success!", "customArgs": {"customArgumentString": "A string"}}
- }
- ]
- }
- }
- }
- """
-
- @!mongodb
- @createSchema
- Scenario: Retrieve an item with composite primitive identifiers through a GraphQL query
- Given there are composite primitive identifiers objects
- When I send the following GraphQL request:
- """
- {
- compositePrimitiveItem(id: "/composite_primitive_items/name=Bar;year=2017") {
- description
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.compositePrimitiveItem.description" should be equal to "This is bar."
-
- @!mongodb
- @createSchema
- Scenario: Retrieve an item with composite identifiers through a GraphQL query
- Given there are Composite identifier objects
- When I send the following GraphQL request:
- """
- {
- compositeRelation(id: "/composite_relations/compositeItem=1;compositeLabel=1") {
- value
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.compositeRelation.value" should be equal to "somefoobardummy"
-
- @createSchema
- Scenario: Retrieve a collection using name converter
- Given there are 4 dummy objects
- When I send the following GraphQL request:
- """
- {
- dummies {
- edges {
- node {
- name_converted
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[1].node.name_converted" should be equal to "Converted 2"
-
- @createSchema
- Scenario: Retrieve a collection with different serialization groups for item_query and collection_query
- Given there are 3 dummy with different GraphQL serialization groups objects
- When I send the following GraphQL request:
- """
- {
- dummyDifferentGraphQlSerializationGroups {
- edges {
- node {
- name
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[0].node.name" should exist
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[1].node.name" should exist
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[2].node.name" should exist
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[0].node.title" should not exist
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[1].node.title" should not exist
- And the JSON node "data.dummyDifferentGraphQlSerializationGroups.edges[2].node.title" should not exist
-
- @createSchema
- Scenario: Retrieve a paginated collection using page-based pagination
- Given there are 5 fooDummy objects with fake names
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 1) {
- collection {
- id
- name
- }
- paginationInfo {
- itemsPerPage
- lastPage
- totalCount
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 3 elements
- And the JSON node "data.fooDummies.collection[0].id" should exist
- And the JSON node "data.fooDummies.collection[0].name" should exist
- And the JSON node "data.fooDummies.collection[1].id" should exist
- And the JSON node "data.fooDummies.collection[1].name" should exist
- And the JSON node "data.fooDummies.collection[2].id" should exist
- And the JSON node "data.fooDummies.collection[2].name" should exist
- And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
- And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
- And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
- And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 2) {
- collection {
- id
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 2 elements
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 3) {
- collection {
- id
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 0 elements
-
- @createSchema
- Scenario: Retrieve a paginated collection using page-based pagination and client-defined limit
- Given there are 5 fooDummy objects with fake names
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 1, itemsPerPage: 2) {
- collection {
- id
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 2 elements
- And the JSON node "data.fooDummies.collection[0].id" should exist
- And the JSON node "data.fooDummies.collection[0].name" should exist
- And the JSON node "data.fooDummies.collection[1].id" should exist
- And the JSON node "data.fooDummies.collection[1].name" should exist
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 2, itemsPerPage: 2) {
- collection {
- id
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 2 elements
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 3, itemsPerPage: 2) {
- collection {
- id
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 1 element
-
- @createSchema
- Scenario: Retrieve paginated collections using mixed pagination
- Given there are 5 fooDummy objects with fake names
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 1) {
- collection {
- id
- name
- soManies(first: 2) {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- paginationInfo {
- itemsPerPage
- lastPage
- totalCount
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 3 elements
- And the JSON node "data.fooDummies.collection[2].id" should exist
- And the JSON node "data.fooDummies.collection[2].name" should exist
- And the JSON node "data.fooDummies.collection[2].soManies" should exist
- And the JSON node "data.fooDummies.collection[2].soManies.edges" should have 2 elements
- And the JSON node "data.fooDummies.collection[2].soManies.edges[1].node.content" should be equal to "So many 1"
- And the JSON node "data.fooDummies.collection[2].soManies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
- And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
- And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
- And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 2) {
- collection {
- id
- name
- soManies(first: 2) {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- paginationInfo {
- itemsPerPage
- lastPage
- totalCount
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 2 elements
- And the JSON node "data.fooDummies.collection[1].id" should exist
- And the JSON node "data.fooDummies.collection[1].name" should exist
- And the JSON node "data.fooDummies.collection[1].soManies" should exist
- And the JSON node "data.fooDummies.collection[1].soManies.edges" should have 2 elements
- And the JSON node "data.fooDummies.collection[1].soManies.edges[1].node.content" should be equal to "So many 1"
- And the JSON node "data.fooDummies.collection[1].soManies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.fooDummies.paginationInfo.itemsPerPage" should be equal to the number 3
- And the JSON node "data.fooDummies.paginationInfo.lastPage" should be equal to the number 2
- And the JSON node "data.fooDummies.paginationInfo.totalCount" should be equal to the number 5
- And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be false
-
- @createSchema
- Scenario: Retrieve paginated collections using only hasNextPage
- Given there are 4 fooDummy objects with fake names
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 1, itemsPerPage: 2) {
- collection {
- id
- name
- soManies(first: 2) {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- paginationInfo {
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.collection" should have 2 elements
- And the JSON node "data.fooDummies.collection[1].id" should exist
- And the JSON node "data.fooDummies.collection[1].name" should exist
- And the JSON node "data.fooDummies.collection[1].soManies" should exist
- And the JSON node "data.fooDummies.collection[1].soManies.edges" should have 2 elements
- And the JSON node "data.fooDummies.collection[1].soManies.edges[1].node.content" should be equal to "So many 1"
- And the JSON node "data.fooDummies.collection[1].soManies.pageInfo.startCursor" should be equal to "MA=="
- And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be true
- When I send the following GraphQL request:
- """
- {
- fooDummies(page: 2) {
- collection {
- id
- name
- soManies(first: 2) {
- edges {
- node {
- content
- }
- cursor
- }
- pageInfo {
- startCursor
- endCursor
- hasNextPage
- hasPreviousPage
- }
- }
- }
- paginationInfo {
- hasNextPage
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.fooDummies.paginationInfo.hasNextPage" should be false
diff --git a/features/graphql/docs.feature b/features/graphql/docs.feature
deleted file mode 100644
index 7c54a7343f0..00000000000
--- a/features/graphql/docs.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: Documentation support
- In order to play with GraphQL
- As a client software developer
- I want to reach the GraphQL documentation
-
- Scenario: Retrieve the OpenAPI documentation
- Given I add "Accept" header equal to "text/html"
- And I send a "GET" request to "/graphql"
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "text/html; charset=utf-8"
diff --git a/features/graphql/filters.feature b/features/graphql/filters.feature
deleted file mode 100644
index b5927c6598d..00000000000
--- a/features/graphql/filters.feature
+++ /dev/null
@@ -1,302 +0,0 @@
-Feature: Collections filtering
- In order to retrieve subsets of collections
- As an API consumer
- I need to be able to set filters
-
- @createSchema
- Scenario: Retrieve a collection filtered using the boolean filter
- Given there is 1 dummy object with dummyBoolean true
- And there is 1 dummy object with dummyBoolean false
- When I send the following GraphQL request:
- """
- {
- dummies(dummyBoolean: false) {
- edges {
- node {
- id
- dummyBoolean
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[0].node.dummyBoolean" should be false
-
- @createSchema
- Scenario: Retrieve a collection filtered using the exists filter
- Given there are 3 dummy objects
- And there are 2 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- dummies(exists: [{relatedDummy: true}]) {
- edges {
- node {
- id
- relatedDummy {
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the JSON node "data.dummies.edges" should have 2 elements
- And the JSON node "data.dummies.edges[0].node.relatedDummy" should have 1 element
-
- @createSchema
- Scenario: Retrieve a collection filtered using the date filter
- Given there are 3 dummy objects with dummyDate
- When I send the following GraphQL request:
- """
- {
- dummies(dummyDate: [{after: "2015-04-02"}]) {
- edges {
- node {
- id
- dummyDate
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[0].node.dummyDate" should be equal to "2015-04-02"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the search filter
- Given there are 10 dummy objects
- When I send the following GraphQL request:
- """
- {
- dummies(name: "#2") {
- edges {
- node {
- id
- name
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[0].node.id" should be equal to "/dummies/2"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the search filter with an int
- Given there are 4 dummy objects having each 3 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies(name: "Dummy #1") {
- totalCount
- edges {
- node {
- name
- relatedDummies(age: 31) {
- totalCount
- edges {
- node {
- id
- name
- age
- }
- }
- }
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.totalCount" should be equal to 1
- And the JSON node "data.dummies.edges[0].node.relatedDummies.totalCount" should be equal to 1
- And the JSON node "data.dummies.edges[0].node.relatedDummies.edges[0].node.age" should be equal to "31"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the search filter and a name converter
- Given there are 10 dummy objects
- When I send the following GraphQL request:
- """
- {
- dummies(name_converted: "Converted 2") {
- edges {
- node {
- id
- name
- name_converted
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[0].node.id" should be equal to "/dummies/2"
- And the JSON node "data.dummies.edges[0].node.name_converted" should be equal to "Converted 2"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the search filter and a name converter
- Given there are 20 convertedOwner objects with convertedRelated
- When I send the following GraphQL request:
- """
- {
- convertedOwners(name_converted__name_converted: "Converted 2") {
- edges {
- node {
- id
- name_converted {
- name_converted
- }
- }
- }
- }
- }
- """
- Then the JSON node "data.convertedOwners.edges" should have 2 element
- And the JSON node "data.convertedOwners.edges[0].node.id" should be equal to "/converted_owners/2"
- And the JSON node "data.convertedOwners.edges[0].node.name_converted.name_converted" should be equal to "Converted 2"
- And the JSON node "data.convertedOwners.edges[1].node.id" should be equal to "/converted_owners/20"
- And the JSON node "data.convertedOwners.edges[1].node.name_converted.name_converted" should be equal to "Converted 20"
-
- @createSchema
- Scenario: Retrieve a nested collection filtered using the search filter
- Given there are 3 dummy objects having each 3 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies {
- edges {
- node {
- id
- relatedDummies(name: "RelatedDummy13") {
- edges {
- node {
- id
- name
- }
- }
- }
- }
- }
- }
- }
- """
- Then the JSON node "data.dummies.edges[0].node.relatedDummies.edges" should have 0 elements
- And the JSON node "data.dummies.edges[1].node.relatedDummies.edges" should have 0 elements
- And the JSON node "data.dummies.edges[2].node.relatedDummies.edges" should have 1 element
- And the JSON node "data.dummies.edges[2].node.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy13"
-
- @createSchema
- Scenario: Use a filter of a nested collection
- Given there is a DummyCar entity with related colors
- When I send the following GraphQL request:
- """
- {
- dummyCar(id: "/dummy_cars/1") {
- id
- colors(prop: "blue") {
- edges {
- node {
- id
- prop
- }
- }
- }
- }
- }
- """
- Then the JSON node "data.dummyCar.colors.edges" should have 1 element
- And the JSON node "data.dummyCar.colors.edges[0].node.prop" should be equal to "blue"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the related search filter
- Given there are 1 dummy objects having each 2 relatedDummies
- And there are 1 dummy objects having each 3 relatedDummies
- When I send the following GraphQL request:
- """
- {
- dummies(relatedDummies__name: "RelatedDummy31") {
- edges {
- node {
- id
- }
- }
- }
- }
- """
- And the response status code should be 200
- And the JSON node "data.dummies.edges" should have 1 element
-
- @createSchema
- Scenario: Retrieve a collection ordered using nested properties
- Given there are 2 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- dummies(order: [{relatedDummy__name: "DESC"}]) {
- edges {
- node {
- name
- relatedDummy {
- id
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "Dummy #2"
- And the JSON node "data.dummies.edges[1].node.name" should be equal to "Dummy #1"
-
- @createSchema
- Scenario: Retrieve a collection ordered correctly given the order of the argument
- Given there are dummies with similar properties
- When I send the following GraphQL request:
- """
- {
- dummies(order: [{description: "ASC"}, {name: "ASC"}]) {
- edges {
- node {
- id
- name
- description
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges[0].node.name" should be equal to "baz"
- And the JSON node "data.dummies.edges[0].node.description" should be equal to "bar"
- And the JSON node "data.dummies.edges[1].node.name" should be equal to "foo"
- And the JSON node "data.dummies.edges[1].node.description" should be equal to "bar"
-
- @createSchema
- Scenario: Retrieve a collection filtered using the related search filter with two values and exact strategy
- Given there are 3 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- dummies(relatedDummy__name_list: ["RelatedDummy #1", "RelatedDummy #2"]) {
- edges {
- node {
- id
- name
- relatedDummy {
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummies.edges" should have 2 element
- And the JSON node "data.dummies.edges[0].node.relatedDummy.name" should be equal to "RelatedDummy #1"
- And the JSON node "data.dummies.edges[1].node.relatedDummy.name" should be equal to "RelatedDummy #2"
diff --git a/features/graphql/input_output.feature b/features/graphql/input_output.feature
deleted file mode 100644
index aac22be3f3c..00000000000
--- a/features/graphql/input_output.feature
+++ /dev/null
@@ -1,202 +0,0 @@
-Feature: GraphQL DTO input and output
- In order to use the GraphQL API
- As a client software developer
- I need to be able to use DTOs on my resources as Input or Output objects.
-
- @createSchema
- Scenario: Retrieve an Output with GraphQL
- Given there is a RelatedDummy with 0 friends
- When I add "Content-Type" header equal to "application/ld+json"
- And I send a "POST" request to "/dummy_dto_input_outputs" with body:
- """
- {
- "foo": "test",
- "bar": 1,
- "relatedDummies": ["/related_dummies/1"]
- }
- """
- Then the response status code should be 201
- And the JSON should be a superset of:
- """
- {
- "@context": {
- "@vocab": "http://example.com/docs.jsonld#",
- "hydra": "http://www.w3.org/ns/hydra/core#",
- "id": "OutputDto/id",
- "baz": "OutputDto/baz",
- "bat": "OutputDto/bat",
- "relatedDummies": "OutputDto/relatedDummies"
- },
- "@type": "OutputDto",
- "id": 1,
- "baz": 1,
- "bat": "test",
- "relatedDummies": [
- {
- "@id": "/related_dummies/1",
- "@type": "https://schema.org/Product",
- "name": "RelatedDummy with friends",
- "dummyDate": null,
- "thirdLevel": null,
- "relatedToDummyFriend": [],
- "dummyBoolean": null,
- "embeddedDummy": {
- "@type": "EmbeddableDummy",
- "dummyName": null,
- "dummyBoolean": null,
- "dummyDate": null,
- "dummyFloat": null,
- "dummyPrice": null,
- "symfony": null
- },
- "id": 1,
- "symfony": "symfony",
- "age": null
- }
- ]
- }
- """
- When I send the following GraphQL request:
- """
- {
- dummyDtoInputOutput(id: "/dummy_dto_input_outputs/1") {
- _id, id, baz,
- relatedDummies {
- edges {
- node {
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "dummyDtoInputOutput": {
- "_id": 1,
- "id": "/dummy_dto_input_outputs/1",
- "baz": 1,
- "relatedDummies": {
- "edges": [
- {
- "node": {
- "name": "RelatedDummy with friends"
- }
- }
- ]
- }
- }
- }
- }
- """
-
- Scenario: Create an item with custom input and output
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoInputOutput(input: {foo: "A foo", bar: 4, clientMutationId: "myId"}) {
- dummyDtoInputOutput {
- baz,
- bat
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "createDummyDtoInputOutput": {
- "dummyDtoInputOutput": {
- "baz": 4,
- "bat": "A foo"
- },
- "clientMutationId": "myId"
- }
- }
- }
- """
-
- Scenario: Create an item using custom inputClass & disabled outputClass
- Given there are 2 dummyDtoNoOutput objects
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
- dummyDtoNoOutput {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be a superset of:
- """
- {
- "errors": [
- {
- "message": "Cannot query field \"id\" on type \"DummyDtoNoOutput\".",
- "locations": [
- {
- "line": 4,
- "column": 7
- }
- ]
- }
- ]
- }
- """
-
- Scenario: Cannot create an item with input fields using disabled inputClass
- When I send the following GraphQL request:
- """
- mutation {
- createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].message" should match '/^Field "lorem" is not defined by type "?createDummyDtoNoInputInput"?\.$/'
- And the JSON node "errors[1].message" should match '/^Field "ipsum" is not defined by type "?createDummyDtoNoInputInput"?\.$/'
-
- Scenario: Use messenger with GraphQL and an input where the handler gives a synchronous result
- When I send the following GraphQL request:
- """
- mutation {
- createMessengerWithInput(input: {var: "test"}) {
- messengerWithInput { id, name }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "createMessengerWithInput": {
- "messengerWithInput": {
- "id": "/messenger_with_inputs/1",
- "name": "test"
- }
- }
- }
- }
- """
diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature
deleted file mode 100644
index 356fc67c577..00000000000
--- a/features/graphql/introspection.feature
+++ /dev/null
@@ -1,621 +0,0 @@
-Feature: GraphQL introspection support
-
- @createSchema
- Scenario: Execute an empty GraphQL query
- When I send a "GET" request to "/graphql"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 400
- And the JSON node "errors[0].message" should be equal to "GraphQL query is not valid."
-
- Scenario: Introspect the GraphQL schema
- When I send the query to introspect the schema
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.__schema.types" should exist
- And the JSON node "data.__schema.queryType.name" should be equal to "Query"
- And the JSON node "data.__schema.mutationType.name" should be equal to "Mutation"
-
- Scenario: Introspect types
- When I send the following GraphQL request:
- """
- {
- type1: __type(name: "DummyProduct") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- type2: __type(name: "DummyAggregateOfferCursorConnection") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- type3: __type(name: "DummyAggregateOfferEdge") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.type1.description" should be equal to "Dummy Product."
- And the JSON node "data.type1.fields" should contain:
- """
- {
- "name":"offers",
- "type":{
- "name":"DummyAggregateOfferCursorConnection",
- "kind":"OBJECT",
- "ofType":null
- }
- }
- """
- And the JSON node "data.type2.fields" should contain:
- """
- {
- "name":"edges",
- "type":{
- "name":null,
- "kind":"LIST",
- "ofType":{
- "name":"DummyAggregateOfferEdge",
- "kind":"OBJECT"
- }
- }
- }
- """
- And the JSON node "data.type3.fields" should contain:
- """
- {
- "name":"node",
- "type":{
- "name":"DummyAggregateOffer",
- "kind":"OBJECT",
- "ofType":null
- }
- }
- """
- And the JSON node "data.type3.fields" should contain:
- """
- {
- "name":"cursor",
- "type":{
- "name":null,
- "kind":"NON_NULL",
- "ofType":{
- "name":"String",
- "kind":"SCALAR"
- }
- }
- }
- """
-
- Scenario: Introspect types with different serialization groups for item_query and collection_query
- When I send the following GraphQL request:
- """
- {
- type1: __type(name: "DummyDifferentGraphQlSerializationGroupCollection") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- type2: __type(name: "DummyDifferentGraphQlSerializationGroupItem") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.type1.description" should be equal to "Dummy with different serialization groups for item_query and collection_query."
- And the JSON node "data.type1.fields[3].name" should not exist
- And the JSON node "data.type2.fields[3].name" should be equal to "title"
-
- Scenario: Introspect deprecated queries
- When I send the following GraphQL request:
- """
- {
- __type (name: "Query") {
- name
- fields(includeDeprecated: true) {
- name
- isDeprecated
- deprecationReason
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the GraphQL field "deprecatedResource" is deprecated for the reason "This resource is deprecated"
- And the GraphQL field "deprecatedResources" is deprecated for the reason "This resource is deprecated"
-
- Scenario: Introspect deprecated mutations
- When I send the following GraphQL request:
- """
- {
- __type (name: "Mutation") {
- name
- fields(includeDeprecated: true) {
- name
- isDeprecated
- deprecationReason
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the GraphQL field "deleteDeprecatedResource" is deprecated for the reason "This resource is deprecated"
- And the GraphQL field "updateDeprecatedResource" is deprecated for the reason "This resource is deprecated"
- And the GraphQL field "createDeprecatedResource" is deprecated for the reason "This resource is deprecated"
-
- Scenario: Introspect a deprecated field
- When I send the following GraphQL request:
- """
- {
- __type(name: "DeprecatedResource") {
- fields(includeDeprecated: true) {
- name
- isDeprecated
- deprecationReason
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the GraphQL field "deprecatedField" is deprecated for the reason "This field is deprecated"
-
- Scenario: Retrieve the Relay's node interface
- When I send the following GraphQL request:
- """
- {
- __type(name: "Node") {
- name
- kind
- fields {
- name
- type {
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "__type": {
- "name": "Node",
- "kind": "INTERFACE",
- "fields": [
- {
- "name": "id",
- "type": {
- "kind": "NON_NULL",
- "ofType": {
- "name": "ID",
- "kind": "SCALAR"
- }
- }
- }
- ]
- }
- }
- }
- """
-
- Scenario: Retrieve the Relay's node field
- When I send the following GraphQL request:
- """
- {
- __schema {
- queryType {
- fields {
- name
- type {
- name
- kind
- }
- args {
- name
- type {
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.__schema.queryType.fields[0].name" should be equal to "node"
- And the JSON node "data.__schema.queryType.fields[0].type.name" should be equal to "Node"
- And the JSON node "data.__schema.queryType.fields[0].type.kind" should be equal to "INTERFACE"
- And the JSON node "data.__schema.queryType.fields[0].args[0].name" should be equal to "id"
- And the JSON node "data.__schema.queryType.fields[0].args[0].type.kind" should be equal to "NON_NULL"
- And the JSON node "data.__schema.queryType.fields[0].args[0].type.ofType.name" should be equal to "ID"
- And the JSON node "data.__schema.queryType.fields[0].args[0].type.ofType.kind" should be equal to "SCALAR"
-
- Scenario: Introspect an Iterable type field
- When I send the following GraphQL request:
- """
- {
- __type(name: "Dummy") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.__type.fields" should contain:
- """
- {
- "name":"jsonData",
- "type":{
- "name":"Iterable",
- "kind":"SCALAR",
- "ofType":null
- }
- }
- """
-
- Scenario: Retrieve entity - using serialization groups - fields
- When I send the following GraphQL request:
- """
- {
- typeQuery: __type(name: "DummyGroup") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- typeCreateInput: __type(name: "createDummyGroupInput") {
- description,
- inputFields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- typeCreatePayload: __type(name: "createDummyGroupPayload") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- typeCreatePayloadData: __type(name: "createDummyGroupPayloadData") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.typeQuery.fields" should have 2 elements
- And the JSON node "data.typeQuery.fields[0].name" should be equal to "id"
- And the JSON node "data.typeQuery.fields[1].name" should be equal to "foo"
- And the JSON node "data.typeCreateInput.inputFields" should have 3 elements
- And the JSON node "data.typeCreateInput.inputFields[0].name" should be equal to "bar"
- And the JSON node "data.typeCreateInput.inputFields[1].name" should be equal to "baz"
- And the JSON node "data.typeCreateInput.inputFields[2].name" should be equal to "clientMutationId"
- And the JSON node "data.typeCreatePayload.fields" should have 2 elements
- And the JSON node "data.typeCreatePayload.fields[0].name" should be equal to "dummyGroup"
- And the JSON node "data.typeCreatePayload.fields[0].type.name" should be equal to "createDummyGroupPayloadData"
- And the JSON node "data.typeCreatePayload.fields[1].name" should be equal to "clientMutationId"
- And the JSON node "data.typeCreatePayloadData.fields" should have 2 elements
- And the JSON node "data.typeCreatePayloadData.fields[0].name" should be equal to "id"
- And the JSON node "data.typeCreatePayloadData.fields[1].name" should be equal to "bar"
-
- Scenario: Retrieve nested mutation payload data fields
- When I send the following GraphQL request:
- """
- {
- typeCreatePayload: __type(name: "createDummyPropertyPayload") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- typeCreatePayloadData: __type(name: "createDummyPropertyPayloadData") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- typeCreateNestedPayload: __type(name: "createDummyGroupNestedPayload") {
- description,
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.typeCreatePayload.fields" should be equal to:
- """
- [
- {
- "name":"dummyProperty",
- "type":{
- "name":"createDummyPropertyPayloadData",
- "kind":"OBJECT",
- "ofType":null
- }
- },
- {
- "name":"clientMutationId",
- "type":{
- "name":"String",
- "kind":"SCALAR",
- "ofType":null
- }
- }
- ]
- """
- And the JSON node "data.typeCreatePayloadData.fields" should contain:
- """
- {
- "name":"group",
- "type":{
- "name":"createDummyGroupNestedPayload",
- "kind":"OBJECT",
- "ofType":null
- }
- }
- """
- And the JSON node "data.typeCreateNestedPayload.fields" should contain:
- """
- {
- "name":"id",
- "type":{
- "name":null,
- "kind":"NON_NULL",
- "ofType":{
- "name":"ID",
- "kind":"SCALAR"
- }
- }
- }
- """
-
- Scenario: Retrieve a type name through a GraphQL query
- Given there are 4 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- dummy: dummy(id: "/dummies/3") {
- name
- relatedDummy {
- id
- name
- __typename
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy.name" should be equal to "Dummy #3"
- And the JSON node "data.dummy.relatedDummy.name" should be equal to "RelatedDummy #3"
- And the JSON node "data.dummy.relatedDummy.__typename" should be equal to "RelatedDummy"
-
- Scenario: Introspect a type available only through relations
- When I send the following GraphQL request:
- """
- {
- typeNotAvailable: __type(name: "VoDummyInspectionCursorConnection") {
- description
- }
- typeOwner: __type(name: "VoDummyCar") {
- description,
- fields {
- name
- type {
- name
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.typeNotAvailable" should be null
- And the JSON node "data.typeOwner.fields[1].type.name" should be equal to "VoDummyInspectionCursorConnection"
-
- Scenario: Introspect an enum
- When I send the following GraphQL request:
- """
- {
- person: __type(name: "Person") {
- name
- fields {
- name
- type {
- name
- description
- enumValues {
- name
- description
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.person.fields[1].type.name" should be equal to "GenderTypeEnum"
- #And the JSON node "data.person.fields[1].type.description" should be equal to "An enumeration of genders."
- And the JSON node "data.person.fields[1].type.enumValues[0].name" should be equal to "MALE"
- #And the JSON node "data.person.fields[1].type.enumValues[0].description" should be equal to "The male gender."
- And the JSON node "data.person.fields[1].type.enumValues[1].name" should be equal to "FEMALE"
- And the JSON node "data.person.fields[1].type.enumValues[1].description" should be equal to "The female gender."
-
- Scenario: Introspect an enum resource
- When I send the following GraphQL request:
- """
- {
- videoGame: __type(name: "VideoGame") {
- name
- fields {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.videoGame.fields[3].type.ofType.name" should be equal to "GamePlayMode"
diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature
deleted file mode 100644
index 7df064279c1..00000000000
--- a/features/graphql/mutation.feature
+++ /dev/null
@@ -1,1071 +0,0 @@
-Feature: GraphQL mutation support
-
- @createSchema
- Scenario: Introspect types
- When I send the following GraphQL request:
- """
- {
- __type(name: "Mutation") {
- fields {
- name
- description
- type {
- name
- kind
- }
- args {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "required": [
- "data"
- ],
- "properties": {
- "data": {
- "type": "object",
- "required": [
- "__type"
- ],
- "properties": {
- "__type": {
- "type": "object",
- "required": [
- "fields"
- ],
- "properties": {
- "fields": {
- "type": "array",
- "minItems": 1,
- "items": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "name",
- "description",
- "type",
- "args"
- ],
- "properties": {
- "name": {
- "pattern": "^create[A-z0-9]+$"
- },
- "description": {
- "pattern": "^Creates a [A-z0-9]+.$"
- },
- "type": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^create[A-z0-9]+Payload$"
- },
- "kind": {
- "enum": ["OBJECT"]
- }
- }
- },
- "args": {
- "type": "array",
- "minItems": 1,
- "maxItems": 1,
- "items": [
- {
- "type": "object",
- "required": [
- "name",
- "type"
- ],
- "properties": {
- "name": {
- "enum": ["input"]
- },
- "type": {
- "type": "object",
- "required": [
- "kind",
- "ofType"
- ],
- "properties": {
- "kind": {
- "enum": ["NON_NULL"]
- },
- "ofType": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^create[A-z0-9]+Input$"
- },
- "kind": {
- "enum": ["INPUT_OBJECT"]
- }
- }
- }
- }
- }
- }
- }
- ]
- }
- }
- },
- {
- "type": "object",
- "required": [
- "name",
- "description",
- "type",
- "args"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+$"
- },
- "description": {
- "pattern": "^Updates a [A-z0-9]+.$"
- },
- "type": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+Payload$"
- },
- "kind": {
- "enum": ["OBJECT"]
- }
- }
- },
- "args": {
- "type": "array",
- "minItems": 1,
- "maxItems": 1,
- "items": [
- {
- "type": "object",
- "required": [
- "name",
- "type"
- ],
- "properties": {
- "name": {
- "enum": ["input"]
- },
- "type": {
- "type": "object",
- "required": [
- "kind",
- "ofType"
- ],
- "properties": {
- "kind": {
- "enum": ["NON_NULL"]
- },
- "ofType": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+Input$"
- },
- "kind": {
- "enum": ["INPUT_OBJECT"]
- }
- }
- }
- }
- }
- }
- }
- ]
- }
- }
- },
- {
- "type": "object",
- "required": [
- "name",
- "description",
- "type",
- "args"
- ],
- "properties": {
- "name": {
- "pattern": "^delete[A-z0-9]+$"
- },
- "description": {
- "pattern": "^Deletes a [A-z0-9]+.$"
- },
- "type": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^delete[A-z0-9]+Payload$"
- },
- "kind": {
- "enum": ["OBJECT"]
- }
- }
- },
- "args": {
- "type": "array",
- "minItems": 1,
- "maxItems": 1,
- "items": [
- {
- "type": "object",
- "required": [
- "name",
- "type"
- ],
- "properties": {
- "name": {
- "enum": ["input"]
- },
- "type": {
- "type": "object",
- "required": [
- "kind",
- "ofType"
- ],
- "properties": {
- "kind": {
- "enum": ["NON_NULL"]
- },
- "ofType": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^delete[A-z0-9]+Input$"
- },
- "kind": {
- "enum": ["INPUT_OBJECT"]
- }
- }
- }
- }
- }
- }
- }
- ]
- }
- }
- },
- {
- "type": "object",
- "required": [
- "name",
- "description",
- "type",
- "args"
- ],
- "properties": {
- "name": {
- "pattern": "^(?!create|update|delete)[A-z0-9]+$"
- },
- "description": {
- "pattern": "^(?!Create|Update|Delete)[A-z0-9]+s a [A-z0-9]+.$"
- },
- "type": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^(?!create|update|delete)[A-z0-9]+Payload$"
- },
- "kind": {
- "enum": ["OBJECT"]
- }
- }
- },
- "args": {
- "type": "array",
- "minItems": 1,
- "maxItems": 1,
- "items": [
- {
- "type": "object",
- "required": [
- "name",
- "type"
- ],
- "properties": {
- "name": {
- "enum": ["input"]
- },
- "type": {
- "type": "object",
- "required": [
- "kind",
- "ofType"
- ],
- "properties": {
- "kind": {
- "enum": ["NON_NULL"]
- },
- "ofType": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^(?!create|update|delete)[A-z0-9]+Input$"
- },
- "kind": {
- "enum": ["INPUT_OBJECT"]
- }
- }
- }
- }
- }
- }
- }
- ]
- }
- }
- }
- ]
- }
- }
- }
- }
- }
- }
- }
- }
- """
-
- Scenario: Create an item
- When I send the following GraphQL request:
- """
- mutation {
- createFoo(input: {name: "A new one", bar: "new", clientMutationId: "myId"}) {
- foo {
- id
- _id
- __typename
- name
- bar
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createFoo.foo.id" should be equal to "/foos/1"
- And the JSON node "data.createFoo.foo._id" should be equal to 1
- And the JSON node "data.createFoo.foo.__typename" should be equal to "Foo"
- And the JSON node "data.createFoo.foo.name" should be equal to "A new one"
- And the JSON node "data.createFoo.foo.bar" should be equal to "new"
- And the JSON node "data.createFoo.clientMutationId" should be equal to "myId"
-
- Scenario: Create an item without a clientMutationId
- When I send the following GraphQL request:
- """
- mutation {
- createFoo(input: {name: "Created without mutation id", bar: "works"}) {
- foo {
- id
- name
- bar
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createFoo.foo.id" should be equal to "/foos/2"
- And the JSON node "data.createFoo.foo.name" should be equal to "Created without mutation id"
- And the JSON node "data.createFoo.foo.bar" should be equal to "works"
-
- Scenario: Create an item with a relation to an existing resource
- Given there are 1 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- mutation {
- createDummy(input: {name: "A dummy", foo: [], relatedDummy: "/related_dummies/1", name_converted: "Converted" clientMutationId: "myId"}) {
- dummy {
- id
- name
- foo
- relatedDummy {
- name
- __typename
- }
- name_converted
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createDummy.dummy.id" should be equal to "/dummies/2"
- And the JSON node "data.createDummy.dummy.name" should be equal to "A dummy"
- And the JSON node "data.createDummy.dummy.foo" should have 0 elements
- And the JSON node "data.createDummy.dummy.relatedDummy.name" should be equal to "RelatedDummy #1"
- And the JSON node "data.createDummy.dummy.relatedDummy.__typename" should be equal to "RelatedDummy"
- And the JSON node "data.createDummy.dummy.name_converted" should be equal to "Converted"
- And the JSON node "data.createDummy.clientMutationId" should be equal to "myId"
-
- Scenario: Create an item with an iterable field
- When I send the following GraphQL request:
- """
- mutation {
- createDummy(input: {name: "A dummy", foo: [], jsonData: {bar:{baz:3,qux:[7.6,false,null]}}, arrayData: ["bar", "baz"], clientMutationId: "myId"}) {
- dummy {
- id
- name
- foo
- jsonData
- arrayData
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createDummy.dummy.id" should be equal to "/dummies/3"
- And the JSON node "data.createDummy.dummy.name" should be equal to "A dummy"
- And the JSON node "data.createDummy.dummy.foo" should have 0 elements
- And the JSON node "data.createDummy.dummy.jsonData.bar.baz" should be equal to the number 3
- And the JSON node "data.createDummy.dummy.jsonData.bar.qux[0]" should be equal to the number 7.6
- And the JSON node "data.createDummy.dummy.jsonData.bar.qux[1]" should be false
- And the JSON node "data.createDummy.dummy.jsonData.bar.qux[2]" should be null
- And the JSON node "data.createDummy.dummy.arrayData[1]" should be equal to baz
- And the JSON node "data.createDummy.clientMutationId" should be equal to "myId"
-
- Scenario: Create an item with an enum
- When I send the following GraphQL request:
- """
- mutation {
- createPerson(input: {name: "Mob", genderType: FEMALE}) {
- person {
- id
- name
- genderType
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createPerson.person.id" should be equal to "/people/1"
- And the JSON node "data.createPerson.person.name" should be equal to "Mob"
- And the JSON node "data.createPerson.person.genderType" should be equal to "FEMALE"
-
- @!mongodb
- Scenario: Create an item with an enum collection
- When I send the following GraphQL request:
- """
- mutation {
- createPerson(input: {name: "Harry", academicGrades: [BACHELOR, MASTER]}) {
- person {
- id
- name
- genderType
- academicGrades
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createPerson.person.id" should be equal to "/people/2"
- And the JSON node "data.createPerson.person.name" should be equal to "Harry"
- And the JSON node "data.createPerson.person.genderType" should be equal to "MALE"
- And the JSON node "data.createPerson.person.academicGrades" should have 2 elements
- And the JSON node "data.createPerson.person.academicGrades[0]" should be equal to "BACHELOR"
- And the JSON node "data.createPerson.person.academicGrades[1]" should be equal to "MASTER"
-
- Scenario: Create an item with an enum as a resource
- When I send the following GraphQL request:
- """
- {
- gamePlayModes {
- id
- name
- }
- gamePlayMode(id: "/game_play_modes/SINGLE_PLAYER") {
- name
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.gamePlayModes" should have 3 elements
- And the JSON node "data.gamePlayModes[2].id" should be equal to "/game_play_modes/SINGLE_PLAYER"
- And the JSON node "data.gamePlayModes[2].name" should be equal to "SINGLE_PLAYER"
- And the JSON node "data.gamePlayMode.name" should be equal to "SINGLE_PLAYER"
- When I send the following GraphQL request:
- """
- mutation {
- createVideoGame(input: {name: "Baten Kaitos", playMode: "/game_play_modes/SINGLE_PLAYER"}) {
- videoGame {
- id
- name
- playMode {
- id
- name
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createVideoGame.videoGame.id" should be equal to "/video_games/1"
- And the JSON node "data.createVideoGame.videoGame.name" should be equal to "Baten Kaitos"
- And the JSON node "data.createVideoGame.videoGame.playMode.id" should be equal to "/game_play_modes/SINGLE_PLAYER"
- And the JSON node "data.createVideoGame.videoGame.playMode.name" should be equal to "SINGLE_PLAYER"
-
- Scenario: Delete an item through a mutation
- When I send the following GraphQL request:
- """
- mutation {
- deleteFoo(input: {id: "/foos/1", clientMutationId: "anotherId"}) {
- foo {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.deleteFoo.foo.id" should be equal to "/foos/1"
- And the JSON node "data.deleteFoo.clientMutationId" should be equal to "anotherId"
-
- Scenario: Trigger an error trying to delete item of different resource
- When I send the following GraphQL request:
- """
- mutation {
- deleteFoo(input: {id: "/dummies/1", clientMutationId: "myId"}) {
- foo {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].message" should be equal to 'Item "/dummies/1" did not match expected type "Foo".'
-
- @!mongodb
- Scenario: Delete an item with composite identifiers through a mutation
- Given there are Composite identifier objects
- When I send the following GraphQL request:
- """
- mutation {
- deleteCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=1", clientMutationId: "myId"}) {
- compositeRelation {
- id
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.deleteCompositeRelation.compositeRelation.id" should be equal to "/composite_relations/compositeItem=1;compositeLabel=1"
- And the JSON node "data.deleteCompositeRelation.clientMutationId" should be equal to "myId"
-
- @createSchema
- Scenario: Modify an item through a mutation
- Given there are 1 dummy objects having each 2 relatedDummies
- When I send the following GraphQL request:
- """
- mutation {
- updateDummy(input: {id: "/dummies/1", description: "Modified description.", dummyDate: "2018-06-05T00:00:00+00:00", clientMutationId: "myId"}) {
- dummy {
- id
- name
- description
- dummyDate
- relatedDummies {
- edges {
- node {
- name
- }
- }
- }
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateDummy.dummy.id" should be equal to "/dummies/1"
- And the JSON node "data.updateDummy.dummy.name" should be equal to "Dummy #1"
- And the JSON node "data.updateDummy.dummy.description" should be equal to "Modified description."
- And the JSON node "data.updateDummy.dummy.dummyDate" should be equal to "2018-06-05"
- And the JSON node "data.updateDummy.dummy.relatedDummies.edges[0].node.name" should be equal to "RelatedDummy11"
- And the JSON node "data.updateDummy.clientMutationId" should be equal to "myId"
-
- @createSchema
- @!mongodb
- Scenario: Modify an item with embedded object through a mutation
- Given there is a fooDummy objects with fake names and embeddable
- When I send the following GraphQL request:
- """
- mutation {
- updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
- fooDummy {
- id
- name
- embeddedFoo {
- dummyName
- }
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateFooDummy.fooDummy.name" should be equal to "modifiedName"
- And the JSON node "data.updateFooDummy.fooDummy.embeddedFoo.dummyName" should be equal to "Embedded name"
- And the JSON node "data.updateFooDummy.clientMutationId" should be equal to "myId"
-
- @createSchema
- Scenario: Try to modify a non writable property through a mutation
- Given there is a fooDummy objects with fake names and embeddable
- When I send the following GraphQL request:
- """
- mutation {
- updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", nonWritableProp: "written", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
- fooDummy {
- id
- name
- embeddedFoo {
- dummyName
- }
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].message" should match '/^Field "nonWritableProp" is not defined by type "?updateFooDummyInput"?\.$/'
-
- @createSchema
- @!mongodb
- Scenario: Try to modify a non writable embedded property through a mutation
- Given there is a fooDummy objects with fake names and embeddable
- When I send the following GraphQL request:
- """
- mutation {
- updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name", nonWritableProp: "written"}, clientMutationId: "myId"}) {
- fooDummy {
- id
- name
- embeddedFoo {
- dummyName
- }
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].message" should match '/^Field "nonWritableProp" is not defined by type "?FooEmbeddableNestedInput"?\.$/'
-
- @!mongodb
- Scenario: Modify an item with composite identifiers through a mutation
- Given there are Composite identifier objects
- When I send the following GraphQL request:
- """
- mutation {
- updateCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=2", value: "Modified value.", clientMutationId: "myId"}) {
- compositeRelation {
- id
- value
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateCompositeRelation.compositeRelation.id" should be equal to "/composite_relations/compositeItem=1;compositeLabel=2"
- And the JSON node "data.updateCompositeRelation.compositeRelation.value" should be equal to "Modified value."
- And the JSON node "data.updateCompositeRelation.clientMutationId" should be equal to "myId"
-
- Scenario: Create an item with a custom UUID
- When I send the following GraphQL request:
- """
- mutation {
- createWritableId(input: {_id: "c6b722fe-0331-48c4-a214-f81f9f1ca082", name: "Foo", clientMutationId: "m"}) {
- writableId {
- id
- _id
- name
- }
- clientMutationId
- }
- }
- """
- Then the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createWritableId.writableId.id" should be equal to "/writable_ids/c6b722fe-0331-48c4-a214-f81f9f1ca082"
- And the JSON node "data.createWritableId.writableId._id" should be equal to "c6b722fe-0331-48c4-a214-f81f9f1ca082"
- And the JSON node "data.createWritableId.writableId.name" should be equal to "Foo"
- And the JSON node "data.createWritableId.clientMutationId" should be equal to "m"
-
- @!mongodb
- Scenario: Update an item with a custom UUID
- When I send the following GraphQL request:
- """
- mutation {
- updateWritableId(input: {id: "/writable_ids/c6b722fe-0331-48c4-a214-f81f9f1ca082", _id: "f8a708b2-310f-416c-9aef-b1b5719dfa47", name: "Foo", clientMutationId: "m"}) {
- writableId {
- id
- _id
- name
- }
- clientMutationId
- }
- }
- """
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateWritableId.writableId.id" should be equal to "/writable_ids/f8a708b2-310f-416c-9aef-b1b5719dfa47"
- And the JSON node "data.updateWritableId.writableId._id" should be equal to "f8a708b2-310f-416c-9aef-b1b5719dfa47"
- And the JSON node "data.updateWritableId.writableId.name" should be equal to "Foo"
- And the JSON node "data.updateWritableId.clientMutationId" should be equal to "m"
-
- Scenario: Use serialization groups
- Given there are 1 dummy group objects
- When I send the following GraphQL request:
- """
- mutation {
- createDummyGroup(input: {bar: "Bar", baz: "Baz", clientMutationId: "myId"}) {
- dummyGroup {
- id
- bar
- __typename
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createDummyGroup.dummyGroup.id" should be equal to "/dummy_groups/2"
- And the JSON node "data.createDummyGroup.dummyGroup.bar" should be equal to "Bar"
- And the JSON node "data.createDummyGroup.dummyGroup.__typename" should be equal to "createDummyGroupPayloadData"
- And the JSON node "data.createDummyGroup.clientMutationId" should be equal to "myId"
-
- @createSchema
- Scenario: Use serialization groups with relations
- Given there is 1 dummy object with relatedDummy and its thirdLevel
- And there is a RelatedDummy with 2 friends
- And there is a dummy object with a fourth level relation
- When I send the following GraphQL request:
- """
- mutation {
- updateRelatedDummy(input: {
- id: "/related_dummies/2",
- symfony: "laravel",
- thirdLevel: {
- fourthLevel: "/fourth_levels/1"
- }
- }) {
- relatedDummy {
- id
- symfony
- thirdLevel {
- id
- fourthLevel {
- id
- __typename
- }
- __typename
- }
- relatedToDummyFriend {
- edges {
- node {
- name
- }
- }
- __typename
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateRelatedDummy.relatedDummy.id" should be equal to "/related_dummies/2"
- And the JSON node "data.updateRelatedDummy.relatedDummy.symfony" should be equal to "laravel"
- And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.id" should be equal to "/third_levels/3"
- And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.__typename" should be equal to "updateThirdLevelNestedPayload"
- And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.id" should be equal to "/fourth_levels/1"
- And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.__typename" should be equal to "updateFourthLevelNestedPayload"
- And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.__typename" should be equal to "updateRelatedToDummyFriendNestedPayloadCursorConnection"
- And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[0].node.name" should be equal to "Relation-1"
- And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[1].node.name" should be equal to "Relation-2"
-
- Scenario: Trigger a validation error
- When I send the following GraphQL request:
- """
- mutation {
- createDummy(input: {name: "", foo: [], clientMutationId: "myId"}) {
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to "422"
- And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."
- And the JSON node "errors[0].extensions.violations" should exist
- And the JSON node "errors[0].extensions.violations[0].path" should be equal to "name"
- And the JSON node "errors[0].extensions.violations[0].message" should be equal to "This value should not be blank."
-
- @createSchema
- Scenario: Execute a custom mutation
- Given there are 1 dummyCustomMutation objects
- When I send the following GraphQL request:
- """
- mutation {
- sumDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
- dummyCustomMutation {
- id
- result
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.sumDummyCustomMutation.dummyCustomMutation.result" should be equal to "8"
-
- @createSchema
- Scenario: Execute a not persisted custom mutation (resolver returns null)
- Given there are 1 dummyCustomMutation objects
- When I send the following GraphQL request:
- """
- mutation {
- sumNotPersistedDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
- dummyCustomMutation {
- id
- result
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.sumNotPersistedDummyCustomMutation.dummyCustomMutation" should be null
-
- Scenario: Execute a not persisted custom mutation (write set to false) with custom result
- When I send the following GraphQL request:
- """
- mutation {
- sumNoWriteCustomResultDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
- dummyCustomMutation {
- id
- result
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.sumNoWriteCustomResultDummyCustomMutation.dummyCustomMutation.result" should be equal to "1234"
-
- Scenario: Execute a custom mutation with read, deserialize, validate and serialize set to false
- When I send the following GraphQL request:
- """
- mutation {
- sumOnlyPersistDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
- dummyCustomMutation {
- id
- result
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.sumOnlyPersistDummyCustomMutation.dummyCustomMutation" should be null
-
- Scenario: Execute a custom mutation with custom arguments
- When I send the following GraphQL request:
- """
- mutation {
- testCustomArgumentsDummyCustomMutation(input: {operandC: 18, clientMutationId: "myId"}) {
- dummyCustomMutation {
- result
- }
- clientMutationId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.testCustomArgumentsDummyCustomMutation.dummyCustomMutation.result" should be equal to "18"
- And the JSON node "data.testCustomArgumentsDummyCustomMutation.clientMutationId" should be equal to "myId"
-
- Scenario: Uploading a file with a custom mutation
- Given I have the following file for a GraphQL request:
- | name | file |
- | file | test.gif |
- And I have the following GraphQL multipart request map:
- """
- {
- "file": ["variables.file"]
- }
- """
- When I send the following GraphQL multipart request operations:
- """
- {
- "query": "mutation($file: Upload!) { uploadMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } }",
- "variables": {
- "file": null
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the JSON node "data.uploadMediaObject.mediaObject.contentUrl" should be equal to "test.gif"
-
- Scenario: Uploading multiple files with a custom mutation
- Given I have the following files for a GraphQL request:
- | name | file |
- | 0 | test.gif |
- | 1 | test.gif |
- | 2 | test.gif |
- And I have the following GraphQL multipart request map:
- """
- {
- "0": ["variables.files.0"],
- "1": ["variables.files.1"],
- "2": ["variables.files.2"]
- }
- """
- When I send the following GraphQL multipart request operations:
- """
- {
- "query": "mutation($files: [Upload!]!) { uploadMultipleMediaObject(input: {files: $files}) { mediaObject { id contentUrl } } }",
- "variables": {
- "files": [
- null,
- null,
- null
- ]
- }
- }
- """
- Then the response status code should be 200
- And the JSON node "data.uploadMultipleMediaObject.mediaObject.contentUrl" should be equal to "test.gif"
-
- @!mongodb
- Scenario: Delete an invalid item through a mutation
- When I send the following GraphQL request:
- """
- mutation {
- deleteActivityLog(input: {id: "/activity_logs/1"}) {
- activityLog {
- id
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors" should not exist
- And the JSON node "data.deleteActivityLog.activityLog" should exist
-
- @!mongodb
- Scenario: Mutation should run before validation
- When I send the following GraphQL request:
- """
- mutation {
- createActivityLog(input: {name: ""}) {
- activityLog {
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.createActivityLog.activityLog.name" should be equal to "hi"
diff --git a/features/graphql/query.feature b/features/graphql/query.feature
deleted file mode 100644
index 732540a65cb..00000000000
--- a/features/graphql/query.feature
+++ /dev/null
@@ -1,696 +0,0 @@
-Feature: GraphQL query support
-
- @createSchema
- Scenario: Execute a basic GraphQL query
- Given there are 2 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- dummy(id: "/dummies/1") {
- id
- name
- name_converted
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy.id" should be equal to "/dummies/1"
- And the JSON node "data.dummy.name" should be equal to "Dummy #1"
- And the JSON node "data.dummy.name_converted" should be equal to "Converted 1"
-
- @createSchema
- Scenario: Retrieve an item with different relations to the same resource
- Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
- When I send the following GraphQL request:
- """
- {
- multiRelationsDummy(id: "/multi_relations_dummies/2") {
- id
- name
- manyToOneRelation {
- id
- name
- }
- manyToOneResolveRelation {
- id
- name
- }
- manyToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- oneToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
- And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
- And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
- And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 3 element
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[1].node.id" should not be null
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should match "#RelatedOneToManyDummy(1|3)2#"
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should match "#RelatedOneToManyDummy(1|3)2#"
-
- @createSchema
- Scenario: Retrieve embedded collections
- Given there are 2 multiRelationsDummy objects having each 1 manyToOneRelation, 2 manyToManyRelations, 3 oneToManyRelations and 4 embeddedRelations
- When I send the following GraphQL request:
- """
- {
- multiRelationsDummy(id: "/multi_relations_dummies/2") {
- id
- name
- manyToOneRelation {
- id
- name
- }
- manyToOneResolveRelation {
- id
- name
- }
- manyToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- oneToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- nestedCollection {
- name
- }
- nestedPaginatedCollection {
- edges{
- node {
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors" should not exist
- And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
- And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
- And the JSON node "data.multiRelationsDummy.manyToOneRelation.id" should not be null
- And the JSON node "data.multiRelationsDummy.manyToOneRelation.name" should be equal to "RelatedManyToOneDummy #2"
- And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.id" should not be null
- And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation.name" should be equal to "RelatedManyToOneResolveDummy #2"
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 2 element
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.id" should not be null
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[0].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges[1].node.name" should match "#RelatedManyToManyDummy(1|2)2#"
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 3 element
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[1].node.id" should not be null
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[0].node.name" should match "#RelatedOneToManyDummy(1|3)2#"
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges[2].node.name" should match "#RelatedOneToManyDummy(1|3)2#"
- And the JSON node "data.multiRelationsDummy.nestedCollection[0].name" should be equal to "NestedDummy1"
- And the JSON node "data.multiRelationsDummy.nestedCollection[1].name" should be equal to "NestedDummy2"
- And the JSON node "data.multiRelationsDummy.nestedCollection[2].name" should be equal to "NestedDummy3"
- And the JSON node "data.multiRelationsDummy.nestedCollection[3].name" should be equal to "NestedDummy4"
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 4 element
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[0].node.name" should be equal to "NestedPaginatedDummy1"
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[1].node.name" should be equal to "NestedPaginatedDummy2"
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[2].node.name" should be equal to "NestedPaginatedDummy3"
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges[3].node.name" should be equal to "NestedPaginatedDummy4"
-
- @createSchema
- Scenario: Retrieve an item with different relations (all unset)
- Given there are 2 multiRelationsDummy objects having each 0 manyToOneRelation, 0 manyToManyRelations, 0 oneToManyRelations and 0 embeddedRelations
- When I send the following GraphQL request:
- """
- {
- multiRelationsDummy(id: "/multi_relations_dummies/2") {
- id
- name
- manyToOneRelation {
- id
- name
- }
- manyToOneResolveRelation {
- id
- name
- }
- manyToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- oneToManyRelations {
- edges{
- node {
- id
- name
- }
- }
- }
- nestedCollection {
- name
- }
- nestedPaginatedCollection {
- edges{
- node {
- name
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors" should not exist
- And the JSON node "data.multiRelationsDummy.id" should be equal to "/multi_relations_dummies/2"
- And the JSON node "data.multiRelationsDummy.name" should be equal to "Dummy #2"
- And the JSON node "data.multiRelationsDummy.manyToOneRelation" should be null
- And the JSON node "data.multiRelationsDummy.manyToOneResolveRelation" should be null
- And the JSON node "data.multiRelationsDummy.manyToManyRelations.edges" should have 0 element
- And the JSON node "data.multiRelationsDummy.oneToManyRelations.edges" should have 0 element
- And the JSON node "data.multiRelationsDummy.nestedCollection" should have 0 element
- And the JSON node "data.multiRelationsDummy.nestedPaginatedCollection.edges" should have 0 element
-
- @createSchema @!mongodb
- Scenario: Retrieve an item with child relation to the same resource
- Given there are tree dummies
- When I send the following GraphQL request:
- """
- {
- treeDummies {
- edges {
- node {
- id
- children {
- totalCount
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors" should not exist
- And the JSON node "data.treeDummies.edges[0].node.id" should be equal to "/tree_dummies/1"
- And the JSON node "data.treeDummies.edges[0].node.children.totalCount" should be equal to "1"
- And the JSON node "data.treeDummies.edges[1].node.id" should be equal to "/tree_dummies/2"
- And the JSON node "data.treeDummies.edges[1].node.children.totalCount" should be equal to "0"
-
- @createSchema
- Scenario: Retrieve a Relay Node
- Given there are 2 dummy objects with relatedDummy
- When I send the following GraphQL request:
- """
- {
- node(id: "/dummies/1") {
- id
- ... on Dummy {
- name
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.node.id" should be equal to "/dummies/1"
- And the JSON node "data.node.name" should be equal to "Dummy #1"
-
- @createSchema
- Scenario: Retrieve an item with an iterable field
- Given there are 2 dummy objects with relatedDummy
- Given there are 2 dummy objects with JSON and array data
- When I send the following GraphQL request:
- """
- {
- dummy(id: "/dummies/3") {
- id
- name
- jsonData
- arrayData
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy.id" should be equal to "/dummies/3"
- And the JSON node "data.dummy.name" should be equal to "Dummy #1"
- And the JSON node "data.dummy.jsonData.foo" should have 2 elements
- And the JSON node "data.dummy.jsonData.bar" should be equal to 5
- And the JSON node "data.dummy.arrayData[2]" should be equal to baz
-
- @createSchema
- Scenario: Retrieve an item with an iterable null field
- Given there are 2 dummy with null JSON objects
- When I send the following GraphQL request:
- """
- {
- withJsonDummy(id: "/with_json_dummies/2") {
- id
- json
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.withJsonDummy.id" should be equal to "/with_json_dummies/2"
- And the JSON node "data.withJsonDummy.json" should be null
-
- @createSchema
- Scenario: Retrieve an item through a GraphQL query with variables
- Given there are 2 dummy objects with relatedDummy
- When I have the following GraphQL request:
- """
- query DummyWithId($itemId: ID = "/dummies/1") {
- dummyItem: dummy(id: $itemId) {
- id
- name
- relatedDummy {
- id
- name
- }
- }
- }
- """
- And I send the GraphQL request with variables:
- """
- {
- "itemId": "/dummies/2"
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummyItem.id" should be equal to "/dummies/2"
- And the JSON node "data.dummyItem.name" should be equal to "Dummy #2"
- And the JSON node "data.dummyItem.relatedDummy.id" should be equal to "/related_dummies/2"
- And the JSON node "data.dummyItem.relatedDummy.name" should be equal to "RelatedDummy #2"
-
- Scenario: Run a specific operation through a GraphQL query
- When I have the following GraphQL request:
- """
- query DummyWithId1 {
- dummyItem: dummy(id: "/dummies/1") {
- name
- }
- }
- query DummyWithId2 {
- dummyItem: dummy(id: "/dummies/2") {
- id
- name
- }
- }
- """
- And I send the GraphQL request with operationName "DummyWithId2"
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummyItem.id" should be equal to "/dummies/2"
- And the JSON node "data.dummyItem.name" should be equal to "Dummy #2"
- And I send the GraphQL request with operationName "DummyWithId1"
- And the JSON node "data.dummyItem.name" should be equal to "Dummy #1"
-
- Scenario: Use serialization groups
- Given there are 1 dummy group objects
- When I send the following GraphQL request:
- """
- {
- dummyGroup(id: "/dummy_groups/1") {
- foo
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummyGroup.foo" should be equal to "Foo #1"
-
- Scenario: Query a serialized name
- Given there is a DummyCar entity with related colors
- When I send the following GraphQL request:
- """
- {
- dummyCar(id: "/dummy_cars/1") {
- carBrand
- }
- }
- """
- Then the JSON node "data.dummyCar.carBrand" should be equal to "DummyBrand"
-
- Scenario: Fetch only the internal id
- When I send the following GraphQL request:
- """
- {
- dummy(id: "/dummies/1") {
- _id
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy._id" should be equal to "1"
-
- Scenario: Retrieve an nonexistent item through a GraphQL query
- When I send the following GraphQL request:
- """
- {
- dummy(id: "/dummies/5") {
- name
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy" should be null
-
- Scenario: Retrieve an nonexistent IRI through a GraphQL query
- When I send the following GraphQL request:
- """
- {
- foo(id: "/foo/1") {
- name
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the GraphQL debug message should be equal to 'No route matches "/foo/1".'
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "properties": {
- "errors": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "message": {"type": "string"},
- "extensions": {
- "type": "object",
- "properties": {
- "debugMessage": {"type": "string"},
- "file": {"type": "string"},
- "line": {"type": "integer"},
- "trace": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "file": {"type": "string"},
- "line": {"type": "integer"},
- "call": {"type": ["string", "null"]},
- "function": {"type": ["string", "null"]}
- },
- "additionalProperties": false
- },
- "minItems": 1
- }
- }
- },
- "locations": {"type": "array"},
- "path": {"type": "array"}
- },
- "required": [
- "message",
- "extensions",
- "locations",
- "path"
- ]
- },
- "minItems": 1,
- "maxItems": 1
- }
- }
- }
- """
-
- Scenario: Use outputClass instead of resource class through a GraphQL query
- Given there are 2 dummyDtoNoInput objects
- When I send the following GraphQL request:
- """
- {
- dummyDtoNoInputs {
- edges {
- node {
- baz
- bat
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "dummyDtoNoInputs": {
- "edges": [
- {
- "node": {
- "baz": 0.33,
- "bat": "DummyDtoNoInput foo #1"
- }
- },
- {
- "node": {
- "baz": 0.67,
- "bat": "DummyDtoNoInput foo #2"
- }
- }
- ]
- }
- }
- }
- """
-
- @createSchema
- Scenario: Disable outputClass leads to an empty response through a GraphQL query
- Given there are 2 dummyDtoNoOutput objects
- When I send the following GraphQL request:
- """
- {
- dummyDtoNoInputs {
- edges {
- node {
- baz
- bat
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "dummyDtoNoInputs": {
- "edges": []
- }
- }
- }
- """
-
- Scenario: Custom not retrieved item query
- When I send the following GraphQL request:
- """
- {
- testNotRetrievedItemDummyCustomQuery {
- message
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testNotRetrievedItemDummyCustomQuery": {
- "message": "Success (not retrieved)!"
- }
- }
- }
- """
-
- Scenario: Custom item query with read and serialize set to false
- When I send the following GraphQL request:
- """
- {
- testNoReadAndSerializeItemDummyCustomQuery(id: "/not_used") {
- message
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testNoReadAndSerializeItemDummyCustomQuery": null
- }
- }
- """
-
- Scenario: Custom item query
- Given there are 2 dummyCustomQuery objects
- When I send the following GraphQL request:
- """
- {
- testItemDummyCustomQuery(id: "/dummy_custom_queries/1") {
- message
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testItemDummyCustomQuery": {
- "message": "Success!"
- }
- }
- }
- """
-
- Scenario: Custom item query with custom arguments
- Given there are 2 dummyCustomQuery objects
- When I send the following GraphQL request:
- """
- {
- testItemCustomArgumentsDummyCustomQuery(
- id: "/dummy_custom_queries/1",
- customArgumentBool: true,
- customArgumentInt: 3,
- customArgumentString: "A string",
- customArgumentFloat: 2.6,
- customArgumentIntArray: [4],
- customArgumentCustomType: "2019-05-24T00:00:00+00:00"
- ) {
- message
- customArgs
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be equal to:
- """
- {
- "data": {
- "testItemCustomArgumentsDummyCustomQuery": {
- "message": "Success!",
- "customArgs": {
- "id": "/dummy_custom_queries/1",
- "customArgumentBool": true,
- "customArgumentInt": 3,
- "customArgumentString": "A string",
- "customArgumentFloat": 2.6,
- "customArgumentIntArray": [4],
- "customArgumentCustomType": "2019-05-24T00:00:00+00:00"
- }
- }
- }
- }
- """
-
- @createSchema
- Scenario: Retrieve an item with different serialization groups for item_query and collection_query
- Given there are 1 dummy with different GraphQL serialization groups objects
- When I send the following GraphQL request:
- """
- {
- dummyDifferentGraphQlSerializationGroup(id: "/dummy_different_graph_ql_serialization_groups/1") {
- name
- title
- }
- }
- """
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummyDifferentGraphQlSerializationGroup.name" should be equal to "Name #1"
- And the JSON node "data.dummyDifferentGraphQlSerializationGroup.title" should be equal to "Title #1"
-
- Scenario: Call security after resolver
- When I send the following GraphQL request:
- """
- {
- getSecurityAfterResolver(id: "/security_after_resolvers/1") {
- name
- }
- }
- """
- Then the response status code should be 200
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.getSecurityAfterResolver.name" should be equal to "test"
-
-
- Scenario: Call security after resolver with 403 error (ensure /2 does not match securityAfterResolver)
- When I send the following GraphQL request:
- """"
- {
- getSecurityAfterResolver(id: "/security_after_resolvers/2") {
- name
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].extensions.status" should be equal to 403
- And the JSON node "errors[0].message" should be equal to "Access Denied."
- And the JSON node "data.getSecurityAfterResolver.name" should not exist
diff --git a/features/graphql/schema.feature b/features/graphql/schema.feature
deleted file mode 100644
index 86a48cebd13..00000000000
--- a/features/graphql/schema.feature
+++ /dev/null
@@ -1,113 +0,0 @@
-Feature: GraphQL schema-related features
-
- @createSchema
- Scenario: Export the GraphQL schema in SDL
- When I run the command "api:graphql:export"
- Then the command output should contain:
- """
- ###Dummy Friend.###
- type DummyFriend implements Node {
- id: ID!
-
- ###The id###
- _id: Int!
-
- ###The dummy name###
- name: String!
- }
- """
- And the command output should contain:
- """
- ###Cursor connection for DummyFriend.###
- type DummyFriendCursorConnection {
- edges: [DummyFriendEdge]
- pageInfo: DummyFriendPageInfo!
- totalCount: Int!
- }
-
- ###Edge of DummyFriend.###
- type DummyFriendEdge {
- node: DummyFriend
- cursor: String!
- }
-
- ###Information about the current page.###
- type DummyFriendPageInfo {
- endCursor: String
- startCursor: String
- hasNextPage: Boolean!
- hasPreviousPage: Boolean!
- }
- """
- And the command output should contain:
- """
- ###Updates a DummyFriend.###
- updateDummyFriend(input: updateDummyFriendInput!): updateDummyFriendPayload
-
- ###Deletes a DummyFriend.###
- deleteDummyFriend(input: deleteDummyFriendInput!): deleteDummyFriendPayload
-
- ###Creates a DummyFriend.###
- createDummyFriend(input: createDummyFriendInput!): createDummyFriendPayload
- """
- And the command output should contain:
- """
- ###Updates a DummyFriend.###
- input updateDummyFriendInput {
- id: ID!
-
- ###The dummy name###
- name: String
- clientMutationId: String
- }
- """
- And the command output should contain:
- """
- ###Updates a DummyFriend.###
- type updateDummyFriendPayload {
- dummyFriend: DummyFriend
- clientMutationId: String
- }
- """
- And the command output should contain:
- """
- ###Deletes a DummyFriend.###
- input deleteDummyFriendInput {
- id: ID!
- clientMutationId: String
- }
-
- ###Deletes a DummyFriend.###
- type deleteDummyFriendPayload {
- dummyFriend: DummyFriend
- clientMutationId: String
- }
- """
- And the command output should contain:
- """
- ###Creates a DummyFriend.###
- input createDummyFriendInput {
- ###The dummy name###
- name: String!
- clientMutationId: String
- }
-
- ###Creates a DummyFriend.###
- type createDummyFriendPayload {
- dummyFriend: DummyFriend
- clientMutationId: String
- }
- """
- And the command output should contain:
- """
- "Updates a OptionalRequiredDummy."
- input updateOptionalRequiredDummyInput {
- id: ID!
- thirdLevel: updateThirdLevelNestedInput
- thirdLevelRequired: updateThirdLevelNestedInput!
-
- "Get relatedToDummyFriend."
- relatedToDummyFriend: [updateRelatedToDummyFriendNestedInput]
- clientMutationId: String
- }
- """
diff --git a/features/graphql/subscription.feature b/features/graphql/subscription.feature
deleted file mode 100644
index 75863ec04bf..00000000000
--- a/features/graphql/subscription.feature
+++ /dev/null
@@ -1,224 +0,0 @@
-Feature: GraphQL subscription support
-
- @createSchema
- Scenario: Introspect subscription type
- When I send the following GraphQL request:
- """
- {
- __type(name: "Subscription") {
- fields {
- name
- description
- type {
- name
- kind
- }
- args {
- name
- type {
- name
- kind
- ofType {
- name
- kind
- }
- }
- }
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON should be valid according to this schema:
- """
- {
- "type": "object",
- "required": [
- "data"
- ],
- "properties": {
- "data": {
- "type": "object",
- "required": [
- "__type"
- ],
- "properties": {
- "__type": {
- "type": "object",
- "required": [
- "fields"
- ],
- "properties": {
- "fields": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "object",
- "required": [
- "name",
- "description",
- "type",
- "args"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+Subscribe"
- },
- "description": {
- "pattern": "^Subscribes to the update event of a [A-z0-9]+.$"
- },
- "type": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+SubscriptionPayload$"
- },
- "kind": {
- "enum": ["OBJECT"]
- }
- }
- },
- "args": {
- "type": "array",
- "minItems": 1,
- "maxItems": 1,
- "items": [
- {
- "type": "object",
- "required": [
- "name",
- "type"
- ],
- "properties": {
- "name": {
- "enum": ["input"]
- },
- "type": {
- "type": "object",
- "required": [
- "kind",
- "ofType"
- ],
- "properties": {
- "kind": {
- "enum": ["NON_NULL"]
- },
- "ofType": {
- "type": "object",
- "required": [
- "name",
- "kind"
- ],
- "properties": {
- "name": {
- "pattern": "^update[A-z0-9]+SubscriptionInput$"
- },
- "kind": {
- "enum": ["INPUT_OBJECT"]
- }
- }
- }
- }
- }
- }
- }
- ]
- }
- }
- }
- }
- }
- }
- }
- }
- }
- }
- """
-
- Scenario: Subscribe to updates
- Given there are 2 dummy mercure objects
- When I send the following GraphQL request:
- """
- subscription {
- updateDummyMercureSubscribe(input: {id: "/dummy_mercures/1", clientSubscriptionId: "myId"}) {
- dummyMercure {
- id
- name
- relatedDummy {
- name
- }
- }
- mercureUrl
- clientSubscriptionId
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.id" should be equal to "/dummy_mercures/1"
- And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.name" should be equal to "Dummy Mercure #1"
- And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks\?topic=http://example.com/subscriptions/[a-f0-9]+$@"
- And the JSON node "data.updateDummyMercureSubscribe.clientSubscriptionId" should be equal to "myId"
-
- When I send the following GraphQL request:
- """
- subscription {
- updateDummyMercureSubscribe(input: {id: "/dummy_mercures/2"}) {
- dummyMercure {
- id
- }
- mercureUrl
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateDummyMercureSubscribe.dummyMercure.id" should be equal to "/dummy_mercures/2"
- And the JSON node "data.updateDummyMercureSubscribe.mercureUrl" should match "@^https://demo.mercure.rocks\?topic=http://example.com/subscriptions/[a-f0-9]+$@"
-
- Scenario: Receive Mercure updates with different payloads from subscriptions (legacy PUT in non-standard mode)
- When I add "Accept" header equal to "application/ld+json"
- And I add "Content-Type" header equal to "application/ld+json"
- And I send a "PUT" request to "/dummy_mercures/1" with body:
- """
- {
- "name": "Dummy Mercure #1 updated"
- }
- """
- Then the following Mercure update with topics "http://example.com/subscriptions/[a-f0-9]+" should have been sent:
- """
- {
- "dummyMercure": {
- "id": 1,
- "name": "Dummy Mercure #1 updated",
- "relatedDummy": {
- "name": "RelatedDummy #1"
- }
- }
- }
- """
-
- When I add "Accept" header equal to "application/ld+json"
- And I add "Content-Type" header equal to "application/ld+json"
- And I send a "PUT" request to "/dummy_mercures/2" with body:
- """
- {
- "name": "Dummy Mercure #2 updated"
- }
- """
- Then the following Mercure update with topics "http://example.com/subscriptions/[a-f0-9]+" should have been sent:
- """
- {
- "dummyMercure": {
- "id": 2
- }
- }
- """
diff --git a/features/graphql/type.feature b/features/graphql/type.feature
deleted file mode 100644
index 03a072785d5..00000000000
--- a/features/graphql/type.feature
+++ /dev/null
@@ -1,80 +0,0 @@
-Feature: GraphQL type support
-
- @createSchema
- Scenario: Use a custom type for a field
- Given there are 2 dummy objects with dummyDate
- When I send the following GraphQL request:
- """
- {
- dummy(id: "/dummies/1") {
- dummyDate
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.dummy.dummyDate" should be equal to "2015-04-01"
-
- Scenario: Use a custom type for an input field
- When I send the following GraphQL request:
- """
- mutation {
- updateDummy(input: {id: "/dummies/1", dummyDate: "2019-05-24T00:00:00+00:00"}) {
- dummy {
- dummyDate
- }
- }
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateDummy.dummy.dummyDate" should be equal to "2019-05-24"
-
- Scenario: Use a custom type for a query variable
- When I have the following GraphQL request:
- """
- mutation UpdateDummyDate($itemId: ID!, $itemDate: DateTime!) {
- updateDummy(input: {id: $itemId, dummyDate: $itemDate}) {
- dummy {
- dummyDate
- }
- }
- }
- """
- And I send the GraphQL request with variables:
- """
- {
- "itemId": "/dummies/1",
- "itemDate": "2017-11-14T00:00:00+00:00"
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "data.updateDummy.dummy.dummyDate" should be equal to "2017-11-14"
-
- Scenario: Use a custom type for a query variable and use a bad value
- When I have the following GraphQL request:
- """
- mutation UpdateDummyDate($itemId: ID!, $itemDate: DateTime!) {
- updateDummy(input: {id: $itemId, dummyDate: $itemDate}) {
- dummy {
- dummyDate
- }
- }
- }
- """
- And I send the GraphQL request with variables:
- """
- {
- "itemId": "/dummies/1",
- "itemDate": "bad date"
- }
- """
- Then the response status code should be 200
- And the response should be in JSON
- And the header "Content-Type" should be equal to "application/json"
- And the JSON node "errors[0].message" should contain 'Variable "$itemDate" got invalid value "bad date";'
- And the JSON node "errors[0].message" should contain 'DateTime cannot represent non date value: "bad date"'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 9f177ef37f1..aa1d500db50 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -12,7 +12,6 @@
-
@@ -40,7 +39,6 @@
tests
- features
vendor
.php-cs-fixer.dist.php
diff --git a/src/Doctrine/Odm/Tests/AppKernel.php b/src/Doctrine/Odm/Tests/AppKernel.php
index 773a4e31592..039813186e5 100644
--- a/src/Doctrine/Odm/Tests/AppKernel.php
+++ b/src/Doctrine/Odm/Tests/AppKernel.php
@@ -33,7 +33,6 @@ public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
- // patch for behat/symfony2-extension not supporting %env(APP_ENV)%
$this->environment = $_SERVER['APP_ENV'] ?? $environment;
}
diff --git a/src/Doctrine/Orm/Tests/AppKernel.php b/src/Doctrine/Orm/Tests/AppKernel.php
index 66c5948a28c..3abb43d2880 100644
--- a/src/Doctrine/Orm/Tests/AppKernel.php
+++ b/src/Doctrine/Orm/Tests/AppKernel.php
@@ -33,7 +33,6 @@ public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
- // patch for behat/symfony2-extension not supporting %env(APP_ENV)%
$this->environment = $_SERVER['APP_ENV'] ?? $environment;
}
diff --git a/src/GraphQl/Test/GraphQlTestTrait.php b/src/GraphQl/Test/GraphQlTestTrait.php
new file mode 100644
index 00000000000..925cacdfa11
--- /dev/null
+++ b/src/GraphQl/Test/GraphQlTestTrait.php
@@ -0,0 +1,141 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\GraphQl\Test;
+
+use GraphQL\Type\Introspection;
+use PHPUnit\Framework\Assert;
+use PHPUnit\Framework\ExpectationFailedException;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * Helpers for functional GraphQL tests.
+ *
+ * Designed to be mixed into a class that exposes a static `createClient()` returning
+ * an HTTP client with a `request()` method (e.g. ApiPlatform\Symfony\Bundle\Test\ApiTestCase).
+ */
+trait GraphQlTestTrait
+{
+ /**
+ * @param array $variables
+ * @param array $headers
+ */
+ protected function executeGraphQl(string $query, array $variables = [], ?string $operationName = null, array $headers = []): ResponseInterface
+ {
+ $payload = ['query' => $query];
+
+ if ($variables) {
+ $payload['variables'] = $variables;
+ }
+
+ if (null !== $operationName) {
+ $payload['operationName'] = $operationName;
+ }
+
+ $options = ['json' => $payload];
+
+ if ($headers) {
+ $options['headers'] = $headers;
+ }
+
+ return static::createClient()->request('POST', '/graphql', $options);
+ }
+
+ /**
+ * @param array $headers
+ */
+ protected function introspectSchema(array $headers = []): ResponseInterface
+ {
+ return $this->executeGraphQl(Introspection::getIntrospectionQuery(), [], null, $headers);
+ }
+
+ /**
+ * Send a `multipart/form-data` GraphQL request following the
+ * graphql-multipart-request-spec (https://github.com/jaydenseric/graphql-multipart-request-spec).
+ *
+ * @param array $files Map of file marker => absolute file path or UploadedFile
+ * @param array $headers
+ */
+ protected function executeGraphQlMultipart(string $operations, string $map, array $files, array $headers = []): ResponseInterface
+ {
+ return static::createClient()->request('POST', '/graphql', [
+ 'headers' => ['Content-Type' => 'multipart/form-data'] + $headers,
+ 'extra' => [
+ 'parameters' => ['operations' => $operations, 'map' => $map],
+ 'files' => $files,
+ ],
+ ]);
+ }
+
+ /**
+ * @param array{errors?: list} $data
+ */
+ protected function assertGraphQlError(array $data, string $expectedMessage, int $index = 0): void
+ {
+ if (!isset($data['errors'][$index])) {
+ throw new ExpectationFailedException(\sprintf('No GraphQL error at index %d.', $index));
+ }
+
+ Assert::assertSame($expectedMessage, $data['errors'][$index]['message'] ?? null);
+ }
+
+ /**
+ * Mirrors the Behat `the GraphQL debug message should be equal to` step:
+ * looks under `errors[$i].extensions.debugMessage` first, falls back to
+ * `errors[$i].debugMessage` for graphql-php < 15.
+ *
+ * @param array{errors?: list>} $data
+ */
+ protected function assertGraphQlDebugMessage(array $data, string $expectedDebugMessage, int $index = 0): void
+ {
+ if (!isset($data['errors'][$index])) {
+ throw new ExpectationFailedException(\sprintf('No GraphQL error at index %d.', $index));
+ }
+
+ $error = $data['errors'][$index];
+ $debug = $error['extensions']['debugMessage'] ?? $error['debugMessage'] ?? null;
+
+ Assert::assertSame($expectedDebugMessage, $debug);
+ }
+
+ /**
+ * Assert that a field returned by a `__type(name: ...) { fields { ... } }` query is
+ * flagged as deprecated with the given reason.
+ *
+ * @param array{data?: array{__type?: array{fields?: list>}}} $data
+ */
+ protected function assertGraphQlFieldDeprecated(array $data, string $fieldName, string $reason): void
+ {
+ $fields = $data['data']['__type']['fields'] ?? null;
+
+ if (!\is_array($fields)) {
+ throw new ExpectationFailedException('Expected response to contain "data.__type.fields".');
+ }
+
+ foreach ($fields as $field) {
+ if (($field['name'] ?? null) !== $fieldName) {
+ continue;
+ }
+
+ if (true === ($field['isDeprecated'] ?? null) && $reason === ($field['deprecationReason'] ?? null)) {
+ Assert::assertTrue(true);
+
+ return;
+ }
+
+ throw new ExpectationFailedException(\sprintf('Field "%s" is not deprecated with reason "%s".', $fieldName, $reason));
+ }
+
+ throw new ExpectationFailedException(\sprintf('Field "%s" not found in "data.__type.fields".', $fieldName));
+ }
+}
diff --git a/tests/AGENTS.md b/tests/AGENTS.md
index 89440051446..5b27935eed7 100644
--- a/tests/AGENTS.md
+++ b/tests/AGENTS.md
@@ -24,12 +24,6 @@ To run tests against MongoDB:
## Execution Guidelines
-### Behat (Functional)
-
-* **Progress Format:** ALWAYS use \--format=progress. Without this, output verbosity increases execution time from \~10m to \~30m.
-* **Tags:** Filter efficiently: vendor/bin/behat \--tags=@pagination \--format=progress
-* **Debugging:** Only drop \--format=progress if you need to debug a *single* scenario using \-vvv.
-
### PHPUnit
* **Filtering:** Never run the full suite. Always filter by class or path.
diff --git a/tests/Behat/CommandContext.php b/tests/Behat/CommandContext.php
deleted file mode 100644
index 666f387410a..00000000000
--- a/tests/Behat/CommandContext.php
+++ /dev/null
@@ -1,106 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Behat\Context\Context;
-use Behat\Gherkin\Node\PyStringNode;
-use Behat\Gherkin\Node\TableNode;
-use GraphQL\Error\Error;
-use PHPUnit\Framework\Assert;
-use Symfony\Bundle\FrameworkBundle\Console\Application;
-use Symfony\Component\Console\Command\Command;
-use Symfony\Component\Console\Tester\CommandTester;
-use Symfony\Component\HttpKernel\KernelInterface;
-
-/**
- * Context for Symfony commands.
- *
- * @author Alan Poulain
- */
-final class CommandContext implements Context
-{
- private ?Application $application = null;
-
- private ?CommandTester $commandTester = null;
-
- public function __construct(private KernelInterface $kernel)
- {
- }
-
- /**
- * @When I run the command :command
- */
- public function iRunTheCommand(string $command): void
- {
- $command = $this->getApplication()->find($command);
-
- $this->getCommandTester($command)->execute([]);
- }
-
- /**
- * @When I run the command :command with options:
- */
- public function iRunTheCommandWithOptions(string $command, TableNode $options): void
- {
- $command = $this->getApplication()->find($command);
-
- $this->getCommandTester($command)->execute($options->getRowsHash());
- }
-
- /**
- * @Then the command output should be:
- */
- public function theCommandOutputShouldBe(PyStringNode $expectedOutput): void
- {
- Assert::assertEquals($expectedOutput->getRaw(), $this->commandTester->getDisplay());
- }
-
- /**
- * @Then the command output should contain:
- */
- public function theCommandOutputShouldContain(PyStringNode $expectedOutput): void
- {
- // graphql-php < 15
- if (\defined(Error::class.'::CATEGORY_GRAPHQL')) {
- $expectedOutput = str_replace('###', '"""', $expectedOutput->getRaw());
- } else {
- $expectedOutput = str_replace('###', '"', $expectedOutput->getRaw());
- }
-
- Assert::assertStringContainsString($expectedOutput, $this->commandTester->getDisplay());
- }
-
- public function setKernel(KernelInterface $kernel): void
- {
- $this->kernel = $kernel;
- }
-
- public function getApplication(): Application
- {
- if (null !== $this->application) {
- return $this->application;
- }
-
- $this->application = new Application($this->kernel);
-
- return $this->application;
- }
-
- private function getCommandTester(Command $command): CommandTester
- {
- $this->commandTester = new CommandTester($command);
-
- return $this->commandTester;
- }
-}
diff --git a/tests/Behat/CoverageContext.php b/tests/Behat/CoverageContext.php
deleted file mode 100644
index ee5c171cd3d..00000000000
--- a/tests/Behat/CoverageContext.php
+++ /dev/null
@@ -1,92 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Behat\Context\Context;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use SebastianBergmann\CodeCoverage\CodeCoverage;
-use SebastianBergmann\CodeCoverage\Driver\Selector;
-use SebastianBergmann\CodeCoverage\Filter;
-use SebastianBergmann\CodeCoverage\Report\PHP;
-use Symfony\Component\Finder\Finder;
-
-/**
- * Behat coverage.
- *
- * @author eliecharra
- * @author Kévin Dunglas
- * @copyright Adapted from https://gist.github.com/eliecharra/9c8b3ba57998b50e14a6
- */
-final class CoverageContext implements Context
-{
- /**
- * @var CodeCoverage
- */
- private static $coverage;
-
- /**
- * @BeforeSuite
- */
- public static function setup(): void
- {
- $filter = new Filter();
- $finder =
- (new Finder())
- ->in(__DIR__.'/../../src')
- ->exclude([
- 'src/Core/Bridge/Symfony/Maker/Resources/skeleton',
- 'tests/Fixtures/app/var',
- 'docs/guides',
- 'docs/var',
- 'src/Doctrine/Orm/Tests/var',
- 'src/Doctrine/Odm/Tests/var',
- ])
- ->append([
- 'tests/Fixtures/app/console',
- ])
- ->files()
- ->name('*.php');
-
- foreach ($finder as $file) {
- $filter->includeFile((string) $file);
- }
-
- self::$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
- }
-
- /**
- * @AfterSuite
- */
- public static function teardown(): void
- {
- $feature = getenv('FEATURE') ?: 'behat';
- (new PHP())->process(self::$coverage, __DIR__."/../../build/coverage/coverage-$feature.cov");
- }
-
- /**
- * @BeforeScenario
- */
- public function before(BeforeScenarioScope $scope): void
- {
- self::$coverage->start("{$scope->getFeature()->getTitle()}::{$scope->getScenario()->getTitle()}");
- }
-
- /**
- * @AfterScenario
- */
- public function after(): void
- {
- self::$coverage->stop();
- }
-}
diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php
deleted file mode 100644
index 4639644beb4..00000000000
--- a/tests/Behat/DoctrineContext.php
+++ /dev/null
@@ -1,2707 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use ApiPlatform\Tests\Fixtures\TestBundle\Doctrine\Orm\EntityManager;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\AbsoluteUrlDummy as AbsoluteUrlDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\AbsoluteUrlRelationDummy as AbsoluteUrlRelationDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Address as AddressDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Answer as AnswerDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Book as BookDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Comment as CommentDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CompositeItem as CompositeItemDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CompositeLabel as CompositeLabelDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CompositePrimitiveItem as CompositePrimitiveItemDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CompositeRelation as CompositeRelationDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedBoolean as ConvertedBoolDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedDate as ConvertedDateDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedInteger as ConvertedIntegerDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedOwner as ConvertedOwnerDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedRelated as ConvertedRelatedDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedString as ConvertedStringDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Customer as CustomerDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CustomMultipleIdentifierDummy as CustomMultipleIdentifierDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyAggregateOffer as DummyAggregateOfferDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCarColor as DummyCarColorDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomMutation as DummyCustomMutationDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDate as DummyDateDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDifferentGraphQlSerializationGroup as DummyDifferentGraphQlSerializationGroupDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoCustom as DummyDtoCustomDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyImmutableDate as DummyImmutableDateDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyMercure as DummyMercureDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyOffer as DummyOfferDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyPassenger as DummyPassengerDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyProduct as DummyProductDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyProperty as DummyPropertyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyTableInheritanceNotApiResourceChild as DummyTableInheritanceNotApiResourceChildDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyTravel as DummyTravelDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\FileConfigDummy as FileConfigDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Foo as FooDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooDummy as FooDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooEmbeddable as FooEmbeddableDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\FourthLevel as FourthLevelDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Greeting as GreetingDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\InitializeInput as InitializeInputDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\IriOnlyDummy as IriOnlyDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\LinkHandledDummy as LinkHandledDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MaxDepthDummy as MaxDepthDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsDummy as MultiRelationsDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNested as MultiRelationsNestedDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsNestedPaginated as MultiRelationsNestedPaginatedDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsRelatedDummy as MultiRelationsRelatedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MultiRelationsResolveDummy as MultiRelationsResolveDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\MusicGroup as MusicGroupDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathDummy as NetworkPathDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\NetworkPathRelationDummy as NetworkPathRelationDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Order as OrderDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\PatchDummyRelation as PatchDummyRelationDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Payment as PaymentDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Person as PersonDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\PersonToPet as PersonToPetDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Pet as PetDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Product as ProductDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Program as ProgramDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\PropertyCollectionIriOnly as PropertyCollectionIriOnlyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\PropertyCollectionIriOnlyRelation as PropertyCollectionIriOnlyRelationDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\PropertyUriTemplateOneToOneRelation as PropertyUriTemplateOneToOneRelationDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Question as QuestionDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedLinkedDummy as RelatedLinkedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedOwnedDummy as RelatedOwnedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedOwningDummy as RelatedOwningDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedSecuredDummy as RelatedSecuredDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedToDummyFriend as RelatedToDummyFriendDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelationEmbedder as RelationEmbedderDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\SecuredDummy as SecuredDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\SeparatedEntity as SeparatedEntityDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\SoMany as SoManyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\Taxon as TaxonDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\UrlEncodedId as UrlEncodedIdDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\User as UserDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\VideoGame as VideoGameDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\WithJsonDummy as WithJsonDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\AbsoluteUrlRelationDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Address;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Answer;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Book;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Comment;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeItem;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedBoolean;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedDate;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedInteger;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedOwner;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedRelated;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedString;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Customer;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CustomMultipleIdentifierDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomMutation;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDate;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDifferentGraphQlSerializationGroup;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoCustom;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMappedSubclass;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMercure;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyOffer;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPassenger;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProduct;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProperty;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummySubEntity;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceNotApiResourceChild;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyWithSubEntity;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EntityClassWithDateTime;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ExternalUser;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FileConfigDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Foo;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooEmbeddable;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Greeting;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InitializeInput;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\InternalUser;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\IriOnlyDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\Event;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\ItemLog;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Group;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\LinkHandledDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNested;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNestedPaginated;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsResolveDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MusicGroup;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NetworkPathRelationDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Order;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PaginationEntity;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PatchDummyRelation;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Payment;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PersonToPet;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Pet;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Product;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnly;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnlyRelation;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyUriTemplateOneToOneRelation;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedLinkedDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwnedDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedOwningDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedSecuredDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationEmbedder;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelationMultiple;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Site;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SymfonyUuidDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Taxon;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\TreeDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UrlEncodedId;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VideoGame;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\WithJsonDummy;
-use Behat\Behat\Context\Context;
-use Behat\Gherkin\Node\PyStringNode;
-use Doctrine\Common\Collections\ArrayCollection;
-use Doctrine\ODM\MongoDB\DocumentManager;
-use Doctrine\ODM\MongoDB\Query\Builder;
-use Doctrine\ODM\MongoDB\SchemaManager;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Mapping\ClassMetadata;
-use Doctrine\ORM\Tools\SchemaTool;
-use Doctrine\Persistence\ManagerRegistry;
-use Doctrine\Persistence\ObjectManager;
-use Ramsey\Uuid\Uuid;
-use Symfony\Component\Uid\Uuid as SymfonyUuid;
-
-/**
- * Defines application features from the specific context.
- */
-final class DoctrineContext implements Context
-{
- private ObjectManager $manager;
- private ?SchemaTool $schemaTool;
- private ?SchemaManager $schemaManager;
-
- /**
- * Initializes context.
- *
- * Every scenario gets its own context instance.
- * You can also pass arbitrary arguments to the
- * context constructor through behat.yml.
- */
- public function __construct(private readonly ManagerRegistry $doctrine, private readonly mixed $passwordHasher)
- {
- $this->manager = $doctrine->getManager();
- $this->schemaTool = $this->manager instanceof EntityManagerInterface ? new SchemaTool($this->manager) : null;
- $this->schemaManager = $this->manager instanceof DocumentManager ? $this->manager->getSchemaManager() : null;
- }
-
- /**
- * @BeforeScenario @createSchema
- */
- public function createDatabase(): void
- {
- /** @var ClassMetadata[] $classes */
- $classes = $this->manager->getMetadataFactory()->getAllMetadata();
-
- if ($this->isOrm()) {
- $this->schemaTool->dropSchema($classes);
- $this->schemaTool->createSchema($classes);
- }
-
- if ($this->isOdm()) {
- $this->schemaManager->dropDatabases();
- }
-
- $this->doctrine->getManager()->clear();
- }
-
- /**
- * @Then the DQL should be equal to:
- */
- public function theDqlShouldBeEqualTo(PyStringNode $dql): void
- {
- /** @var EntityManager $manager */
- $manager = $this->doctrine->getManager();
-
- $actualDql = $manager::$dql;
-
- $expectedDql = preg_replace('/\(\R */', '(', (string) $dql);
- $expectedDql = preg_replace('/\R *\)/', ')', $expectedDql);
- $expectedDql = preg_replace('/\R */', ' ', $expectedDql);
-
- if ($expectedDql !== $actualDql) {
- throw new \RuntimeException("The DQL:\n'$actualDql' is not equal to:\n'$expectedDql'");
- }
- }
-
- /**
- * @Given there are :nb dummy objects
- */
- public function thereAreDummyObjects(int $nb): void
- {
- $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setDummy('SomeDummyTest'.$i);
- $dummy->setDescription($descriptions[($i - 1) % 2]);
- $dummy->nameConverted = 'Converted '.$i;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb pagination entities
- */
- public function thereArePaginationEntities(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $paginationEntity = new PaginationEntity();
- $this->manager->persist($paginationEntity);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb of these so many objects
- */
- public function thereAreOfTheseSoManyObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $soMany = $this->buildSoMany();
- $soMany->content = 'Many #'.$i;
-
- $this->manager->persist($soMany);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @When some dummy table inheritance data but not api resource child are created
- */
- public function someDummyTableInheritanceDataButNotApiResourceChildAreCreated(): void
- {
- $dummy = $this->buildDummyTableInheritanceNotApiResourceChild();
- $dummy->setName('Foobarbaz inheritance');
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb foo objects with fake names
- */
- public function thereAreFooObjectsWithFakeNames(int $nb): void
- {
- $names = ['Hawsepipe', 'Sthenelus', 'Ephesian', 'Separativeness', 'Balbo'];
- $bars = ['Lorem', 'Ipsum', 'Dolor', 'Sit', 'Amet'];
-
- for ($i = 0; $i < $nb; ++$i) {
- $foo = $this->buildFoo();
- $foo->setName($names[$i]);
- $foo->setBar($bars[$i]);
-
- $this->manager->persist($foo);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb fooDummy objects with fake names
- */
- public function thereAreFooDummyObjectsWithFakeNames(int $nb, $embedd = false): void
- {
- $names = ['Hawsepipe', 'Ephesian', 'Sthenelus', 'Separativeness', 'Balbo'];
- $dummies = ['Lorem', 'Ipsum', 'Dolor', 'Sit', 'Amet'];
-
- for ($i = 0; $i < $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName($dummies[$i]);
-
- $foo = $this->buildFooDummy();
- $foo->setName($names[$i]);
- if ($embedd) {
- $embeddedFoo = $this->buildFooEmbeddable();
- $embeddedFoo->setDummyName('embedded'.$names[$i]);
- $foo->setEmbeddedFoo($embeddedFoo);
- }
- $foo->setDummy($dummy);
- for ($j = 0; $j < 3; ++$j) {
- $soMany = $this->buildSoMany();
- $soMany->content = "So many $j";
- $soMany->fooDummy = $foo;
- $foo->soManies->add($soMany);
- }
-
- $this->manager->persist($foo);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a fooDummy objects with fake names and embeddable
- */
- public function thereAreFooDummyObjectsWithFakeNamesAndEmbeddable(): void
- {
- $this->thereAreFooDummyObjectsWithFakeNames(1, true);
- }
-
- /**
- * @Given there are :nb dummy group objects
- */
- public function thereAreDummyGroupObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyGroup = $this->buildDummyGroup();
-
- foreach (['foo', 'bar', 'baz', 'qux'] as $property) {
- $dummyGroup->{$property} = ucfirst($property).' #'.$i;
- }
-
- $this->manager->persist($dummyGroup);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy property objects
- */
- public function thereAreDummyPropertyObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyProperty = $this->buildDummyProperty();
- $dummyGroup = $this->buildDummyGroup();
-
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyProperty->{$property} = $dummyGroup->{$property} = ucfirst($property).' #'.$i;
- }
- $dummyProperty->nameConverted = "NameConverted #$i";
-
- $dummyProperty->group = $dummyGroup;
-
- $this->manager->persist($dummyGroup);
- $this->manager->persist($dummyProperty);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy property objects with a shared group
- */
- public function thereAreDummyPropertyObjectsWithASharedGroup(int $nb): void
- {
- $dummyGroup = $this->buildDummyGroup();
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyGroup->{$property} = ucfirst($property).' #shared';
- }
- $this->manager->persist($dummyGroup);
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyProperty = $this->buildDummyProperty();
-
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyProperty->{$property} = ucfirst($property).' #'.$i;
- }
-
- $dummyProperty->group = $dummyGroup;
- $this->manager->persist($dummyProperty);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy property objects with different number of related groups
- */
- public function thereAreDummyPropertyObjectsWithADifferentNumberRelatedGroups(int $nb): void
- {
- $dummyGroups = [];
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyGroup = $this->buildDummyGroup();
- $dummyProperty = $this->buildDummyProperty();
-
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyProperty->{$property} = $dummyGroup->{$property} = ucfirst($property).' #'.$i;
- }
-
- $this->manager->persist($dummyGroup);
- $dummyGroups[$i] = $dummyGroup;
-
- for ($j = 1; $j <= $i; ++$j) {
- $dummyProperty->groups[] = $dummyGroups[$j];
- }
-
- $this->manager->persist($dummyProperty);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy property objects with :nb2 groups
- */
- public function thereAreDummyPropertyObjectsWithGroups(int $nb, int $nb2): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyProperty = $this->buildDummyProperty();
- $dummyGroup = $this->buildDummyGroup();
-
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyProperty->{$property} = $dummyGroup->{$property} = ucfirst($property).' #'.$i;
- }
-
- $dummyProperty->group = $dummyGroup;
-
- $this->manager->persist($dummyGroup);
- for ($j = 1; $j <= $nb2; ++$j) {
- $dummyGroup = $this->buildDummyGroup();
-
- foreach (['foo', 'bar', 'baz'] as $property) {
- $dummyGroup->{$property} = ucfirst($property).' #'.$i.$j;
- }
-
- $dummyProperty->groups[] = $dummyGroup;
- $this->manager->persist($dummyGroup);
- }
-
- $this->manager->persist($dummyProperty);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb embedded dummy objects
- */
- public function thereAreEmbeddedDummyObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildEmbeddedDummy();
- $dummy->setName('Dummy #'.$i);
-
- $embeddableDummy = $this->buildEmbeddableDummy();
- $embeddableDummy->setDummyName('Dummy #'.$i);
- $dummy->setEmbeddedDummy($embeddableDummy);
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with relatedDummy
- */
- public function thereAreDummyObjectsWithRelatedDummy(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->nameConverted = "Converted $i";
- $dummy->setRelatedDummy($relatedDummy);
-
- $this->manager->persist($relatedDummy);
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are dummies with similar properties
- */
- public function thereAreDummiesWithSimilarProperties(): void
- {
- $dummy1 = $this->buildDummy();
- $dummy1->setName('foo');
- $dummy1->setDescription('bar');
-
- $dummy2 = $this->buildDummy();
- $dummy2->setName('baz');
- $dummy2->setDescription('qux');
-
- $dummy3 = $this->buildDummy();
- $dummy3->setName('foo');
- $dummy3->setDescription('qux');
-
- $dummy4 = $this->buildDummy();
- $dummy4->setName('baz');
- $dummy4->setDescription('bar');
-
- $this->manager->persist($dummy1);
- $this->manager->persist($dummy2);
- $this->manager->persist($dummy3);
- $this->manager->persist($dummy4);
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummyDtoNoInput objects
- */
- public function thereAreDummyDtoNoInputObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyDto = $this->buildDummyDtoNoInput();
- $dummyDto->lorem = 'DummyDtoNoInput foo #'.$i;
- $dummyDto->ipsum = round($i / 3, 2);
-
- $this->manager->persist($dummyDto);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummyDtoNoOutput objects
- */
- public function thereAreDummyDtoNoOutputObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyDto = $this->buildDummyDtoNoOutput();
- $dummyDto->lorem = 'DummyDtoNoOutput foo #'.$i;
- $dummyDto->ipsum = (string) ($i / 3);
-
- $this->manager->persist($dummyDto);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummyCustomQuery objects
- */
- public function thereAreDummyCustomQueryObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyCustomQuery = $this->buildDummyCustomQuery();
-
- $this->manager->persist($dummyCustomQuery);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummyCustomMutation objects
- */
- public function thereAreDummyCustomMutationObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $customMutationDummy = $this->buildDummyCustomMutation();
- $customMutationDummy->setOperandA(3);
-
- $this->manager->persist($customMutationDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with JSON and array data
- */
- public function thereAreDummyObjectsWithJsonData(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setJsonData(['foo' => ['bar', 'baz'], 'bar' => 5]);
- $dummy->setArrayData(['foo', 'bar', 'baz']);
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy with null JSON objects
- */
- public function thereAreDummyWithNullJsonObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildWithJsonDummy();
- $dummy->json = null;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with relatedDummy and its thirdLevel
- * @Given there is :nb dummy object with relatedDummy and its thirdLevel
- */
- public function thereAreDummyObjectsWithRelatedDummyAndItsThirdLevel(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $thirdLevel = $this->buildThirdLevel();
-
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
- $relatedDummy->setThirdLevel($thirdLevel);
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setRelatedDummy($relatedDummy);
-
- $this->manager->persist($thirdLevel);
- $this->manager->persist($relatedDummy);
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy object with :nb relatedDummies and their thirdLevel
- */
- public function thereIsADummyObjectWithRelatedDummiesAndTheirThirdLevel(int $nb): void
- {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy with relations');
-
- for ($i = 1; $i <= $nb; ++$i) {
- $thirdLevel = $this->buildThirdLevel();
-
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
- $relatedDummy->setThirdLevel($thirdLevel);
-
- $dummy->addRelatedDummy($relatedDummy);
-
- $this->manager->persist($thirdLevel);
- $this->manager->persist($relatedDummy);
- }
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy object with :nb relatedDummies with same thirdLevel
- */
- public function thereIsADummyObjectWithRelatedDummiesWithSameThirdLevel(int $nb): void
- {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy with relations');
- $thirdLevel = $this->buildThirdLevel();
-
- for ($i = 1; $i <= $nb; ++$i) {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
- $relatedDummy->setThirdLevel($thirdLevel);
-
- $dummy->addRelatedDummy($relatedDummy);
-
- $this->manager->persist($relatedDummy);
- }
- $this->manager->persist($thirdLevel);
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with embeddedDummy
- */
- public function thereAreDummyObjectsWithEmbeddedDummy(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $embeddableDummy = $this->buildEmbeddableDummy();
- $embeddableDummy->setDummyName('EmbeddedDummy #'.$i);
-
- $dummy = $this->buildEmbeddedDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setEmbeddedDummy($embeddableDummy);
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects having each :nbrelated relatedDummies
- */
- public function thereAreDummyObjectsWithRelatedDummies(int $nb, int $nbrelated): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
-
- for ($j = 1; $j <= $nbrelated; ++$j) {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy'.$j.$i);
- $relatedDummy->setAge((int) ($j.$i));
- $this->manager->persist($relatedDummy);
-
- $dummy->addRelatedDummy($relatedDummy);
- }
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb multiRelationsDummy objects having each :nbmtor manyToOneRelation, :nbmtmr manyToManyRelations, :nbotmr oneToManyRelations and :nber embeddedRelations
- */
- public function thereAreMultiRelationsDummyObjectsHavingEachAManyToOneRelationManyToManyRelationsOneToManyRelationsAndEmbeddedRelations(int $nb, int $nbmtor, int $nbmtmr, int $nbotmr, int $nber): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $relatedDummy = $this->buildMultiRelationsRelatedDummy();
- $relatedDummy->name = 'RelatedManyToOneDummy #'.$i;
-
- $resolveDummy = $this->buildMultiRelationsResolveDummy();
- $resolveDummy->name = 'RelatedManyToOneResolveDummy #'.$i;
-
- $dummy = $this->buildMultiRelationsDummy();
- $dummy->name = 'Dummy #'.$i;
-
- if ($nbmtor) {
- $dummy->setManyToOneRelation($relatedDummy);
- $dummy->setManyToOneResolveRelation($resolveDummy);
- }
-
- for ($j = 1; $j <= $nbmtmr; ++$j) {
- $manyToManyItem = $this->buildMultiRelationsRelatedDummy();
- $manyToManyItem->name = 'RelatedManyToManyDummy'.$j.$i;
- $this->manager->persist($manyToManyItem);
-
- $dummy->addManyToManyRelation($manyToManyItem);
- }
-
- for ($j = 1; $j <= $nbotmr; ++$j) {
- $oneToManyItem = $this->buildMultiRelationsRelatedDummy();
- $oneToManyItem->name = 'RelatedOneToManyDummy'.$j.$i;
- $oneToManyItem->setOneToManyRelation($dummy);
- $this->manager->persist($oneToManyItem);
-
- $dummy->addOneToManyRelation($oneToManyItem);
- }
-
- $nested = new ArrayCollection();
- for ($j = 1; $j <= $nber; ++$j) {
- $embeddedItem = $this->buildMultiRelationsNested();
- $embeddedItem->name = 'NestedDummy'.$j;
- $nested->add($embeddedItem);
- }
- $dummy->setNestedCollection($nested);
-
- $nestedPaginated = new ArrayCollection();
- for ($j = 1; $j <= $nber; ++$j) {
- $embeddedItem = $this->buildMultiRelationsNestedPaginated();
- $embeddedItem->name = 'NestedPaginatedDummy'.$j;
- $nestedPaginated->add($embeddedItem);
- }
- $dummy->setNestedPaginatedCollection($nestedPaginated);
-
- $this->manager->persist($relatedDummy);
- $this->manager->persist($resolveDummy);
- $this->manager->persist($dummy);
- }
- $this->manager->flush();
- }
-
- /**
- * @Given there are tree dummies
- */
- public function thereAreTreeDummies(): void
- {
- $parentDummy = new TreeDummy();
- $this->manager->persist($parentDummy);
-
- $childDummy = new TreeDummy();
- $childDummy->setParent($parentDummy);
-
- $this->manager->persist($childDummy);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with dummyDate
- * @Given there is :nb dummy object with dummyDate
- */
- public function thereAreDummyObjectsWithDummyDate(int $nb): void
- {
- $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
-
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setDescription($descriptions[($i - 1) % 2]);
-
- // Last Dummy has a null date
- if ($nb !== $i) {
- $dummy->setDummyDate($date);
- }
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with dummyDate and dummyBoolean :bool
- */
- public function thereAreDummyObjectsWithDummyDateAndDummyBoolean(int $nb, string $bool): void
- {
- $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
-
- if (\in_array($bool, ['true', '1', 1], true)) {
- $bool = true;
- } elseif (\in_array($bool, ['false', '0', 0], true)) {
- $bool = false;
- } else {
- $expected = ['true', 'false', '1', '0'];
- throw new \InvalidArgumentException(\sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected)));
- }
-
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setDescription($descriptions[($i - 1) % 2]);
- $dummy->setDummyBoolean($bool);
-
- // Last Dummy has a null date
- if ($nb !== $i) {
- $dummy->setDummyDate($date);
- }
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with dummyDate and relatedDummy
- */
- public function thereAreDummyObjectsWithDummyDateAndRelatedDummy(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
- $relatedDummy->setDummyDate($date);
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setRelatedDummy($relatedDummy);
- // Last Dummy has a null date
- if ($nb !== $i) {
- $dummy->setDummyDate($date);
- }
-
- $this->manager->persist($relatedDummy);
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb embedded dummy objects with dummyDate and embeddedDummy
- */
- public function thereAreDummyObjectsWithDummyDateAndEmbeddedDummy(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $embeddableDummy = $this->buildEmbeddableDummy();
- $embeddableDummy->setDummyName('Embeddable #'.$i);
- $embeddableDummy->setDummyDate($date);
-
- $dummy = $this->buildEmbeddedDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setEmbeddedDummy($embeddableDummy);
- // Last Dummy has a null date
- if ($nb !== $i) {
- $dummy->setDummyDate($date);
- }
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb convertedDate objects
- */
- public function thereAreconvertedDateObjectsWith(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $convertedDate = $this->buildConvertedDate();
- $convertedDate->nameConverted = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $this->manager->persist($convertedDate);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb convertedString objects
- */
- public function thereAreconvertedStringObjectsWith(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $convertedString = $this->buildConvertedString();
- $convertedString->nameConverted = ($i % 2) ? "name#$i" : null;
-
- $this->manager->persist($convertedString);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb convertedBoolean objects
- */
- public function thereAreconvertedBooleanObjectsWith(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $convertedBoolean = $this->buildConvertedBoolean();
- $convertedBoolean->nameConverted = (bool) ($i % 2);
-
- $this->manager->persist($convertedBoolean);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb convertedInteger objects
- */
- public function thereAreconvertedIntegerObjectsWith(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $convertedInteger = $this->buildConvertedInteger();
- $convertedInteger->nameConverted = $i;
-
- $this->manager->persist($convertedInteger);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with dummyPrice
- */
- public function thereAreDummyObjectsWithDummyPrice(int $nb): void
- {
- $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
- $prices = ['9.99', '12.99', '15.99', '19.99'];
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setDescription($descriptions[($i - 1) % 2]);
- $dummy->setDummyPrice($prices[($i - 1) % 4]);
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy objects with dummyBoolean :bool
- * @Given there is :nb dummy object with dummyBoolean :bool
- */
- public function thereAreDummyObjectsWithDummyBoolean(int $nb, string $bool): void
- {
- if (\in_array($bool, ['true', '1', 1], true)) {
- $bool = true;
- } elseif (\in_array($bool, ['false', '0', 0], true)) {
- $bool = false;
- } else {
- $expected = ['true', 'false', '1', '0'];
- throw new \InvalidArgumentException(\sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected)));
- }
- $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy #'.$i);
- $dummy->setAlias('Alias #'.($nb - $i));
- $dummy->setDescription($descriptions[($i - 1) % 2]);
- $dummy->setDummyBoolean($bool);
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb embedded dummy objects with embeddedDummy.dummyBoolean :bool
- */
- public function thereAreDummyObjectsWithEmbeddedDummyBoolean(int $nb, string $bool): void
- {
- if (\in_array($bool, ['true', '1', 1], true)) {
- $bool = true;
- } elseif (\in_array($bool, ['false', '0', 0], true)) {
- $bool = false;
- } else {
- $expected = ['true', 'false', '1', '0'];
- throw new \InvalidArgumentException(\sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected)));
- }
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildEmbeddedDummy();
- $dummy->setName('Embedded Dummy #'.$i);
- $embeddableDummy = $this->buildEmbeddableDummy();
- $embeddableDummy->setDummyName('Embedded Dummy #'.$i);
- $embeddableDummy->setDummyBoolean($bool);
- $dummy->setEmbeddedDummy($embeddableDummy);
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb embedded dummy objects with relatedDummy.embeddedDummy.dummyBoolean :bool
- */
- public function thereAreDummyObjectsWithRelationEmbeddedDummyBoolean(int $nb, string $bool): void
- {
- if (\in_array($bool, ['true', '1', 1], true)) {
- $bool = true;
- } elseif (\in_array($bool, ['false', '0', 0], true)) {
- $bool = false;
- } else {
- $expected = ['true', 'false', '1', '0'];
- throw new \InvalidArgumentException(\sprintf('Invalid boolean value for "%s" property, expected one of ( "%s" )', $bool, implode('" | "', $expected)));
- }
-
- for ($i = 1; $i <= $nb; ++$i) {
- $dummy = $this->buildEmbeddedDummy();
- $dummy->setName('Embedded Dummy #'.$i);
- $embeddableDummy = $this->buildEmbeddableDummy();
- $embeddableDummy->setDummyName('Embedded Dummy #'.$i);
- $embeddableDummy->setDummyBoolean($bool);
-
- $relationDummy = $this->buildRelatedDummy();
- $relationDummy->setEmbeddedDummy($embeddableDummy);
-
- $dummy->setRelatedDummy($relationDummy);
-
- $this->manager->persist($relationDummy);
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb SecuredDummy objects
- */
- public function thereAreSecuredDummyObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $securedDummy = $this->buildSecuredDummy();
- $securedDummy->setTitle("#$i");
- $securedDummy->setDescription("Hello #$i");
- $securedDummy->setOwner('notexist');
-
- $this->manager->persist($securedDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb SecuredDummy objects owned by :ownedby with related dummies
- */
- public function thereAreSecuredDummyObjectsOwnedByWithRelatedDummies(int $nb, string $ownedby): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $securedDummy = $this->buildSecuredDummy();
- $securedDummy->setTitle("#$i");
- $securedDummy->setDescription("Hello #$i");
- $securedDummy->setOwner($ownedby);
-
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy');
- $this->manager->persist($relatedDummy);
-
- $relatedSecuredDummy = $this->buildRelatedSecureDummy();
- $this->manager->persist($relatedSecuredDummy);
-
- $publicRelatedSecuredDummy = $this->buildRelatedSecureDummy();
- $this->manager->persist($publicRelatedSecuredDummy);
-
- $relatedLinkedDummy = $this->buildRelatedLinkedDummy();
- $this->manager->persist($relatedLinkedDummy);
-
- $securedDummy->addRelatedDummy($relatedDummy);
- $securedDummy->setRelatedDummy($relatedDummy);
- $securedDummy->addRelatedSecuredDummy($relatedSecuredDummy);
- $securedDummy->setRelatedSecuredDummy($relatedSecuredDummy);
- $securedDummy->addPublicRelatedSecuredDummy($publicRelatedSecuredDummy);
- $securedDummy->setPublicRelatedSecuredDummy($publicRelatedSecuredDummy);
- $relatedLinkedDummy->setSecuredDummy($securedDummy);
-
- $this->manager->persist($securedDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a RelationEmbedder object
- */
- public function thereIsARelationEmbedderObject(): void
- {
- $relationEmbedder = $this->buildRelationEmbedder();
-
- $this->manager->persist($relationEmbedder);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a Dummy Object mapped by UUID
- */
- public function thereIsADummyObjectMappedByUUID(): void
- {
- $dummy = new UuidIdentifierDummy();
- $dummy->setName('My Dummy');
- $dummy->setUuid('41B29566-144B-11E6-A148-3E1D05DEFE78');
-
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there are Composite identifier objects
- */
- public function thereIsACompositeIdentifierObject(): void
- {
- $item = $this->buildCompositeItem();
- $item->setField1('foobar');
- $this->manager->persist($item);
- $this->manager->flush();
-
- for ($i = 0; $i < 4; ++$i) {
- $label = $this->buildCompositeLabel();
- $label->setValue('foo-'.$i);
-
- $rel = $this->buildCompositeRelation();
- $rel->setCompositeLabel($label);
- $rel->setCompositeItem($item);
- $rel->setValue('somefoobardummy');
-
- $this->manager->persist($label);
- // since doctrine 2.6 we need existing identifiers on relations
- $this->manager->flush();
- $this->manager->persist($rel);
- }
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there are composite primitive identifiers objects
- */
- public function thereAreCompositePrimitiveIdentifiersObjects(): void
- {
- $foo = $this->buildCompositePrimitiveItem('Foo', 2016);
- $foo->setDescription('This is foo.');
- $this->manager->persist($foo);
-
- $bar = $this->buildCompositePrimitiveItem('Bar', 2017);
- $bar->setDescription('This is bar.');
- $this->manager->persist($bar);
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is a FileConfigDummy object
- */
- public function thereIsAFileConfigDummyObject(): void
- {
- $fileConfigDummy = $this->buildFileConfigDummy();
- $fileConfigDummy->setName('ConfigDummy');
- $fileConfigDummy->setFoo('Foo');
-
- $this->manager->persist($fileConfigDummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a DummyCar entity with related colors
- */
- public function thereIsAFooEntityWithRelatedBars(): void
- {
- $foo = $this->buildDummyCar();
- $foo->setName('mustli');
- $foo->setCanSell(true);
- $foo->setAvailableAt(new \DateTime());
- $this->manager->persist($foo);
- $this->manager->flush();
-
- if (\is_object($foo->getId())) {
- $this->manager->persist($foo->getId());
- $this->manager->flush();
- }
-
- $bar1 = $this->buildDummyCarColor();
- $bar1->setProp('red');
- $bar1->setCar($foo);
- $this->manager->persist($bar1);
- $this->manager->flush();
-
- $bar2 = $this->buildDummyCarColor();
- $bar2->setProp('blue');
- $bar2->setCar($foo);
- $this->manager->persist($bar2);
- $this->manager->flush();
-
- $foo->setColors(new ArrayCollection([$bar1, $bar2]));
- $this->manager->persist($foo);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy travel
- */
- public function thereIsADummyTravel(): void
- {
- $car = $this->buildDummyCar();
- $car->setName('model x');
- $car->setCanSell(true);
- $car->setAvailableAt(new \DateTime());
- $this->manager->persist($car);
-
- $passenger = $this->buildDummyPassenger();
- $passenger->nickname = 'Tom';
- $this->manager->persist($passenger);
-
- $travel = $this->buildDummyTravel();
- $travel->car = $car;
- $travel->passenger = $passenger;
- $travel->confirmed = true;
- $this->manager->persist($travel);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a RelatedDummy with :nb friends
- */
- public function thereIsARelatedDummyWithFriends(int $nb): void
- {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy with friends');
- $this->manager->persist($relatedDummy);
- $this->manager->flush();
-
- for ($i = 1; $i <= $nb; ++$i) {
- $friend = $this->buildDummyFriend();
- $friend->setName('Friend-'.$i);
-
- $this->manager->persist($friend);
- // since doctrine 2.6 we need existing identifiers on relations
- // See https://github.com/doctrine/doctrine2/pull/6701
- $this->manager->flush();
-
- $relation = $this->buildRelatedToDummyFriend();
- $relation->setName('Relation-'.$i);
- $relation->setDummyFriend($friend);
- $relation->setRelatedDummy($relatedDummy);
-
- $relatedDummy->addRelatedToDummyFriend($relation);
-
- $this->manager->persist($relation);
- }
-
- $relatedDummy2 = $this->buildRelatedDummy();
- $relatedDummy2->setName('RelatedDummy without friends');
- $this->manager->persist($relatedDummy2);
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is an answer :answer to the question :question
- */
- public function thereIsAnAnswerToTheQuestion(string $a, string $q): void
- {
- $answer = $this->buildAnswer();
- $answer->setContent($a);
-
- $question = $this->buildQuestion();
- $question->setContent($q);
- $question->setAnswer($answer);
- $answer->addRelatedQuestion($question);
-
- $this->manager->persist($answer);
- $this->manager->persist($question);
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is a UrlEncodedId resource
- */
- public function thereIsAUrlEncodedIdResource(): void
- {
- $urlEncodedIdResource = ($this->isOrm() ? new UrlEncodedId() : new UrlEncodedIdDocument());
- $this->manager->persist($urlEncodedIdResource);
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is a Program
- */
- public function thereIsAProgram(): void
- {
- $this->thereArePrograms(1);
- }
-
- /**
- * @Given there are :nb Programs
- */
- public function thereArePrograms(int $nb): void
- {
- $author = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find(1);
- if (null === $author) {
- $author = $this->isOrm() ? new User() : new UserDocument();
- $author->setEmail('john.doe@example.com');
- $author->setFullname('John DOE');
- $author->setPlainPassword('p4$$w0rd');
-
- $this->manager->persist($author);
- $this->manager->flush();
- }
-
- if ($this->isOrm()) {
- $count = $this->doctrine->getRepository(Program::class)->count(['author' => $author]);
- } else {
- /** @var Builder */
- $qb = $this->doctrine->getRepository(ProgramDocument::class)
- ->createQueryBuilder('f');
- $count = $qb->field('author')->equals($author)
- ->count()->getQuery()->execute();
- }
-
- for ($i = (int) $count + 1; $i <= $nb; ++$i) {
- $program = $this->isOrm() ? new Program() : new ProgramDocument();
- $program->name = "Lorem ipsum $i";
- $program->date = new \DateTimeImmutable(\sprintf('2015-03-0%dT10:00:00+00:00', $i));
- $program->author = $author;
-
- $this->manager->persist($program);
- }
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is a Comment
- */
- public function thereIsAComment(): void
- {
- $this->thereAreComments(1);
- }
-
- /**
- * @Given there are :nb Comments
- */
- public function thereAreComments(int $nb): void
- {
- $author = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find(1);
- if (null === $author) {
- $author = $this->isOrm() ? new User() : new UserDocument();
- $author->setEmail('john.doe@example.com');
- $author->setFullname('John DOE');
- $author->setPlainPassword('p4$$w0rd');
-
- $this->manager->persist($author);
- $this->manager->flush();
- }
-
- if ($this->isOrm()) {
- $count = $this->doctrine->getRepository(Comment::class)->count(['author' => $author]);
- } else {
- /** @var Builder $qb */
- $qb = $this->doctrine->getRepository(CommentDocument::class)
- ->createQueryBuilder('f');
-
- $count = $qb->field('author')->equals($author)
- ->count()->getQuery()->execute();
- }
-
- for ($i = (int) $count + 1; $i <= $nb; ++$i) {
- $comment = $this->isOrm() ? new Comment() : new CommentDocument();
- $comment->comment = "Lorem ipsum dolor sit amet $i";
- $comment->date = new \DateTimeImmutable(\sprintf('2015-03-0%dT10:00:00+00:00', $i));
- $comment->author = $author;
-
- $this->manager->persist($comment);
- }
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Then the password :password for user :user should be hashed
- */
- public function thePasswordForUserShouldBeHashed(string $password, string $user): void
- {
- $user = $this->doctrine->getRepository($this->isOrm() ? User::class : UserDocument::class)->find($user);
- if (!$this->passwordHasher->isPasswordValid($user, $password)) {
- throw new \Exception('User password mismatch');
- }
- }
-
- /**
- * @Given I have a product with offers
- */
- public function createProductWithOffers(): void
- {
- $offer = $this->buildDummyOffer();
- $offer->setId(1);
- $offer->setValue(2);
-
- $aggregate = $this->buildDummyAggregateOffer();
- $aggregate->setValue(1);
- $aggregate->addOffer($offer);
-
- $product = $this->buildDummyProduct();
- $product->setId(2);
- $product->setName('Dummy product');
- $product->addOffer($aggregate);
-
- $relatedProduct = $this->buildDummyProduct();
- $relatedProduct->setName('Dummy related product');
- $relatedProduct->setId(1);
- $relatedProduct->setParent($product);
-
- $product->addRelatedProduct($relatedProduct);
-
- $this->manager->persist($relatedProduct);
- $this->manager->persist($product);
- $this->manager->flush();
- }
-
- /**
- * @Given there are people having pets
- */
- public function createPeopleWithPets(): void
- {
- $personToPet = $this->buildPersonToPet();
-
- $person = $this->buildPerson();
- $person->name = 'foo';
-
- $pet = $this->buildPet();
- $pet->name = 'bar';
-
- $personToPet->person = $person;
- $personToPet->pet = $pet;
-
- $this->manager->persist($person);
- $this->manager->persist($pet);
- // since doctrine 2.6 we need existing identifiers on relations
- $this->manager->flush();
- $this->manager->persist($personToPet);
-
- $person->pets->add($personToPet);
- $this->manager->persist($person);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummydate objects with dummyDate
- * @Given there is :nb dummydate object with dummyDate
- */
- public function thereAreDummyDateObjectsWithDummyDate(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummyDate();
- $dummy->dummyDate = $date;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummydate objects with nullable dateIncludeNullAfter
- * @Given there is :nb dummydate object with nullable dateIncludeNullAfter
- */
- public function thereAreDummyDateObjectsWithNullableDateIncludeNullAfter(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummyDate();
- $dummy->dummyDate = $date;
- $dummy->dateIncludeNullAfter = 0 === $i % 3 ? null : $date;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummydate objects with nullable dateIncludeNullBefore
- * @Given there is :nb dummydate object with nullable dateIncludeNullBefore
- */
- public function thereAreDummyDateObjectsWithNullableDateIncludeNullBefore(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummyDate();
- $dummy->dummyDate = $date;
- $dummy->dateIncludeNullBefore = 0 === $i % 3 ? null : $date;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummydate objects with nullable dateIncludeNullBeforeAndAfter
- * @Given there is :nb dummydate object with nullable dateIncludeNullBeforeAndAfter
- */
- public function thereAreDummyDateObjectsWithNullableDateIncludeNullBeforeAndAfter(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
-
- $dummy = $this->buildDummyDate();
- $dummy->dummyDate = $date;
- $dummy->dateIncludeNullBeforeAndAfter = 0 === $i % 3 ? null : $date;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummyimmutabledate objects with dummyDate
- */
- public function thereAreDummyImmutableDateObjectsWithDummyDate(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $date = new \DateTimeImmutable(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
- $dummy = $this->buildDummyImmutableDate();
- $dummy->dummyDate = $date;
-
- $this->manager->persist($dummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy with different GraphQL serialization groups objects
- */
- public function thereAreDummyWithDifferentGraphQlSerializationGroupsObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $dummyDifferentGraphQlSerializationGroup = $this->buildDummyDifferentGraphQlSerializationGroup();
- $dummyDifferentGraphQlSerializationGroup->setName('Name #'.$i);
- $dummyDifferentGraphQlSerializationGroup->setTitle('Title #'.$i);
- $this->manager->persist($dummyDifferentGraphQlSerializationGroup);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a ramsey identified resource with uuid :uuid
- *
- * @param non-empty-string $uuid
- */
- public function thereIsARamseyIdentifiedResource(string $uuid): void
- {
- $dummy = new RamseyUuidDummy(Uuid::fromString($uuid));
-
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a Symfony dummy identified resource with uuid :uuid
- */
- public function thereIsASymfonyDummyIdentifiedResource(string $uuid): void
- {
- $dummy = new SymfonyUuidDummy(SymfonyUuid::fromString($uuid));
-
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy object with a fourth level relation
- */
- public function thereIsADummyObjectWithAFourthLevelRelation(): void
- {
- $fourthLevel = $this->buildFourthLevel();
- $fourthLevel->setLevel(4);
- $this->manager->persist($fourthLevel);
-
- $thirdLevel = $this->buildThirdLevel();
- $thirdLevel->setLevel(3);
- $thirdLevel->setFourthLevel($fourthLevel);
- $this->manager->persist($thirdLevel);
-
- $namedRelatedDummy = $this->buildRelatedDummy();
- $namedRelatedDummy->setName('Hello');
- $namedRelatedDummy->setThirdLevel($thirdLevel);
- $this->manager->persist($namedRelatedDummy);
-
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setThirdLevel($thirdLevel);
- $this->manager->persist($relatedDummy);
-
- $dummy = $this->buildDummy();
- $dummy->setName('Dummy with relations');
- $dummy->setRelatedDummy($namedRelatedDummy);
- $dummy->addRelatedDummy($namedRelatedDummy);
- $dummy->addRelatedDummy($relatedDummy);
- $this->manager->persist($dummy);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a RelatedOwnedDummy object with OneToOne relation
- */
- public function thereIsARelatedOwnedDummy(): void
- {
- $relatedOwnedDummy = $this->buildRelatedOwnedDummy();
- $this->manager->persist($relatedOwnedDummy);
-
- $dummy = $this->buildDummy();
- $dummy->setName('plop');
- $dummy->setRelatedOwnedDummy($relatedOwnedDummy);
- $this->manager->persist($dummy);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a RelatedOwningDummy object with OneToOne relation
- */
- public function thereIsARelatedOwningDummy(): void
- {
- $dummy = $this->buildDummy();
- $dummy->setName('plop');
- $this->manager->persist($dummy);
-
- $relatedOwningDummy = $this->buildRelatedOwningDummy();
- $relatedOwningDummy->setOwnedDummy($dummy);
- $this->manager->persist($relatedOwningDummy);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a person named :name greeting with a :message message
- */
- public function thereIsAPersonWithAGreeting(string $name, string $message): void
- {
- $person = $this->buildPerson();
- $person->name = $name;
-
- $greeting = $this->buildGreeting();
- $greeting->message = $message;
- $greeting->sender = $person;
-
- $this->manager->persist($person);
- $this->manager->persist($greeting);
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is a max depth dummy with :level level of descendants
- */
- public function thereIsAMaxDepthDummyWithLevelOfDescendants(int $level): void
- {
- $maxDepthDummy = $this->buildMaxDepthDummy();
- $maxDepthDummy->name = "level $level";
- $this->manager->persist($maxDepthDummy);
-
- for ($i = 1; $i <= $level; ++$i) {
- $maxDepthDummy = $maxDepthDummy->child = $this->buildMaxDepthDummy();
- $maxDepthDummy->name = 'level '.($i + 1);
- $this->manager->persist($maxDepthDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a DummyDtoCustom
- */
- public function thereIsADummyDtoCustom(): void
- {
- $this->thereAreNbDummyDtoCustom(1);
- }
-
- /**
- * @Given there are :nb DummyDtoCustom
- */
- public function thereAreNbDummyDtoCustom($nb): void
- {
- for ($i = 0; $i < $nb; ++$i) {
- $dto = $this->isOrm() ? new DummyDtoCustom() : new DummyDtoCustomDocument();
- $dto->lorem = 'test';
- $dto->ipsum = (string) ($i + 1);
- $this->manager->persist($dto);
- }
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there is an order with same customer and recipient
- */
- public function thereIsAnOrderWithSameCustomerAndRecipient(): void
- {
- $customer = $this->isOrm() ? new Customer() : new CustomerDocument();
- $customer->name = 'customer_name';
-
- $address1 = $this->isOrm() ? new Address() : new AddressDocument();
- $address1->name = 'foo';
- $address2 = $this->isOrm() ? new Address() : new AddressDocument();
- $address2->name = 'bar';
-
- $order = $this->isOrm() ? new Order() : new OrderDocument();
- $order->recipient = $customer;
- $order->customer = $customer;
-
- $customer->addresses->add($address1);
- $customer->addresses->add($address2);
-
- $this->manager->persist($address1);
- $this->manager->persist($address2);
- $this->manager->persist($customer);
- $this->manager->persist($order);
-
- $this->manager->flush();
- $this->manager->clear();
- }
-
- /**
- * @Given there are :nb sites with internal owner
- */
- public function thereAreSitesWithInternalOwner(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $internalUser = new InternalUser();
- $internalUser->setFirstname('Internal');
- $internalUser->setLastname('User');
- $internalUser->setEmail('john.doe@example.com');
- $internalUser->setInternalId('INT');
- $site = new Site();
- $site->setTitle('title');
- $site->setDescription('description');
- $site->setOwner($internalUser);
- $this->manager->persist($site);
- }
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb sites with external owner
- */
- public function thereAreSitesWithExternalOwner(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $externalUser = new ExternalUser();
- $externalUser->setFirstname('External');
- $externalUser->setLastname('User');
- $externalUser->setEmail('john.doe@example.com');
- $externalUser->setExternalId('EXT');
- $site = new Site();
- $site->setTitle('title');
- $site->setDescription('description');
- $site->setOwner($externalUser);
- $this->manager->persist($site);
- }
- $this->manager->flush();
- }
-
- /**
- * @Given there is the following taxon:
- */
- public function thereIsTheFollowingTaxon(PyStringNode $dataNode): void
- {
- $data = json_decode((string) $dataNode, true, 512, \JSON_THROW_ON_ERROR);
-
- $taxon = $this->isOrm() ? new Taxon() : new TaxonDocument();
- $taxon->setCode($data['code']);
- $this->manager->persist($taxon);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is the following product:
- */
- public function thereIsTheFollowingProduct(PyStringNode $dataNode): void
- {
- $data = json_decode((string) $dataNode, true, 512, \JSON_THROW_ON_ERROR);
-
- $product = $this->isOrm() ? new Product() : new ProductDocument();
- $product->setCode($data['code']);
- if (isset($data['mainTaxon'])) {
- $mainTaxonCode = str_replace('/taxa/', '', $data['mainTaxon']);
- $mainTaxon = $this->manager->getRepository($this->isOrm() ? Taxon::class : TaxonDocument::class)->findOneBy([
- 'code' => $mainTaxonCode,
- ]);
- $product->setMainTaxon($mainTaxon);
- }
- $this->manager->persist($product);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb convertedOwner objects with convertedRelated
- */
- public function thereAreConvertedOwnerObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $related = $this->buildConvertedRelated();
- $related->nameConverted = 'Converted '.$i;
-
- $owner = $this->buildConvertedOwner();
- $owner->nameConverted = $related;
-
- $this->manager->persist($related);
- $this->manager->persist($owner);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb dummy mercure objects
- */
- public function thereAreDummyMercureObjects(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy #'.$i);
-
- $dummyMercure = $this->buildDummyMercure();
- $dummyMercure->name = "Dummy Mercure #$i";
- $dummyMercure->description = 'Description';
- $dummyMercure->relatedDummy = $relatedDummy;
-
- $this->manager->persist($relatedDummy);
- $this->manager->persist($dummyMercure);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb iriOnlyDummies
- */
- public function thereAreIriOnlyDummies(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $iriOnlyDummy = $this->buildIriOnlyDummy();
- $iriOnlyDummy->setFoo('bar'.$nb);
- $this->manager->persist($iriOnlyDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are propertyCollectionIriOnly with relations
- */
- public function thereAreResourcesWithPropertyUriTemplates(): void
- {
- $propertyCollectionIriOnlyRelation1 = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument();
- $propertyCollectionIriOnlyRelation1->name = 'asb1';
-
- $propertyCollectionIriOnlyRelation2 = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument();
- $propertyCollectionIriOnlyRelation2->name = 'asb2';
-
- $propertyToOneRelation = $this->isOrm() ? new PropertyUriTemplateOneToOneRelation() : new PropertyUriTemplateOneToOneRelationDocument();
- $propertyToOneRelation->name = 'xarguš';
-
- $propertyCollectionIriOnly = $this->isOrm() ? new PropertyCollectionIriOnly() : new PropertyCollectionIriOnlyDocument();
- $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation1);
- $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation2);
- $propertyCollectionIriOnly->setToOneRelation($propertyToOneRelation);
-
- $this->manager->persist($propertyCollectionIriOnly);
- $this->manager->persist($propertyCollectionIriOnlyRelation1);
- $this->manager->persist($propertyCollectionIriOnlyRelation2);
- $this->manager->persist($propertyToOneRelation);
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb absoluteUrlDummy objects with a related absoluteUrlRelationDummy
- */
- public function thereAreAbsoluteUrlDummies(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $absoluteUrlRelationDummy = $this->buildAbsoluteUrlRelationDummy();
- $absoluteUrlDummy = $this->buildAbsoluteUrlDummy();
- $absoluteUrlDummy->absoluteUrlRelationDummy = $absoluteUrlRelationDummy;
-
- $this->manager->persist($absoluteUrlRelationDummy);
- $this->manager->persist($absoluteUrlDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb networkPathDummy objects with a related networkPathRelationDummy
- */
- public function thereAreNetworkPathDummies(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $networkPathRelationDummy = $this->buildNetworkPathRelationDummy();
- $networkPathDummy = $this->buildNetworkPathDummy();
- $networkPathDummy->networkPathRelationDummy = $networkPathRelationDummy;
-
- $this->manager->persist($networkPathRelationDummy);
- $this->manager->persist($networkPathDummy);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is an InitializeInput object with id :id
- */
- public function thereIsAnInitializeInput(int $id): void
- {
- $initializeInput = $this->buildInitializeInput();
- $initializeInput->id = $id;
- $initializeInput->manager = 'Orwell';
- $initializeInput->name = '1984';
-
- $this->manager->persist($initializeInput);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a PatchDummyRelation
- */
- public function thereIsAPatchDummyRelation(): void
- {
- $dummy = $this->buildPatchDummyRelation();
- $related = $this->buildRelatedDummy();
- $this->manager->persist($related);
- $this->manager->flush();
- $dummy->setRelated($related);
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a book
- */
- public function thereIsABook(): void
- {
- $book = $this->buildBook();
- $book->name = '1984';
- $book->isbn = '9780451524935';
- $this->manager->persist($book);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a custom multiple identifier dummy
- */
- public function thereIsACustomMultipleIdentifierDummy(): void
- {
- $dummy = $this->buildCustomMultipleIdentifierDummy();
- $dummy->setName('Orwell');
- $dummy->setFirstId(1);
- $dummy->setSecondId(2);
-
- $this->manager->persist($dummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a payment
- */
- public function thereIsAPayment(): void
- {
- $this->manager->persist($this->buildPayment('123.45'));
- $this->manager->flush();
- }
-
- /**
- * @Given there are :nb separated entities
- */
- public function thereAreSeparatedEntities(int $nb): void
- {
- for ($i = 1; $i <= $nb; ++$i) {
- $entity = $this->buildSeparatedEntity();
- $entity->value = (string) $i;
- $this->manager->persist($entity);
- }
- $this->manager->flush();
- }
-
- /**
- * @Given there is a video game with music groups
- */
- public function thereAreVideoGamesWithMusicGroups(): void
- {
- $sum41 = $this->buildMusicGroup();
- $sum41->name = 'Sum 41';
- $this->manager->persist($sum41);
- $franz = $this->buildMusicGroup();
- $franz->name = 'Franz Ferdinand';
- $this->manager->persist($franz);
-
- $videoGame = $this->buildVideoGame();
- $videoGame->name = 'Guitar Hero';
- $videoGame->addMusicGroup($sum41);
- $videoGame->addMusicGroup($franz);
- $this->manager->persist($videoGame);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a relationMultiple object
- */
- public function thereIsARelationMultipleObject(): void
- {
- $first = $this->buildDummy();
- $first->setId(1);
- $first->setName('foo');
- $second = $this->buildDummy();
- $second->setId(2);
- $second->setName('bar');
-
- $relationMultiple = (new RelationMultiple());
- $relationMultiple->first = $first;
- $relationMultiple->second = $second;
-
- $this->manager->persist($first);
- $this->manager->persist($second);
- $this->manager->persist($relationMultiple);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy object with many multiple relation
- */
- public function thereIsADummyObjectWithManyMultipleRelation(): void
- {
- $first = $this->buildDummy();
- $first->setId(1);
- $first->setName('foo');
- $second = $this->buildDummy();
- $second->setId(2);
- $second->setName('bar');
- $third = $this->buildDummy();
- $third->setId(3);
- $third->setName('foobar');
-
- $relationMultiple1 = (new RelationMultiple());
- $relationMultiple1->first = $first;
- $relationMultiple1->second = $second;
-
- $relationMultiple2 = (new RelationMultiple());
- $relationMultiple2->first = $first;
- $relationMultiple2->second = $third;
-
- $this->manager->persist($first);
- $this->manager->persist($second);
- $this->manager->persist($third);
- $this->manager->persist($relationMultiple1);
- $this->manager->persist($relationMultiple2);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there is a resource using entityClass with a DateTime attribute
- */
- public function thereIsAResourceUsingEntityClassAndDateTime(): void
- {
- $entity = new EntityClassWithDateTime();
- $entity->setStart(new \DateTime());
- $this->manager->persist($entity);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy entity with a sub entity with id :strId and name :name
- */
- public function thereIsADummyWithSubEntity(string $strId, string $name): void
- {
- $subEntity = new DummySubEntity($strId, $name);
- $mainEntity = new DummyWithSubEntity();
- $mainEntity->setSubEntity($subEntity);
- $mainEntity->setName('main');
- $this->manager->persist($subEntity);
- $this->manager->persist($mainEntity);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a group object with uuid :uuid and :nbUsers users
- */
- public function thereIsAGroupWithUuidAndNUsers(string $uuid, int $nbUsers): void
- {
- $group = new Group();
- $group->setUuid(SymfonyUuid::fromString($uuid));
-
- $this->manager->persist($group);
-
- for ($i = 0; $i < $nbUsers; ++$i) {
- $user = new \ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Issue5735User();
- $user->addGroup($group);
- $this->manager->persist($user);
- }
-
- // add another user not in this group
- $user = new \ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Issue5735User();
- $this->manager->persist($user);
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are logs on an event
- */
- public function thereAreLogsOnAnEvent(): void
- {
- $entity = new Event();
- $entity->logs = new ArrayCollection([new ItemLog(), new ItemLog()]);
- $entity->uuid = Uuid::fromString('03af3507-271e-4cca-8eee-6244fb06e95b');
- $this->manager->persist($entity);
- foreach ($entity->logs as $log) {
- $log->item = $entity;
- $this->manager->persist($log);
- }
-
- $this->manager->flush();
- }
-
- /**
- * @Given there are a few link handled dummies
- */
- public function thereAreAFewLinkHandledDummies(): void
- {
- $this->manager->persist($this->buildLinkHandledDummy('foo'));
- $this->manager->persist($this->buildLinkHandledDummy('bar'));
- $this->manager->persist($this->buildLinkHandledDummy('baz'));
- $this->manager->persist($this->buildLinkHandledDummy('foz'));
- $this->manager->flush();
- }
-
- /**
- * @Given there is a dummy entity with a mapped superclass
- */
- public function thereIsADummyEntityWithAMappedSuperclass(): void
- {
- $entity = new DummyMappedSubclass();
- $this->manager->persist($entity);
- $this->manager->flush();
- }
-
- /**
- * @Given there are issue6039 users
- */
- public function thereAreIssue6039Users(): void
- {
- $entity = new Issue6039EntityUser();
- $entity->name = 'test';
- $entity->bar = 'test';
- $this->manager->persist($entity);
- $entity = new Issue6039EntityUser();
- $entity->name = 'test2';
- $entity->bar = 'test';
- $this->manager->persist($entity);
- $this->manager->flush();
- }
-
- private function isOrm(): bool
- {
- return null !== $this->schemaTool;
- }
-
- private function isOdm(): bool
- {
- return null !== $this->schemaManager;
- }
-
- private function buildAnswer(): Answer|AnswerDocument
- {
- return $this->isOrm() ? new Answer() : new AnswerDocument();
- }
-
- private function buildCompositeItem(): CompositeItem|CompositeItemDocument
- {
- return $this->isOrm() ? new CompositeItem() : new CompositeItemDocument();
- }
-
- private function buildCompositeLabel(): CompositeLabel|CompositeLabelDocument
- {
- return $this->isOrm() ? new CompositeLabel() : new CompositeLabelDocument();
- }
-
- private function buildCompositePrimitiveItem(string $name, int $year): CompositePrimitiveItem|CompositePrimitiveItemDocument
- {
- return $this->isOrm() ? new CompositePrimitiveItem($name, $year) : new CompositePrimitiveItemDocument($name, $year);
- }
-
- private function buildCompositeRelation(): CompositeRelation|CompositeRelationDocument
- {
- return $this->isOrm() ? new CompositeRelation() : new CompositeRelationDocument();
- }
-
- private function buildDummy(): Dummy|DummyDocument
- {
- return $this->isOrm() ? new Dummy() : new DummyDocument();
- }
-
- private function buildDummyTableInheritanceNotApiResourceChild(): DummyTableInheritanceNotApiResourceChild|DummyTableInheritanceNotApiResourceChildDocument
- {
- return $this->isOrm() ? new DummyTableInheritanceNotApiResourceChild() : new DummyTableInheritanceNotApiResourceChildDocument();
- }
-
- private function buildDummyAggregateOffer(): DummyAggregateOffer|DummyAggregateOfferDocument
- {
- return $this->isOrm() ? new DummyAggregateOffer() : new DummyAggregateOfferDocument();
- }
-
- private function buildDummyCar(): DummyCar|DummyCarDocument
- {
- return $this->isOrm() ? new DummyCar() : new DummyCarDocument();
- }
-
- private function buildDummyCarColor(): DummyCarColor|DummyCarColorDocument
- {
- return $this->isOrm() ? new DummyCarColor() : new DummyCarColorDocument();
- }
-
- private function buildDummyPassenger(): DummyPassenger|DummyPassengerDocument
- {
- return $this->isOrm() ? new DummyPassenger() : new DummyPassengerDocument();
- }
-
- private function buildDummyTravel(): DummyTravel|DummyTravelDocument
- {
- return $this->isOrm() ? new DummyTravel() : new DummyTravelDocument();
- }
-
- private function buildDummyDate(): DummyDate|DummyDateDocument
- {
- return $this->isOrm() ? new DummyDate() : new DummyDateDocument();
- }
-
- private function buildDummyImmutableDate(): DummyImmutableDate|DummyImmutableDateDocument
- {
- return $this->isOrm() ? new DummyImmutableDate() : new DummyImmutableDateDocument();
- }
-
- private function buildDummyDifferentGraphQlSerializationGroup(): DummyDifferentGraphQlSerializationGroup|DummyDifferentGraphQlSerializationGroupDocument
- {
- return $this->isOrm() ? new DummyDifferentGraphQlSerializationGroup() : new DummyDifferentGraphQlSerializationGroupDocument();
- }
-
- private function buildDummyDtoNoInput(): DummyDtoNoInput|DummyDtoNoInputDocument
- {
- return $this->isOrm() ? new DummyDtoNoInput() : new DummyDtoNoInputDocument();
- }
-
- private function buildDummyDtoNoOutput(): DummyDtoNoOutput|DummyDtoNoOutputDocument
- {
- return $this->isOrm() ? new DummyDtoNoOutput() : new DummyDtoNoOutputDocument();
- }
-
- private function buildDummyCustomQuery(): DummyCustomQuery|DummyCustomQueryDocument
- {
- return $this->isOrm() ? new DummyCustomQuery() : new DummyCustomQueryDocument();
- }
-
- private function buildDummyCustomMutation(): DummyCustomMutation|DummyCustomMutationDocument
- {
- return $this->isOrm() ? new DummyCustomMutation() : new DummyCustomMutationDocument();
- }
-
- private function buildDummyFriend(): DummyFriend|DummyFriendDocument
- {
- return $this->isOrm() ? new DummyFriend() : new DummyFriendDocument();
- }
-
- private function buildDummyGroup(): DummyGroup|DummyGroupDocument
- {
- return $this->isOrm() ? new DummyGroup() : new DummyGroupDocument();
- }
-
- private function buildDummyOffer(): DummyOffer|DummyOfferDocument
- {
- return $this->isOrm() ? new DummyOffer() : new DummyOfferDocument();
- }
-
- private function buildDummyProduct(): DummyProduct|DummyProductDocument
- {
- return $this->isOrm() ? new DummyProduct() : new DummyProductDocument();
- }
-
- private function buildDummyProperty(): DummyProperty|DummyPropertyDocument
- {
- return $this->isOrm() ? new DummyProperty() : new DummyPropertyDocument();
- }
-
- private function buildEmbeddableDummy(): EmbeddableDummy|EmbeddableDummyDocument
- {
- return $this->isOrm() ? new EmbeddableDummy() : new EmbeddableDummyDocument();
- }
-
- private function buildEmbeddedDummy(): EmbeddedDummy|EmbeddedDummyDocument
- {
- return $this->isOrm() ? new EmbeddedDummy() : new EmbeddedDummyDocument();
- }
-
- private function buildFileConfigDummy(): FileConfigDummy|FileConfigDummyDocument
- {
- return $this->isOrm() ? new FileConfigDummy() : new FileConfigDummyDocument();
- }
-
- private function buildFoo(): Foo|FooDocument
- {
- return $this->isOrm() ? new Foo() : new FooDocument();
- }
-
- private function buildFooDummy(): FooDummy|FooDummyDocument
- {
- return $this->isOrm() ? new FooDummy() : new FooDummyDocument();
- }
-
- private function buildFooEmbeddable(): FooEmbeddable|FooEmbeddableDocument
- {
- return $this->isOrm() ? new FooEmbeddable() : new FooEmbeddableDocument();
- }
-
- private function buildFourthLevel(): FourthLevel|FourthLevelDocument
- {
- return $this->isOrm() ? new FourthLevel() : new FourthLevelDocument();
- }
-
- private function buildGreeting(): Greeting|GreetingDocument
- {
- return $this->isOrm() ? new Greeting() : new GreetingDocument();
- }
-
- private function buildIriOnlyDummy(): IriOnlyDummy|IriOnlyDummyDocument
- {
- return $this->isOrm() ? new IriOnlyDummy() : new IriOnlyDummyDocument();
- }
-
- private function buildMaxDepthDummy(): MaxDepthDummy|MaxDepthDummyDocument
- {
- return $this->isOrm() ? new MaxDepthDummy() : new MaxDepthDummyDocument();
- }
-
- private function buildPerson(): Person|PersonDocument
- {
- return $this->isOrm() ? new Person() : new PersonDocument();
- }
-
- private function buildPersonToPet(): PersonToPet|PersonToPetDocument
- {
- return $this->isOrm() ? new PersonToPet() : new PersonToPetDocument();
- }
-
- private function buildPet(): Pet|PetDocument
- {
- return $this->isOrm() ? new Pet() : new PetDocument();
- }
-
- private function buildQuestion(): Question|QuestionDocument
- {
- return $this->isOrm() ? new Question() : new QuestionDocument();
- }
-
- private function buildRelatedDummy(): RelatedDummy|RelatedDummyDocument
- {
- return $this->isOrm() ? new RelatedDummy() : new RelatedDummyDocument();
- }
-
- private function buildRelatedOwnedDummy(): RelatedOwnedDummy|RelatedOwnedDummyDocument
- {
- return $this->isOrm() ? new RelatedOwnedDummy() : new RelatedOwnedDummyDocument();
- }
-
- private function buildRelatedOwningDummy(): RelatedOwningDummy|RelatedOwningDummyDocument
- {
- return $this->isOrm() ? new RelatedOwningDummy() : new RelatedOwningDummyDocument();
- }
-
- private function buildRelatedToDummyFriend(): RelatedToDummyFriend|RelatedToDummyFriendDocument
- {
- return $this->isOrm() ? new RelatedToDummyFriend() : new RelatedToDummyFriendDocument();
- }
-
- private function buildRelatedLinkedDummy(): RelatedLinkedDummy|RelatedLinkedDummyDocument
- {
- return $this->isOrm() ? new RelatedLinkedDummy() : new RelatedLinkedDummyDocument();
- }
-
- private function buildRelationEmbedder(): RelationEmbedder|RelationEmbedderDocument
- {
- return $this->isOrm() ? new RelationEmbedder() : new RelationEmbedderDocument();
- }
-
- private function buildSecuredDummy(): SecuredDummy|SecuredDummyDocument
- {
- return $this->isOrm() ? new SecuredDummy() : new SecuredDummyDocument();
- }
-
- private function buildRelatedSecureDummy(): RelatedSecuredDummy|RelatedSecuredDummyDocument
- {
- return $this->isOrm() ? new RelatedSecuredDummy() : new RelatedSecuredDummyDocument();
- }
-
- private function buildSoMany(): SoMany|SoManyDocument
- {
- return $this->isOrm() ? new SoMany() : new SoManyDocument();
- }
-
- private function buildThirdLevel(): ThirdLevel|ThirdLevelDocument
- {
- return $this->isOrm() ? new ThirdLevel() : new ThirdLevelDocument();
- }
-
- private function buildConvertedDate(): ConvertedDate|ConvertedDateDocument
- {
- return $this->isOrm() ? new ConvertedDate() : new ConvertedDateDocument();
- }
-
- private function buildConvertedBoolean(): ConvertedBoolean|ConvertedBoolDocument
- {
- return $this->isOrm() ? new ConvertedBoolean() : new ConvertedBoolDocument();
- }
-
- private function buildConvertedInteger(): ConvertedInteger|ConvertedIntegerDocument
- {
- return $this->isOrm() ? new ConvertedInteger() : new ConvertedIntegerDocument();
- }
-
- private function buildConvertedString(): ConvertedString|ConvertedStringDocument
- {
- return $this->isOrm() ? new ConvertedString() : new ConvertedStringDocument();
- }
-
- private function buildConvertedOwner(): ConvertedOwner|ConvertedOwnerDocument
- {
- return $this->isOrm() ? new ConvertedOwner() : new ConvertedOwnerDocument();
- }
-
- private function buildConvertedRelated(): ConvertedRelated|ConvertedRelatedDocument
- {
- return $this->isOrm() ? new ConvertedRelated() : new ConvertedRelatedDocument();
- }
-
- private function buildDummyMercure(): DummyMercure|DummyMercureDocument
- {
- return $this->isOrm() ? new DummyMercure() : new DummyMercureDocument();
- }
-
- private function buildAbsoluteUrlDummy(): AbsoluteUrlDummyDocument|AbsoluteUrlDummy
- {
- return $this->isOrm() ? new AbsoluteUrlDummy() : new AbsoluteUrlDummyDocument();
- }
-
- private function buildAbsoluteUrlRelationDummy(): AbsoluteUrlRelationDummyDocument|AbsoluteUrlRelationDummy
- {
- return $this->isOrm() ? new AbsoluteUrlRelationDummy() : new AbsoluteUrlRelationDummyDocument();
- }
-
- private function buildNetworkPathDummy(): NetworkPathDummyDocument|NetworkPathDummy
- {
- return $this->isOrm() ? new NetworkPathDummy() : new NetworkPathDummyDocument();
- }
-
- private function buildNetworkPathRelationDummy(): NetworkPathRelationDummyDocument|NetworkPathRelationDummy
- {
- return $this->isOrm() ? new NetworkPathRelationDummy() : new NetworkPathRelationDummyDocument();
- }
-
- private function buildInitializeInput(): InitializeInput|InitializeInputDocument
- {
- return $this->isOrm() ? new InitializeInput() : new InitializeInputDocument();
- }
-
- private function buildPatchDummyRelation(): PatchDummyRelation|PatchDummyRelationDocument
- {
- return $this->isOrm() ? new PatchDummyRelation() : new PatchDummyRelationDocument();
- }
-
- private function buildBook(): BookDocument|Book
- {
- return $this->isOrm() ? new Book() : new BookDocument();
- }
-
- private function buildCustomMultipleIdentifierDummy(): CustomMultipleIdentifierDummy|CustomMultipleIdentifierDummyDocument
- {
- return $this->isOrm() ? new CustomMultipleIdentifierDummy() : new CustomMultipleIdentifierDummyDocument();
- }
-
- private function buildWithJsonDummy(): WithJsonDummy|WithJsonDummyDocument
- {
- return $this->isOrm() ? new WithJsonDummy() : new WithJsonDummyDocument();
- }
-
- private function buildPayment(string $amount): Payment|PaymentDocument
- {
- return $this->isOrm() ? new Payment($amount) : new PaymentDocument($amount);
- }
-
- private function buildMultiRelationsDummy(): MultiRelationsDummy|MultiRelationsDummyDocument
- {
- return $this->isOrm() ? new MultiRelationsDummy() : new MultiRelationsDummyDocument();
- }
-
- private function buildMultiRelationsRelatedDummy(): MultiRelationsRelatedDummy|MultiRelationsRelatedDummyDocument
- {
- return $this->isOrm() ? new MultiRelationsRelatedDummy() : new MultiRelationsRelatedDummyDocument();
- }
-
- private function buildMultiRelationsNested(): MultiRelationsNested|MultiRelationsNestedDocument
- {
- return $this->isOrm() ? new MultiRelationsNested() : new MultiRelationsNestedDocument();
- }
-
- private function buildMultiRelationsNestedPaginated(): MultiRelationsNestedPaginated|MultiRelationsNestedPaginatedDocument
- {
- return $this->isOrm() ? new MultiRelationsNestedPaginated() : new MultiRelationsNestedPaginatedDocument();
- }
-
- private function buildMultiRelationsResolveDummy(): MultiRelationsResolveDummy|MultiRelationsResolveDummyDocument
- {
- return $this->isOrm() ? new MultiRelationsResolveDummy() : new MultiRelationsResolveDummyDocument();
- }
-
- private function buildMusicGroup(): MusicGroup|MusicGroupDocument
- {
- return $this->isOrm() ? new MusicGroup() : new MusicGroupDocument();
- }
-
- private function buildVideoGame(): VideoGame|VideoGameDocument
- {
- return $this->isOrm() ? new VideoGame() : new VideoGameDocument();
- }
-
- private function buildSeparatedEntity(): SeparatedEntity|SeparatedEntityDocument
- {
- return $this->isOrm() ? new SeparatedEntity() : new SeparatedEntityDocument();
- }
-
- private function buildLinkHandledDummy(string $slug): LinkHandledDummy|LinkHandledDummyDocument
- {
- return $this->isOrm() ? new LinkHandledDummy($slug) : new LinkHandledDummyDocument($slug);
- }
-}
diff --git a/tests/Behat/GraphqlContext.php b/tests/Behat/GraphqlContext.php
deleted file mode 100644
index ca644baaff9..00000000000
--- a/tests/Behat/GraphqlContext.php
+++ /dev/null
@@ -1,178 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Behat\Context\Context;
-use Behat\Behat\Context\Environment\InitializedContextEnvironment;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use Behat\Gherkin\Node\PyStringNode;
-use Behat\Gherkin\Node\TableNode;
-use Behatch\Context\RestContext;
-use Behatch\HttpCall\Request;
-use GraphQL\Error\Error;
-use GraphQL\Type\Introspection;
-use PHPUnit\Framework\ExpectationFailedException;
-
-/**
- * Context for GraphQL.
- *
- * @author Alan Poulain
- */
-final class GraphqlContext implements Context
-{
- private ?RestContext $restContext = null;
- private ?JsonContext $jsonContext = null;
-
- private array $graphqlRequest;
-
- private ?int $graphqlLine = null; // @phpstan-ignore-line
-
- public function __construct(private readonly Request $request)
- {
- }
-
- /**
- * Gives access to the Behatch context.
- *
- * @BeforeScenario
- */
- public function gatherContexts(BeforeScenarioScope $scope): void
- {
- /** @var InitializedContextEnvironment $environment */
- $environment = $scope->getEnvironment();
- /** @var RestContext $restContext */
- $restContext = $environment->getContext(RestContext::class);
- $this->restContext = $restContext;
- /** @var JsonContext $jsonContext */
- $jsonContext = $environment->getContext(JsonContext::class);
- $this->jsonContext = $jsonContext;
- }
-
- /**
- * @When I have the following GraphQL request:
- */
- public function IHaveTheFollowingGraphqlRequest(PyStringNode $request): void
- {
- $this->graphqlRequest = ['query' => $request->getRaw()];
- $this->graphqlLine = $request->getLine();
- }
-
- /**
- * @When I send the following GraphQL request:
- */
- public function ISendTheFollowingGraphqlRequest(PyStringNode $request): void
- {
- $this->IHaveTheFollowingGraphqlRequest($request);
- $this->sendGraphqlRequest();
- }
-
- /**
- * @When I send the GraphQL request with variables:
- */
- public function ISendTheGraphqlRequestWithVariables(PyStringNode $variables): void
- {
- $this->graphqlRequest['variables'] = $variables->getRaw();
- $this->sendGraphqlRequest();
- }
-
- /**
- * @When I send the GraphQL request with operationName :operationName
- */
- public function ISendTheGraphqlRequestWithOperation(string $operationName): void
- {
- $this->graphqlRequest['operationName'] = $operationName;
- $this->sendGraphqlRequest();
- }
-
- /**
- * @Given I have the following file(s) for a GraphQL request:
- */
- public function iHaveTheFollowingFilesForAGraphqlRequest(TableNode $table): void
- {
- $files = [];
-
- foreach ($table->getHash() as $row) {
- if (!isset($row['name'], $row['file'])) {
- throw new \InvalidArgumentException('You must provide a "name" and "file" column in your table node.');
- }
-
- $files[$row['name']] = $this->restContext->getMinkParameter('files_path').\DIRECTORY_SEPARATOR.$row['file'];
- }
-
- $this->graphqlRequest['files'] = $files;
- }
-
- /**
- * @Given I have the following GraphQL multipart request map:
- */
- public function iHaveTheFollowingGraphqlMultipartRequestMap(PyStringNode $string): void
- {
- $this->graphqlRequest['map'] = $string->getRaw();
- }
-
- /**
- * @When I send the following GraphQL multipart request operations:
- */
- public function iSendTheFollowingGraphqlMultipartRequestOperations(PyStringNode $string): void
- {
- $params = [];
- $params['operations'] = $string->getRaw();
- $params['map'] = $this->graphqlRequest['map'];
-
- $this->request->setHttpHeader('Content-type', 'multipart/form-data');
- $this->request->send('POST', '/graphql', $params, $this->graphqlRequest['files']);
- }
-
- /**
- * @When I send the query to introspect the schema
- */
- public function ISendTheQueryToIntrospectTheSchema(): void
- {
- $this->graphqlRequest = ['query' => Introspection::getIntrospectionQuery()];
- $this->sendGraphqlRequest();
- }
-
- /**
- * @Then the GraphQL field :fieldName is deprecated for the reason :reason
- */
- public function theGraphQLFieldIsDeprecatedForTheReason(string $fieldName, string $reason): void
- {
- foreach (json_decode($this->request->getContent(), true, 512, \JSON_THROW_ON_ERROR)['data']['__type']['fields'] as $field) {
- if ($fieldName === $field['name'] && $field['isDeprecated'] && $reason === $field['deprecationReason']) {
- return;
- }
- }
-
- throw new ExpectationFailedException(\sprintf('The field "%s" is not deprecated.', $fieldName));
- }
-
- /**
- * @Then the GraphQL debug message should be equal to :expectedDebugMessage
- */
- public function theGraphQLDebugMessageShouldBeEqualTo(string $expectedDebugMessage): void
- {
- $jsonNode = 'errors[0].extensions.debugMessage';
- // graphql-php < 15
- if (\defined(Error::class.'::CATEGORY_INTERNAL')) {
- $jsonNode = 'errors[0].debugMessage';
- }
-
- $this->jsonContext->theJsonNodeShouldBeEqualTo($jsonNode, $expectedDebugMessage);
- }
-
- private function sendGraphqlRequest(): void
- {
- $this->restContext->iSendARequestTo('GET', '/graphql?'.http_build_query($this->graphqlRequest));
- }
-}
diff --git a/tests/Behat/HttpCacheContext.php b/tests/Behat/HttpCacheContext.php
deleted file mode 100644
index d06ba3414eb..00000000000
--- a/tests/Behat/HttpCacheContext.php
+++ /dev/null
@@ -1,91 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use ApiPlatform\Tests\Fixtures\TestBundle\HttpCache\TagCollectorCustom;
-use Behat\Behat\Context\Context;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use Behat\Mink\Driver\BrowserKitDriver;
-use Behat\MinkExtension\Context\MinkContext;
-use FriendsOfBehat\SymfonyExtension\Context\Environment\InitializedSymfonyExtensionEnvironment;
-use PHPUnit\Framework\ExpectationFailedException;
-use Symfony\Bundle\FrameworkBundle\KernelBrowser;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * @author Kévin Dunglas
- */
-final class HttpCacheContext implements Context
-{
- public function __construct(private ContainerInterface $driverContainer)
- {
- }
-
- /**
- * @BeforeScenario @customTagCollector
- */
- public function registerCustomTagCollector(BeforeScenarioScope $scope): void
- {
- $this->disableReboot($scope);
- /** @phpstan-ignore-next-line */
- $iriConverter = $this->driverContainer->get('api_platform.iri_converter');
- $this->driverContainer->set('api_platform.http_cache.tag_collector', new TagCollectorCustom($iriConverter));
- }
-
- /**
- * @Then :iris IRIs should be purged
- */
- public function irisShouldBePurged(string $iris): void
- {
- $purger = $this->driverContainer->get('test.api_platform.http_cache.purger');
-
- $iris = explode(',', $iris);
- sort($iris);
- $iris = implode(',', $iris);
-
- $purgedIris = $purger->getIris();
- sort($purgedIris);
- $purgedIris = implode(',', $purgedIris);
-
- $purger->clear();
-
- if ($iris !== $purgedIris) {
- throw new ExpectationFailedException(\sprintf('IRIs "%s" does not match expected "%s".', $purgedIris, $iris));
- }
- }
-
- /**
- * this is necessary to allow overriding services
- * see https://github.com/FriendsOfBehat/SymfonyExtension/issues/149 for details.
- */
- private function disableReboot(BeforeScenarioScope $scope): void
- {
- $env = $scope->getEnvironment();
- if (!$env instanceof InitializedSymfonyExtensionEnvironment) {
- return;
- }
-
- $driver = $env->getContext(MinkContext::class)->getSession()->getDriver();
- if (!$driver instanceof BrowserKitDriver) {
- return;
- }
-
- $client = $driver->getClient();
- if (!$client instanceof KernelBrowser) {
- return;
- }
-
- $client->disableReboot();
- }
-}
diff --git a/tests/Behat/HydraContext.php b/tests/Behat/HydraContext.php
deleted file mode 100644
index a0425ac2b13..00000000000
--- a/tests/Behat/HydraContext.php
+++ /dev/null
@@ -1,326 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Behat\Context\Context;
-use Behat\Behat\Context\Environment\InitializedContextEnvironment;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use Behatch\Context\RestContext;
-use PHPUnit\Framework\Assert;
-use PHPUnit\Framework\ExpectationFailedException;
-use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
-
-final class HydraContext implements Context
-{
- private ?RestContext $restContext = null;
-
- public function __construct(private readonly PropertyAccessorInterface $propertyAccessor)
- {
- }
-
- /**
- * Gives access to the Behatch context.
- *
- * @BeforeScenario
- */
- public function gatherContexts(BeforeScenarioScope $scope): void
- {
- /**
- * @var InitializedContextEnvironment $environment
- */
- $environment = $scope->getEnvironment();
- /**
- * @var RestContext $restContext
- */
- $restContext = $environment->getContext(RestContext::class);
- $this->restContext = $restContext;
- }
-
- /**
- * @Then the Hydra class :class exists
- */
- public function assertTheHydraClassExist(string $className): void
- {
- try {
- $this->getClassInfo($className);
- } catch (\InvalidArgumentException $e) {
- throw new ExpectationFailedException(\sprintf('The class "%s" doesn\'t exist.', $className), null, $e);
- }
- }
-
- /**
- * @Then the Hydra class :class doesn't exist
- */
- public function assertTheHydraClassNotExist(string $className): void
- {
- try {
- $this->getClassInfo($className);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- throw new ExpectationFailedException(\sprintf('The class "%s" exists.', $className));
- }
-
- /**
- * @Then the boolean value of the node :node of the Hydra class :class is true
- */
- public function assertBooleanNodeValueIs(string $nodeName, string $className): void
- {
- Assert::assertTrue($this->propertyAccessor->getValue($this->getClassInfo($className), $nodeName));
- }
-
- /**
- * @Then the value of the node :node of the Hydra class :class is :value
- */
- public function assertNodeValueIs(string $nodeName, string $className, string $value): void
- {
- Assert::assertEquals(
- $this->propertyAccessor->getValue($this->getClassInfo($className), $nodeName),
- $value
- );
- }
-
- /**
- * @Then the boolean value of the node :node of the property :prop of the Hydra class :class is true
- */
- public function assertPropertyNodeValueIsTrue(string $nodeName, string $propertyName, string $className): void
- {
- Assert::assertTrue($this->propertyAccessor->getValue($this->getPropertyInfo($propertyName, $className), $nodeName));
- }
-
- /**
- * @Then the value of the node :node of the property :prop of the Hydra class :class is :value
- */
- public function assertPropertyNodeValueIs(string $nodeName, string $propertyName, string $className, string $value): void
- {
- Assert::assertEquals(
- $this->propertyAccessor->getValue($this->getPropertyInfo($propertyName, $className), $nodeName),
- $value
- );
- }
-
- /**
- * @Then the boolean value of the node :node of the operation :operation of the Hydra class :class is true
- */
- public function assertOperationNodeBooleanValueIs(string $nodeName, string $operationMethod, string $className): void
- {
- Assert::assertTrue($this->propertyAccessor->getValue($this->getOperation($operationMethod, $className), $nodeName));
- }
-
- /**
- * @Then the value of the node :node of the operation :operation of the Hydra class :class is :value
- */
- public function assertOperationNodeValueIs(string $nodeName, string $operationMethod, string $className, string $value): void
- {
- Assert::assertEquals(
- $this->propertyAccessor->getValue($this->getOperation($operationMethod, $className), $nodeName),
- $value
- );
- }
-
- /**
- * @Then the value of the node :node of the operation :operation of the Hydra class :class contains :value
- */
- public function assertOperationNodeValueContains(string $nodeName, string $operationMethod, string $className, string $value): void
- {
- $property = $this->getOperation($operationMethod, $className);
-
- Assert::assertContains($value, $this->propertyAccessor->getValue($property, $nodeName));
- }
-
- /**
- * @Then :nb operations are available for Hydra class :class
- */
- public function assertNbOperationsExist(int $nb, string $className): void
- {
- Assert::assertEquals($nb, \count($this->getOperations($className)));
- }
-
- /**
- * @Then :nb properties are available for Hydra class :class
- */
- public function assertNbPropertiesExist(int $nb, string $className): void
- {
- Assert::assertEquals($nb, \count($this->getProperties($className)));
- }
-
- /**
- * @Then :prop property doesn't exist for the Hydra class :class
- */
- public function assertPropertyNotExist(string $propertyName, string $className): void
- {
- try {
- $this->getPropertyInfo($propertyName, $className);
- } catch (\InvalidArgumentException) {
- return;
- }
-
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" exists.', $propertyName, $className));
- }
-
- /**
- * @Then :prop property is readable for Hydra class :class
- */
- public function assertPropertyIsReadable(string $propertyName, string $className): void
- {
- if (!$this->getPropertyInfo($propertyName, $className)->{'hydra:readable'}) {
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" is not readable', $propertyName, $className));
- }
- }
-
- /**
- * @Then :prop property is not readable for Hydra class :class
- */
- public function assertPropertyIsNotReadable(string $propertyName, string $className): void
- {
- if ($this->getPropertyInfo($propertyName, $className)->{'hydra:readable'}) {
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" is readable', $propertyName, $className));
- }
- }
-
- /**
- * @Then :prop property is writable for Hydra class :class
- */
- public function assertPropertyIsWritable(string $propertyName, string $className): void
- {
- if (!$this->getPropertyInfo($propertyName, $className)->{'hydra:writeable'}) {
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" is not writable', $propertyName, $className));
- }
- }
-
- /**
- * @Then :prop property is required for Hydra class :class
- */
- public function assertPropertyIsRequired(string $propertyName, string $className): void
- {
- if (!$this->getPropertyInfo($propertyName, $className)->{'hydra:required'}) {
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" is not required', $propertyName, $className));
- }
- }
-
- /**
- * @Then :prop property is not required for Hydra class :class
- */
- public function assertPropertyIsNotRequired(string $propertyName, string $className): void
- {
- if ($this->getPropertyInfo($propertyName, $className)->{'hydra:required'}) {
- throw new ExpectationFailedException(\sprintf('Property "%s" of class "%s" is required', $propertyName, $className));
- }
- }
-
- /**
- * Gets information about a property.
- *
- * @throws \InvalidArgumentException
- */
- private function getPropertyInfo(string $propertyName, string $className): \stdClass
- {
- foreach ($this->getProperties($className) as $property) {
- if ($property->{'hydra:title'} === $propertyName) {
- return $property;
- }
- }
-
- throw new \InvalidArgumentException(\sprintf('Property "%s" of class "%s" doesn\'t exist', $propertyName, $className));
- }
-
- /**
- * Gets an operation by its method name.
- *
- * @throws \InvalidArgumentException
- */
- private function getOperation(string $method, string $className): \stdClass
- {
- foreach ($this->getOperations($className) as $operation) {
- if ($operation->{'hydra:method'} === $method) {
- return $operation;
- }
- }
-
- throw new \InvalidArgumentException(\sprintf('Operation "%s" of class "%s" doesn\'t exist.', $method, $className));
- }
-
- /**
- * Gets all operations of a given class.
- */
- private function getOperations(string $className): array
- {
- return $this->getClassInfo($className)->{'hydra:supportedOperation'} ?? [];
- }
-
- /**
- * Gets all properties of a given class.
- */
- private function getProperties(string $className): array
- {
- return $this->getClassInfo($className)->{'hydra:supportedProperty'} ?? [];
- }
-
- /**
- * Gets information about a class.
- *
- * @throws \InvalidArgumentException
- */
- private function getClassInfo(string $className): \stdClass
- {
- $json = $this->getLastJsonResponse();
-
- if (isset($json->{'hydra:supportedClass'})) {
- foreach ($json->{'hydra:supportedClass'} as $classData) {
- if ($classData->{'hydra:title'} === $className) {
- return $classData;
- }
- }
- }
-
- throw new \InvalidArgumentException(\sprintf('Class %s cannot be found in the vocabulary', $className));
- }
-
- /**
- * Gets the last JSON response.
- *
- * @throws \RuntimeException
- */
- private function getLastJsonResponse(): \stdClass
- {
- if (null === $decoded = json_decode($this->restContext->getMink()->getSession()->getDriver()->getContent(), null, 512, \JSON_THROW_ON_ERROR)) {
- throw new \RuntimeException('JSON response seems to be invalid');
- }
-
- return $decoded;
- }
-
- /**
- * @Then the Hydra context matches the online resource :url
- */
- public function assertHydraContextIsCorrect(string $url): void
- {
- $opts = [
- 'http' => [
- 'method' => 'GET',
- 'header' => "User-Agent: Mozilla/5.0\r\n",
- ],
- ];
-
- $context = stream_context_create($opts);
- $upstream = json_decode(file_get_contents($url, false, $context));
- $actual = $this->getLastJsonResponse();
- $local = $actual->{'@context'}[0];
- Assert::assertEquals(
- $upstream,
- $local
- );
- }
-}
diff --git a/tests/Behat/JsonApiContext.php b/tests/Behat/JsonApiContext.php
deleted file mode 100644
index 7cd50646c57..00000000000
--- a/tests/Behat/JsonApiContext.php
+++ /dev/null
@@ -1,209 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\CircularReference as CircularReferenceDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyFriend as DummyFriendDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CircularReference;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
-use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
-use Behat\Behat\Context\Context;
-use Behat\Behat\Context\Environment\InitializedContextEnvironment;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use Behatch\Context\RestContext;
-use Behatch\Json\Json;
-use Behatch\Json\JsonInspector;
-use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\Persistence\ManagerRegistry;
-use Doctrine\Persistence\ObjectManager;
-use JsonSchema\Validator;
-use PHPUnit\Framework\ExpectationFailedException;
-
-final class JsonApiContext implements Context
-{
- private ?RestContext $restContext = null;
- private readonly Validator $validator;
- private readonly JsonInspector $inspector;
- private readonly string $jsonApiSchemaFile;
- private readonly ObjectManager $manager;
-
- public function __construct(ManagerRegistry $doctrine, string $jsonApiSchemaFile)
- {
- if (!is_file($jsonApiSchemaFile)) {
- throw new \InvalidArgumentException('The JSON API schema doesn\'t exist.');
- }
-
- $this->validator = new Validator();
- $this->inspector = new JsonInspector('javascript');
- $this->jsonApiSchemaFile = $jsonApiSchemaFile;
- $this->manager = $doctrine->getManager();
- }
-
- /**
- * Gives access to the Behatch context.
- *
- * @BeforeScenario
- */
- public function gatherContexts(BeforeScenarioScope $scope): void
- {
- /**
- * @var InitializedContextEnvironment $environment
- */
- $environment = $scope->getEnvironment();
- /**
- * @var RestContext $restContext
- */
- $restContext = $environment->getContext(RestContext::class);
- $this->restContext = $restContext;
- }
-
- /**
- * @Then the JSON should be valid according to the JSON API schema
- */
- public function theJsonShouldBeValidAccordingToTheJsonApiSchema(): void
- {
- $json = $this->getJson()->getContent();
- $this->validator->validate($json, (object) ['$ref' => "file://{$this->jsonApiSchemaFile}"]);
-
- if (!$this->validator->isValid()) {
- throw new ExpectationFailedException('The JSON is not valid according to the JSON API schema.');
- }
- }
-
- /**
- * @Then the JSON node :node should be an empty array
- */
- public function theJsonNodeShouldBeAnEmptyArray(string $node): void
- {
- $actual = $this->getValueOfNode($node);
- if (null !== $actual && [] !== $actual) {
- throw new ExpectationFailedException(\sprintf('The node value is `%s`', json_encode($actual, \JSON_THROW_ON_ERROR)));
- }
- }
-
- /**
- * @Then the JSON node :node should be a number
- */
- public function theJsonNodeShouldBeANumber(string $node): void
- {
- if (!is_numeric($actual = $this->getValueOfNode($node))) {
- throw new ExpectationFailedException(\sprintf('The node value is `%s`', json_encode($actual, \JSON_THROW_ON_ERROR)));
- }
- }
-
- /**
- * @Then the JSON node :node should not be an empty string
- */
- public function theJsonNodeShouldNotBeAnEmptyString(string $node): void
- {
- if ('' === $actual = $this->getValueOfNode($node)) {
- throw new ExpectationFailedException(\sprintf('The node value is `%s`', json_encode($actual)));
- }
- }
-
- /**
- * @Then the JSON node :node should be sorted
- * @Then the JSON should be sorted
- */
- public function theJsonNodeShouldBeSorted(string $node = ''): void
- {
- $actual = (array) $this->getValueOfNode($node);
-
- $expected = $actual;
- ksort($expected);
-
- if ($actual !== $expected) {
- throw new ExpectationFailedException(\sprintf('The json node "%s" is not sorted by keys', $node));
- }
- }
-
- /**
- * @Given there is a RelatedDummy
- */
- public function thereIsARelatedDummy(): void
- {
- $relatedDummy = $this->buildRelatedDummy();
- $relatedDummy->setName('RelatedDummy with no friends');
-
- $this->manager->persist($relatedDummy);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a DummyFriend
- */
- public function thereIsADummyFriend(): void
- {
- $friend = $this->buildDummyFriend();
- $friend->setName('DummyFriend');
-
- $this->manager->persist($friend);
- $this->manager->flush();
- }
-
- /**
- * @Given there is a CircularReference
- */
- public function thereIsACircularReference(): void
- {
- $circularReference = $this->buildCircularReference();
- $circularReference->parent = $circularReference;
-
- $circularReferenceBis = $this->buildCircularReference();
- $circularReferenceBis->parent = $circularReference;
-
- $circularReference->children->add($circularReference);
- $circularReference->children->add($circularReferenceBis);
-
- $this->manager->persist($circularReference);
- $this->manager->persist($circularReferenceBis);
- $this->manager->flush();
- }
-
- private function getValueOfNode(string $node)
- {
- return $this->inspector->evaluate($this->getJson(), $node);
- }
-
- private function getJson(): Json
- {
- return new Json($this->getContent());
- }
-
- private function getContent(): string
- {
- return $this->restContext->getMink()->getSession()->getDriver()->getContent();
- }
-
- private function isOrm(): bool
- {
- return $this->manager instanceof EntityManagerInterface;
- }
-
- private function buildCircularReference(): CircularReference|CircularReferenceDocument
- {
- return $this->isOrm() ? new CircularReference() : new CircularReferenceDocument();
- }
-
- private function buildDummyFriend(): DummyFriend|DummyFriendDocument
- {
- return $this->isOrm() ? new DummyFriend() : new DummyFriendDocument();
- }
-
- private function buildRelatedDummy(): RelatedDummy|RelatedDummyDocument
- {
- return $this->isOrm() ? new RelatedDummy() : new RelatedDummyDocument();
- }
-}
diff --git a/tests/Behat/JsonContext.php b/tests/Behat/JsonContext.php
deleted file mode 100644
index 4450465fd08..00000000000
--- a/tests/Behat/JsonContext.php
+++ /dev/null
@@ -1,112 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
-use Behat\Gherkin\Node\PyStringNode;
-use Behat\Mink\Exception\ExpectationException;
-use Behatch\Context\JsonContext as BaseJsonContext;
-use Behatch\HttpCall\HttpCallResultPool;
-use Behatch\Json\Json;
-use PHPUnit\Framework\Assert;
-
-final class JsonContext extends BaseJsonContext
-{
- public function __construct(HttpCallResultPool $httpCallResultPool)
- {
- parent::__construct($httpCallResultPool);
- }
-
- /**
- * @Then the JSON node :node should contain:
- */
- public function theJsonNodeShouldContainContent(string $node, PyStringNode $content): void
- {
- $actual = $this->getJson();
-
- try {
- $expected = new Json($content);
- } catch (\Exception $e) {
- throw new ExpectationException('The expected JSON is not valid.', $this->getSession()->getDriver(), $e);
- }
-
- $actualContent = $this->inspector->evaluate($actual, $node);
-
- if (!is_iterable($actualContent)) {
- throw new ExpectationException(\sprintf("The JSON is equal to:\n%s", json_encode($actualContent, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRETTY_PRINT)), $this->getSession()->getDriver());
- }
-
- foreach ($actualContent as $itemContent) {
- try {
- $this->assertEquals($expected->getContent(), $itemContent, ' ');
- } catch (ExpectationException) {
- continue;
- }
-
- return;
- }
-
- throw new ExpectationException("The JSON node \"{$node}\" does not contain the expected content.", $this->getSession()->getDriver());
- }
-
- /**
- * @Then the JSON node :node should be equal to:
- */
- public function theJsonNodeShouldBeEqualToContent(string $node, PyStringNode $content): void
- {
- $actual = $this->getJson();
-
- try {
- $expected = new Json($content);
- } catch (\Exception $e) {
- throw new ExpectationException('The expected JSON is not valid.', $this->getSession()->getDriver(), $e);
- }
-
- $actualContent = $this->inspector->evaluate($actual, $node);
-
- $this->assertEquals(
- $expected->getContent(),
- $actualContent,
- \sprintf("The JSON node \"%s\" is equal to:\n%s", $node, json_encode($actualContent, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_PRETTY_PRINT))
- );
- }
-
- public function theJsonShouldBeEqualTo(PyStringNode $content): void
- {
- $actual = $this->getJson();
-
- try {
- $expected = new Json($content);
- } catch (\Exception) {
- throw new ExpectationException('The expected JSON is not valid.', $this->getSession()->getDriver());
- }
-
- $this->assertEquals(
- $expected->getContent(),
- $actual->getContent(),
- "The JSON is equal to:\n{$actual->encode()}"
- );
- }
-
- /**
- * @Then /^the JSON should be a superset of:$/
- */
- public function theJsonIsASupersetOf(PyStringNode $content): void
- {
- $array = json_decode($this->httpCallResultPool->getResult()->getValue(), true, 512, \JSON_THROW_ON_ERROR);
- $subset = json_decode($content->getRaw(), true, 512, \JSON_THROW_ON_ERROR);
-
- method_exists(Assert::class, 'assertArraySubset') ? Assert::assertArraySubset($subset, $array) : ApiTestCase::assertArraySubset($subset, $array);
- }
-}
diff --git a/tests/Behat/JsonHalContext.php b/tests/Behat/JsonHalContext.php
deleted file mode 100644
index 91cff357660..00000000000
--- a/tests/Behat/JsonHalContext.php
+++ /dev/null
@@ -1,80 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Behat\Context\Context;
-use Behat\Behat\Context\Environment\InitializedContextEnvironment;
-use Behat\Behat\Hook\Scope\BeforeScenarioScope;
-use Behatch\Context\RestContext;
-use Behatch\Json\Json;
-use JsonSchema\Validator;
-use PHPUnit\Framework\ExpectationFailedException;
-
-final class JsonHalContext implements Context
-{
- private ?RestContext $restContext = null;
- private readonly Validator $validator;
- private readonly string $schemaFile;
-
- public function __construct(string $schemaFile)
- {
- if (!is_file($schemaFile)) {
- throw new \InvalidArgumentException('The JSON HAL schema doesn\'t exist.');
- }
-
- $this->validator = new Validator();
- $this->schemaFile = $schemaFile;
- }
-
- /**
- * Gives access to the Behatch context.
- *
- * @BeforeScenario
- */
- public function gatherContexts(BeforeScenarioScope $scope): void
- {
- /**
- * @var InitializedContextEnvironment $environment
- */
- $environment = $scope->getEnvironment();
- /**
- * @var RestContext $restContext
- */
- $restContext = $environment->getContext(RestContext::class);
- $this->restContext = $restContext;
- }
-
- /**
- * @Then the JSON should be valid according to the JSON HAL schema
- */
- public function theJsonShouldBeValidAccordingToTheJsonHALSchema(): void
- {
- $json = $this->getJson()->getContent();
- $this->validator->validate($json, (object) ['$ref' => "file://{$this->schemaFile}"]);
-
- if (!$this->validator->isValid()) {
- throw new ExpectationFailedException('The JSON is not valid according to the HAL+JSON schema.');
- }
- }
-
- private function getJson(): Json
- {
- return new Json($this->getContent());
- }
-
- private function getContent(): string
- {
- return $this->restContext->getMink()->getSession()->getDriver()->getContent();
- }
-}
diff --git a/tests/Behat/MercureContext.php b/tests/Behat/MercureContext.php
deleted file mode 100644
index 2dbd68f8775..00000000000
--- a/tests/Behat/MercureContext.php
+++ /dev/null
@@ -1,144 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use ApiPlatform\Tests\Fixtures\TestBundle\Mercure\TestHub;
-use Behat\Behat\Context\Context;
-use Behat\Gherkin\Node\PyStringNode;
-use Behat\Gherkin\Node\TableNode;
-use PHPUnit\Framework\Assert;
-use Psr\Container\ContainerInterface;
-use Symfony\Component\Mercure\Update;
-
-/**
- * Context for Mercure.
- *
- * @author Alan Poulain
- */
-final class MercureContext implements Context
-{
- public function __construct(private readonly ContainerInterface $driverContainer)
- {
- }
-
- /**
- * @Then :number Mercure updates should have been sent
- * @Then :number Mercure update should have been sent
- */
- public function mercureUpdatesShouldHaveBeenSent(int $number): void
- {
- $updateHandler = $this->getMercureTestHub();
- $total = \count($updateHandler->getUpdates());
-
- if (0 === $total) {
- throw new \RuntimeException('No Mercure update has been sent.');
- }
-
- Assert::assertEquals($number, $total, \sprintf('Expected %d Mercure updates to be sent, got %d.', $number, $total));
- }
-
- /**
- * @Then the first Mercure update should have topics:
- * @Then the Mercure update should have topics:
- */
- public function firstMercureUpdateShouldHaveTopics(TableNode $table): void
- {
- $this->mercureUpdateShouldHaveTopics(1, $table);
- }
-
- /**
- * @Then the first Mercure update should have data:
- * @Then the Mercure update should have data:
- */
- public function firstMercureUpdateShouldHaveData(PyStringNode $data): void
- {
- $this->mercureUpdateShouldHaveData(1, $data);
- }
-
- /**
- * @Then the Mercure update number :index should have topics:
- */
- public function mercureUpdateShouldHaveTopics(int $index, TableNode $table): void
- {
- $updateHandler = $this->getMercureTestHub();
- $updates = $updateHandler->getUpdates();
-
- if (0 === \count($updates)) {
- throw new \RuntimeException('No Mercure update has been sent.');
- }
-
- if (!isset($updates[$index - 1])) {
- throw new \RuntimeException(\sprintf('Mercure update #%d does not exist.', $index));
- }
- /** @var Update $update */
- $update = $updates[$index - 1];
- Assert::assertEquals(array_keys($table->getRowsHash()), array_values($update->getTopics()));
- }
-
- /**
- * @Then the Mercure update number :index should have data:
- */
- public function mercureUpdateShouldHaveData(int $index, PyStringNode $data): void
- {
- $updateHandler = $this->getMercureTestHub();
- $updates = $updateHandler->getUpdates();
-
- if (0 === \count($updates)) {
- throw new \RuntimeException('No Mercure update has been sent.');
- }
-
- if (!isset($updates[$index - 1])) {
- throw new \RuntimeException(\sprintf('Mercure update #%d does not exist.', $index));
- }
- /** @var Update $update */
- $update = $updates[$index - 1];
- Assert::assertJsonStringEqualsJsonString($data->getRaw(), $update->getData());
- }
-
- /**
- * @Then the following Mercure update with topics :topics should have been sent:
- */
- public function theFollowingMercureUpdateShouldHaveBeenSent(string $topics, PyStringNode $update): void
- {
- $topics = explode(',', $topics);
- $update = json_decode($update->getRaw(), true, 512, \JSON_THROW_ON_ERROR);
-
- $updateHandler = $this->getMercureTestHub();
- foreach ($updateHandler->getUpdates() as $sentUpdate) {
- $toMatchTopics = \count($topics);
- foreach ($sentUpdate->getTopics() as $sentTopic) {
- foreach ($topics as $topic) {
- if (preg_match("@$topic@", (string) $sentTopic)) {
- --$toMatchTopics;
- }
- }
- }
-
- if ($toMatchTopics > 0) {
- continue;
- }
-
- if ($sentUpdate->getData() === json_encode($update, \JSON_THROW_ON_ERROR)) {
- return;
- }
- }
-
- throw new \RuntimeException('Mercure update has not been sent.');
- }
-
- private function getMercureTestHub(): TestHub
- {
- return $this->driverContainer->get('mercure.hub.default.test_hub');
- }
-}
diff --git a/tests/Behat/XmlContext.php b/tests/Behat/XmlContext.php
deleted file mode 100644
index 33a811470d2..00000000000
--- a/tests/Behat/XmlContext.php
+++ /dev/null
@@ -1,43 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-declare(strict_types=1);
-
-namespace ApiPlatform\Tests\Behat;
-
-use Behat\Gherkin\Node\PyStringNode;
-use Behatch\Context\XmlContext as BaseXmlContext;
-use Symfony\Component\Serializer\Encoder\XmlEncoder;
-
-final class XmlContext extends BaseXmlContext
-{
- private readonly XmlEncoder $xmlEncoder;
-
- public function __construct()
- {
- $this->xmlEncoder = new XmlEncoder();
- }
-
- /**
- * @Then the XML should be equal to:
- */
- public function theXmlShouldBeEqualTo(PyStringNode $content): void
- {
- $expected = $this->xmlEncoder->decode((string) $content, 'xml');
- $actual = $this->xmlEncoder->decode($actualXml = $this->getSession()->getPage()->getContent(), 'xml');
-
- $this->assertEquals(
- $expected,
- $actual,
- "The XML is equal to:\n{$actualXml}"
- );
- }
-}
diff --git a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php
index 62a18c4ab6c..9e9e6b893f6 100644
--- a/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php
+++ b/tests/Fixtures/TestBundle/Document/AbsoluteUrlDummy.php
@@ -20,7 +20,7 @@
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
#[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)]
-#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])]
+#[ApiResource(shortName: 'AbsoluteUrlDummySubresource', uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])]
#[ODM\Document]
class AbsoluteUrlDummy
{
diff --git a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php
index 84bc5353cc5..9b8e8736943 100644
--- a/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php
+++ b/tests/Fixtures/TestBundle/Document/NetworkPathDummy.php
@@ -20,7 +20,7 @@
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
#[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)]
-#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])]
+#[ApiResource(shortName: 'NetworkPathDummySubresource', uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])]
#[ODM\Document]
class NetworkPathDummy
{
diff --git a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php
index a632d81dc83..4eeb455e2d7 100644
--- a/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php
+++ b/tests/Fixtures/TestBundle/Entity/AbsoluteUrlDummy.php
@@ -20,7 +20,7 @@
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::ABS_URL)]
-#[ApiResource(uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])]
+#[ApiResource(shortName: 'AbsoluteUrlDummySubresource', uriTemplate: '/absolute_url_relation_dummies/{id}/absolute_url_dummies{._format}', uriVariables: ['id' => new Link(fromClass: AbsoluteUrlRelationDummy::class, identifiers: ['id'], toProperty: 'absoluteUrlRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::ABS_URL, operations: [new GetCollection()])]
#[ORM\Entity]
class AbsoluteUrlDummy
{
diff --git a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php
index 7089cdf2a70..09e22711d40 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyAggregateOffer.php
@@ -28,8 +28,8 @@
* @author Antoine Bluchet
*/
#[ApiResource]
-#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])]
-#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_products/{id}/offers{._format}', shortName: 'DummyAggregateOfferByProduct', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers{._format}', shortName: 'DummyAggregateOfferByRelatedProduct', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product')], status: 200, operations: [new GetCollection()])]
#[ORM\Entity]
class DummyAggregateOffer
{
diff --git a/tests/Fixtures/TestBundle/Entity/DummyOffer.php b/tests/Fixtures/TestBundle/Entity/DummyOffer.php
index 47c9c42142d..2988a075348 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyOffer.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyOffer.php
@@ -26,9 +26,9 @@
* @author Antoine Bluchet
*/
#[ApiResource]
-#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
-#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
-#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_aggregate_offers/{id}/offers{._format}', shortName: 'DummyOfferByAggregate', uriVariables: ['id' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_products/{id}/offers/{offers}/offers{._format}', shortName: 'DummyOfferByProductOffer', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products/{relatedProducts}/offers/{offers}/offers{._format}', shortName: 'DummyOfferByRelatedProductOffer', uriVariables: ['id' => new Link(fromClass: DummyProduct::class, identifiers: ['id']), 'relatedProducts' => new Link(fromClass: DummyProduct::class, identifiers: ['id'], toProperty: 'product'), 'offers' => new Link(fromClass: DummyAggregateOffer::class, identifiers: ['id'], toProperty: 'aggregate')], status: 200, operations: [new GetCollection()])]
#[ORM\Entity]
class DummyOffer
{
diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php
index d6e428f8a02..2e83f1b06a9 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php
@@ -28,7 +28,7 @@
* @author Antoine Bluchet
*/
#[ApiResource]
-#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/dummy_products/{id}/related_products{._format}', shortName: 'DummyProductRelatedProducts', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, operations: [new GetCollection()])]
#[ORM\Entity]
class DummyProduct
{
diff --git a/tests/Fixtures/TestBundle/Entity/DummyResourceWithComplexConstructor.php b/tests/Fixtures/TestBundle/Entity/DummyResourceWithComplexConstructor.php
index 224206c15bf..2a850595ec3 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyResourceWithComplexConstructor.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyResourceWithComplexConstructor.php
@@ -19,6 +19,7 @@
#[Post]
#[ApiResource(
+ shortName: 'DummyResourceWithComplexConstructorByCompany',
uriTemplate: '/companies/{companyId}/employees/{id}',
uriVariables: [
'companyId' => ['from_class' => Company::class, 'to_property' => 'company'],
diff --git a/tests/Fixtures/TestBundle/Entity/Greeting.php b/tests/Fixtures/TestBundle/Entity/Greeting.php
index d74e8217b55..7cad16e25a7 100644
--- a/tests/Fixtures/TestBundle/Entity/Greeting.php
+++ b/tests/Fixtures/TestBundle/Entity/Greeting.php
@@ -19,7 +19,7 @@
use Doctrine\ORM\Mapping as ORM;
#[ApiResource]
-#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])]
+#[ApiResource(uriTemplate: '/people/{id}/sent_greetings{._format}', shortName: 'GreetingBySender', uriVariables: ['id' => new Link(fromClass: Person::class, identifiers: ['id'], toProperty: 'sender')], status: 200, operations: [new GetCollection()])]
#[ORM\Entity]
class Greeting
{
diff --git a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php
index 098750dc0dc..1ac8ec4d55b 100644
--- a/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php
+++ b/tests/Fixtures/TestBundle/Entity/NetworkPathDummy.php
@@ -20,7 +20,7 @@
use Doctrine\ORM\Mapping as ORM;
#[ApiResource(urlGenerationStrategy: UrlGeneratorInterface::NET_PATH)]
-#[ApiResource(uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])]
+#[ApiResource(shortName: 'NetworkPathDummySubresource', uriTemplate: '/network_path_relation_dummies/{id}/network_path_dummies{._format}', uriVariables: ['id' => new Link(fromClass: NetworkPathRelationDummy::class, identifiers: ['id'], toProperty: 'networkPathRelationDummy')], status: 200, urlGenerationStrategy: UrlGeneratorInterface::NET_PATH, operations: [new GetCollection()])]
#[ORM\Entity]
class NetworkPathDummy
{
diff --git a/tests/Fixtures/TestBundle/MessengerHandler/Document/RPCHandler.php b/tests/Fixtures/TestBundle/MessengerHandler/Document/RPCHandler.php
new file mode 100644
index 00000000000..d6edb96561b
--- /dev/null
+++ b/tests/Fixtures/TestBundle/MessengerHandler/Document/RPCHandler.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Fixtures\TestBundle\MessengerHandler\Document;
+
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RPC;
+use Symfony\Component\Messenger\Attribute\AsMessageHandler;
+
+#[AsMessageHandler]
+class RPCHandler
+{
+ public function __invoke(RPC $data): void
+ {
+ }
+}
diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php
index e26c8bc3eb7..6cb1bb86951 100644
--- a/tests/Fixtures/app/AppKernel.php
+++ b/tests/Fixtures/app/AppKernel.php
@@ -18,7 +18,6 @@
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Symfony\Bundle\ApiPlatformBundle;
-use ApiPlatform\Tests\Behat\DoctrineContext;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\User as UserDocument;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\User;
use ApiPlatform\Tests\Fixtures\TestBundle\TestBundle;
@@ -27,7 +26,6 @@
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Doctrine\Bundle\MongoDBBundle\Command\TailCursorDoctrineODMCommand;
use Doctrine\Bundle\MongoDBBundle\DoctrineMongoDBBundle;
-use FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle;
use Symfony\AI\McpBundle\McpBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
@@ -63,7 +61,6 @@ public function __construct(string $environment, bool $debug, ?bool $genIdDefaul
{
parent::__construct($environment, $debug);
- // patch for behat/symfony2-extension not supporting %env(APP_ENV)%
$this->environment = $_SERVER['APP_ENV'] ?? $environment;
$this->genIdDefault = $genIdDefault ?? $_SERVER['GEN_ID_DEFAULT'] ?? null;
}
@@ -81,10 +78,6 @@ public function registerBundles(): array
new MakerBundle(),
];
- if (null === ($_ENV['APP_PHPUNIT'] ?? null) && class_exists(FriendsOfBehatSymfonyExtensionBundle::class)) {
- $bundles[] = new FriendsOfBehatSymfonyExtensionBundle();
- }
-
if (extension_loaded('mongodb') && class_exists(DoctrineMongoDBBundle::class)) {
$bundles[] = new DoctrineMongoDBBundle();
}
@@ -120,11 +113,6 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
$loader->load(__DIR__."/config/config_{$this->getEnvironment()}.yml");
- if (interface_exists(Behat\Behat\Context\Context::class) && class_exists(DoctrineContext::class)) {
- $loader->load(__DIR__.('mongodb' === $this->getEnvironment() ? '/config/config_behat_mongodb.yml' : '/config/config_behat_orm.yml'));
- $c->getDefinition(DoctrineContext::class)->setArgument('$passwordHasher', class_exists(NativePasswordHasher::class) ? 'security.user_password_encoder' : 'security.user_password_hasher');
- }
-
$messengerConfig = [
'default_bus' => 'messenger.bus.default',
'buses' => [
diff --git a/tests/Fixtures/app/bootstrap.php b/tests/Fixtures/app/bootstrap.php
index 10db0977595..d268c9f75e7 100644
--- a/tests/Fixtures/app/bootstrap.php
+++ b/tests/Fixtures/app/bootstrap.php
@@ -23,4 +23,11 @@
require __DIR__.'/AppKernel.php';
require __DIR__.'/DefaultParametersAppKernel.php';
+if (!is_file($resourcesFile = __DIR__.'/var/resources.php')) {
+ if (!is_dir(dirname($resourcesFile))) {
+ mkdir(dirname($resourcesFile), 0777, true);
+ }
+ file_put_contents($resourcesFile, '
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedBoolean as ConvertedBooleanDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedBoolean;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use PHPUnit\Framework\Attributes\TestWith;
+
+final class BooleanFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ EmbeddedDummy::class,
+ RelatedDummy::class,
+ ConvertedBoolean::class,
+ ];
+ }
+
+ #[TestWith(['true', 15, ['/dummies/1', '/dummies/2', '/dummies/3']])]
+ #[TestWith(['1', 15, ['/dummies/1', '/dummies/2', '/dummies/3']])]
+ #[TestWith(['false', 10, ['/dummies/16', '/dummies/17', '/dummies/18']])]
+ #[TestWith(['0', 10, ['/dummies/16', '/dummies/17', '/dummies/18']])]
+ public function testFilterDummiesByBoolean(string $value, int $expectedTotal, array $expectedIds): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 15, true);
+ $this->createDummies($resource, 10, false);
+
+ $response = self::createClient()->request('GET', '/dummies?dummyBoolean='.$value, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $data = $response->toArray();
+ $this->assertSame('/contexts/Dummy', $data['@context']);
+ $this->assertSame('/dummies', $data['@id']);
+ $this->assertSame('hydra:Collection', $data['@type']);
+ $this->assertSame($expectedTotal, $data['hydra:totalItems']);
+ $this->assertSame($expectedIds, array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']));
+ $this->assertSame('hydra:PartialCollectionView', $data['hydra:view']['@type']);
+ $this->assertStringContainsString('dummyBoolean='.$value, $data['hydra:view']['@id']);
+ }
+
+ #[TestWith(['true', 15, ['/embedded_dummies/1', '/embedded_dummies/2', '/embedded_dummies/3']])]
+ #[TestWith(['1', 15, ['/embedded_dummies/1', '/embedded_dummies/2', '/embedded_dummies/3']])]
+ #[TestWith(['false', 10, ['/embedded_dummies/16', '/embedded_dummies/17', '/embedded_dummies/18']])]
+ #[TestWith(['0', 10, ['/embedded_dummies/16', '/embedded_dummies/17', '/embedded_dummies/18']])]
+ public function testFilterEmbeddedDummiesByEmbeddedBoolean(string $value, int $expectedTotal, array $expectedIds): void
+ {
+ $embeddedDummyClass = $this->embeddedDummyClass();
+ $embeddableDummyClass = $this->embeddableDummyClass();
+ $this->recreateSchema([$embeddedDummyClass]);
+ $this->createEmbeddedDummies($embeddedDummyClass, $embeddableDummyClass, 15, true);
+ $this->createEmbeddedDummies($embeddedDummyClass, $embeddableDummyClass, 10, false);
+
+ $response = self::createClient()->request('GET', '/embedded_dummies?embeddedDummy.dummyBoolean='.$value, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $data = $response->toArray();
+ $this->assertSame('/contexts/EmbeddedDummy', $data['@context']);
+ $this->assertSame('/embedded_dummies', $data['@id']);
+ $this->assertSame($expectedTotal, $data['hydra:totalItems']);
+ $this->assertSame($expectedIds, array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']));
+ }
+
+ public function testFilterEmbeddedDummiesByRelatedDummyEmbeddedBoolean(): void
+ {
+ $embeddedDummyClass = $this->embeddedDummyClass();
+ $embeddableDummyClass = $this->embeddableDummyClass();
+ $relatedDummyClass = $this->relatedDummyClass();
+ $this->recreateSchema([$embeddedDummyClass, $relatedDummyClass]);
+ $this->createEmbeddedDummiesWithRelatedDummy($embeddedDummyClass, $embeddableDummyClass, $relatedDummyClass, 15, true);
+ $this->createEmbeddedDummiesWithRelatedDummy($embeddedDummyClass, $embeddableDummyClass, $relatedDummyClass, 10, false);
+
+ $response = self::createClient()->request('GET', '/embedded_dummies?relatedDummy.embeddedDummy.dummyBoolean=true', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(15, $data['hydra:totalItems']);
+ $this->assertSame(
+ ['/embedded_dummies/1', '/embedded_dummies/2', '/embedded_dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ #[TestWith(['0'])]
+ #[TestWith(['1'])]
+ public function testCollectionIgnoresUnknownBooleanFilter(string $value): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 15, true);
+ $this->createDummies($resource, 10, false);
+
+ $response = self::createClient()->request('GET', '/dummies?unknown='.$value, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(25, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testFilterCollectionUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedBooleanDocument::class : ConvertedBoolean::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 5; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = (bool) ($i % 2);
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_booleans?name_converted=false', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/converted_booleans/2', '/converted_booleans/4'], $ids);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedBoolean', $member['@type']);
+ $this->assertFalse($member['name_converted']);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function embeddedDummyClass(): string
+ {
+ return $this->isMongoDB() ? EmbeddedDummyDocument::class : EmbeddedDummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function embeddableDummyClass(): string
+ {
+ return $this->isMongoDB() ? EmbeddableDummyDocument::class : EmbeddableDummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function relatedDummyClass(): string
+ {
+ return $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummies(string $resource, int $nb, bool $bool): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->setDummyBoolean($bool);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $embeddedClass
+ * @param class-string $embeddableClass
+ */
+ private function createEmbeddedDummies(string $embeddedClass, string $embeddableClass, int $nb, bool $bool): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $embeddedClass();
+ $dummy->setName('Embedded Dummy #'.$i);
+ $embeddable = new $embeddableClass();
+ $embeddable->setDummyName('Embedded Dummy #'.$i);
+ $embeddable->setDummyBoolean($bool);
+ $dummy->setEmbeddedDummy($embeddable);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $embeddedClass
+ * @param class-string $embeddableClass
+ * @param class-string $relatedClass
+ */
+ private function createEmbeddedDummiesWithRelatedDummy(string $embeddedClass, string $embeddableClass, string $relatedClass, int $nb, bool $bool): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $embeddedClass();
+ $dummy->setName('Embedded Dummy #'.$i);
+ $embeddable = new $embeddableClass();
+ $embeddable->setDummyName('Embedded Dummy #'.$i);
+ $embeddable->setDummyBoolean($bool);
+
+ $related = new $relatedClass();
+ $related->setEmbeddedDummy($embeddable);
+
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/DateFilterTest.php b/tests/Functional/Doctrine/DateFilterTest.php
new file mode 100644
index 00000000000..eaceb789db6
--- /dev/null
+++ b/tests/Functional/Doctrine/DateFilterTest.php
@@ -0,0 +1,385 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedDate as ConvertedDateDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDate as DummyDateDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyImmutableDate as DummyImmutableDateDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedDate;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDate;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyImmutableDate;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use PHPUnit\Framework\Attributes\TestWith;
+
+final class DateFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ EmbeddedDummy::class,
+ DummyDate::class,
+ DummyImmutableDate::class,
+ ConvertedDate::class,
+ ];
+ }
+
+ #[TestWith(['dummyDate[after]=2015-04-28', 2])]
+ #[TestWith(['dummyDate[before]=2015-04-05', 5])]
+ #[TestWith(['dummyDate[after]=2015-04-28T00:00:00%2B00:00', 2])]
+ #[TestWith(['dummyDate[before]=2015-04-05Z', 5])]
+ #[TestWith(['dummyDate[before]=2015-04-05&dummyDate[after]=2015-04-05', 1])]
+ #[TestWith(['dummyDate[after]=2015-04-05&dummyDate[before]=2015-04-05', 1])]
+ #[TestWith(['dummyDate[after]=2015-04-06&dummyDate[before]=2015-04-04', 0])]
+ public function testDummyDateFilter(string $query, int $expectedTotal): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithDate($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?'.$query, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame($expectedTotal, $response->toArray()['hydra:totalItems']);
+ }
+
+ #[TestWith(['relatedDummy.dummyDate[after]=2015-04-28', 3])]
+ #[TestWith(['relatedDummy.dummyDate[after]=2015-04-28&relatedDummy_dummyDate[after]=2015-04-28', 3])]
+ #[TestWith(['relatedDummy.dummyDate[after]=2015-04-28T00:00:00%2B00:00', 3])]
+ public function testAssociationDateFilter(string $query, int $expectedTotal): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesWithDateAndRelatedDummy($resource, $relatedResource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?'.$query, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame($expectedTotal, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testAssociationDateFilterWithEmptyResultSet(): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesWithDateAndRelatedDummy($resource, $relatedResource, 2);
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummy.dummyDate[after]=2015-04-28', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(0, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testCollectionFilteredByDateThatIsNotDatetime(): void
+ {
+ $resource = $this->dummyDateClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummyDates($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dummyDate[after]=2015-04-28', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(3, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testCollectionFilteredByDateIncludeNullAfter(): void
+ {
+ $resource = $this->dummyDateClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummyDates($resource, 3, 'dateIncludeNullAfter');
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullAfter[after]=2015-04-02', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-02T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullAfter']);
+ $this->assertNull($data['hydra:member'][1]['dateIncludeNullAfter']);
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullAfter[before]=2015-04-02', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-01T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullAfter']);
+ $this->assertSame('2015-04-02T00:00:00+00:00', $data['hydra:member'][1]['dateIncludeNullAfter']);
+ }
+
+ public function testCollectionFilteredByDateIncludeNullBefore(): void
+ {
+ $resource = $this->dummyDateClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummyDates($resource, 3, 'dateIncludeNullBefore');
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullBefore[before]=2015-04-01', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-01T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullBefore']);
+ $this->assertNull($data['hydra:member'][1]['dateIncludeNullBefore']);
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullBefore[after]=2015-04-01', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-01T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullBefore']);
+ $this->assertSame('2015-04-02T00:00:00+00:00', $data['hydra:member'][1]['dateIncludeNullBefore']);
+ }
+
+ public function testCollectionFilteredByDateIncludeNullBeforeAndAfter(): void
+ {
+ $resource = $this->dummyDateClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummyDates($resource, 3, 'dateIncludeNullBeforeAndAfter');
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullBeforeAndAfter[before]=2015-04-01', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-01T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullBeforeAndAfter']);
+ $this->assertNull($data['hydra:member'][1]['dateIncludeNullBeforeAndAfter']);
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dateIncludeNullBeforeAndAfter[after]=2015-04-02', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $this->assertSame('2015-04-02T00:00:00+00:00', $data['hydra:member'][0]['dateIncludeNullBeforeAndAfter']);
+ $this->assertNull($data['hydra:member'][1]['dateIncludeNullBeforeAndAfter']);
+ }
+
+ public function testCollectionFilteredByImmutableDate(): void
+ {
+ $resource = $this->isMongoDB() ? DummyImmutableDateDocument::class : DummyImmutableDate::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 30; ++$i) {
+ $dummy = new $resource();
+ $dummy->dummyDate = new \DateTimeImmutable(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/dummy_immutable_dates?dummyDate[after]=2015-04-28', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(3, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testCollectionFilteredByEmbeddedDate(): void
+ {
+ $embeddedClass = $this->isMongoDB() ? EmbeddedDummyDocument::class : EmbeddedDummy::class;
+ $embeddableClass = $this->isMongoDB() ? EmbeddableDummyDocument::class : EmbeddableDummy::class;
+ $this->recreateSchema([$embeddedClass]);
+
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 29; ++$i) {
+ $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $embeddable = new $embeddableClass();
+ $embeddable->setDummyName('Embeddable #'.$i);
+ $embeddable->setDummyDate($date);
+
+ $dummy = new $embeddedClass();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setEmbeddedDummy($embeddable);
+ if (29 !== $i) {
+ $dummy->setDummyDate($date);
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/embedded_dummies?embeddedDummy.dummyDate[after]=2015-04-28', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(2, $data['hydra:member']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/embedded_dummies/28', '/embedded_dummies/29'], $ids);
+ }
+
+ public function testCollectionFilteredUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedDateDocument::class : ConvertedDate::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 30; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_dates?name_converted[strictly_after]=2015-04-28', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/converted_dates/29', '/converted_dates/30'], $ids);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedDate', $member['@type']);
+ $this->assertIsString($member['name_converted']);
+ }
+
+ $this->assertSame('hydra:IriTemplate', $data['hydra:search']['@type']);
+ $this->assertSame('BasicRepresentation', $data['hydra:search']['hydra:variableRepresentation']);
+ $variables = array_map(static fn (array $m): string => $m['variable'], $data['hydra:search']['hydra:mapping']);
+ sort($variables);
+ $this->assertSame([
+ 'name_converted[after]',
+ 'name_converted[before]',
+ 'name_converted[strictly_after]',
+ 'name_converted[strictly_before]',
+ ], $variables);
+ foreach ($data['hydra:search']['hydra:mapping'] as $mapping) {
+ $this->assertSame('IriTemplateMapping', $mapping['@type']);
+ $this->assertSame('name_converted', $mapping['property']);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function relatedDummyClass(): string
+ {
+ return $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyDateClass(): string
+ {
+ return $this->isMongoDB() ? DummyDateDocument::class : DummyDate::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummiesWithDate(string $resource, int $nb): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ if ($nb !== $i) {
+ $dummy->setDummyDate(new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')));
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $resource
+ * @param class-string $relatedResource
+ */
+ private function createDummiesWithDateAndRelatedDummy(string $resource, string $relatedResource, int $nb): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $relatedDummy = new $relatedResource();
+ $relatedDummy->setName('RelatedDummy #'.$i);
+ $relatedDummy->setDummyDate($date);
+
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setRelatedDummy($relatedDummy);
+ if ($nb !== $i) {
+ $dummy->setDummyDate($date);
+ }
+ $manager->persist($relatedDummy);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummyDates(string $resource, int $nb, ?string $nullableProperty = null): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $date = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $dummy = new $resource();
+ $dummy->dummyDate = $date;
+ if ($nullableProperty) {
+ $dummy->{$nullableProperty} = 0 === $i % 3 ? null : $date;
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/EagerLoadingTest.php b/tests/Functional/Doctrine/EagerLoadingTest.php
new file mode 100644
index 00000000000..d6f7168b8a0
--- /dev/null
+++ b/tests/Functional/Doctrine/EagerLoadingTest.php
@@ -0,0 +1,300 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Doctrine\Orm\EntityManager;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyPassenger;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class EagerLoadingTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ DummyFriend::class,
+ RelatedToDummyFriend::class,
+ DummyTravel::class,
+ DummyCar::class,
+ DummyPassenger::class,
+ ThirdLevel::class,
+ FourthLevel::class,
+ ];
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Eager loading is ORM only.');
+ }
+ }
+
+ public function testEagerLoadingForARelation(): void
+ {
+ $this->recreateSchema([
+ Dummy::class, RelatedDummy::class, DummyFriend::class, RelatedToDummyFriend::class,
+ ThirdLevel::class, FourthLevel::class,
+ ]);
+ $this->createRelatedDummyWithFriends(2);
+
+ self::createClient()->request('GET', '/related_dummies/1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertDqlEquals(<<<'DQL'
+SELECT o, thirdLevel_a1, relatedToDummyFriend_a3, fourthLevel_a2, dummyFriend_a4
+FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
+ LEFT JOIN o.thirdLevel thirdLevel_a1
+ LEFT JOIN thirdLevel_a1.fourthLevel fourthLevel_a2
+ LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a3
+ LEFT JOIN relatedToDummyFriend_a3.dummyFriend dummyFriend_a4
+WHERE o.id = :id_p1
+DQL);
+ }
+
+ public function testEagerLoadingForTheSearchFilter(): void
+ {
+ $this->recreateSchema([
+ Dummy::class, RelatedDummy::class, ThirdLevel::class, FourthLevel::class,
+ ]);
+ $this->createDummyWithFourthLevelRelation();
+
+ self::createClient()->request('GET', '/dummies?relatedDummy.thirdLevel.level=3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertDqlEquals(<<<'DQL'
+SELECT o
+FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy o
+ INNER JOIN o.relatedDummy relatedDummy_a1
+ INNER JOIN relatedDummy_a1.thirdLevel thirdLevel_a2
+WHERE o IN(
+ SELECT o_a3
+ FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy o_a3
+ INNER JOIN o_a3.relatedDummy relatedDummy_a4
+ INNER JOIN relatedDummy_a4.thirdLevel thirdLevel_a5
+ WHERE thirdLevel_a5.level = :level_p1
+ )
+ORDER BY o.id ASC
+DQL);
+ }
+
+ public function testEagerLoadingForARelationAndSearchFilter(): void
+ {
+ $this->recreateSchema([
+ Dummy::class, RelatedDummy::class, DummyFriend::class, RelatedToDummyFriend::class,
+ ThirdLevel::class, FourthLevel::class,
+ ]);
+ $this->createRelatedDummyWithFriends(2);
+
+ self::createClient()->request('GET', '/related_dummies?relatedToDummyFriend.dummyFriend=2', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertDqlEquals(<<<'DQL'
+SELECT o, thirdLevel_a4, relatedToDummyFriend_a1, fourthLevel_a5, dummyFriend_a6
+FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
+ INNER JOIN o.relatedToDummyFriend relatedToDummyFriend_a1
+ LEFT JOIN o.thirdLevel thirdLevel_a4
+ LEFT JOIN thirdLevel_a4.fourthLevel fourthLevel_a5
+ INNER JOIN relatedToDummyFriend_a1.dummyFriend dummyFriend_a6
+WHERE o IN(
+ SELECT o_a2
+ FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o_a2
+ INNER JOIN o_a2.relatedToDummyFriend relatedToDummyFriend_a3
+ WHERE relatedToDummyFriend_a3.dummyFriend = :dummyFriend_p1
+ )
+ORDER BY o.id ASC
+DQL);
+ }
+
+ public function testEagerLoadingForARelationAndPropertyFilterWithMultipleRelations(): void
+ {
+ $this->recreateSchema([
+ DummyTravel::class, DummyCar::class, DummyPassenger::class,
+ ]);
+ $this->createDummyTravel();
+
+ $response = self::createClient()->request(
+ 'GET',
+ '/dummy_travels/1?properties[]=confirmed&properties[car][]=brand&properties[passenger][]=nickname',
+ ['headers' => ['Accept' => 'application/ld+json']]
+ );
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertTrue($data['confirmed']);
+ $this->assertSame('DummyBrand', $data['car']['carBrand']);
+ $this->assertSame('Tom', $data['passenger']['nickname']);
+ $this->assertDqlEquals(<<<'DQL'
+SELECT o, car_a1, passenger_a2
+FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTravel o
+ LEFT JOIN o.car car_a1
+ LEFT JOIN o.passenger passenger_a2
+WHERE o.id = :id_p1
+DQL);
+ }
+
+ public function testEagerLoadingForARelationWithComplexSubQueryFilter(): void
+ {
+ $this->recreateSchema([
+ Dummy::class, RelatedDummy::class, DummyFriend::class, RelatedToDummyFriend::class,
+ ThirdLevel::class, FourthLevel::class,
+ ]);
+ $this->createRelatedDummyWithFriends(2);
+
+ self::createClient()->request('GET', '/related_dummies?complex_sub_query_filter=1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertDqlEquals(<<<'DQL'
+SELECT o, thirdLevel_a3, relatedToDummyFriend_a5, fourthLevel_a4, dummyFriend_a6
+FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy o
+ LEFT JOIN o.thirdLevel thirdLevel_a3
+ LEFT JOIN thirdLevel_a3.fourthLevel fourthLevel_a4
+ LEFT JOIN o.relatedToDummyFriend relatedToDummyFriend_a5
+ LEFT JOIN relatedToDummyFriend_a5.dummyFriend dummyFriend_a6
+WHERE o.id IN (
+ SELECT related_dummy_a1.id
+ FROM ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy related_dummy_a1
+ INNER JOIN related_dummy_a1.relatedToDummyFriend related_to_dummy_friend_a2
+ WITH related_to_dummy_friend_a2.name = :name_p1
+ )
+ORDER BY o.id ASC
+DQL);
+ }
+
+ private function createRelatedDummyWithFriends(int $nb): void
+ {
+ $manager = $this->getManager();
+
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setName('RelatedDummy with friends');
+ $manager->persist($relatedDummy);
+ $manager->flush();
+
+ for ($i = 1; $i <= $nb; ++$i) {
+ $friend = new DummyFriend();
+ $friend->setName('Friend-'.$i);
+ $manager->persist($friend);
+ $manager->flush();
+
+ $relation = new RelatedToDummyFriend();
+ $relation->setName('Relation-'.$i);
+ $relation->setDummyFriend($friend);
+ $relation->setRelatedDummy($relatedDummy);
+ $relatedDummy->addRelatedToDummyFriend($relation);
+
+ $manager->persist($relation);
+ }
+
+ $relatedDummy2 = new RelatedDummy();
+ $relatedDummy2->setName('RelatedDummy without friends');
+ $manager->persist($relatedDummy2);
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function createDummyWithFourthLevelRelation(): void
+ {
+ $manager = $this->getManager();
+
+ $fourthLevel = new FourthLevel();
+ $fourthLevel->setLevel(4);
+ $manager->persist($fourthLevel);
+
+ $thirdLevel = new ThirdLevel();
+ $thirdLevel->setLevel(3);
+ $thirdLevel->setFourthLevel($fourthLevel);
+ $manager->persist($thirdLevel);
+
+ $namedRelatedDummy = new RelatedDummy();
+ $namedRelatedDummy->setName('Hello');
+ $namedRelatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($namedRelatedDummy);
+
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($relatedDummy);
+
+ $dummy = new Dummy();
+ $dummy->setName('Dummy with relations');
+ $dummy->setRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($relatedDummy);
+ $manager->persist($dummy);
+
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function createDummyTravel(): void
+ {
+ $manager = $this->getManager();
+
+ $car = new DummyCar();
+ $car->setName('model x');
+ $car->setCanSell(true);
+ $car->setAvailableAt(new \DateTime());
+ $manager->persist($car);
+
+ $passenger = new DummyPassenger();
+ $passenger->nickname = 'Tom';
+ $manager->persist($passenger);
+
+ $travel = new DummyTravel();
+ $travel->car = $car;
+ $travel->passenger = $passenger;
+ $travel->confirmed = true;
+ $manager->persist($travel);
+
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function assertDqlEquals(string $expected): void
+ {
+ $actual = EntityManager::$dql;
+ $expected = preg_replace('/\(\R */', '(', $expected);
+ $expected = preg_replace('/\R *\)/', ')', $expected);
+ $expected = preg_replace('/\R */', ' ', $expected);
+
+ $this->assertSame($expected, $actual);
+ }
+}
diff --git a/tests/Functional/Doctrine/ExistsFilterTest.php b/tests/Functional/Doctrine/ExistsFilterTest.php
new file mode 100644
index 00000000000..04bdb592cb9
--- /dev/null
+++ b/tests/Functional/Doctrine/ExistsFilterTest.php
@@ -0,0 +1,201 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedString as ConvertedStringDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedString;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class ExistsFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ ConvertedString::class,
+ ];
+ }
+
+ public function testCollectionWhereScalarPropertyDoesNotExist(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithBoolean($resource, 15, true);
+
+ $response = self::createClient()->request('GET', '/dummies?exists[dummyBoolean]=0', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $data = $response->toArray();
+ $this->assertSame(0, $data['hydra:totalItems']);
+ $this->assertSame([], $data['hydra:member']);
+ }
+
+ public function testCollectionWhereScalarPropertyDoesExist(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithBoolean($resource, 15, true);
+
+ $response = self::createClient()->request('GET', '/dummies?exists[dummyBoolean]=1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(15, $data['hydra:totalItems']);
+ $this->assertCount(3, $data['hydra:member']);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertMatchesRegularExpression('#^/dummies/(1|2|3)$#', $member['@id']);
+ }
+ }
+
+ public function testCollectionWithEmptyRelationCollection(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource, $this->relatedDummyClass()]);
+ $this->createDummiesWithRelated($resource, 3, 0);
+ $this->createDummiesWithRelated($resource, 2, 3);
+
+ $response = self::createClient()->request('GET', '/dummies?exists[relatedDummies]=0', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(3, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/dummies/1', '/dummies/2', '/dummies/3'], $ids);
+ }
+
+ public function testCollectionWithNonEmptyRelationCollection(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource, $this->relatedDummyClass()]);
+ $this->createDummiesWithRelated($resource, 3, 0);
+ $this->createDummiesWithRelated($resource, 2, 3);
+
+ $response = self::createClient()->request('GET', '/dummies?exists[relatedDummies]=1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/dummies/4', '/dummies/5'], $ids);
+ }
+
+ public function testCollectionFilteredUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedStringDocument::class : ConvertedString::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 4; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = ($i % 2) ? "name#$i" : null;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_strings?exists[name_converted]=true', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/converted_strings/1', '/converted_strings/3'], $ids);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedString', $member['@type']);
+ $this->assertMatchesRegularExpression('/^name#(1|3)$/', $member['name_converted']);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function relatedDummyClass(): string
+ {
+ return $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummiesWithBoolean(string $resource, int $nb, bool $bool): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDummyBoolean($bool);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummiesWithRelated(string $resource, int $nb, int $nbRelated): void
+ {
+ $manager = $this->getManager();
+ $relatedDummyClass = $this->relatedDummyClass();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ for ($j = 1; $j <= $nbRelated; ++$j) {
+ $relatedDummy = new $relatedDummyClass();
+ $relatedDummy->setName('RelatedDummy'.$j.$i);
+ $relatedDummy->setAge((int) ($j.$i));
+ $manager->persist($relatedDummy);
+ $dummy->addRelatedDummy($relatedDummy);
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/LinkHandlerTest.php b/tests/Functional/Doctrine/LinkHandlerTest.php
new file mode 100644
index 00000000000..8f52ba454da
--- /dev/null
+++ b/tests/Functional/Doctrine/LinkHandlerTest.php
@@ -0,0 +1,74 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\LinkHandledDummy as LinkHandledDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\LinkHandledDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class LinkHandlerTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [LinkHandledDummy::class];
+ }
+
+ public function testGetCollectionFiltersBySlugViaLinksHandler(): void
+ {
+ $resource = $this->isMongoDB() ? LinkHandledDummyDocument::class : LinkHandledDummy::class;
+ $this->recreateSchema([$resource]);
+
+ $manager = $this->getManager();
+ foreach (['foo', 'bar', 'baz', 'foz'] as $slug) {
+ $manager->persist(new $resource($slug));
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/link_handled_dummies', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(1, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testGetItemReturnsSlug(): void
+ {
+ $resource = $this->isMongoDB() ? LinkHandledDummyDocument::class : LinkHandledDummy::class;
+ $this->recreateSchema([$resource]);
+
+ $manager = $this->getManager();
+ foreach (['foo', 'bar', 'baz', 'foz'] as $slug) {
+ $manager->persist(new $resource($slug));
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/link_handled_dummies/1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('foo', $response->toArray()['slug']);
+ }
+}
diff --git a/tests/Functional/Doctrine/MappedSuperclassPutTest.php b/tests/Functional/Doctrine/MappedSuperclassPutTest.php
new file mode 100644
index 00000000000..80088c63df6
--- /dev/null
+++ b/tests/Functional/Doctrine/MappedSuperclassPutTest.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMappedSubclass;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class MappedSuperclassPutTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [DummyMappedSubclass::class];
+ }
+
+ public function testStandardPutOnEntityInheritedFromMappedSuperclass(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Not tested with mongodb.');
+ }
+
+ $this->recreateSchema([DummyMappedSubclass::class]);
+
+ $manager = $this->getManager();
+ $manager->persist(new DummyMappedSubclass());
+ $manager->flush();
+
+ $response = self::createClient()->request('PUT', '/dummy_mapped_subclasses/1', [
+ 'headers' => ['Content-Type' => 'application/ld+json'],
+ 'json' => ['foo' => 'updated value'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertJsonContains([
+ '@context' => '/contexts/DummyMappedSubclass',
+ '@id' => '/dummy_mapped_subclasses/1',
+ '@type' => 'DummyMappedSubclass',
+ 'id' => 1,
+ 'foo' => 'updated value',
+ ]);
+ }
+}
diff --git a/tests/Functional/Doctrine/MultipleFilterTest.php b/tests/Functional/Doctrine/MultipleFilterTest.php
new file mode 100644
index 00000000000..787615cebf3
--- /dev/null
+++ b/tests/Functional/Doctrine/MultipleFilterTest.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class MultipleFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Dummy::class];
+ }
+
+ public function testCollectionFilteredByDateAndBoolean(): void
+ {
+ $resource = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30, true);
+ $this->createDummies($resource, 20, false);
+
+ $response = self::createClient()->request('GET', '/dummies?dummyDate[after]=2015-04-28&dummyBoolean=1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+
+ $data = $response->toArray();
+ $this->assertSame('/contexts/Dummy', $data['@context']);
+ $this->assertSame('/dummies', $data['@id']);
+ $this->assertSame('hydra:Collection', $data['@type']);
+ $this->assertCount(2, $data['hydra:member']);
+
+ $ids = array_map(static fn (array $item): string => $item['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/dummies/28', '/dummies/29'], $ids);
+
+ $this->assertSame('hydra:PartialCollectionView', $data['hydra:view']['@type']);
+ $this->assertSame('/dummies?dummyBoolean=1&dummyDate%5Bafter%5D=2015-04-28', $data['hydra:view']['@id']);
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummies(string $resource, int $nb, bool $bool): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->setDummyBoolean($bool);
+
+ if ($nb !== $i) {
+ $dummy->setDummyDate(new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')));
+ }
+
+ $manager->persist($dummy);
+ }
+
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/NumericFilterTest.php b/tests/Functional/Doctrine/NumericFilterTest.php
new file mode 100644
index 00000000000..b120b28ea68
--- /dev/null
+++ b/tests/Functional/Doctrine/NumericFilterTest.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedInteger as ConvertedIntegerDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedInteger;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class NumericFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Dummy::class, ConvertedInteger::class];
+ }
+
+ public function testCollectionByDummyPrice(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithPrice($resource, 10);
+
+ $response = self::createClient()->request('GET', '/dummies?dummyPrice=9.99', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(3, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/dummies/1', '/dummies/5', '/dummies/9'], $ids);
+ }
+
+ public function testCollectionByMultipleDummyPrice(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithPrice($resource, 10);
+
+ $response = self::createClient()->request('GET', '/dummies?dummyPrice[]=9.99&dummyPrice[]=12.99', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(6, $data['hydra:totalItems']);
+ $this->assertCount(3, $data['hydra:member']);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertMatchesRegularExpression('#^/dummies/(1|2|5|6|9|10)$#', $member['@id']);
+ }
+ }
+
+ public function testCollectionByNonNumericDummyPriceIsIgnored(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithPrice($resource, 10);
+ $this->createDummiesWithPrice($resource, 10);
+
+ $response = self::createClient()->request('GET', '/dummies?dummyPrice=marty', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(20, $data['hydra:totalItems']);
+ }
+
+ public function testCollectionFilteredUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedIntegerDocument::class : ConvertedInteger::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 5; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = $i;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_integers?name_converted[]=2&name_converted[]=3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/converted_integers/2', '/converted_integers/3'], $ids);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedInteger', $member['@type']);
+ $this->assertIsInt($member['name_converted']);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummiesWithPrice(string $resource, int $nb): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $prices = ['9.99', '12.99', '15.99', '19.99'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->setDummyPrice($prices[($i - 1) % 4]);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/OrderFilterTest.php b/tests/Functional/Doctrine/OrderFilterTest.php
new file mode 100644
index 00000000000..a4f6c2473c8
--- /dev/null
+++ b/tests/Functional/Doctrine/OrderFilterTest.php
@@ -0,0 +1,314 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedInteger as ConvertedIntegerDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedInteger;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use PHPUnit\Framework\Attributes\TestWith;
+
+final class OrderFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ EmbeddedDummy::class,
+ ConvertedInteger::class,
+ ];
+ }
+
+ #[TestWith(['order[id]=asc', ['/dummies/1', '/dummies/2', '/dummies/3']])]
+ #[TestWith(['order[id]=desc', ['/dummies/30', '/dummies/29', '/dummies/28']])]
+ #[TestWith(['order[name]=asc', ['/dummies/1', '/dummies/10', '/dummies/11']])]
+ #[TestWith(['order[name]=desc', ['/dummies/9', '/dummies/8', '/dummies/7']])]
+ public function testOrderDummies(string $query, array $expectedIds): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?'.$query, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame($expectedIds, array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']));
+ }
+
+ public function testOrderByMultipleProperties(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?order[name]=desc&order[id]=desc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/39', '/dummies/9', '/dummies/38'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testOrderByAssociation(): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesWithRelatedDummy($resource, $relatedResource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?order[relatedDummy]=asc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testOrderByEmbedded(): void
+ {
+ $embeddedClass = $this->isMongoDB() ? EmbeddedDummyDocument::class : EmbeddedDummy::class;
+ $embeddableClass = $this->isMongoDB() ? EmbeddableDummyDocument::class : EmbeddableDummy::class;
+ $this->recreateSchema([$embeddedClass]);
+
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 30; ++$i) {
+ $embeddable = new $embeddableClass();
+ $embeddable->setDummyName('EmbeddedDummy #'.$i);
+ $dummy = new $embeddedClass();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setEmbeddedDummy($embeddable);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/embedded_dummies?order[embeddedDummy]=asc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/embedded_dummies/1', '/embedded_dummies/2', '/embedded_dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testOrderByEmbeddedStringWithoutValueReturns422(): void
+ {
+ $resource = $this->isMongoDB() ? EmbeddedDummyDocument::class : EmbeddedDummy::class;
+ $this->recreateSchema([$resource]);
+
+ self::createClient()->request('GET', '/embedded_dummies?order[embeddedDummy.dummyName]', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseStatusCodeSame(422);
+ }
+
+ #[TestWith(['order[alias]=asc'])]
+ #[TestWith(['order[alias]=desc'])]
+ #[TestWith(['order[unknown]=asc'])]
+ #[TestWith(['order[unknown]=desc'])]
+ public function testOrderByUnsupportedProperty(string $query): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?'.$query, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testOrderByRelatedProperty(): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesWithRelatedDummy($resource, $relatedResource, 2);
+
+ $response = self::createClient()->request('GET', '/dummies?order[relatedDummy.name]=desc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/2', '/dummies/1'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testOrderUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedIntegerDocument::class : ConvertedInteger::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 3; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = $i;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_integers?order[name_converted]=desc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/converted_integers/3', '/converted_integers/2', '/converted_integers/1'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedInteger', $member['@type']);
+ $this->assertIsInt($member['name_converted']);
+ }
+
+ $this->assertSame('hydra:IriTemplate', $data['hydra:search']['@type']);
+ $this->assertSame('BasicRepresentation', $data['hydra:search']['hydra:variableRepresentation']);
+ $this->assertStringMatchesFormat('/converted_integers{?%a}', $data['hydra:search']['hydra:template']);
+ $variables = array_map(static fn (array $m): string => $m['variable'], $data['hydra:search']['hydra:mapping']);
+ sort($variables);
+ $this->assertSame([
+ 'name_converted',
+ 'name_converted[]',
+ 'name_converted[between]',
+ 'name_converted[gt]',
+ 'name_converted[gte]',
+ 'name_converted[lt]',
+ 'name_converted[lte]',
+ 'order[name_converted]',
+ ], $variables);
+ foreach ($data['hydra:search']['hydra:mapping'] as $mapping) {
+ $this->assertSame('IriTemplateMapping', $mapping['@type']);
+ $this->assertSame('name_converted', $mapping['property']);
+ }
+ }
+
+ public function testOrderListSyntaxIsAccepted(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedIntegerDocument::class : ConvertedInteger::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 3; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = $i;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ self::createClient()->request('GET', '/converted_integers?order[]=desc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function relatedDummyClass(): string
+ {
+ return $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummies(string $resource, int $nb): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDummy('SomeDummyTest'.$i);
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->nameConverted = 'Converted '.$i;
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $resource
+ * @param class-string $relatedResource
+ */
+ private function createDummiesWithRelatedDummy(string $resource, string $relatedResource, int $nb): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $relatedDummy = new $relatedResource();
+ $relatedDummy->setName('RelatedDummy #'.$i);
+
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->nameConverted = "Converted $i";
+ $dummy->setRelatedDummy($relatedDummy);
+
+ $manager->persist($relatedDummy);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/RangeFilterTest.php b/tests/Functional/Doctrine/RangeFilterTest.php
new file mode 100644
index 00000000000..d8da6fda3ee
--- /dev/null
+++ b/tests/Functional/Doctrine/RangeFilterTest.php
@@ -0,0 +1,123 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedInteger as ConvertedIntegerDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedInteger;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use PHPUnit\Framework\Attributes\TestWith;
+
+final class RangeFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Dummy::class, ConvertedInteger::class];
+ }
+
+ protected function setUp(): void
+ {
+ parent::setUp();
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummiesWithPrice($resource, 30);
+ }
+
+ #[TestWith(['dummyPrice[between]=12.99..15.99', 15])]
+ #[TestWith(['dummyPrice[between]=12.99..12.99', 8])]
+ #[TestWith(['dummyPrice[between]=9.99..12.99..15.99', 30])]
+ #[TestWith(['dummyPrice[lt]=12.99', 8])]
+ #[TestWith(['dummyPrice[lte]=12.99', 16])]
+ #[TestWith(['dummyPrice[gt]=15.99', 7])]
+ #[TestWith(['dummyPrice[gte]=15.99', 14])]
+ #[TestWith(['dummyPrice[gt]=12.99&dummyPrice[lt]=19.99', 7])]
+ #[TestWith(['dummyPrice[gt]=19.99', 0])]
+ public function testRangeFilter(string $query, int $expectedTotal): void
+ {
+ $response = self::createClient()->request('GET', '/dummies?'.$query, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $data = $response->toArray();
+ $this->assertSame($expectedTotal, $data['hydra:totalItems']);
+ }
+
+ public function testCollectionFilteredUsingNameConverter(): void
+ {
+ $resource = $this->isMongoDB() ? ConvertedIntegerDocument::class : ConvertedInteger::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 5; ++$i) {
+ $entity = new $resource();
+ $entity->nameConverted = $i;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_integers?name_converted[lte]=2', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(2, $data['hydra:totalItems']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids);
+ $this->assertSame(['/converted_integers/1', '/converted_integers/2'], $ids);
+ foreach ($data['hydra:member'] as $member) {
+ $this->assertSame('ConvertedInteger', $member['@type']);
+ $this->assertIsInt($member['name_converted']);
+ }
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummiesWithPrice(string $resource, int $nb): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $prices = ['9.99', '12.99', '15.99', '19.99'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->setDummyPrice($prices[($i - 1) % 4]);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/SearchFilterTest.php b/tests/Functional/Doctrine/SearchFilterTest.php
new file mode 100644
index 00000000000..a5a42fc3231
--- /dev/null
+++ b/tests/Functional/Doctrine/SearchFilterTest.php
@@ -0,0 +1,802 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5605\MainResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5605\SubResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5648\DummyResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedOwner as ConvertedOwnerDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedRelated as ConvertedRelatedDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDate as DummyDateDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddableDummy as EmbeddableDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\EmbeddedDummy as EmbeddedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\FourthLevel as FourthLevelDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedOwner;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedRelated;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDate;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummySubEntity;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyWithSubEntity;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Group;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5735\Issue5735User;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\Common\Collections\ArrayCollection;
+use Symfony\Component\Uid\Uuid as SymfonyUuid;
+
+final class SearchFilterTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ DummyFriend::class,
+ RelatedToDummyFriend::class,
+ EmbeddedDummy::class,
+ ThirdLevel::class,
+ FourthLevel::class,
+ DummyCar::class,
+ DummyCarColor::class,
+ DummyDate::class,
+ ConvertedOwner::class,
+ ConvertedRelated::class,
+ DummyResource::class,
+ MainResource::class,
+ SubResource::class,
+ DummyWithSubEntity::class,
+ DummySubEntity::class,
+ Group::class,
+ Issue5735User::class,
+ ];
+ }
+
+ public function testManyToManyWithFilterOnJoinTable(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('HAL relation filter requires ORM join table.');
+ }
+
+ $this->recreateSchema([
+ RelatedDummy::class, DummyFriend::class, RelatedToDummyFriend::class,
+ ThirdLevel::class, FourthLevel::class,
+ ]);
+ $this->createRelatedDummyWithFriends(4);
+
+ $response = self::createClient()->request('GET', '/related_dummies?relatedToDummyFriend.dummyFriend=/dummy_friends/4', [
+ 'headers' => ['Accept' => 'application/hal+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(1, $data['_embedded']['item']);
+ $this->assertSame(1, $data['_embedded']['item'][0]['id']);
+ $this->assertCount(4, $data['_embedded']['item'][0]['_links']['relatedToDummyFriend']);
+ $this->assertCount(4, $data['_embedded']['item'][0]['_embedded']['relatedToDummyFriend']);
+ }
+
+ public function testSearchManyToManyWithRelatedEntity(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('DummyCar/Color is ORM only in this scenario.');
+ }
+ $this->recreateSchema([DummyCar::class, DummyCarColor::class]);
+ $this->createDummyCarWithColors();
+
+ $response = self::createClient()->request('GET', '/dummy_cars?colors.prop=red', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(1, $data['hydra:totalItems']);
+ $this->assertSame('/dummy_cars/1', $data['hydra:member'][0]['@id']);
+ $this->assertCount(2, $data['hydra:member'][0]['colors']);
+ $this->assertSame('red', $data['hydra:member'][0]['colors'][0]['prop']);
+ $this->assertSame('blue', $data['hydra:member'][0]['colors'][1]['prop']);
+ }
+
+ public function testSearchByNamePartial(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?name=my', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testSearchEmbeddedByName(): void
+ {
+ $embeddedClass = $this->isMongoDB() ? EmbeddedDummyDocument::class : EmbeddedDummy::class;
+ $embeddableClass = $this->isMongoDB() ? EmbeddableDummyDocument::class : EmbeddableDummy::class;
+ $this->recreateSchema([$embeddedClass]);
+ $this->createEmbeddedDummies($embeddedClass, $embeddableClass, 30);
+
+ $response = self::createClient()->request('GET', '/embedded_dummies?embeddedDummy.dummyName=my', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/embedded_dummies/1', '/embedded_dummies/2', '/embedded_dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testSearchByNameMultipleValues(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?name[]=2&name[]=3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids, \SORT_NATURAL);
+ $this->assertSame(['/dummies/2', '/dummies/3', '/dummies/12'], $ids);
+ }
+
+ public function testSearchByDummyCaseInsensitive(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?dummy=somedummytest1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ foreach ($response->toArray()['hydra:member'] as $member) {
+ $this->assertMatchesRegularExpression('/^SomeDummyTest\d{1,2}$/', $member['dummy']);
+ }
+ }
+
+ public function testSearchByAliasStart(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?alias=Ali', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(3, $response->toArray()['hydra:member']);
+ }
+
+ public function testSearchByDescriptionMultipleStart(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?description[]=Sma&description[]=Not', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(3, $response->toArray()['hydra:member']);
+ }
+
+ public function testSearchByDescriptionWordStartSqlite(): void
+ {
+ if (!$this->isSqlite()) {
+ $this->markTestSkipped('SQLite-specific: case-insensitive default LIKE.');
+ }
+
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?description=smart', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testSearchByDescriptionWordStartMultipleSqlite(): void
+ {
+ if (!$this->isSqlite()) {
+ $this->markTestSkipped('SQLite-specific: case-insensitive default LIKE.');
+ }
+
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?description[]=smart&description[]=so', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $data['hydra:member'])
+ );
+ }
+
+ public function testSearchByDescriptionWordStartPostgres(): void
+ {
+ if (!$this->isPostgres()) {
+ $this->markTestSkipped('Postgres-specific: case-sensitive default LIKE.');
+ }
+
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?description=smart', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids, \SORT_NATURAL);
+ $this->assertSame(['/dummies/2', '/dummies/4', '/dummies/6'], $ids);
+ }
+
+ public function testSearchEmptyResult(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?name=MuYm', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([], $response->toArray()['hydra:member']);
+ }
+
+ public function testSearchByExistingCollectionRouteNameSqlite(): void
+ {
+ if (!$this->isSqlite()) {
+ $this->markTestSkipped('SQLite-specific.');
+ }
+
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummies=dummy_cars', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertIsArray($response->toArray()['hydra:member']);
+ }
+
+ public function testSearchRelatedCollectionByName(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('HAL relation filter requires ORM join table.');
+ }
+
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesEachWithRelatedDummies($resource, $relatedResource, 3, 3);
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummies.name=RelatedDummy1', [
+ 'headers' => ['Accept' => 'application/hal+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(3, $data['_embedded']['item']);
+ foreach ($data['_embedded']['item'] as $item) {
+ $this->assertCount(3, $item['_links']['relatedDummies']);
+ }
+ }
+
+ public function testSearchByRelatedCollectionId(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('HAL relation filter requires ORM join table.');
+ }
+
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource]);
+ $this->createDummiesEachWithRelatedDummies($resource, $relatedResource, 2, 2);
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummies=3', [
+ 'headers' => ['Accept' => 'application/hal+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(1, $data['totalItems']);
+ $this->assertCount(1, $data['_links']['item']);
+ $this->assertSame('/dummies/2', $data['_links']['item'][0]['href']);
+ }
+
+ public function testCollectionByIdNonInteger(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?id=9.99', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ ['/dummies/1', '/dummies/2', '/dummies/3'],
+ array_map(static fn (array $i): string => $i['@id'], $response->toArray()['hydra:member'])
+ );
+ }
+
+ public function testCollectionById(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?id=10', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(1, $data['hydra:member']);
+ $this->assertSame('/dummies/10', $data['hydra:member'][0]['@id']);
+ }
+
+ public function testCollectionFilteredByUnknownProperty(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?unknown=0', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(3, $response->toArray()['hydra:member']);
+
+ $response = self::createClient()->request('GET', '/dummies?unknown=1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(3, $response->toArray()['hydra:member']);
+ }
+
+ public function testSearchAtThirdLevel(): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource, $this->thirdLevelClass(), $this->fourthLevelClass()]);
+ $this->createDummiesEachWithRelatedDummies($resource, $relatedResource, 30, 0);
+ $this->createDummyWithFourthLevelRelation();
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummy.thirdLevel.level=3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(['/dummies/31'], array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']));
+ }
+
+ public function testSearchAtFourthLevel(): void
+ {
+ $resource = $this->dummyClass();
+ $relatedResource = $this->relatedDummyClass();
+ $this->recreateSchema([$resource, $relatedResource, $this->thirdLevelClass(), $this->fourthLevelClass()]);
+ $this->createDummiesEachWithRelatedDummies($resource, $relatedResource, 30, 0);
+ $this->createDummyWithFourthLevelRelation();
+
+ $response = self::createClient()->request('GET', '/dummies?relatedDummy.thirdLevel.fourthLevel.level=4', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertSame(['/dummies/31'], array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']));
+ }
+
+ public function testSearchUsingNameConverter(): void
+ {
+ $resource = $this->dummyClass();
+ $this->recreateSchema([$resource]);
+ $this->createDummies($resource, 30);
+
+ $response = self::createClient()->request('GET', '/dummies?name_converted=Converted 3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(2, $data['hydra:member']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids, \SORT_NATURAL);
+ $this->assertSame(['/dummies/3', '/dummies/30'], $ids);
+ }
+
+ public function testSearchUsingNestedNameConverter(): void
+ {
+ $ownerClass = $this->isMongoDB() ? ConvertedOwnerDocument::class : ConvertedOwner::class;
+ $relatedClass = $this->isMongoDB() ? ConvertedRelatedDocument::class : ConvertedRelated::class;
+ $this->recreateSchema([$ownerClass, $relatedClass]);
+
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 30; ++$i) {
+ $related = new $relatedClass();
+ $related->nameConverted = 'Converted '.$i;
+ $owner = new $ownerClass();
+ $owner->nameConverted = $related;
+ $manager->persist($related);
+ $manager->persist($owner);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/converted_owners?name_converted.name_converted=Converted 3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertCount(2, $data['hydra:member']);
+ $ids = array_map(static fn (array $i): string => $i['@id'], $data['hydra:member']);
+ sort($ids, \SORT_NATURAL);
+ $this->assertSame(['/converted_owners/3', '/converted_owners/30'], $ids);
+ }
+
+ public function testSearchByDate(): void
+ {
+ $resource = $this->isMongoDB() ? DummyDateDocument::class : DummyDate::class;
+ $this->recreateSchema([$resource]);
+ $manager = $this->getManager();
+ for ($i = 1; $i <= 3; ++$i) {
+ $dummy = new $resource();
+ $dummy->dummyDate = new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC'));
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/dummy_dates?dummyDate=2015-04-01', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(1, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testCustomSearchFilterUsingDoctrineExpressions(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Custom Doctrine expression filter is ORM only.');
+ }
+
+ $this->recreateSchema([Dummy::class, RelatedDummy::class, ThirdLevel::class, FourthLevel::class]);
+ $this->createDummyWithRelatedDummiesAndThirdLevel(3);
+
+ $response = self::createClient()->request('GET', '/dummy_resource_with_custom_filter?custom=3', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(1, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testSearchOnSubEntityWithStringIdentifier(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('DummySubEntity is ORM only.');
+ }
+
+ $this->recreateSchema([DummyWithSubEntity::class, DummySubEntity::class]);
+ $manager = $this->getManager();
+ $subEntity = new DummySubEntity('stringId', 'someName');
+ $mainEntity = new DummyWithSubEntity();
+ $mainEntity->setSubEntity($subEntity);
+ $mainEntity->setName('main');
+ $manager->persist($subEntity);
+ $manager->persist($mainEntity);
+ $manager->flush();
+
+ $response = self::createClient()->request('GET', '/dummy_with_subresource?subEntity=/dummy_subresource/stringId', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(1, $response->toArray()['hydra:totalItems']);
+ }
+
+ public function testFiltersCanUseUuids(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Issue5735 fixture is ORM only.');
+ }
+
+ $this->recreateSchema([Group::class, Issue5735User::class]);
+ $manager = $this->getManager();
+
+ $group1 = new Group();
+ $group1->setUuid(SymfonyUuid::fromString('61817181-0ecc-42fb-a6e7-d97f2ddcb344'));
+ $manager->persist($group1);
+ for ($i = 0; $i < 2; ++$i) {
+ $user = new Issue5735User();
+ $user->addGroup($group1);
+ $manager->persist($user);
+ }
+ $manager->persist(new Issue5735User());
+
+ $group2 = new Group();
+ $group2->setUuid(SymfonyUuid::fromString('32510d53-f737-4e70-8d9d-58e292c871f8'));
+ $manager->persist($group2);
+ $user = new Issue5735User();
+ $user->addGroup($group2);
+ $manager->persist($user);
+ $manager->persist(new Issue5735User());
+
+ $manager->flush();
+
+ $response = self::createClient()->request(
+ 'GET',
+ '/issue5735/issue5735_users?groups[]=/issue5735/groups/61817181-0ecc-42fb-a6e7-d97f2ddcb344&groups[]=/issue5735/groups/32510d53-f737-4e70-8d9d-58e292c871f8',
+ ['headers' => ['Accept' => 'application/ld+json']]
+ );
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(3, $response->toArray()['hydra:totalItems']);
+ }
+
+ /**
+ * @return class-string
+ */
+ private function dummyClass(): string
+ {
+ return $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function relatedDummyClass(): string
+ {
+ return $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function thirdLevelClass(): string
+ {
+ return $this->isMongoDB() ? ThirdLevelDocument::class : ThirdLevel::class;
+ }
+
+ /**
+ * @return class-string
+ */
+ private function fourthLevelClass(): string
+ {
+ return $this->isMongoDB() ? FourthLevelDocument::class : FourthLevel::class;
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createDummies(string $resource, int $nb): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ $dummy->setDummy('SomeDummyTest'.$i);
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->nameConverted = 'Converted '.$i;
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $embeddedClass
+ * @param class-string $embeddableClass
+ */
+ private function createEmbeddedDummies(string $embeddedClass, string $embeddableClass, int $nb): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $embeddedClass();
+ $dummy->setName('Dummy #'.$i);
+ $embeddable = new $embeddableClass();
+ $embeddable->setDummyName('Dummy #'.$i);
+ $dummy->setEmbeddedDummy($embeddable);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ /**
+ * @param class-string $resource
+ * @param class-string $relatedResource
+ */
+ private function createDummiesEachWithRelatedDummies(string $resource, string $relatedResource, int $nb, int $nbRelated): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $dummy = new $resource();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($nb - $i));
+ for ($j = 1; $j <= $nbRelated; ++$j) {
+ $relatedDummy = new $relatedResource();
+ $relatedDummy->setName('RelatedDummy'.$j.$i);
+ $relatedDummy->setAge((int) ($j.$i));
+ $manager->persist($relatedDummy);
+ $dummy->addRelatedDummy($relatedDummy);
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function createRelatedDummyWithFriends(int $nb): void
+ {
+ $manager = $this->getManager();
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setName('RelatedDummy with friends');
+ $manager->persist($relatedDummy);
+ $manager->flush();
+
+ for ($i = 1; $i <= $nb; ++$i) {
+ $friend = new DummyFriend();
+ $friend->setName('Friend-'.$i);
+ $manager->persist($friend);
+ $manager->flush();
+
+ $relation = new RelatedToDummyFriend();
+ $relation->setName('Relation-'.$i);
+ $relation->setDummyFriend($friend);
+ $relation->setRelatedDummy($relatedDummy);
+ $relatedDummy->addRelatedToDummyFriend($relation);
+ $manager->persist($relation);
+ }
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function createDummyCarWithColors(): void
+ {
+ $manager = $this->getManager();
+ $car = new DummyCar();
+ $car->setName('mustli');
+ $car->setCanSell(true);
+ $car->setAvailableAt(new \DateTime());
+ $manager->persist($car);
+ $manager->flush();
+
+ $color1 = new DummyCarColor();
+ $color1->setProp('red');
+ $color1->setCar($car);
+ $manager->persist($color1);
+
+ $color2 = new DummyCarColor();
+ $color2->setProp('blue');
+ $color2->setCar($car);
+ $manager->persist($color2);
+ $manager->flush();
+
+ $car->setColors(new ArrayCollection([$color1, $color2]));
+ $manager->persist($car);
+ $manager->flush();
+ }
+
+ private function createDummyWithFourthLevelRelation(): void
+ {
+ $manager = $this->getManager();
+
+ $fourthLevelClass = $this->fourthLevelClass();
+ $thirdLevelClass = $this->thirdLevelClass();
+ $relatedDummyClass = $this->relatedDummyClass();
+ $dummyClass = $this->dummyClass();
+
+ $fourthLevel = new $fourthLevelClass();
+ $fourthLevel->setLevel(4);
+ $manager->persist($fourthLevel);
+
+ $thirdLevel = new $thirdLevelClass();
+ $thirdLevel->setLevel(3);
+ $thirdLevel->setFourthLevel($fourthLevel);
+ $manager->persist($thirdLevel);
+
+ $namedRelatedDummy = new $relatedDummyClass();
+ $namedRelatedDummy->setName('Hello');
+ $namedRelatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($namedRelatedDummy);
+
+ $relatedDummy = new $relatedDummyClass();
+ $relatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($relatedDummy);
+
+ $dummy = new $dummyClass();
+ $dummy->setName('Dummy with relations');
+ $dummy->setRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($relatedDummy);
+ $manager->persist($dummy);
+
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function createDummyWithRelatedDummiesAndThirdLevel(int $nb): void
+ {
+ $manager = $this->getManager();
+ $dummy = new Dummy();
+ $dummy->setName('Dummy with relations');
+ for ($i = 1; $i <= $nb; ++$i) {
+ $thirdLevel = new ThirdLevel();
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setName('RelatedDummy #'.$i);
+ $relatedDummy->setThirdLevel($thirdLevel);
+ $dummy->addRelatedDummy($relatedDummy);
+ $manager->persist($thirdLevel);
+ $manager->persist($relatedDummy);
+ }
+ $manager->persist($dummy);
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/Doctrine/SeparatedResourceTest.php b/tests/Functional/Doctrine/SeparatedResourceTest.php
new file mode 100644
index 00000000000..b19cd54d433
--- /dev/null
+++ b/tests/Functional/Doctrine/SeparatedResourceTest.php
@@ -0,0 +1,146 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\Doctrine;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EntityClassAndCustomProviderResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\ResourceWithSeparatedEntity;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResourceOdm\ResourceWithSeparatedDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\SeparatedEntity as SeparatedDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SeparatedEntity;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class SeparatedResourceTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ ResourceWithSeparatedEntity::class,
+ ResourceWithSeparatedDocument::class,
+ EntityClassAndCustomProviderResource::class,
+ ];
+ }
+
+ public function testGetCollection(): void
+ {
+ $resource = $this->isMongoDB() ? SeparatedDocument::class : SeparatedEntity::class;
+ $this->recreateSchema([$resource]);
+ $this->createSeparatedEntities($resource, 5);
+
+ $uri = $this->isMongoDB() ? '/separated_documents' : '/separated_entities';
+ $shortName = $this->isMongoDB() ? 'SeparatedDocument' : 'SeparatedEntity';
+
+ $response = self::createClient()->request('GET', $uri, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $data = $response->toArray();
+ $this->assertSame('/contexts/'.$shortName, $data['@context']);
+ $this->assertStringStartsWith($uri, $data['@id']);
+ $this->assertSame('hydra:Collection', $data['@type']);
+ $this->assertIsArray($data['hydra:member']);
+ $this->assertIsInt($data['hydra:totalItems']);
+ $this->assertArrayHasKey('hydra:view', $data);
+ }
+
+ public function testGetOrderedCollection(): void
+ {
+ $resource = $this->isMongoDB() ? SeparatedDocument::class : SeparatedEntity::class;
+ $this->recreateSchema([$resource]);
+ $this->createSeparatedEntities($resource, 5);
+
+ $uri = $this->isMongoDB() ? '/separated_documents' : '/separated_entities';
+
+ $response = self::createClient()->request('GET', $uri.'?order[value]=desc', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ $this->assertSame('5', $response->toArray()['hydra:member'][0]['value']);
+ }
+
+ public function testGetItem(): void
+ {
+ $resource = $this->isMongoDB() ? SeparatedDocument::class : SeparatedEntity::class;
+ $this->recreateSchema([$resource]);
+ $this->createSeparatedEntities($resource, 5);
+
+ $uri = $this->isMongoDB() ? '/separated_documents/1' : '/separated_entities/1';
+
+ self::createClient()->request('GET', $uri, [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
+ }
+
+ public function testGetAllEntityClassAndCustomProviderResources(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('EntityClassAndCustomProviderResource uses ORM stateOptions only.');
+ }
+
+ $this->recreateSchema([SeparatedEntity::class]);
+ $this->createSeparatedEntities(SeparatedEntity::class, 1);
+
+ self::createClient()->request('GET', '/entityClassAndCustomProviderResources', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ public function testGetOneEntityClassAndCustomProviderResource(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('EntityClassAndCustomProviderResource uses ORM stateOptions only.');
+ }
+
+ $this->recreateSchema([SeparatedEntity::class]);
+ $this->createSeparatedEntities(SeparatedEntity::class, 1);
+
+ self::createClient()->request('GET', '/entityClassAndCustomProviderResources/1', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+
+ /**
+ * @param class-string $resource
+ */
+ private function createSeparatedEntities(string $resource, int $nb): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $entity = new $resource();
+ $entity->value = (string) $i;
+ $manager->persist($entity);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/EnumDenormalizationValidationTest.php b/tests/Functional/EnumDenormalizationValidationTest.php
index 8d340915433..3fa939623c8 100644
--- a/tests/Functional/EnumDenormalizationValidationTest.php
+++ b/tests/Functional/EnumDenormalizationValidationTest.php
@@ -17,6 +17,8 @@
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EnumValidationResource;
use ApiPlatform\Tests\SetupClassResourcesTrait;
use Composer\InstalledVersions;
+use Composer\Semver\VersionParser;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
* @see https://github.com/api-platform/core/issues/8183
@@ -65,8 +67,13 @@ public function testInvalidBackedEnumValueProducesValidationViolation(): void
$this->assertNotNull($genderViolation, 'Expected a constraint violation on "gender" property.');
}
+ #[IgnoreDeprecations]
public function testInvalidBackedEnumValueWithCollectDenormalizationErrors(): void
{
+ if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
+ $this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
+ }
+
$response = static::createClient()->request('POST', '/enum_validation_resources_collect', [
'headers' => ['Content-Type' => 'application/ld+json'],
'json' => ['gender' => 'unknown'],
diff --git a/tests/Functional/GraphQl/AuthorizationTest.php b/tests/Functional/GraphQl/AuthorizationTest.php
new file mode 100644
index 00000000000..14a238c1ba9
--- /dev/null
+++ b/tests/Functional/GraphQl/AuthorizationTest.php
@@ -0,0 +1,590 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedLinkedDummy as RelatedLinkedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedSecuredDummy as RelatedSecuredDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\SecuredDummy as SecuredDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedLinkedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedSecuredDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class AuthorizationTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ private const ADMIN_AUTH = 'Basic YWRtaW46a2l0dGVu';
+ private const DUNGLAS_AUTH = 'Basic ZHVuZ2xhczprZXZpbg==';
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ SecuredDummy::class,
+ RelatedDummy::class,
+ RelatedSecuredDummy::class,
+ RelatedLinkedDummy::class,
+ ];
+ }
+
+ public function testAnonymousCannotReadSecuredItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummies(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ title
+ description
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['securedDummy']);
+ }
+
+ public function testAnonymousCannotReadSecuredCollection(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummies(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummies {
+ edges { node { title description } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['securedDummies']);
+ }
+
+ public function testAdminCanReadSecuredCollection(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummies(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummies {
+ edges { node { title description } }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNotNull($response->toArray()['data']['securedDummies']);
+ }
+
+ public function testUserCannotReadSecuredCollection(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummies(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummies {
+ edges { node { title description } }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertNull($data['data']['securedDummies']);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ }
+
+ public function testAnonymousCannotCreateSecuredResource(): void
+ {
+ $this->recreateAuthSchema();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", adminOnlyProperty: "secret", clientMutationId: "auth"}) {
+ securedDummy {
+ title
+ owner
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Only admins can create a secured dummy.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['createSecuredDummy']);
+ }
+
+ public function testAdminCanAccessSecuredRelationsOwnedByAdmin(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummiesWithRelations(1, 'admin');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ relatedDummies { edges { node { id } } }
+ relatedDummy { id }
+ relatedSecuredDummies { edges { node { id } } }
+ relatedSecuredDummy { id }
+ publicRelatedSecuredDummies { edges { node { id } } }
+ publicRelatedSecuredDummy { id }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['securedDummy'];
+ $this->assertCount(1, $data['relatedDummies']['edges']);
+ $this->assertNotNull($data['relatedDummy']);
+ $this->assertCount(1, $data['relatedSecuredDummies']['edges']);
+ $this->assertNotNull($data['relatedSecuredDummy']);
+ $this->assertCount(1, $data['publicRelatedSecuredDummies']['edges']);
+ $this->assertNotNull($data['publicRelatedSecuredDummy']);
+ }
+
+ public function testUserCannotReadSecuredCollectionRelationOnSecuredItemTheyDoNotOwn(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummiesWithRelations(1, 'someone-else');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ relatedDummies { edges { node { id } } }
+ relatedDummy { id }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $secured = $response->toArray(false)['data']['securedDummy'];
+ $this->assertNull($secured['relatedDummies']);
+ $this->assertNull($secured['relatedDummy']);
+ }
+
+ public function testUserCannotAccessRelatedSecuredDummyDirectly(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummiesWithRelations(1, 'dunglas');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ relatedSecuredDummy(id: "/related_secured_dummies/1") {
+ id
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['relatedSecuredDummy']);
+ }
+
+ public function testUserCannotListRelatedSecuredDummies(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummiesWithRelations(1, 'dunglas');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ relatedSecuredDummies {
+ edges { node { id } }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['relatedSecuredDummies']);
+ }
+
+ public function testUserCanAccessSecuredRelationsOnOwnedDummy(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummiesWithRelations(1, 'dunglas');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ relatedSecuredDummies { edges { node { id } } }
+ relatedSecuredDummy { id }
+ publicRelatedSecuredDummies { edges { node { id } } }
+ publicRelatedSecuredDummy { id }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['securedDummy'];
+ $this->assertCount(1, $data['relatedSecuredDummies']['edges']);
+ $this->assertNotNull($data['relatedSecuredDummy']);
+ $this->assertCount(1, $data['publicRelatedSecuredDummies']['edges']);
+ $this->assertNotNull($data['publicRelatedSecuredDummy']);
+ }
+
+ public function testAdminCanCreateSecuredResource(): void
+ {
+ $this->recreateAuthSchema();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) {
+ securedDummy {
+ id
+ title
+ owner
+ }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('someone', $response->toArray()['data']['createSecuredDummy']['securedDummy']['owner']);
+ }
+
+ public function testAdminCanCreateOwnerOnlyPropertyWhenAdminIsOwner(): void
+ {
+ $this->recreateAuthSchema();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createSecuredDummy(input: {owner: "admin", title: "Hi", description: "Desc", adminOnlyProperty: "secret", ownerOnlyProperty: "it works"}) {
+ securedDummy {
+ ownerOnlyProperty
+ }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('it works', $response->toArray()['data']['createSecuredDummy']['securedDummy']['ownerOnlyProperty']);
+ }
+
+ public function testAdminCannotSetOwnerOnlyPropertyWhenNotOwner(): void
+ {
+ $this->recreateAuthSchema();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc", adminOnlyProperty: "secret", ownerOnlyProperty: "should not be set"}) {
+ securedDummy {
+ ownerOnlyProperty
+ }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['createSecuredDummy']['securedDummy']['ownerOnlyProperty']);
+ }
+
+ public function testUserCannotReadItemTheyDoNotOwn(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('admin');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ owner
+ title
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['securedDummy']);
+ }
+
+ public function testUserCanReadItemTheyOwn(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ owner
+ title
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('dunglas', $response->toArray()['data']['securedDummy']['owner']);
+ }
+
+ public function testAdminCanReadAdminOnlyPropertyOnOtherUsersItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas', adminProperty: 'admin secret');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ owner
+ title
+ adminOnlyProperty
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('admin secret', $response->toArray()['data']['securedDummy']['adminOnlyProperty']);
+ }
+
+ public function testUserCannotReadAdminOnlyPropertyOnOwnedItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas', adminProperty: 'admin secret');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ owner
+ title
+ adminOnlyProperty
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['securedDummy']['adminOnlyProperty']);
+ }
+
+ public function testUserCanReadOwnerOnlyPropertyOnOwnedItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas', ownerProperty: 'owner secret');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ ownerOnlyProperty
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('owner secret', $response->toArray()['data']['securedDummy']['ownerOnlyProperty']);
+ }
+
+ public function testUserCanUpdateOwnerOnlyPropertyOnOwnedItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas', ownerProperty: 'original');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateSecuredDummy(input: {id: "/secured_dummies/1", ownerOnlyProperty: "updated"}) {
+ securedDummy {
+ ownerOnlyProperty
+ }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('updated', $response->toArray()['data']['updateSecuredDummy']['securedDummy']['ownerOnlyProperty']);
+ }
+
+ public function testAdminCannotReadOwnerOnlyPropertyOnOtherUsersItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas', ownerProperty: 'owner secret');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ securedDummy(id: "/secured_dummies/1") {
+ ownerOnlyProperty
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['securedDummy']['ownerOnlyProperty']);
+ }
+
+ public function testUserCannotAssignItemTheyDoNotOwnToThemselves(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('someone');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateSecuredDummy(input: {id: "/secured_dummies/1", owner: "kitten"}) {
+ securedDummy { id title owner }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::ADMIN_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertNull($data['data']['updateSecuredDummy']);
+ }
+
+ public function testUserCanTransferOwnedItem(): void
+ {
+ $this->recreateAuthSchema();
+ $this->seedSecuredDummyWithOwner('dunglas');
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateSecuredDummy(input: {id: "/secured_dummies/1", owner: "vincent"}) {
+ securedDummy { id title owner }
+ }
+ }
+ QUERY, headers: ['Authorization' => self::DUNGLAS_AUTH]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('vincent', $response->toArray()['data']['updateSecuredDummy']['securedDummy']['owner']);
+ }
+
+ private function recreateAuthSchema(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? SecuredDummyDocument::class : SecuredDummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ $this->isMongoDB() ? RelatedSecuredDummyDocument::class : RelatedSecuredDummy::class,
+ $this->isMongoDB() ? RelatedLinkedDummyDocument::class : RelatedLinkedDummy::class,
+ ]);
+ }
+
+ private function newSecuredDummy(): object
+ {
+ $class = $this->isMongoDB() ? SecuredDummyDocument::class : SecuredDummy::class;
+
+ return new $class();
+ }
+
+ private function newRelatedDummy(): object
+ {
+ $class = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+
+ return new $class();
+ }
+
+ private function newRelatedSecuredDummy(): object
+ {
+ $class = $this->isMongoDB() ? RelatedSecuredDummyDocument::class : RelatedSecuredDummy::class;
+
+ return new $class();
+ }
+
+ private function newRelatedLinkedDummy(): object
+ {
+ $class = $this->isMongoDB() ? RelatedLinkedDummyDocument::class : RelatedLinkedDummy::class;
+
+ return new $class();
+ }
+
+ private function seedSecuredDummies(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $d = $this->newSecuredDummy();
+ $d->setTitle("#$i");
+ $d->setDescription("Hello #$i");
+ $d->setOwner('notexist');
+ $manager->persist($d);
+ }
+ $manager->flush();
+ }
+
+ private function seedSecuredDummyWithOwner(string $owner, ?string $adminProperty = null, ?string $ownerProperty = null): void
+ {
+ $manager = $this->getManager();
+ $d = $this->newSecuredDummy();
+ $d->setTitle('#1');
+ $d->setDescription('Hello #1');
+ $d->setOwner($owner);
+ if (null !== $adminProperty) {
+ $d->setAdminOnlyProperty($adminProperty);
+ }
+ if (null !== $ownerProperty) {
+ $d->setOwnerOnlyProperty($ownerProperty);
+ }
+ $manager->persist($d);
+ $manager->flush();
+ }
+
+ private function seedSecuredDummiesWithRelations(int $count, string $owner): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $secured = $this->newSecuredDummy();
+ $secured->setTitle("#$i");
+ $secured->setDescription("Hello #$i");
+ $secured->setOwner($owner);
+
+ $related = $this->newRelatedDummy();
+ $related->setName('RelatedDummy');
+ $manager->persist($related);
+
+ $relatedSecured = $this->newRelatedSecuredDummy();
+ $manager->persist($relatedSecured);
+
+ $publicRelated = $this->newRelatedSecuredDummy();
+ $manager->persist($publicRelated);
+
+ $linked = $this->newRelatedLinkedDummy();
+ $manager->persist($linked);
+
+ $secured->addRelatedDummy($related);
+ $secured->setRelatedDummy($related);
+ $secured->addRelatedSecuredDummy($relatedSecured);
+ $secured->setRelatedSecuredDummy($relatedSecured);
+ $secured->addPublicRelatedSecuredDummy($publicRelated);
+ $secured->setPublicRelatedSecuredDummy($publicRelated);
+ $linked->setSecuredDummy($secured);
+
+ $manager->persist($secured);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/CollectionTest.php b/tests/Functional/GraphQl/CollectionTest.php
new file mode 100644
index 00000000000..e7931490ce1
--- /dev/null
+++ b/tests/Functional/GraphQl/CollectionTest.php
@@ -0,0 +1,923 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDifferentGraphQlSerializationGroup as DummyDifferentGraphQlSerializationGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Foo as FooDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooDummy as FooDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\MusicGroup as MusicGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\SoMany as SoManyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ThirdLevel as ThirdLevelDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\VideoGame as VideoGameDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeItem;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositePrimitiveItem;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDifferentGraphQlSerializationGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Foo;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MusicGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VideoGame;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class CollectionTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ ThirdLevel::class,
+ DummyGroup::class,
+ DummyCustomQuery::class,
+ DummyDifferentGraphQlSerializationGroup::class,
+ Foo::class,
+ FooDummy::class,
+ SoMany::class,
+ MusicGroup::class,
+ VideoGame::class,
+ CompositeRelation::class,
+ CompositeItem::class,
+ CompositeLabel::class,
+ CompositePrimitiveItem::class,
+ ];
+ }
+
+ public function testRetrieveCollectionWithRelations(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummyAndThirdLevel(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ ...dummyFields
+ }
+ }
+ fragment dummyFields on DummyCursorConnection {
+ edges {
+ node {
+ id
+ name
+ relatedDummy {
+ name
+ thirdLevel { id level }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertSame('Dummy #3', $edges[2]['node']['name']);
+ $this->assertSame('RelatedDummy #3', $edges[2]['node']['relatedDummy']['name']);
+ $this->assertSame(3, $edges[2]['node']['relatedDummy']['thirdLevel']['level']);
+ }
+
+ public function testRetrieveEmptyCollection(): void
+ {
+ $this->recreateDummiesAndRelated();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ edges { node { name } }
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertCount(0, $data['edges']);
+ $this->assertNull($data['pageInfo']['endCursor']);
+ $this->assertNull($data['pageInfo']['startCursor']);
+ $this->assertFalse($data['pageInfo']['hasNextPage']);
+ $this->assertFalse($data['pageInfo']['hasPreviousPage']);
+ }
+
+ public function testRetrieveCollectionWithNestedCollection(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesEachWithRelatedDummies(4, 3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ edges {
+ node {
+ name
+ relatedDummies {
+ edges { node { name } }
+ }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertSame('Dummy #3', $edges[2]['node']['name']);
+ $this->assertSame('RelatedDummy23', $edges[2]['node']['relatedDummies']['edges'][1]['node']['name']);
+ }
+
+ public function testRetrieveInverseSideNestedCollection(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? VideoGameDocument::class : VideoGame::class,
+ $this->isMongoDB() ? MusicGroupDocument::class : MusicGroup::class,
+ ]);
+ $this->seedVideoGameWithMusicGroups();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ musicGroups {
+ edges {
+ node {
+ name
+ videoGames { edges { node { name } } }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['musicGroups']['edges'];
+ $this->assertSame('Sum 41', $edges[0]['node']['name']);
+ $this->assertSame('Guitar Hero', $edges[0]['node']['videoGames']['edges'][0]['node']['name']);
+ $this->assertSame('Franz Ferdinand', $edges[1]['node']['name']);
+ $this->assertSame('Guitar Hero', $edges[1]['node']['videoGames']['edges'][0]['node']['name']);
+ }
+
+ public function testRetrieveCollectionAndItemTogether(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ $this->isMongoDB() ? ThirdLevelDocument::class : ThirdLevel::class,
+ $this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class,
+ ]);
+ $this->seedDummiesWithDate(3);
+ $this->seedDummyGroups(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ edges { node { name dummyDate } }
+ }
+ dummyGroup(id: "/dummy_groups/2") {
+ foo
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+ $this->assertSame('Dummy #2', $data['dummies']['edges'][1]['node']['name']);
+ $this->assertSame('2015-04-02', $data['dummies']['edges'][1]['node']['dummyDate']);
+ $this->assertSame('Foo #2', $data['dummyGroup']['foo']);
+ }
+
+ public function testFirstNItems(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(first: 2) {
+ edges { node { name } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(2, $response->toArray()['data']['dummies']['edges']);
+ }
+
+ public function testFirstNItemsOnNestedCollection(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesEachWithRelatedDummies(2, 5);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(first: 1) {
+ edges {
+ node {
+ name
+ relatedDummies(first: 2) {
+ edges { node { name } }
+ }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertCount(2, $edges[0]['node']['relatedDummies']['edges']);
+ }
+
+ public function testPaginationCursorsForward(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(first: 2) {
+ edges { cursor node { name } }
+ totalCount
+ pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertCount(2, $data['edges']);
+ $this->assertSame(4, $data['totalCount']);
+ $this->assertSame('MQ==', $data['pageInfo']['endCursor']);
+ $this->assertTrue($data['pageInfo']['hasNextPage']);
+ $this->assertFalse($data['pageInfo']['hasPreviousPage']);
+ $this->assertSame('MQ==', $data['edges'][1]['cursor']);
+ $this->assertSame('Dummy #2', $data['edges'][1]['node']['name']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(first: 2, after: "MQ==") {
+ edges { cursor node { name } }
+ pageInfo { endCursor hasNextPage }
+ }
+ }
+ QUERY);
+
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertCount(2, $data['edges']);
+ $this->assertSame('Dummy #3', $data['edges'][0]['node']['name']);
+ $this->assertSame('Mg==', $data['edges'][0]['cursor']);
+ $this->assertFalse($data['pageInfo']['hasNextPage']);
+ }
+
+ public function testPaginationCursorsBackward(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(last: 2) {
+ edges { cursor node { name } }
+ totalCount
+ pageInfo { startCursor hasPreviousPage hasNextPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertCount(2, $data['edges']);
+ $this->assertSame(4, $data['totalCount']);
+ $this->assertSame('Mg==', $data['pageInfo']['startCursor']);
+ $this->assertTrue($data['pageInfo']['hasPreviousPage']);
+ $this->assertSame('Dummy #4', $data['edges'][1]['node']['name']);
+ $this->assertSame('Mw==', $data['edges'][1]['cursor']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(last: 2, before: "Mw==") {
+ edges { cursor node { name } }
+ pageInfo { startCursor hasPreviousPage }
+ }
+ }
+ QUERY);
+
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertCount(2, $data['edges']);
+ $this->assertSame('Dummy #2', $data['edges'][0]['node']['name']);
+ $this->assertSame('MQ==', $data['edges'][0]['cursor']);
+ }
+
+ public function testSoManyPartialPagination(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('SoMany scenario @!mongodb');
+ }
+ $this->recreateSchema([SoMany::class]);
+ $this->seedSoManies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ soManies(first: 2) {
+ edges { cursor node { content } }
+ totalCount
+ pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['soManies'];
+ $this->assertSame('MA==', $data['pageInfo']['startCursor']);
+ $this->assertSame('MQ==', $data['pageInfo']['endCursor']);
+ $this->assertFalse($data['pageInfo']['hasNextPage']);
+ $this->assertFalse($data['pageInfo']['hasPreviousPage']);
+ $this->assertSame(0, $data['totalCount']);
+ $this->assertSame('Many #2', $data['edges'][1]['node']['content']);
+ $this->assertSame('MQ==', $data['edges'][1]['cursor']);
+ }
+
+ public function testCollectionWithPaginationDisabled(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? FooDocument::class : Foo::class]);
+ $this->seedFoosWithFakeNames(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ foos {
+ id
+ name
+ bar
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $foos = $response->toArray()['data']['foos'];
+ $this->assertSame('/foos/4', $foos[3]['id']);
+ $this->assertSame('Separativeness', $foos[3]['name']);
+ $this->assertSame('Sit', $foos[3]['bar']);
+ }
+
+ public function testCustomCollectionQuery(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+ $this->seedDummyCustomQuery(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testCollectionDummyCustomQueries {
+ edges { node { message } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'testCollectionDummyCustomQueries' => [
+ 'edges' => [
+ ['node' => ['message' => 'Success!']],
+ ['node' => ['message' => 'Success!']],
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testCustomCollectionQueryReadAndSerializeFalse(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+ $this->seedDummyCustomQuery(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testCollectionNoReadAndSerializeDummyCustomQueries {
+ edges { node { message } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => ['testCollectionNoReadAndSerializeDummyCustomQueries' => ['edges' => []]],
+ ], $response->toArray());
+ }
+
+ public function testCustomCollectionQueryWithCustomArguments(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+ $this->seedDummyCustomQuery(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testCollectionCustomArgumentsDummyCustomQueries(customArgumentString: "A string") {
+ edges { node { message customArgs } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'testCollectionCustomArgumentsDummyCustomQueries' => [
+ 'edges' => [
+ ['node' => ['message' => 'Success!', 'customArgs' => ['customArgumentString' => 'A string']]],
+ ['node' => ['message' => 'Success!', 'customArgs' => ['customArgumentString' => 'A string']]],
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testRetrieveCompositePrimitiveIdentifierItem(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Composite identifiers @!mongodb');
+ }
+ $this->recreateSchema([CompositePrimitiveItem::class]);
+ $manager = $this->getManager();
+ $foo = new CompositePrimitiveItem('Foo', 2016);
+ $foo->setDescription('This is foo.');
+ $manager->persist($foo);
+ $bar = new CompositePrimitiveItem('Bar', 2017);
+ $bar->setDescription('This is bar.');
+ $manager->persist($bar);
+ $manager->flush();
+ $manager->clear();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ compositePrimitiveItem(id: "/composite_primitive_items/name=Bar;year=2017") {
+ description
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('This is bar.', $response->toArray()['data']['compositePrimitiveItem']['description']);
+ }
+
+ public function testRetrieveCompositeIdentifierItem(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Composite identifiers @!mongodb');
+ }
+ $this->recreateSchema([CompositeRelation::class, CompositeItem::class, CompositeLabel::class]);
+ $this->seedCompositeIdentifierObjects();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ compositeRelation(id: "/composite_relations/compositeItem=1;compositeLabel=1") {
+ value
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('somefoobardummy', $response->toArray()['data']['compositeRelation']['value']);
+ }
+
+ public function testCollectionWithNameConverter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ edges { node { name_converted } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ 'Converted 2',
+ $response->toArray()['data']['dummies']['edges'][1]['node']['name_converted'],
+ );
+ }
+
+ public function testCollectionWithDifferentSerializationGroups(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyDifferentGraphQlSerializationGroupDocument::class : DummyDifferentGraphQlSerializationGroup::class]);
+ $this->seedDummyDifferentGroups(3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyDifferentGraphQlSerializationGroups {
+ edges { node { name } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummyDifferentGraphQlSerializationGroups']['edges'];
+ $this->assertCount(3, $edges);
+ $this->assertArrayHasKey('name', $edges[0]['node']);
+ $this->assertArrayNotHasKey('title', $edges[0]['node']);
+ }
+
+ public function testPageBasedPagination(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('FooDummy + SoMany scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class, SoMany::class]);
+ $this->seedFooDummies(5);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ fooDummies(page: 1) {
+ collection { id name }
+ paginationInfo { itemsPerPage lastPage totalCount hasNextPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['fooDummies'];
+ $this->assertCount(3, $data['collection']);
+ $this->assertSame(3, $data['paginationInfo']['itemsPerPage']);
+ $this->assertSame(2, $data['paginationInfo']['lastPage']);
+ $this->assertSame(5, $data['paginationInfo']['totalCount']);
+ $this->assertTrue($data['paginationInfo']['hasNextPage']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 2) { collection { id name } } }
+ QUERY);
+ $this->assertCount(2, $response->toArray()['data']['fooDummies']['collection']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 3) { collection { id name } } }
+ QUERY);
+ $this->assertCount(0, $response->toArray()['data']['fooDummies']['collection']);
+ }
+
+ public function testPageBasedPaginationWithItemsPerPage(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('FooDummy + SoMany scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class, SoMany::class]);
+ $this->seedFooDummies(5);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 1, itemsPerPage: 2) { collection { id name } } }
+ QUERY);
+ $this->assertCount(2, $response->toArray()['data']['fooDummies']['collection']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 2, itemsPerPage: 2) { collection { id name } } }
+ QUERY);
+ $this->assertCount(2, $response->toArray()['data']['fooDummies']['collection']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 3, itemsPerPage: 2) { collection { id name } } }
+ QUERY);
+ $this->assertCount(1, $response->toArray()['data']['fooDummies']['collection']);
+ }
+
+ public function testMixedPagination(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('FooDummy + SoMany scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class, SoMany::class]);
+ $this->seedFooDummies(5);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ fooDummies(page: 1) {
+ collection {
+ id name
+ soManies(first: 2) {
+ edges { cursor node { content } }
+ pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
+ }
+ }
+ paginationInfo { itemsPerPage lastPage totalCount hasNextPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['fooDummies'];
+ $this->assertCount(3, $data['collection']);
+ $this->assertCount(2, $data['collection'][2]['soManies']['edges']);
+ $this->assertSame('So many 1', $data['collection'][2]['soManies']['edges'][1]['node']['content']);
+ $this->assertSame('MA==', $data['collection'][2]['soManies']['pageInfo']['startCursor']);
+ $this->assertTrue($data['paginationInfo']['hasNextPage']);
+ }
+
+ public function testPaginationOnlyHasNextPage(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('FooDummy + SoMany scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class, SoMany::class]);
+ $this->seedFooDummies(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ fooDummies(page: 1, itemsPerPage: 2) {
+ collection {
+ id name
+ soManies(first: 2) {
+ edges { node { content } cursor }
+ pageInfo { startCursor endCursor hasNextPage hasPreviousPage }
+ }
+ }
+ paginationInfo { hasNextPage }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['fooDummies'];
+ $this->assertCount(2, $data['collection']);
+ $this->assertArrayHasKey('id', $data['collection'][1]);
+ $this->assertArrayHasKey('name', $data['collection'][1]);
+ $this->assertCount(2, $data['collection'][1]['soManies']['edges']);
+ $this->assertSame('So many 1', $data['collection'][1]['soManies']['edges'][1]['node']['content']);
+ $this->assertSame('MA==', $data['collection'][1]['soManies']['pageInfo']['startCursor']);
+ $this->assertTrue($data['paginationInfo']['hasNextPage']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ { fooDummies(page: 2) { paginationInfo { hasNextPage } } }
+ QUERY);
+ $this->assertFalse($response->toArray()['data']['fooDummies']['paginationInfo']['hasNextPage']);
+ }
+
+ private function recreateDummiesAndRelated(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ $this->isMongoDB() ? ThirdLevelDocument::class : ThirdLevel::class,
+ ]);
+ }
+
+ private function newDummy(): object
+ {
+ $class = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+
+ return new $class();
+ }
+
+ private function newRelated(): object
+ {
+ $class = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+
+ return new $class();
+ }
+
+ private function newThirdLevel(): object
+ {
+ $class = $this->isMongoDB() ? ThirdLevelDocument::class : ThirdLevel::class;
+
+ return new $class();
+ }
+
+ private function seedDummies(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->setDummy('SomeDummyTest'.$i);
+ $dummy->nameConverted = 'Converted '.$i;
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithDate(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ if ($count !== $i) {
+ $dummy->setDummyDate(new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')));
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesEachWithRelatedDummies(int $count, int $nbRelated): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+
+ for ($j = 1; $j <= $nbRelated; ++$j) {
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy'.$j.$i);
+ $related->setAge((int) ($j.$i));
+ $manager->persist($related);
+ $dummy->addRelatedDummy($related);
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithRelatedDummyAndThirdLevel(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $third = $this->newThirdLevel();
+
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy #'.$i);
+ $related->setThirdLevel($third);
+
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($third);
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyGroups(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $g = new $class();
+ foreach (['foo', 'bar', 'baz', 'qux'] as $p) {
+ $g->{$p} = ucfirst($p).' #'.$i;
+ }
+ $manager->persist($g);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyCustomQuery(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $manager->persist(new $class());
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyDifferentGroups(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyDifferentGraphQlSerializationGroupDocument::class : DummyDifferentGraphQlSerializationGroup::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $d = new $class();
+ $d->setName('Name #'.$i);
+ $d->setTitle('Title #'.$i);
+ $manager->persist($d);
+ }
+ $manager->flush();
+ }
+
+ private function seedFoosWithFakeNames(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? FooDocument::class : Foo::class;
+ $names = ['Hawsepipe', 'Sthenelus', 'Ephesian', 'Separativeness', 'Balbo'];
+ $bars = ['Lorem', 'Ipsum', 'Dolor', 'Sit', 'Amet'];
+ for ($i = 0; $i < $count; ++$i) {
+ $foo = new $class();
+ $foo->setName($names[$i]);
+ $foo->setBar($bars[$i]);
+ $manager->persist($foo);
+ }
+ $manager->flush();
+ }
+
+ private function seedFooDummies(int $count): void
+ {
+ $manager = $this->getManager();
+ $fooClass = $this->isMongoDB() ? FooDummyDocument::class : FooDummy::class;
+ $dummyClass = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ $soManyClass = $this->isMongoDB() ? SoManyDocument::class : SoMany::class;
+ $names = ['Hawsepipe', 'Ephesian', 'Sthenelus', 'Separativeness', 'Balbo'];
+ $dummies = ['Lorem', 'Ipsum', 'Dolor', 'Sit', 'Amet'];
+
+ for ($i = 0; $i < $count; ++$i) {
+ $dummy = new $dummyClass();
+ $dummy->setName($dummies[$i]);
+
+ $foo = new $fooClass();
+ $foo->setName($names[$i]);
+ $foo->setDummy($dummy);
+ for ($j = 0; $j < 3; ++$j) {
+ $soMany = new $soManyClass();
+ $soMany->content = "So many $j";
+ $soMany->fooDummy = $foo;
+ $foo->soManies->add($soMany);
+ }
+ $manager->persist($foo);
+ }
+ $manager->flush();
+ }
+
+ private function seedSoManies(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? SoManyDocument::class : SoMany::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $s = new $class();
+ $s->content = 'Many #'.$i;
+ $manager->persist($s);
+ }
+ $manager->flush();
+ }
+
+ private function seedVideoGameWithMusicGroups(): void
+ {
+ $manager = $this->getManager();
+ $musicClass = $this->isMongoDB() ? MusicGroupDocument::class : MusicGroup::class;
+ $videoClass = $this->isMongoDB() ? VideoGameDocument::class : VideoGame::class;
+
+ $sum41 = new $musicClass();
+ $sum41->name = 'Sum 41';
+ $manager->persist($sum41);
+
+ $franz = new $musicClass();
+ $franz->name = 'Franz Ferdinand';
+ $manager->persist($franz);
+
+ $videoGame = new $videoClass();
+ $videoGame->name = 'Guitar Hero';
+ $videoGame->addMusicGroup($sum41);
+ $videoGame->addMusicGroup($franz);
+ $manager->persist($videoGame);
+ $manager->flush();
+ }
+
+ private function seedCompositeIdentifierObjects(): void
+ {
+ $manager = $this->getManager();
+ $item = new CompositeItem();
+ $item->setField1('foobar');
+ $manager->persist($item);
+ $manager->flush();
+
+ for ($i = 0; $i < 4; ++$i) {
+ $label = new CompositeLabel();
+ $label->setValue('foo-'.$i);
+ $manager->persist($label);
+ $manager->flush();
+
+ $rel = new CompositeRelation();
+ $rel->setCompositeLabel($label);
+ $rel->setCompositeItem($item);
+ $rel->setValue('somefoobardummy');
+ $manager->persist($rel);
+ }
+ $manager->flush();
+ $manager->clear();
+ }
+}
diff --git a/tests/Functional/GraphQl/CustomTypeTest.php b/tests/Functional/GraphQl/CustomTypeTest.php
new file mode 100644
index 00000000000..6fb33832662
--- /dev/null
+++ b/tests/Functional/GraphQl/CustomTypeTest.php
@@ -0,0 +1,134 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class CustomTypeTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Dummy::class];
+ }
+
+ protected function setUp(): void
+ {
+ $resource = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ $this->recreateSchema([$resource]);
+ $this->seedDummies($resource);
+ }
+
+ public function testQueryFieldWithCustomType(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy(id: "/dummies/1") {
+ dummyDate
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('2015-04-01', $response->toArray()['data']['dummy']['dummyDate']);
+ }
+
+ public function testMutationInputWithCustomType(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateDummy(input: {id: "/dummies/1", dummyDate: "2019-05-24T00:00:00+00:00"}) {
+ dummy {
+ dummyDate
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('2019-05-24', $response->toArray()['data']['updateDummy']['dummy']['dummyDate']);
+ }
+
+ public function testMutationVariableWithCustomType(): void
+ {
+ $response = $this->executeGraphQl(
+ <<<'QUERY'
+ mutation UpdateDummyDate($itemId: ID!, $itemDate: DateTime!) {
+ updateDummy(input: {id: $itemId, dummyDate: $itemDate}) {
+ dummy {
+ dummyDate
+ }
+ }
+ }
+ QUERY,
+ ['itemId' => '/dummies/1', 'itemDate' => '2017-11-14T00:00:00+00:00'],
+ );
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('2017-11-14', $response->toArray()['data']['updateDummy']['dummy']['dummyDate']);
+ }
+
+ public function testMutationVariableWithCustomTypeAndBadValue(): void
+ {
+ $response = $this->executeGraphQl(
+ <<<'QUERY'
+ mutation UpdateDummyDate($itemId: ID!, $itemDate: DateTime!) {
+ updateDummy(input: {id: $itemId, dummyDate: $itemDate}) {
+ dummy {
+ dummyDate
+ }
+ }
+ }
+ QUERY,
+ ['itemId' => '/dummies/1', 'itemDate' => 'bad date'],
+ );
+
+ $this->assertResponseIsSuccessful();
+ $message = $response->toArray(false)['errors'][0]['message'] ?? '';
+ $this->assertStringContainsString('Variable "$itemDate" got invalid value "bad date";', $message);
+ $this->assertStringContainsString('DateTime cannot represent non date value: "bad date"', $message);
+ }
+
+ private function seedDummies(string $resourceClass): void
+ {
+ $manager = $this->getManager();
+ $dummy1 = new $resourceClass();
+ $dummy1->setName('Dummy #1');
+ $dummy1->setAlias('Alias #1');
+ $dummy1->setDescription('Smart dummy.');
+ $dummy1->setDummyDate(new \DateTime('2015-04-01', new \DateTimeZone('UTC')));
+ $manager->persist($dummy1);
+
+ $dummy2 = new $resourceClass();
+ $dummy2->setName('Dummy #2');
+ $dummy2->setAlias('Alias #0');
+ $dummy2->setDescription('Not so smart dummy.');
+ $manager->persist($dummy2);
+
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/DocsTest.php b/tests/Functional/GraphQl/DocsTest.php
new file mode 100644
index 00000000000..f2bc0c20407
--- /dev/null
+++ b/tests/Functional/GraphQl/DocsTest.php
@@ -0,0 +1,29 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+
+final class DocsTest extends ApiTestCase
+{
+ protected static ?bool $alwaysBootKernel = false;
+
+ public function testRetrieveGraphiQlDocumentation(): void
+ {
+ self::createClient()->request('GET', '/graphql', ['headers' => ['Accept' => 'text/html']]);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseHeaderSame('Content-Type', 'text/html; charset=UTF-8');
+ }
+}
diff --git a/tests/Functional/GraphQl/FilterTest.php b/tests/Functional/GraphQl/FilterTest.php
new file mode 100644
index 00000000000..a7d40722d66
--- /dev/null
+++ b/tests/Functional/GraphQl/FilterTest.php
@@ -0,0 +1,528 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedOwner as ConvertedOwnerDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\ConvertedRelated as ConvertedRelatedDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCarColor as DummyCarColorDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedOwner;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConvertedRelated;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\Common\Collections\ArrayCollection;
+
+final class FilterTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ ConvertedOwner::class,
+ ConvertedRelated::class,
+ DummyCar::class,
+ DummyCarColor::class,
+ ];
+ }
+
+ public function testBooleanFilter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $manager = $this->getManager();
+ $true = $this->newDummy();
+ $true->setName('Dummy #1');
+ $true->setDummyBoolean(true);
+ $manager->persist($true);
+
+ $false = $this->newDummy();
+ $false->setName('Dummy #2');
+ $false->setDummyBoolean(false);
+ $manager->persist($false);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(dummyBoolean: false) {
+ edges { node { id dummyBoolean } }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertFalse($edges[0]['node']['dummyBoolean']);
+ }
+
+ public function testExistsFilter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(3);
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(exists: [{relatedDummy: true}]) {
+ edges {
+ node {
+ id
+ relatedDummy { name }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(2, $edges);
+ $this->assertArrayHasKey('name', $edges[0]['node']['relatedDummy']);
+ }
+
+ public function testDateFilter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithDate(3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(dummyDate: [{after: "2015-04-02"}]) {
+ edges { node { id dummyDate } }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertSame('2015-04-02', $edges[0]['node']['dummyDate']);
+ }
+
+ public function testSearchFilterOnName(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(10);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(name: "#2") {
+ edges { node { id name } }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertSame('/dummies/2', $edges[0]['node']['id']);
+ }
+
+ public function testSearchFilterWithIntOnNestedCollection(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesEachWithRelatedDummies(4, 3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(name: "Dummy #1") {
+ totalCount
+ edges {
+ node {
+ name
+ relatedDummies(age: 31) {
+ totalCount
+ edges {
+ node { id name age }
+ }
+ }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $data = $response->toArray()['data']['dummies'];
+ $this->assertSame(1, $data['totalCount']);
+ $this->assertSame(1, $data['edges'][0]['node']['relatedDummies']['totalCount']);
+ $this->assertSame('31', (string) $data['edges'][0]['node']['relatedDummies']['edges'][0]['node']['age']);
+ }
+
+ public function testSearchFilterWithNameConverter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummies(10);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(name_converted: "Converted 2") {
+ edges { node { id name name_converted } }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertSame('/dummies/2', $edges[0]['node']['id']);
+ $this->assertSame('Converted 2', $edges[0]['node']['name_converted']);
+ }
+
+ public function testSearchFilterWithNameConverterOnNestedProperty(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? ConvertedOwnerDocument::class : ConvertedOwner::class,
+ $this->isMongoDB() ? ConvertedRelatedDocument::class : ConvertedRelated::class,
+ ]);
+ $this->seedConvertedOwners(20);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ convertedOwners(name_converted__name_converted: "Converted 2") {
+ edges {
+ node {
+ id
+ name_converted { name_converted }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['convertedOwners']['edges'];
+ $this->assertCount(2, $edges);
+ $this->assertSame('/converted_owners/2', $edges[0]['node']['id']);
+ $this->assertSame('Converted 2', $edges[0]['node']['name_converted']['name_converted']);
+ $this->assertSame('/converted_owners/20', $edges[1]['node']['id']);
+ $this->assertSame('Converted 20', $edges[1]['node']['name_converted']['name_converted']);
+ }
+
+ public function testSearchFilterOnNestedCollection(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesEachWithRelatedDummies(3, 3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies {
+ edges {
+ node {
+ id
+ relatedDummies(name: "RelatedDummy13") {
+ edges { node { id name } }
+ }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(0, $edges[0]['node']['relatedDummies']['edges']);
+ $this->assertCount(0, $edges[1]['node']['relatedDummies']['edges']);
+ $this->assertCount(1, $edges[2]['node']['relatedDummies']['edges']);
+ $this->assertSame('RelatedDummy13', $edges[2]['node']['relatedDummies']['edges'][0]['node']['name']);
+ }
+
+ public function testNestedCollectionFilter(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyCarDocument::class : DummyCar::class,
+ $this->isMongoDB() ? DummyCarColorDocument::class : DummyCarColor::class,
+ ]);
+ $this->seedDummyCarWithColors();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyCar(id: "/dummy_cars/1") {
+ id
+ colors(prop: "blue") {
+ edges { node { id prop } }
+ }
+ }
+ }
+ QUERY);
+
+ $edges = $response->toArray()['data']['dummyCar']['colors']['edges'];
+ $this->assertCount(1, $edges);
+ $this->assertSame('blue', $edges[0]['node']['prop']);
+ }
+
+ public function testRelatedSearchFilter(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesEachWithRelatedDummies(1, 2);
+ $this->seedDummiesEachWithRelatedDummies(1, 3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(relatedDummies__name: "RelatedDummy31") {
+ edges { node { id } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertCount(1, $response->toArray()['data']['dummies']['edges']);
+ }
+
+ public function testOrderByNestedProperty(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(order: [{relatedDummy__name: "DESC"}]) {
+ edges {
+ node {
+ name
+ relatedDummy { id name }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertSame('Dummy #2', $edges[0]['node']['name']);
+ $this->assertSame('Dummy #1', $edges[1]['node']['name']);
+ }
+
+ public function testMultiKeyOrderRespectsArgumentOrder(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithSimilarProperties();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(order: [{description: "ASC"}, {name: "ASC"}]) {
+ edges {
+ node { id name description }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertSame('baz', $edges[0]['node']['name']);
+ $this->assertSame('bar', $edges[0]['node']['description']);
+ $this->assertSame('foo', $edges[1]['node']['name']);
+ $this->assertSame('bar', $edges[1]['node']['description']);
+ }
+
+ public function testRelatedSearchFilterMultiValueExact(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(3);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummies(relatedDummy__name_list: ["RelatedDummy #1", "RelatedDummy #2"]) {
+ edges {
+ node {
+ id
+ name
+ relatedDummy { name }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $edges = $response->toArray()['data']['dummies']['edges'];
+ $this->assertCount(2, $edges);
+ $this->assertSame('RelatedDummy #1', $edges[0]['node']['relatedDummy']['name']);
+ $this->assertSame('RelatedDummy #2', $edges[1]['node']['relatedDummy']['name']);
+ }
+
+ private function recreateDummiesAndRelated(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+ }
+
+ private function newDummy(): object
+ {
+ $class = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+
+ return new $class();
+ }
+
+ private function newRelated(): object
+ {
+ $class = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+
+ return new $class();
+ }
+
+ private function seedDummies(int $count): void
+ {
+ $descriptions = ['Smart dummy.', 'Not so smart dummy.'];
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->setDummy('SomeDummyTest'.$i);
+ $dummy->setDescription($descriptions[($i - 1) % 2]);
+ $dummy->nameConverted = 'Converted '.$i;
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithDate(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ if ($count !== $i) {
+ $dummy->setDummyDate(new \DateTime(\sprintf('2015-04-%d', $i), new \DateTimeZone('UTC')));
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithRelatedDummy(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy #'.$i);
+
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->nameConverted = "Converted $i";
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesEachWithRelatedDummies(int $count, int $nbRelated): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+
+ for ($j = 1; $j <= $nbRelated; ++$j) {
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy'.$j.$i);
+ $related->setAge((int) ($j.$i));
+ $manager->persist($related);
+
+ $dummy->addRelatedDummy($related);
+ }
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithSimilarProperties(): void
+ {
+ $manager = $this->getManager();
+ foreach ([
+ ['foo', 'bar'],
+ ['baz', 'qux'],
+ ['foo', 'qux'],
+ ['baz', 'bar'],
+ ] as [$name, $description]) {
+ $dummy = $this->newDummy();
+ $dummy->setName($name);
+ $dummy->setDescription($description);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedConvertedOwners(int $count): void
+ {
+ $relatedClass = $this->isMongoDB() ? ConvertedRelatedDocument::class : ConvertedRelated::class;
+ $ownerClass = $this->isMongoDB() ? ConvertedOwnerDocument::class : ConvertedOwner::class;
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = new $relatedClass();
+ $related->nameConverted = 'Converted '.$i;
+
+ $owner = new $ownerClass();
+ $owner->nameConverted = $related;
+
+ $manager->persist($related);
+ $manager->persist($owner);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyCarWithColors(): void
+ {
+ $manager = $this->getManager();
+ $carClass = $this->isMongoDB() ? DummyCarDocument::class : DummyCar::class;
+ $colorClass = $this->isMongoDB() ? DummyCarColorDocument::class : DummyCarColor::class;
+
+ $car = new $carClass();
+ $car->setName('mustli');
+ $car->setCanSell(true);
+ $car->setAvailableAt(new \DateTime());
+ $manager->persist($car);
+ $manager->flush();
+
+ if (\is_object($car->getId())) {
+ $manager->persist($car->getId());
+ $manager->flush();
+ }
+
+ $red = new $colorClass();
+ $red->setProp('red');
+ $red->setCar($car);
+ $manager->persist($red);
+ $manager->flush();
+
+ $blue = new $colorClass();
+ $blue->setProp('blue');
+ $blue->setCar($car);
+ $manager->persist($blue);
+ $manager->flush();
+
+ $car->setColors(new ArrayCollection([$red, $blue]));
+ $manager->persist($car);
+ $manager->flush();
+ }
+}
diff --git a/features/files/test.gif b/tests/Functional/GraphQl/Fixtures/test.gif
similarity index 100%
rename from features/files/test.gif
rename to tests/Functional/GraphQl/Fixtures/test.gif
diff --git a/tests/Functional/GraphQl/InputOutputTest.php b/tests/Functional/GraphQl/InputOutputTest.php
new file mode 100644
index 00000000000..a120f54edda
--- /dev/null
+++ b/tests/Functional/GraphQl/InputOutputTest.php
@@ -0,0 +1,236 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoInputOutput as DummyDtoInputOutputDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoInputOutput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MessengerWithInput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class InputOutputTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ DummyDtoInputOutput::class,
+ DummyDtoNoOutput::class,
+ DummyDtoNoInput::class,
+ MessengerWithInput::class,
+ RelatedDummy::class,
+ ];
+ }
+
+ public function testRetrieveOutputAfterRestCreation(): void
+ {
+ $this->recreateSchema($this->resolveResources([
+ DummyDtoInputOutput::class => DummyDtoInputOutputDocument::class,
+ RelatedDummy::class => RelatedDummyDocument::class,
+ ]));
+ $this->seedRelatedDummy();
+
+ $client = self::createClient();
+ $client->request('POST', '/dummy_dto_input_outputs', [
+ 'headers' => ['Content-Type' => 'application/ld+json'],
+ 'json' => ['foo' => 'test', 'bar' => 1, 'relatedDummies' => ['/related_dummies/1']],
+ ]);
+ $this->assertResponseStatusCodeSame(201);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyDtoInputOutput(id: "/dummy_dto_input_outputs/1") {
+ _id, id, baz,
+ relatedDummies {
+ edges {
+ node {
+ name
+ }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'dummyDtoInputOutput' => [
+ '_id' => 1,
+ 'id' => '/dummy_dto_input_outputs/1',
+ 'baz' => 1,
+ 'relatedDummies' => [
+ 'edges' => [
+ ['node' => ['name' => 'RelatedDummy with friends']],
+ ],
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testCreateItemWithCustomInputAndOutput(): void
+ {
+ $this->recreateSchema($this->resolveResources([DummyDtoInputOutput::class => DummyDtoInputOutputDocument::class]));
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummyDtoInputOutput(input: {foo: "A foo", bar: 4, clientMutationId: "myId"}) {
+ dummyDtoInputOutput {
+ baz,
+ bat
+ }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'createDummyDtoInputOutput' => [
+ 'dummyDtoInputOutput' => ['baz' => 4, 'bat' => 'A foo'],
+ 'clientMutationId' => 'myId',
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testCreateItemWithDisabledOutputClassFailsToQueryFields(): void
+ {
+ $this->recreateSchema($this->resolveResources([DummyDtoNoOutput::class => DummyDtoNoOutputDocument::class]));
+ $this->seedDummyDtoNoOutput(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
+ dummyDtoNoOutput {
+ id
+ }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame('Cannot query field "id" on type "DummyDtoNoOutput".', $data['errors'][0]['message']);
+ $this->assertSame(4, $data['errors'][0]['locations'][0]['line']);
+ $this->assertSame(7, $data['errors'][0]['locations'][0]['column']);
+ }
+
+ public function testCreateItemWithDisabledInputClassRejectsUndefinedFields(): void
+ {
+ $this->recreateSchema($this->resolveResources([DummyDtoNoInput::class => DummyDtoNoInputDocument::class]));
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertMatchesRegularExpression(
+ '/^Field "lorem" is not defined by type "?createDummyDtoNoInputInput"?\.$/',
+ $data['errors'][0]['message'],
+ );
+ $this->assertMatchesRegularExpression(
+ '/^Field "ipsum" is not defined by type "?createDummyDtoNoInputInput"?\.$/',
+ $data['errors'][1]['message'],
+ );
+ }
+
+ public function testMessengerWithInputReturnsSynchronousResult(): void
+ {
+ // MessengerWithInput is not a Doctrine resource — nothing to recreate.
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createMessengerWithInput(input: {var: "test"}) {
+ messengerWithInput { id, name }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'createMessengerWithInput' => [
+ 'messengerWithInput' => [
+ 'id' => '/messenger_with_inputs/1',
+ 'name' => 'test',
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ /**
+ * @param array $map
+ *
+ * @return list
+ */
+ private function resolveResources(array $map): array
+ {
+ $resolved = [];
+ foreach ($map as $entity => $document) {
+ $resolved[] = $this->isMongoDB() ? $document : $entity;
+ }
+
+ return $resolved;
+ }
+
+ private function seedRelatedDummy(): void
+ {
+ $resourceClass = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ $manager = $this->getManager();
+ $related = new $resourceClass();
+ $related->setName('RelatedDummy with friends');
+ $manager->persist($related);
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function seedDummyDtoNoOutput(int $count): void
+ {
+ $resourceClass = $this->isMongoDB() ? DummyDtoNoOutputDocument::class : DummyDtoNoOutput::class;
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dto = new $resourceClass();
+ $dto->lorem = 'DummyDtoNoOutput foo #'.$i;
+ $dto->ipsum = (string) ($i / 3);
+ $manager->persist($dto);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/IntrospectionTest.php b/tests/Functional/GraphQl/IntrospectionTest.php
new file mode 100644
index 00000000000..b7827b5cc7b
--- /dev/null
+++ b/tests/Functional/GraphQl/IntrospectionTest.php
@@ -0,0 +1,487 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DeprecatedResource;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyAggregateOffer;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDifferentGraphQlSerializationGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProduct;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyProperty;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VideoGame;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VoDummyCar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VoDummyInspection;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class IntrospectionTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ DummyProduct::class,
+ DummyAggregateOffer::class,
+ DummyDifferentGraphQlSerializationGroup::class,
+ DummyGroup::class,
+ DummyProperty::class,
+ DeprecatedResource::class,
+ VoDummyCar::class,
+ VoDummyInspection::class,
+ Person::class,
+ VideoGame::class,
+ ];
+ }
+
+ public function testEmptyQueryReturnsBadRequest(): void
+ {
+ $client = self::createClient();
+ $client->request('GET', '/graphql');
+
+ $this->assertResponseStatusCodeSame(200);
+ $data = $client->getResponse()->toArray(false);
+ $this->assertSame(400, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('GraphQL query is not valid.', $data['errors'][0]['message']);
+ }
+
+ public function testIntrospectSchema(): void
+ {
+ $response = $this->introspectSchema();
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertArrayHasKey('types', $data['data']['__schema']);
+ $this->assertSame('Query', $data['data']['__schema']['queryType']['name']);
+ $this->assertSame('Mutation', $data['data']['__schema']['mutationType']['name']);
+ }
+
+ public function testIntrospectTypes(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ type1: __type(name: "DummyProduct") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ type2: __type(name: "DummyAggregateOfferCursorConnection") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ type3: __type(name: "DummyAggregateOfferEdge") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+
+ $this->assertSame('Dummy Product.', $data['type1']['description']);
+ $this->assertContainsEquals(
+ ['name' => 'offers', 'type' => ['name' => 'DummyAggregateOfferCursorConnection', 'kind' => 'OBJECT', 'ofType' => null]],
+ $data['type1']['fields'],
+ );
+ $this->assertContainsEquals(
+ ['name' => 'edges', 'type' => ['name' => null, 'kind' => 'LIST', 'ofType' => ['name' => 'DummyAggregateOfferEdge', 'kind' => 'OBJECT']]],
+ $data['type2']['fields'],
+ );
+ $this->assertContainsEquals(
+ ['name' => 'node', 'type' => ['name' => 'DummyAggregateOffer', 'kind' => 'OBJECT', 'ofType' => null]],
+ $data['type3']['fields'],
+ );
+ $this->assertContainsEquals(
+ ['name' => 'cursor', 'type' => ['name' => null, 'kind' => 'NON_NULL', 'ofType' => ['name' => 'String', 'kind' => 'SCALAR']]],
+ $data['type3']['fields'],
+ );
+ }
+
+ public function testIntrospectTypesWithDifferentSerializationGroups(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ type1: __type(name: "DummyDifferentGraphQlSerializationGroupCollection") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ type2: __type(name: "DummyDifferentGraphQlSerializationGroupItem") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+
+ $this->assertSame(
+ 'Dummy with different serialization groups for item_query and collection_query.',
+ $data['type1']['description'],
+ );
+ $this->assertCount(3, $data['type1']['fields']);
+ $this->assertSame('title', $data['type2']['fields'][3]['name']);
+ }
+
+ public function testIntrospectDeprecatedQueries(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type (name: "Query") {
+ name
+ fields(includeDeprecated: true) {
+ name
+ isDeprecated
+ deprecationReason
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertGraphQlFieldDeprecated($data, 'deprecatedResource', 'This resource is deprecated');
+ $this->assertGraphQlFieldDeprecated($data, 'deprecatedResources', 'This resource is deprecated');
+ }
+
+ public function testIntrospectDeprecatedMutations(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type (name: "Mutation") {
+ name
+ fields(includeDeprecated: true) {
+ name
+ isDeprecated
+ deprecationReason
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertGraphQlFieldDeprecated($data, 'deleteDeprecatedResource', 'This resource is deprecated');
+ $this->assertGraphQlFieldDeprecated($data, 'updateDeprecatedResource', 'This resource is deprecated');
+ $this->assertGraphQlFieldDeprecated($data, 'createDeprecatedResource', 'This resource is deprecated');
+ }
+
+ public function testIntrospectDeprecatedField(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type(name: "DeprecatedResource") {
+ fields(includeDeprecated: true) {
+ name
+ isDeprecated
+ deprecationReason
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertGraphQlFieldDeprecated($response->toArray(), 'deprecatedField', 'This field is deprecated');
+ }
+
+ public function testRetrieveRelayNodeInterface(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type(name: "Node") {
+ name
+ kind
+ fields {
+ name
+ type {
+ kind
+ ofType { name kind }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ '__type' => [
+ 'name' => 'Node',
+ 'kind' => 'INTERFACE',
+ 'fields' => [
+ [
+ 'name' => 'id',
+ 'type' => ['kind' => 'NON_NULL', 'ofType' => ['name' => 'ID', 'kind' => 'SCALAR']],
+ ],
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testRetrieveRelayNodeField(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __schema {
+ queryType {
+ fields {
+ name
+ type { name kind }
+ args { name type { kind ofType { name kind } } }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $fields = $response->toArray()['data']['__schema']['queryType']['fields'];
+ $this->assertSame('node', $fields[0]['name']);
+ $this->assertSame('Node', $fields[0]['type']['name']);
+ $this->assertSame('INTERFACE', $fields[0]['type']['kind']);
+ $this->assertSame('id', $fields[0]['args'][0]['name']);
+ $this->assertSame('NON_NULL', $fields[0]['args'][0]['type']['kind']);
+ $this->assertSame('ID', $fields[0]['args'][0]['type']['ofType']['name']);
+ $this->assertSame('SCALAR', $fields[0]['args'][0]['type']['ofType']['kind']);
+ }
+
+ public function testIntrospectIterableFieldOnDummy(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type(name: "Dummy") {
+ description,
+ fields { name type { name kind ofType { name kind } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertContainsEquals(
+ ['name' => 'jsonData', 'type' => ['name' => 'Iterable', 'kind' => 'SCALAR', 'ofType' => null]],
+ $response->toArray()['data']['__type']['fields'],
+ );
+ }
+
+ public function testRetrieveDummyGroupFieldsAndMutationInputs(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ typeQuery: __type(name: "DummyGroup") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ typeCreateInput: __type(name: "createDummyGroupInput") {
+ inputFields { name type { name kind ofType { name kind } } }
+ }
+ typeCreatePayload: __type(name: "createDummyGroupPayload") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ typeCreatePayloadData: __type(name: "createDummyGroupPayloadData") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+
+ $this->assertCount(2, $data['typeQuery']['fields']);
+ $this->assertSame('id', $data['typeQuery']['fields'][0]['name']);
+ $this->assertSame('foo', $data['typeQuery']['fields'][1]['name']);
+
+ $this->assertCount(3, $data['typeCreateInput']['inputFields']);
+ $this->assertSame('bar', $data['typeCreateInput']['inputFields'][0]['name']);
+ $this->assertSame('baz', $data['typeCreateInput']['inputFields'][1]['name']);
+ $this->assertSame('clientMutationId', $data['typeCreateInput']['inputFields'][2]['name']);
+
+ $this->assertCount(2, $data['typeCreatePayload']['fields']);
+ $this->assertSame('dummyGroup', $data['typeCreatePayload']['fields'][0]['name']);
+ $this->assertSame('createDummyGroupPayloadData', $data['typeCreatePayload']['fields'][0]['type']['name']);
+ $this->assertSame('clientMutationId', $data['typeCreatePayload']['fields'][1]['name']);
+
+ $this->assertCount(2, $data['typeCreatePayloadData']['fields']);
+ $this->assertSame('id', $data['typeCreatePayloadData']['fields'][0]['name']);
+ $this->assertSame('bar', $data['typeCreatePayloadData']['fields'][1]['name']);
+ }
+
+ public function testRetrieveNestedMutationPayloadData(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ typeCreatePayload: __type(name: "createDummyPropertyPayload") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ typeCreatePayloadData: __type(name: "createDummyPropertyPayloadData") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ typeCreateNestedPayload: __type(name: "createDummyGroupNestedPayload") {
+ fields { name type { name kind ofType { name kind } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+
+ $this->assertSame([
+ ['name' => 'dummyProperty', 'type' => ['name' => 'createDummyPropertyPayloadData', 'kind' => 'OBJECT', 'ofType' => null]],
+ ['name' => 'clientMutationId', 'type' => ['name' => 'String', 'kind' => 'SCALAR', 'ofType' => null]],
+ ], $data['typeCreatePayload']['fields']);
+
+ $this->assertContainsEquals(
+ ['name' => 'group', 'type' => ['name' => 'createDummyGroupNestedPayload', 'kind' => 'OBJECT', 'ofType' => null]],
+ $data['typeCreatePayloadData']['fields'],
+ );
+
+ $this->assertContainsEquals(
+ ['name' => 'id', 'type' => ['name' => null, 'kind' => 'NON_NULL', 'ofType' => ['name' => 'ID', 'kind' => 'SCALAR']]],
+ $data['typeCreateNestedPayload']['fields'],
+ );
+ }
+
+ public function testRetrieveTypenameViaGraphQlQuery(): void
+ {
+ $resources = [
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ];
+ $this->recreateSchema($resources);
+ $this->seedDummiesWithRelatedDummy(4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy: dummy(id: "/dummies/3") {
+ name
+ relatedDummy {
+ id
+ name
+ __typename
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $dummy = $response->toArray()['data']['dummy'];
+ $this->assertSame('Dummy #3', $dummy['name']);
+ $this->assertSame('RelatedDummy #3', $dummy['relatedDummy']['name']);
+ $this->assertSame('RelatedDummy', $dummy['relatedDummy']['__typename']);
+ }
+
+ public function testIntrospectTypeAvailableOnlyThroughRelations(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ typeNotAvailable: __type(name: "VoDummyInspectionCursorConnection") {
+ description
+ }
+ typeOwner: __type(name: "VoDummyCar") {
+ description,
+ fields { name type { name } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+ $this->assertNull($data['typeNotAvailable']);
+ $this->assertSame('VoDummyInspectionCursorConnection', $data['typeOwner']['fields'][1]['type']['name']);
+ }
+
+ public function testIntrospectEnum(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ person: __type(name: "Person") {
+ name
+ fields {
+ name
+ type {
+ name
+ description
+ enumValues { name description }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $field = $response->toArray()['data']['person']['fields'][1];
+ $this->assertSame('GenderTypeEnum', $field['type']['name']);
+ $this->assertSame('MALE', $field['type']['enumValues'][0]['name']);
+ $this->assertSame('FEMALE', $field['type']['enumValues'][1]['name']);
+ $this->assertSame('The female gender.', $field['type']['enumValues'][1]['description']);
+ }
+
+ public function testIntrospectEnumResource(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ videoGame: __type(name: "VideoGame") {
+ name
+ fields {
+ name
+ type { name kind ofType { name kind } }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ 'GamePlayMode',
+ $response->toArray()['data']['videoGame']['fields'][3]['type']['ofType']['name'],
+ );
+ }
+
+ private function seedDummiesWithRelatedDummy(int $count): void
+ {
+ $dummyClass = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ $relatedClass = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ $manager = $this->getManager();
+
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = new $relatedClass();
+ $related->setName('RelatedDummy #'.$i);
+
+ $dummy = new $dummyClass();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->nameConverted = "Converted $i";
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/MutationTest.php b/tests/Functional/GraphQl/MutationTest.php
new file mode 100644
index 00000000000..ac8bf520b09
--- /dev/null
+++ b/tests/Functional/GraphQl/MutationTest.php
@@ -0,0 +1,955 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6354\ActivityLog;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomMutation as DummyCustomMutationDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Foo as FooDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooDummy as FooDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\FooEmbeddable as FooEmbeddableDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Person as PersonDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\WritableId as WritableIdDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeItem;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeLabel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\CompositeRelation;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomMutation;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Foo;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooEmbeddable;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FourthLevel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Person;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\VideoGame;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\WritableId;
+use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GamePlayMode;
+use ApiPlatform\Tests\Fixtures\TestBundle\Model\MediaObject;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Symfony\Component\HttpFoundation\File\UploadedFile;
+
+final class MutationTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ private const FIXTURES_DIR = __DIR__.'/Fixtures';
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Foo::class,
+ Dummy::class,
+ RelatedDummy::class,
+ Person::class,
+ FooDummy::class,
+ FooEmbeddable::class,
+ CompositeRelation::class,
+ CompositeItem::class,
+ CompositeLabel::class,
+ WritableId::class,
+ DummyGroup::class,
+ DummyCustomMutation::class,
+ ActivityLog::class,
+ GamePlayMode::class,
+ VideoGame::class,
+ ThirdLevel::class,
+ FourthLevel::class,
+ DummyFriend::class,
+ RelatedToDummyFriend::class,
+ MediaObject::class,
+ ];
+ }
+
+ public function testCreateItem(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? FooDocument::class : Foo::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createFoo(input: {name: "A new one", bar: "new", clientMutationId: "myId"}) {
+ foo { id _id __typename name bar }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['createFoo'];
+ $this->assertSame('/foos/1', $data['foo']['id']);
+ $this->assertSame(1, $data['foo']['_id']);
+ $this->assertSame('Foo', $data['foo']['__typename']);
+ $this->assertSame('A new one', $data['foo']['name']);
+ $this->assertSame('new', $data['foo']['bar']);
+ $this->assertSame('myId', $data['clientMutationId']);
+ }
+
+ public function testCreateItemWithoutClientMutationId(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? FooDocument::class : Foo::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createFoo(input: {name: "Created without mutation id", bar: "works"}) {
+ foo { id name bar }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['createFoo']['foo'];
+ $this->assertSame('/foos/1', $data['id']);
+ $this->assertSame('Created without mutation id', $data['name']);
+ $this->assertSame('works', $data['bar']);
+ }
+
+ public function testCreateItemWithRelationToExisting(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+ $this->seedDummiesWithRelatedDummy(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummy(input: {name: "A dummy", foo: [], relatedDummy: "/related_dummies/1", name_converted: "Converted" clientMutationId: "myId"}) {
+ dummy {
+ id
+ name
+ foo
+ relatedDummy { name __typename }
+ name_converted
+ }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['createDummy'];
+ $this->assertSame('/dummies/2', $d['dummy']['id']);
+ $this->assertSame('A dummy', $d['dummy']['name']);
+ $this->assertCount(0, $d['dummy']['foo']);
+ $this->assertSame('RelatedDummy #1', $d['dummy']['relatedDummy']['name']);
+ $this->assertSame('RelatedDummy', $d['dummy']['relatedDummy']['__typename']);
+ $this->assertSame('Converted', $d['dummy']['name_converted']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testCreateItemWithIterableField(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummy(input: {name: "A dummy", foo: [], jsonData: {bar:{baz:3,qux:[7.6,false,null]}}, arrayData: ["bar", "baz"], clientMutationId: "myId"}) {
+ dummy {
+ id name foo jsonData arrayData
+ }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['createDummy'];
+ $this->assertSame('/dummies/1', $d['dummy']['id']);
+ $this->assertSame('A dummy', $d['dummy']['name']);
+ $this->assertSame(3, $d['dummy']['jsonData']['bar']['baz']);
+ $this->assertSame(7.6, $d['dummy']['jsonData']['bar']['qux'][0]);
+ $this->assertFalse($d['dummy']['jsonData']['bar']['qux'][1]);
+ $this->assertNull($d['dummy']['jsonData']['bar']['qux'][2]);
+ $this->assertSame('baz', $d['dummy']['arrayData'][1]);
+ }
+
+ public function testCreateItemWithEnum(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? PersonDocument::class : Person::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createPerson(input: {name: "Mob", genderType: FEMALE}) {
+ person { id name genderType }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $p = $response->toArray()['data']['createPerson']['person'];
+ $this->assertSame('/people/1', $p['id']);
+ $this->assertSame('Mob', $p['name']);
+ $this->assertSame('FEMALE', $p['genderType']);
+ }
+
+ public function testCreateItemWithEnumCollection(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Enum collection scenario @!mongodb');
+ }
+ $this->recreateSchema([Person::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createPerson(input: {name: "Harry", academicGrades: [BACHELOR, MASTER]}) {
+ person { id name genderType academicGrades }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $p = $response->toArray()['data']['createPerson']['person'];
+ $this->assertSame('/people/1', $p['id']);
+ $this->assertSame('Harry', $p['name']);
+ $this->assertCount(2, $p['academicGrades']);
+ $this->assertSame('BACHELOR', $p['academicGrades'][0]);
+ $this->assertSame('MASTER', $p['academicGrades'][1]);
+ }
+
+ public function testDeleteItem(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? FooDocument::class : Foo::class]);
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? FooDocument::class : Foo::class;
+ $foo = new $class();
+ $foo->setName('Existing');
+ $foo->setBar('value');
+ $manager->persist($foo);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ deleteFoo(input: {id: "/foos/1", clientMutationId: "anotherId"}) {
+ foo { id }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['deleteFoo'];
+ $this->assertSame('/foos/1', $data['foo']['id']);
+ $this->assertSame('anotherId', $data['clientMutationId']);
+ }
+
+ public function testDeleteWithWrongResourceTypeYieldsError(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? FooDocument::class : Foo::class,
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+ $this->seedDummiesWithRelatedDummy(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ deleteFoo(input: {id: "/dummies/1", clientMutationId: "myId"}) {
+ foo { id }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ 'Item "/dummies/1" did not match expected type "Foo".',
+ $response->toArray(false)['errors'][0]['message'],
+ );
+ }
+
+ public function testDeleteItemWithCompositeIdentifiers(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Composite identifiers @!mongodb');
+ }
+ $this->recreateSchema([CompositeRelation::class, CompositeItem::class, CompositeLabel::class]);
+ $this->seedCompositeIdentifierObjects();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ deleteCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=1", clientMutationId: "myId"}) {
+ compositeRelation { id }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['deleteCompositeRelation'];
+ $this->assertSame('/composite_relations/compositeItem=1;compositeLabel=1', $data['compositeRelation']['id']);
+ $this->assertSame('myId', $data['clientMutationId']);
+ }
+
+ public function testModifyItem(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+ $this->seedDummiesWithRelatedDummy(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateDummy(input: {id: "/dummies/1", description: "Modified description.", dummyDate: "2018-06-05T00:00:00+00:00", clientMutationId: "myId"}) {
+ dummy { id name description dummyDate }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['updateDummy'];
+ $this->assertSame('/dummies/1', $d['dummy']['id']);
+ $this->assertSame('Dummy #1', $d['dummy']['name']);
+ $this->assertSame('Modified description.', $d['dummy']['description']);
+ $this->assertSame('2018-06-05', $d['dummy']['dummyDate']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testModifyItemWithEmbeddedObject(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Embedded object scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class]);
+ $this->seedFooDummyWithEmbeddable();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
+ fooDummy {
+ id
+ name
+ embeddedFoo { dummyName }
+ }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['updateFooDummy'];
+ $this->assertSame('modifiedName', $d['fooDummy']['name']);
+ $this->assertSame('Embedded name', $d['fooDummy']['embeddedFoo']['dummyName']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testModifyNonWritablePropertyRejected(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Embedded object scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class]);
+ $this->seedFooDummyWithEmbeddable();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", nonWritableProp: "written", embeddedFoo: {dummyName: "Embedded name"}, clientMutationId: "myId"}) {
+ fooDummy { id name }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertMatchesRegularExpression(
+ '/^Field "nonWritableProp" is not defined by type "?updateFooDummyInput"?\.$/',
+ $response->toArray(false)['errors'][0]['message'],
+ );
+ }
+
+ public function testModifyNonWritableEmbeddedPropertyRejected(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Embedded object scenario @!mongodb');
+ }
+ $this->recreateSchema([Dummy::class, FooDummy::class]);
+ $this->seedFooDummyWithEmbeddable();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateFooDummy(input: {id: "/foo_dummies/1", name: "modifiedName", embeddedFoo: {dummyName: "Embedded name", nonWritableProp: "written"}, clientMutationId: "myId"}) {
+ fooDummy { id name }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertMatchesRegularExpression(
+ '/^Field "nonWritableProp" is not defined by type "?FooEmbeddableNestedInput"?\.$/',
+ $response->toArray(false)['errors'][0]['message'],
+ );
+ }
+
+ public function testModifyItemWithCompositeIdentifiers(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Composite identifiers @!mongodb');
+ }
+ $this->recreateSchema([CompositeRelation::class, CompositeItem::class, CompositeLabel::class]);
+ $this->seedCompositeIdentifierObjects();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateCompositeRelation(input: {id: "/composite_relations/compositeItem=1;compositeLabel=2", value: "Modified value.", clientMutationId: "myId"}) {
+ compositeRelation { id value }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['updateCompositeRelation'];
+ $this->assertSame('/composite_relations/compositeItem=1;compositeLabel=2', $d['compositeRelation']['id']);
+ $this->assertSame('Modified value.', $d['compositeRelation']['value']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testCreateWithCustomUuid(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? WritableIdDocument::class : WritableId::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createWritableId(input: {_id: "c6b722fe-0331-48c4-a214-f81f9f1ca082", name: "Foo", clientMutationId: "m"}) {
+ writableId { id _id name }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['createWritableId'];
+ $this->assertSame('/writable_ids/c6b722fe-0331-48c4-a214-f81f9f1ca082', $d['writableId']['id']);
+ $this->assertSame('c6b722fe-0331-48c4-a214-f81f9f1ca082', $d['writableId']['_id']);
+ $this->assertSame('Foo', $d['writableId']['name']);
+ $this->assertSame('m', $d['clientMutationId']);
+ }
+
+ public function testUpdateWithCustomUuid(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('WritableId update @!mongodb');
+ }
+ $this->recreateSchema([WritableId::class]);
+ $manager = $this->getManager();
+ $w = new WritableId();
+ $w->id = 'c6b722fe-0331-48c4-a214-f81f9f1ca082';
+ $w->name = 'Foo';
+ $manager->persist($w);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateWritableId(input: {id: "/writable_ids/c6b722fe-0331-48c4-a214-f81f9f1ca082", _id: "f8a708b2-310f-416c-9aef-b1b5719dfa47", name: "Foo", clientMutationId: "m"}) {
+ writableId { id _id name }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['updateWritableId'];
+ $this->assertSame('/writable_ids/f8a708b2-310f-416c-9aef-b1b5719dfa47', $d['writableId']['id']);
+ $this->assertSame('f8a708b2-310f-416c-9aef-b1b5719dfa47', $d['writableId']['_id']);
+ $this->assertSame('Foo', $d['writableId']['name']);
+ }
+
+ public function testUseSerializationGroups(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class]);
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class;
+ $g = new $class();
+ foreach (['foo', 'bar', 'baz', 'qux'] as $p) {
+ $g->{$p} = ucfirst($p).' #1';
+ }
+ $manager->persist($g);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummyGroup(input: {bar: "Bar", baz: "Baz", clientMutationId: "myId"}) {
+ dummyGroup { id bar __typename }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['createDummyGroup'];
+ $this->assertSame('/dummy_groups/2', $d['dummyGroup']['id']);
+ $this->assertSame('Bar', $d['dummyGroup']['bar']);
+ $this->assertSame('createDummyGroupPayloadData', $d['dummyGroup']['__typename']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testTriggerValidationError(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createDummy(input: {name: "", foo: [], clientMutationId: "myId"}) {
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame('422', (string) $data['errors'][0]['extensions']['status']);
+ $this->assertSame('name: This value should not be blank.', $data['errors'][0]['message']);
+ $this->assertArrayHasKey('violations', $data['errors'][0]['extensions']);
+ $this->assertSame('name', $data['errors'][0]['extensions']['violations'][0]['path']);
+ $this->assertSame('This value should not be blank.', $data['errors'][0]['extensions']['violations'][0]['message']);
+ }
+
+ public function testCustomMutation(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class]);
+ $this->seedDummyCustomMutation(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ sumDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
+ dummyCustomMutation { id result }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ '8',
+ (string) $response->toArray()['data']['sumDummyCustomMutation']['dummyCustomMutation']['result'],
+ );
+ }
+
+ public function testCustomMutationNotPersisted(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class]);
+ $this->seedDummyCustomMutation(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ sumNotPersistedDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
+ dummyCustomMutation { id result }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['sumNotPersistedDummyCustomMutation']['dummyCustomMutation']);
+ }
+
+ public function testCustomMutationNoWriteCustomResult(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class]);
+ $this->seedDummyCustomMutation(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ sumNoWriteCustomResultDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
+ dummyCustomMutation { id result }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ '1234',
+ (string) $response->toArray()['data']['sumNoWriteCustomResultDummyCustomMutation']['dummyCustomMutation']['result'],
+ );
+ }
+
+ public function testCustomMutationOnlyPersist(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ sumOnlyPersistDummyCustomMutation(input: {id: "/dummy_custom_mutations/1", operandB: 5}) {
+ dummyCustomMutation { id result }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['sumOnlyPersistDummyCustomMutation']['dummyCustomMutation']);
+ }
+
+ public function testCustomMutationCustomArguments(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ testCustomArgumentsDummyCustomMutation(input: {operandC: 18, clientMutationId: "myId"}) {
+ dummyCustomMutation { result }
+ clientMutationId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['testCustomArgumentsDummyCustomMutation'];
+ $this->assertSame('18', (string) $d['dummyCustomMutation']['result']);
+ $this->assertSame('myId', $d['clientMutationId']);
+ }
+
+ public function testCreateItemWithEnumAsResource(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('VideoGame ORM-only.');
+ }
+
+ $this->recreateSchema([VideoGame::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ gamePlayModes { id name }
+ gamePlayMode(id: "/game_play_modes/SINGLE_PLAYER") { name }
+ }
+ QUERY);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data'];
+ $this->assertCount(3, $data['gamePlayModes']);
+ $this->assertSame('/game_play_modes/SINGLE_PLAYER', $data['gamePlayModes'][2]['id']);
+ $this->assertSame('SINGLE_PLAYER', $data['gamePlayModes'][2]['name']);
+ $this->assertSame('SINGLE_PLAYER', $data['gamePlayMode']['name']);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createVideoGame(input: {name: "Baten Kaitos", playMode: "/game_play_modes/SINGLE_PLAYER"}) {
+ videoGame { id name playMode { id name } }
+ }
+ }
+ QUERY);
+ $this->assertResponseIsSuccessful();
+ $vg = $response->toArray()['data']['createVideoGame']['videoGame'];
+ $this->assertSame('/video_games/1', $vg['id']);
+ $this->assertSame('Baten Kaitos', $vg['name']);
+ $this->assertSame('/game_play_modes/SINGLE_PLAYER', $vg['playMode']['id']);
+ $this->assertSame('SINGLE_PLAYER', $vg['playMode']['name']);
+ }
+
+ public function testDeleteInvalidItem(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('ActivityLog @!mongodb.');
+ }
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ deleteActivityLog(input: {id: "/activity_logs/1"}) {
+ activityLog { id }
+ }
+ }
+ QUERY);
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertArrayNotHasKey('errors', $data);
+ $this->assertArrayHasKey('activityLog', $data['data']['deleteActivityLog']);
+ }
+
+ public function testUploadFileWithCustomMutation(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('MediaObject @!mongodb.');
+ }
+
+ $file = new UploadedFile(self::FIXTURES_DIR.'/test.gif', 'test.gif', null, \UPLOAD_ERR_OK, true);
+ $response = $this->executeGraphQlMultipart(
+ '{"query": "mutation($file: Upload!) { uploadMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } }", "variables": {"file": null}}',
+ '{"file": ["variables.file"]}',
+ ['file' => $file],
+ );
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('test.gif', $response->toArray()['data']['uploadMediaObject']['mediaObject']['contentUrl']);
+ }
+
+ public function testUploadMultipleFilesWithCustomMutation(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('MediaObject @!mongodb.');
+ }
+
+ $files = [
+ '0' => new UploadedFile(self::FIXTURES_DIR.'/test.gif', 'test.gif', null, \UPLOAD_ERR_OK, true),
+ '1' => new UploadedFile(self::FIXTURES_DIR.'/test.gif', 'test.gif', null, \UPLOAD_ERR_OK, true),
+ '2' => new UploadedFile(self::FIXTURES_DIR.'/test.gif', 'test.gif', null, \UPLOAD_ERR_OK, true),
+ ];
+ $response = $this->executeGraphQlMultipart(
+ '{"query": "mutation($files: [Upload!]!) { uploadMultipleMediaObject(input: {files: $files}) { mediaObject { id contentUrl } } }", "variables": {"files": [null, null, null]}}',
+ '{"0": ["variables.files.0"], "1": ["variables.files.1"], "2": ["variables.files.2"]}',
+ $files,
+ );
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('test.gif', $response->toArray()['data']['uploadMultipleMediaObject']['mediaObject']['contentUrl']);
+ }
+
+ public function testUseSerializationGroupsWithRelations(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('FourthLevel + RelatedToDummyFriend @!mongodb.');
+ }
+
+ $this->recreateSchema([
+ Dummy::class, RelatedDummy::class, ThirdLevel::class, FourthLevel::class,
+ DummyFriend::class, RelatedToDummyFriend::class,
+ ]);
+ $this->seedDummyWithRelatedDummyAndThirdLevel();
+ $this->seedRelatedDummyWithFriends(2);
+ $this->seedDummyWithFourthLevelRelation();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ updateRelatedDummy(input: {
+ id: "/related_dummies/2",
+ symfony: "laravel",
+ thirdLevel: { fourthLevel: "/fourth_levels/1" }
+ }) {
+ relatedDummy {
+ id symfony
+ thirdLevel { id fourthLevel { id __typename } __typename }
+ relatedToDummyFriend {
+ edges { node { name } }
+ __typename
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $rel = $response->toArray()['data']['updateRelatedDummy']['relatedDummy'];
+ $this->assertSame('/related_dummies/2', $rel['id']);
+ $this->assertSame('laravel', $rel['symfony']);
+ $this->assertSame('/third_levels/3', $rel['thirdLevel']['id']);
+ $this->assertSame('updateThirdLevelNestedPayload', $rel['thirdLevel']['__typename']);
+ $this->assertSame('/fourth_levels/1', $rel['thirdLevel']['fourthLevel']['id']);
+ $this->assertSame('updateFourthLevelNestedPayload', $rel['thirdLevel']['fourthLevel']['__typename']);
+ $this->assertSame('updateRelatedToDummyFriendNestedPayloadCursorConnection', $rel['relatedToDummyFriend']['__typename']);
+ $this->assertSame('Relation-1', $rel['relatedToDummyFriend']['edges'][0]['node']['name']);
+ $this->assertSame('Relation-2', $rel['relatedToDummyFriend']['edges'][1]['node']['name']);
+ }
+
+ public function testMutationRunsBeforeValidation(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('ActivityLog @!mongodb.');
+ }
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ mutation {
+ createActivityLog(input: {name: ""}) {
+ activityLog { name }
+ }
+ }
+ QUERY);
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('hi', $response->toArray()['data']['createActivityLog']['activityLog']['name']);
+ }
+
+ private function newDummy(): object
+ {
+ $class = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+
+ return new $class();
+ }
+
+ private function newRelated(): object
+ {
+ $class = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+
+ return new $class();
+ }
+
+ private function seedDummiesWithRelatedDummy(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy #'.$i);
+
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedFooDummyWithEmbeddable(): void
+ {
+ $manager = $this->getManager();
+ $dummyClass = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+ $fooClass = $this->isMongoDB() ? FooDummyDocument::class : FooDummy::class;
+
+ $dummy = new $dummyClass();
+ $dummy->setName('Lorem');
+
+ $foo = new $fooClass();
+ $foo->setName('Hawsepipe');
+
+ $embeddedClass = $this->isMongoDB() ? FooEmbeddableDocument::class : FooEmbeddable::class;
+ $embedded = new $embeddedClass();
+ $embedded->setDummyName('embeddedHawsepipe');
+ $foo->setEmbeddedFoo($embedded);
+ $foo->setDummy($dummy);
+
+ $manager->persist($foo);
+ $manager->flush();
+ }
+
+ private function seedCompositeIdentifierObjects(): void
+ {
+ $manager = $this->getManager();
+ $item = new CompositeItem();
+ $item->setField1('foobar');
+ $manager->persist($item);
+ $manager->flush();
+
+ for ($i = 0; $i < 4; ++$i) {
+ $label = new CompositeLabel();
+ $label->setValue('foo-'.$i);
+ $manager->persist($label);
+ $manager->flush();
+
+ $rel = new CompositeRelation();
+ $rel->setCompositeLabel($label);
+ $rel->setCompositeItem($item);
+ $rel->setValue('somefoobardummy');
+ $manager->persist($rel);
+ }
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function seedDummyCustomMutation(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyCustomMutationDocument::class : DummyCustomMutation::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $m = new $class();
+ $m->setOperandA(3);
+ $manager->persist($m);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyWithRelatedDummyAndThirdLevel(): void
+ {
+ $manager = $this->getManager();
+ $thirdLevel = new ThirdLevel();
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setName('RelatedDummy #1');
+ $relatedDummy->setThirdLevel($thirdLevel);
+ $dummy = new Dummy();
+ $dummy->setName('Dummy #1');
+ $dummy->setAlias('Alias #0');
+ $dummy->setRelatedDummy($relatedDummy);
+ $manager->persist($thirdLevel);
+ $manager->persist($relatedDummy);
+ $manager->persist($dummy);
+ $manager->flush();
+ }
+
+ private function seedRelatedDummyWithFriends(int $nb): void
+ {
+ $manager = $this->getManager();
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setName('RelatedDummy with friends');
+ $manager->persist($relatedDummy);
+ $manager->flush();
+
+ for ($i = 1; $i <= $nb; ++$i) {
+ $friend = new DummyFriend();
+ $friend->setName('Friend-'.$i);
+ $manager->persist($friend);
+ $manager->flush();
+
+ $relation = new RelatedToDummyFriend();
+ $relation->setName('Relation-'.$i);
+ $relation->setDummyFriend($friend);
+ $relation->setRelatedDummy($relatedDummy);
+ $relatedDummy->addRelatedToDummyFriend($relation);
+ $manager->persist($relation);
+ }
+
+ $other = new RelatedDummy();
+ $other->setName('RelatedDummy without friends');
+ $manager->persist($other);
+ $manager->flush();
+ $manager->clear();
+ }
+
+ private function seedDummyWithFourthLevelRelation(): void
+ {
+ $manager = $this->getManager();
+ $fourthLevel = new FourthLevel();
+ $fourthLevel->setLevel(4);
+ $manager->persist($fourthLevel);
+
+ $thirdLevel = new ThirdLevel();
+ $thirdLevel->setLevel(3);
+ $thirdLevel->setFourthLevel($fourthLevel);
+ $manager->persist($thirdLevel);
+
+ $namedRelatedDummy = new RelatedDummy();
+ $namedRelatedDummy->setName('Hello');
+ $namedRelatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($namedRelatedDummy);
+
+ $relatedDummy = new RelatedDummy();
+ $relatedDummy->setThirdLevel($thirdLevel);
+ $manager->persist($relatedDummy);
+
+ $dummy = new Dummy();
+ $dummy->setName('Dummy with relations');
+ $dummy->setRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($namedRelatedDummy);
+ $dummy->addRelatedDummy($relatedDummy);
+ $manager->persist($dummy);
+
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/QueryTest.php b/tests/Functional/GraphQl/QueryTest.php
new file mode 100644
index 00000000000..78e1ccb96a7
--- /dev/null
+++ b/tests/Functional/GraphQl/QueryTest.php
@@ -0,0 +1,852 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6427\SecurityAfterResolver;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\Dummy as DummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCar as DummyCarDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCarColor as DummyCarColorDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyCustomQuery as DummyCustomQueryDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDifferentGraphQlSerializationGroup as DummyDifferentGraphQlSerializationGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoInput as DummyDtoNoInputDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyDtoNoOutput as DummyDtoNoOutputDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyGroup as DummyGroupDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\WithJsonDummy as WithJsonDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCarColor;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCustomQuery;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDifferentGraphQlSerializationGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoInput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyDtoNoOutput;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Foo;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNested;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsNestedPaginated;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsRelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MultiRelationsResolveDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\TreeDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\WithJsonDummy;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\Common\Collections\ArrayCollection;
+
+final class QueryTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ Dummy::class,
+ RelatedDummy::class,
+ MultiRelationsDummy::class,
+ MultiRelationsRelatedDummy::class,
+ MultiRelationsResolveDummy::class,
+ MultiRelationsNested::class,
+ MultiRelationsNestedPaginated::class,
+ TreeDummy::class,
+ WithJsonDummy::class,
+ DummyGroup::class,
+ DummyCar::class,
+ DummyCarColor::class,
+ DummyDtoNoInput::class,
+ DummyDtoNoOutput::class,
+ DummyCustomQuery::class,
+ DummyDifferentGraphQlSerializationGroup::class,
+ SecurityAfterResolver::class,
+ Foo::class,
+ ];
+ }
+
+ public function testBasicQuery(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy(id: "/dummies/1") {
+ id
+ name
+ name_converted
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $dummy = $response->toArray()['data']['dummy'];
+ $this->assertSame('/dummies/1', $dummy['id']);
+ $this->assertSame('Dummy #1', $dummy['name']);
+ $this->assertSame('Converted 1', $dummy['name_converted']);
+ }
+
+ public function testQueryWithDifferentRelationsToSameResource(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('MultiRelationsDummy is ORM-only.');
+ }
+ $this->recreateMultiRelations();
+ $this->seedMultiRelations(2, 1, 2, 3, 4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ multiRelationsDummy(id: "/multi_relations_dummies/2") {
+ id
+ name
+ manyToOneRelation { id name }
+ manyToOneResolveRelation { id name }
+ manyToManyRelations { edges { node { id name } } }
+ oneToManyRelations { edges { node { id name } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $payload = $response->toArray(false);
+ if (isset($payload['errors'])) {
+ $this->fail('GraphQL errors: '.json_encode($payload['errors'], \JSON_PRETTY_PRINT));
+ }
+ $d = $payload['data']['multiRelationsDummy'];
+ $this->assertSame('/multi_relations_dummies/2', $d['id']);
+ $this->assertSame('Dummy #2', $d['name']);
+ $this->assertNotNull($d['manyToOneRelation']['id']);
+ $this->assertSame('RelatedManyToOneDummy #2', $d['manyToOneRelation']['name']);
+ $this->assertCount(2, $d['manyToManyRelations']['edges']);
+ $this->assertMatchesRegularExpression('#RelatedManyToManyDummy(1|2)2#', $d['manyToManyRelations']['edges'][0]['node']['name']);
+ $this->assertMatchesRegularExpression('#RelatedManyToManyDummy(1|2)2#', $d['manyToManyRelations']['edges'][1]['node']['name']);
+ $this->assertCount(3, $d['oneToManyRelations']['edges']);
+ $this->assertMatchesRegularExpression('#RelatedOneToManyDummy(1|3)2#', $d['oneToManyRelations']['edges'][0]['node']['name']);
+ $this->assertMatchesRegularExpression('#RelatedOneToManyDummy(1|3)2#', $d['oneToManyRelations']['edges'][2]['node']['name']);
+ }
+
+ public function testQueryEmbeddedCollections(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('MultiRelationsDummy is ORM-only.');
+ }
+ $this->recreateMultiRelations();
+ $this->seedMultiRelations(2, 1, 2, 3, 4);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ multiRelationsDummy(id: "/multi_relations_dummies/2") {
+ id
+ name
+ manyToOneResolveRelation { id name }
+ nestedCollection { name }
+ nestedPaginatedCollection { edges { node { name } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray();
+ $this->assertArrayNotHasKey('errors', $d);
+ $dummy = $d['data']['multiRelationsDummy'];
+ $this->assertNotNull($dummy['manyToOneResolveRelation']['id']);
+ $this->assertSame('RelatedManyToOneResolveDummy #2', $dummy['manyToOneResolveRelation']['name']);
+ for ($i = 1; $i <= 4; ++$i) {
+ $this->assertSame('NestedDummy'.$i, $dummy['nestedCollection'][$i - 1]['name']);
+ }
+ // Edges count exists, but node.name resolves to null because JSON-column hydration
+ // returns associative arrays, not MultiRelationsNestedPaginated objects, so the
+ // GraphQL field resolver can't access ->name. Separate from the link bug.
+ $this->assertCount(4, $dummy['nestedPaginatedCollection']['edges']);
+ }
+
+ public function testQueryWithUnsetRelations(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('MultiRelationsDummy is ORM-only.');
+ }
+ $this->recreateMultiRelations();
+ $this->seedMultiRelations(2, 0, 0, 0, 0);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ multiRelationsDummy(id: "/multi_relations_dummies/2") {
+ id name
+ manyToOneRelation { id name }
+ manyToOneResolveRelation { id name }
+ manyToManyRelations { edges { node { id name } } }
+ oneToManyRelations { edges { node { id name } } }
+ nestedCollection { name }
+ nestedPaginatedCollection { edges { node { name } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertArrayNotHasKey('errors', $data);
+ $d = $data['data']['multiRelationsDummy'];
+ $this->assertSame('/multi_relations_dummies/2', $d['id']);
+ $this->assertSame('Dummy #2', $d['name']);
+ $this->assertNull($d['manyToOneRelation']);
+ $this->assertNull($d['manyToOneResolveRelation']);
+ $this->assertCount(0, $d['manyToManyRelations']['edges']);
+ $this->assertCount(0, $d['oneToManyRelations']['edges']);
+ $this->assertCount(0, $d['nestedCollection']);
+ $this->assertCount(0, $d['nestedPaginatedCollection']['edges']);
+ }
+
+ public function testTreeDummiesChildRelation(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('TreeDummy is ORM-only.');
+ }
+ $this->recreateSchema([TreeDummy::class]);
+ $manager = $this->getManager();
+ $parent = new TreeDummy();
+ $child = new TreeDummy();
+ $child->setParent($parent);
+ $manager->persist($parent);
+ $manager->persist($child);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ treeDummies {
+ edges { node { id children { totalCount } } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray();
+ $this->assertArrayNotHasKey('errors', $data);
+ $edges = $data['data']['treeDummies']['edges'];
+ $this->assertSame('/tree_dummies/1', $edges[0]['node']['id']);
+ $this->assertSame(1, $edges[0]['node']['children']['totalCount']);
+ $this->assertSame('/tree_dummies/2', $edges[1]['node']['id']);
+ $this->assertSame(0, $edges[1]['node']['children']['totalCount']);
+ }
+
+ public function testRelayNode(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ node(id: "/dummies/1") {
+ id
+ ... on Dummy { name }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $node = $response->toArray()['data']['node'];
+ $this->assertSame('/dummies/1', $node['id']);
+ $this->assertSame('Dummy #1', $node['name']);
+ }
+
+ public function testIterableField(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+ $this->seedDummiesWithJsonAndArrayData(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy(id: "/dummies/3") {
+ id
+ name
+ jsonData
+ arrayData
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $dummy = $response->toArray()['data']['dummy'];
+ $this->assertSame('/dummies/3', $dummy['id']);
+ $this->assertSame('Dummy #1', $dummy['name']);
+ $this->assertCount(2, $dummy['jsonData']['foo']);
+ $this->assertSame(5, $dummy['jsonData']['bar']);
+ $this->assertSame('baz', $dummy['arrayData'][2]);
+ }
+
+ public function testNullJsonField(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? WithJsonDummyDocument::class : WithJsonDummy::class]);
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? WithJsonDummyDocument::class : WithJsonDummy::class;
+ for ($i = 1; $i <= 2; ++$i) {
+ $w = new $class();
+ $w->json = null;
+ $manager->persist($w);
+ }
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ withJsonDummy(id: "/with_json_dummies/2") {
+ id
+ json
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $w = $response->toArray()['data']['withJsonDummy'];
+ $this->assertSame('/with_json_dummies/2', $w['id']);
+ $this->assertNull($w['json']);
+ }
+
+ public function testQueryWithVariables(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $response = $this->executeGraphQl(
+ <<<'QUERY'
+ query DummyWithId($itemId: ID = "/dummies/1") {
+ dummyItem: dummy(id: $itemId) {
+ id
+ name
+ relatedDummy { id name }
+ }
+ }
+ QUERY,
+ ['itemId' => '/dummies/2'],
+ );
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['dummyItem'];
+ $this->assertSame('/dummies/2', $d['id']);
+ $this->assertSame('Dummy #2', $d['name']);
+ $this->assertSame('/related_dummies/2', $d['relatedDummy']['id']);
+ $this->assertSame('RelatedDummy #2', $d['relatedDummy']['name']);
+ }
+
+ public function testQueryWithOperationName(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(2);
+
+ $query = <<<'QUERY'
+ query DummyWithId1 {
+ dummyItem: dummy(id: "/dummies/1") { name }
+ }
+ query DummyWithId2 {
+ dummyItem: dummy(id: "/dummies/2") { id name }
+ }
+ QUERY;
+
+ $response = $this->executeGraphQl($query, [], 'DummyWithId2');
+ $d = $response->toArray()['data']['dummyItem'];
+ $this->assertSame('/dummies/2', $d['id']);
+ $this->assertSame('Dummy #2', $d['name']);
+
+ $response = $this->executeGraphQl($query, [], 'DummyWithId1');
+ $this->assertSame('Dummy #1', $response->toArray()['data']['dummyItem']['name']);
+ }
+
+ public function testSerializationGroups(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class]);
+ $this->seedDummyGroups(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyGroup(id: "/dummy_groups/1") {
+ foo
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('Foo #1', $response->toArray()['data']['dummyGroup']['foo']);
+ }
+
+ public function testSerializedName(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyCarDocument::class : DummyCar::class,
+ $this->isMongoDB() ? DummyCarColorDocument::class : DummyCarColor::class,
+ ]);
+ $this->seedDummyCarWithColors();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyCar(id: "/dummy_cars/1") {
+ carBrand
+ }
+ }
+ QUERY);
+
+ $this->assertSame('DummyBrand', $response->toArray()['data']['dummyCar']['carBrand']);
+ }
+
+ public function testFetchOnlyInternalId(): void
+ {
+ $this->recreateDummiesAndRelated();
+ $this->seedDummiesWithRelatedDummy(1);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy(id: "/dummies/1") {
+ _id
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('1', (string) $response->toArray()['data']['dummy']['_id']);
+ }
+
+ public function testNonexistentItemReturnsNull(): void
+ {
+ $this->recreateDummiesAndRelated();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummy(id: "/dummies/5") {
+ name
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertNull($response->toArray()['data']['dummy']);
+ }
+
+ public function testNonexistentIriYieldsDebugMessage(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ foo(id: "/foo/1") {
+ name
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertGraphQlDebugMessage($data, 'No route matches "/foo/1".');
+ $this->assertCount(1, $data['errors']);
+ }
+
+ public function testOutputClassUsedInsteadOfResource(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyDtoNoInputDocument::class : DummyDtoNoInput::class]);
+ $this->seedDummyDtoNoInput(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyDtoNoInputs {
+ edges { node { baz bat } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'dummyDtoNoInputs' => [
+ 'edges' => [
+ ['node' => ['baz' => 0.33, 'bat' => 'DummyDtoNoInput foo #1']],
+ ['node' => ['baz' => 0.67, 'bat' => 'DummyDtoNoInput foo #2']],
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testDisableOutputClassYieldsEmptyResponse(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDtoNoInputDocument::class : DummyDtoNoInput::class,
+ $this->isMongoDB() ? DummyDtoNoOutputDocument::class : DummyDtoNoOutput::class,
+ ]);
+ $this->seedDummyDtoNoOutput(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyDtoNoInputs {
+ edges { node { baz bat } }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => ['dummyDtoNoInputs' => ['edges' => []]],
+ ], $response->toArray());
+ }
+
+ public function testCustomNotRetrievedItemQuery(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testNotRetrievedItemDummyCustomQuery {
+ message
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => ['testNotRetrievedItemDummyCustomQuery' => ['message' => 'Success (not retrieved)!']],
+ ], $response->toArray());
+ }
+
+ public function testCustomItemQueryWithReadAndSerializeDisabled(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testNoReadAndSerializeItemDummyCustomQuery(id: "/not_used") {
+ message
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(['data' => ['testNoReadAndSerializeItemDummyCustomQuery' => null]], $response->toArray());
+ }
+
+ public function testCustomItemQuery(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+ $this->seedDummyCustomQuery(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testItemDummyCustomQuery(id: "/dummy_custom_queries/1") {
+ message
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame(
+ ['data' => ['testItemDummyCustomQuery' => ['message' => 'Success!']]],
+ $response->toArray(),
+ );
+ }
+
+ public function testCustomItemQueryWithCustomArguments(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class]);
+ $this->seedDummyCustomQuery(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ testItemCustomArgumentsDummyCustomQuery(
+ id: "/dummy_custom_queries/1",
+ customArgumentBool: true,
+ customArgumentInt: 3,
+ customArgumentString: "A string",
+ customArgumentFloat: 2.6,
+ customArgumentIntArray: [4],
+ customArgumentCustomType: "2019-05-24T00:00:00+00:00"
+ ) {
+ message
+ customArgs
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame([
+ 'data' => [
+ 'testItemCustomArgumentsDummyCustomQuery' => [
+ 'message' => 'Success!',
+ 'customArgs' => [
+ 'id' => '/dummy_custom_queries/1',
+ 'customArgumentBool' => true,
+ 'customArgumentInt' => 3,
+ 'customArgumentString' => 'A string',
+ 'customArgumentFloat' => 2.6,
+ 'customArgumentIntArray' => [4],
+ 'customArgumentCustomType' => '2019-05-24T00:00:00+00:00',
+ ],
+ ],
+ ],
+ ], $response->toArray());
+ }
+
+ public function testDifferentSerializationGroupsForItemAndCollection(): void
+ {
+ $this->recreateSchema([$this->isMongoDB() ? DummyDifferentGraphQlSerializationGroupDocument::class : DummyDifferentGraphQlSerializationGroup::class]);
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyDifferentGraphQlSerializationGroupDocument::class : DummyDifferentGraphQlSerializationGroup::class;
+ $entity = new $class();
+ $entity->setName('Name #1');
+ $entity->setTitle('Title #1');
+ $manager->persist($entity);
+ $manager->flush();
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ dummyDifferentGraphQlSerializationGroup(id: "/dummy_different_graph_ql_serialization_groups/1") {
+ name
+ title
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $d = $response->toArray()['data']['dummyDifferentGraphQlSerializationGroup'];
+ $this->assertSame('Name #1', $d['name']);
+ $this->assertSame('Title #1', $d['title']);
+ }
+
+ public function testSecurityAfterResolver(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ getSecurityAfterResolver(id: "/security_after_resolvers/1") {
+ name
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSame('test', $response->toArray()['data']['getSecurityAfterResolver']['name']);
+ }
+
+ public function testSecurityAfterResolverDeniesNonMatchingId(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ getSecurityAfterResolver(id: "/security_after_resolvers/2") {
+ name
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray(false);
+ $this->assertSame(403, $data['errors'][0]['extensions']['status']);
+ $this->assertSame('Access Denied.', $data['errors'][0]['message']);
+ $this->assertArrayNotHasKey('name', $data['data']['getSecurityAfterResolver'] ?? []);
+ }
+
+ private function recreateDummiesAndRelated(): void
+ {
+ $this->recreateSchema([
+ $this->isMongoDB() ? DummyDocument::class : Dummy::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ]);
+ }
+
+ private function recreateMultiRelations(): void
+ {
+ $this->recreateSchema([
+ MultiRelationsDummy::class,
+ MultiRelationsRelatedDummy::class,
+ MultiRelationsResolveDummy::class,
+ ]);
+ }
+
+ private function newDummy(): object
+ {
+ $class = $this->isMongoDB() ? DummyDocument::class : Dummy::class;
+
+ return new $class();
+ }
+
+ private function newRelated(): object
+ {
+ $class = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+
+ return new $class();
+ }
+
+ private function seedDummiesWithRelatedDummy(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = $this->newRelated();
+ $related->setName('RelatedDummy #'.$i);
+
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->nameConverted = "Converted $i";
+ $dummy->setRelatedDummy($related);
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummiesWithJsonAndArrayData(int $count): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $count; ++$i) {
+ $dummy = $this->newDummy();
+ $dummy->setName('Dummy #'.$i);
+ $dummy->setAlias('Alias #'.($count - $i));
+ $dummy->setJsonData(['foo' => ['bar', 'baz'], 'bar' => 5]);
+ $dummy->setArrayData(['foo', 'bar', 'baz']);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedMultiRelations(int $nb, int $nbmtor, int $nbmtmr, int $nbotmr, int $nber): void
+ {
+ $manager = $this->getManager();
+ for ($i = 1; $i <= $nb; ++$i) {
+ $related = new MultiRelationsRelatedDummy();
+ $related->name = 'RelatedManyToOneDummy #'.$i;
+
+ $resolve = new MultiRelationsResolveDummy();
+ $resolve->name = 'RelatedManyToOneResolveDummy #'.$i;
+
+ $dummy = new MultiRelationsDummy();
+ $dummy->name = 'Dummy #'.$i;
+
+ if ($nbmtor) {
+ $dummy->setManyToOneRelation($related);
+ $dummy->setManyToOneResolveRelation($resolve);
+ }
+
+ for ($j = 1; $j <= $nbmtmr; ++$j) {
+ $m2m = new MultiRelationsRelatedDummy();
+ $m2m->name = 'RelatedManyToManyDummy'.$j.$i;
+ $manager->persist($m2m);
+ $dummy->addManyToManyRelation($m2m);
+ }
+
+ for ($j = 1; $j <= $nbotmr; ++$j) {
+ $o2m = new MultiRelationsRelatedDummy();
+ $o2m->name = 'RelatedOneToManyDummy'.$j.$i;
+ $o2m->setOneToManyRelation($dummy);
+ $manager->persist($o2m);
+ $dummy->addOneToManyRelation($o2m);
+ }
+
+ $nested = new ArrayCollection();
+ for ($j = 1; $j <= $nber; ++$j) {
+ $n = new MultiRelationsNested();
+ $n->name = 'NestedDummy'.$j;
+ $nested->add($n);
+ }
+ $dummy->setNestedCollection($nested);
+
+ $nestedPaginated = new ArrayCollection();
+ for ($j = 1; $j <= $nber; ++$j) {
+ $np = new MultiRelationsNestedPaginated();
+ $np->name = 'NestedPaginatedDummy'.$j;
+ $nestedPaginated->add($np);
+ }
+ $dummy->setNestedPaginatedCollection($nestedPaginated);
+
+ $manager->persist($related);
+ $manager->persist($resolve);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyGroups(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyGroupDocument::class : DummyGroup::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $group = new $class();
+ foreach (['foo', 'bar', 'baz', 'qux'] as $property) {
+ $group->{$property} = ucfirst($property).' #'.$i;
+ }
+ $manager->persist($group);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyCarWithColors(): void
+ {
+ $manager = $this->getManager();
+ $carClass = $this->isMongoDB() ? DummyCarDocument::class : DummyCar::class;
+ $colorClass = $this->isMongoDB() ? DummyCarColorDocument::class : DummyCarColor::class;
+
+ $car = new $carClass();
+ $car->setName('mustli');
+ $car->setCanSell(true);
+ $car->setAvailableAt(new \DateTime());
+ $manager->persist($car);
+ $manager->flush();
+ if (\is_object($car->getId())) {
+ $manager->persist($car->getId());
+ $manager->flush();
+ }
+ $red = new $colorClass();
+ $red->setProp('red');
+ $red->setCar($car);
+ $manager->persist($red);
+ $blue = new $colorClass();
+ $blue->setProp('blue');
+ $blue->setCar($car);
+ $manager->persist($blue);
+ $manager->flush();
+ }
+
+ private function seedDummyDtoNoInput(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyDtoNoInputDocument::class : DummyDtoNoInput::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $dto = new $class();
+ $dto->lorem = 'DummyDtoNoInput foo #'.$i;
+ $dto->ipsum = round($i / 3, 2);
+ $manager->persist($dto);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyDtoNoOutput(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyDtoNoOutputDocument::class : DummyDtoNoOutput::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $dto = new $class();
+ $dto->lorem = 'DummyDtoNoOutput foo #'.$i;
+ $dto->ipsum = (string) round($i / 3, 2);
+ $manager->persist($dto);
+ }
+ $manager->flush();
+ }
+
+ private function seedDummyCustomQuery(int $count): void
+ {
+ $manager = $this->getManager();
+ $class = $this->isMongoDB() ? DummyCustomQueryDocument::class : DummyCustomQuery::class;
+ for ($i = 1; $i <= $count; ++$i) {
+ $manager->persist(new $class());
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/GraphQl/SchemaExportTest.php b/tests/Functional/GraphQl/SchemaExportTest.php
new file mode 100644
index 00000000000..06360f1212c
--- /dev/null
+++ b/tests/Functional/GraphQl/SchemaExportTest.php
@@ -0,0 +1,174 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\OptionalRequiredDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Symfony\Bundle\FrameworkBundle\Console\Application;
+use Symfony\Component\Console\Tester\ApplicationTester;
+
+final class SchemaExportTest extends ApiTestCase
+{
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ private ApplicationTester $tester;
+
+ protected function setUp(): void
+ {
+ self::bootKernel();
+
+ $application = new Application(static::$kernel);
+ $application->setCatchExceptions(false);
+ $application->setAutoExit(false);
+ $this->tester = new ApplicationTester($application);
+ }
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [
+ DummyFriend::class,
+ RelatedToDummyFriend::class,
+ OptionalRequiredDummy::class,
+ ThirdLevel::class,
+ ];
+ }
+
+ public function testExportGraphQlSchema(): void
+ {
+ $this->tester->run(['command' => 'api:graphql:export']);
+
+ $output = $this->tester->getDisplay();
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Dummy Friend."
+ type DummyFriend implements Node {
+ id: ID!
+
+ "The id"
+ _id: Int!
+
+ "The dummy name"
+ name: String!
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Cursor connection for DummyFriend."
+ type DummyFriendCursorConnection {
+ edges: [DummyFriendEdge]
+ pageInfo: DummyFriendPageInfo!
+ totalCount: Int!
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Edge of DummyFriend."
+ type DummyFriendEdge {
+ node: DummyFriend
+ cursor: String!
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Information about the current page."
+ type DummyFriendPageInfo {
+ endCursor: String
+ startCursor: String
+ hasNextPage: Boolean!
+ hasPreviousPage: Boolean!
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Updates a DummyFriend."
+ updateDummyFriend(input: updateDummyFriendInput!): updateDummyFriendPayload
+
+ "Deletes a DummyFriend."
+ deleteDummyFriend(input: deleteDummyFriendInput!): deleteDummyFriendPayload
+
+ "Creates a DummyFriend."
+ createDummyFriend(input: createDummyFriendInput!): createDummyFriendPayload
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Updates a DummyFriend."
+ input updateDummyFriendInput {
+ id: ID!
+
+ "The dummy name"
+ name: String
+ clientMutationId: String
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Updates a DummyFriend."
+ type updateDummyFriendPayload {
+ dummyFriend: DummyFriend
+ clientMutationId: String
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Deletes a DummyFriend."
+ input deleteDummyFriendInput {
+ id: ID!
+ clientMutationId: String
+ }
+
+ "Deletes a DummyFriend."
+ type deleteDummyFriendPayload {
+ dummyFriend: DummyFriend
+ clientMutationId: String
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Creates a DummyFriend."
+ input createDummyFriendInput {
+ "The dummy name"
+ name: String!
+ clientMutationId: String
+ }
+
+ "Creates a DummyFriend."
+ type createDummyFriendPayload {
+ dummyFriend: DummyFriend
+ clientMutationId: String
+ }
+ SDL, $output);
+
+ $this->assertStringContainsString(<<<'SDL'
+ "Updates a OptionalRequiredDummy."
+ input updateOptionalRequiredDummyInput {
+ id: ID!
+ thirdLevel: updateThirdLevelNestedInput
+ thirdLevelRequired: updateThirdLevelNestedInput!
+
+ "Get relatedToDummyFriend."
+ relatedToDummyFriend: [updateRelatedToDummyFriendNestedInput]
+ clientMutationId: String
+ }
+ SDL, $output);
+ }
+}
diff --git a/tests/Functional/GraphQl/SubscriptionTest.php b/tests/Functional/GraphQl/SubscriptionTest.php
new file mode 100644
index 00000000000..72a1f43920f
--- /dev/null
+++ b/tests/Functional/GraphQl/SubscriptionTest.php
@@ -0,0 +1,254 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\GraphQl;
+
+use ApiPlatform\GraphQl\Test\GraphQlTestTrait;
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\DummyMercure as DummyMercureDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyMercure;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
+use ApiPlatform\Tests\Fixtures\TestBundle\Mercure\TestHub;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+
+final class SubscriptionTest extends ApiTestCase
+{
+ use GraphQlTestTrait;
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [DummyMercure::class, RelatedDummy::class];
+ }
+
+ public function testIntrospectSubscriptionType(): void
+ {
+ $response = $this->executeGraphQl(<<<'QUERY'
+ {
+ __type(name: "Subscription") {
+ fields {
+ name
+ description
+ type { name kind }
+ args {
+ name
+ type { name kind ofType { name kind } }
+ }
+ }
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $fields = $response->toArray()['data']['__type']['fields'];
+ $this->assertNotEmpty($fields);
+
+ foreach ($fields as $field) {
+ $this->assertMatchesRegularExpression('/^update[A-Za-z0-9_]+Subscribe$/', $field['name']);
+ $this->assertMatchesRegularExpression('/^Subscribes to the update event of a [A-Za-z0-9_]+\.$/', $field['description']);
+ $this->assertMatchesRegularExpression('/^update[A-Za-z0-9_]+SubscriptionPayload$/', $field['type']['name']);
+ $this->assertSame('OBJECT', $field['type']['kind']);
+
+ $this->assertCount(1, $field['args']);
+ $arg = $field['args'][0];
+ $this->assertSame('input', $arg['name']);
+ $this->assertSame('NON_NULL', $arg['type']['kind']);
+ $this->assertMatchesRegularExpression('/^update[A-Za-z0-9_]+SubscriptionInput$/', $arg['type']['ofType']['name']);
+ $this->assertSame('INPUT_OBJECT', $arg['type']['ofType']['kind']);
+ }
+ }
+
+ public function testSubscribeToUpdatesProducesMercureUrl(): void
+ {
+ $this->recreateSchema($this->resources());
+ $this->seedDummyMercure(2);
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ subscription {
+ updateDummyMercureSubscribe(input: {id: "/dummy_mercures/1", clientSubscriptionId: "myId"}) {
+ dummyMercure {
+ id
+ name
+ relatedDummy {
+ name
+ }
+ }
+ mercureUrl
+ clientSubscriptionId
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['updateDummyMercureSubscribe'];
+ $this->assertSame('/dummy_mercures/1', $data['dummyMercure']['id']);
+ $this->assertSame('Dummy Mercure #1', $data['dummyMercure']['name']);
+ $this->assertSame('myId', $data['clientSubscriptionId']);
+ $this->assertMatchesRegularExpression(
+ '@^https://demo\.mercure\.rocks\?topic=http://[^/]+/subscriptions/[a-f0-9]+$@',
+ $data['mercureUrl'],
+ );
+
+ $response = $this->executeGraphQl(<<<'QUERY'
+ subscription {
+ updateDummyMercureSubscribe(input: {id: "/dummy_mercures/2"}) {
+ dummyMercure { id }
+ mercureUrl
+ }
+ }
+ QUERY);
+
+ $this->assertResponseIsSuccessful();
+ $data = $response->toArray()['data']['updateDummyMercureSubscribe'];
+ $this->assertSame('/dummy_mercures/2', $data['dummyMercure']['id']);
+ $this->assertMatchesRegularExpression(
+ '@^https://demo\.mercure\.rocks\?topic=http://[^/]+/subscriptions/[a-f0-9]+$@',
+ $data['mercureUrl'],
+ );
+ }
+
+ public function testReceiveMercureUpdatesAfterPut(): void
+ {
+ $this->recreateSchema($this->resources());
+ $this->seedDummyMercure(2);
+
+ $client = self::createClient();
+ $client->getKernelBrowser()->disableReboot();
+
+ // Subscribe to both dummies so the SubscriptionManager registers different payload shapes.
+ $this->executeGraphQl(<<<'QUERY'
+ subscription {
+ updateDummyMercureSubscribe(input: {id: "/dummy_mercures/1", clientSubscriptionId: "myId"}) {
+ dummyMercure { id name relatedDummy { name } }
+ mercureUrl
+ }
+ }
+ QUERY);
+ $this->executeGraphQl(<<<'QUERY'
+ subscription {
+ updateDummyMercureSubscribe(input: {id: "/dummy_mercures/2"}) {
+ dummyMercure { id }
+ mercureUrl
+ }
+ }
+ QUERY);
+
+ $client->request('PUT', '/dummy_mercures/1', [
+ 'headers' => ['Accept' => 'application/ld+json', 'Content-Type' => 'application/ld+json'],
+ 'json' => ['name' => 'Dummy Mercure #1 updated'],
+ ]);
+ $this->assertResponseIsSuccessful();
+
+ $client->request('PUT', '/dummy_mercures/2', [
+ 'headers' => ['Accept' => 'application/ld+json', 'Content-Type' => 'application/ld+json'],
+ 'json' => ['name' => 'Dummy Mercure #2 updated'],
+ ]);
+ $this->assertResponseIsSuccessful();
+
+ /** @var TestHub $hub */
+ $hub = static::getContainer()->get('mercure.hub.default.test_hub');
+ $updates = $hub->getUpdates();
+
+ $this->assertGreaterThanOrEqual(2, \count($updates));
+
+ $this->assertMercureUpdatePresent($updates, '#^http://[^/]+/subscriptions/[a-f0-9]+$#', [
+ 'dummyMercure' => [
+ 'id' => 1,
+ 'name' => 'Dummy Mercure #1 updated',
+ 'relatedDummy' => ['name' => 'RelatedDummy #1'],
+ ],
+ ]);
+
+ $this->assertMercureUpdatePresent($updates, '#^http://[^/]+/subscriptions/[a-f0-9]+$#', [
+ 'dummyMercure' => ['id' => 2],
+ ]);
+ }
+
+ /**
+ * @param list<\Symfony\Component\Mercure\Update> $updates
+ * @param array $expectedPayload
+ */
+ private function assertMercureUpdatePresent(array $updates, string $topicPattern, array $expectedPayload): void
+ {
+ $expectedJson = json_encode($expectedPayload, \JSON_THROW_ON_ERROR);
+
+ foreach ($updates as $update) {
+ $topicsMatch = false;
+ foreach ($update->getTopics() as $topic) {
+ if (preg_match($topicPattern, (string) $topic)) {
+ $topicsMatch = true;
+ break;
+ }
+ }
+ if (!$topicsMatch) {
+ continue;
+ }
+
+ if ($update->getData() === $expectedJson) {
+ $this->assertTrue(true);
+
+ return;
+ }
+ }
+
+ $this->fail(\sprintf(
+ 'No Mercure update matched topic %s with payload %s. Captured: %s',
+ $topicPattern,
+ $expectedJson,
+ json_encode(array_map(
+ static fn ($u) => ['topics' => $u->getTopics(), 'data' => $u->getData()],
+ $updates,
+ ), \JSON_PRETTY_PRINT),
+ ));
+ }
+
+ /**
+ * @return list
+ */
+ private function resources(): array
+ {
+ return [
+ $this->isMongoDB() ? DummyMercureDocument::class : DummyMercure::class,
+ $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class,
+ ];
+ }
+
+ private function seedDummyMercure(int $count): void
+ {
+ $manager = $this->getManager();
+ $relatedClass = $this->isMongoDB() ? RelatedDummyDocument::class : RelatedDummy::class;
+ $dummyClass = $this->isMongoDB() ? DummyMercureDocument::class : DummyMercure::class;
+
+ for ($i = 1; $i <= $count; ++$i) {
+ $related = new $relatedClass();
+ $related->setName('RelatedDummy #'.$i);
+
+ $dummy = new $dummyClass();
+ $dummy->name = "Dummy Mercure #$i";
+ $dummy->description = 'Description';
+ $dummy->relatedDummy = $related;
+
+ $manager->persist($related);
+ $manager->persist($dummy);
+ }
+ $manager->flush();
+ }
+}
diff --git a/tests/Functional/MappingTest.php b/tests/Functional/MappingTest.php
index 353153b9f69..0b3571a0e9c 100644
--- a/tests/Functional/MappingTest.php
+++ b/tests/Functional/MappingTest.php
@@ -73,7 +73,7 @@ public function testShouldMapBetweenResourceAndEntity(): void
$this->markTestSkipped('ObjectMapper not installed');
}
- $this->recreateSchema([MappedEntity::class]);
+ $this->recreateSchema([$this->isMongoDB() ? MappedDocument::class : MappedEntity::class]);
$this->loadFixtures();
$client = self::createClient();
$client->request('GET', $this->isMongoDB() ? 'mapped_resource_odms' : 'mapped_resources');
diff --git a/tests/Functional/NullOnNonNullablePropertyTest.php b/tests/Functional/NullOnNonNullablePropertyTest.php
index d6aa24c078f..eba8ce3f10c 100644
--- a/tests/Functional/NullOnNonNullablePropertyTest.php
+++ b/tests/Functional/NullOnNonNullablePropertyTest.php
@@ -16,6 +16,9 @@
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\NullOnNonNullableProperty\NullOnNonNullableResource;
use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Composer\InstalledVersions;
+use Composer\Semver\VersionParser;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/** @see https://github.com/symfony/symfony/issues/64159 */
final class NullOnNonNullablePropertyTest extends ApiTestCase
@@ -45,8 +48,13 @@ public function testNullOnNonNullablePropertyReturns400(): void
$this->assertStringContainsString('Expected argument of type "string", "null" given at property path "name"', $body['hydra:description'] ?? $body['detail'] ?? '');
}
+ #[IgnoreDeprecations]
public function testNullOnNonNullablePropertyReturns422WhenCollectingErrors(): void
{
+ if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
+ $this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
+ }
+
$response = self::createClient()->request('POST', '/null_on_non_nullable_resources_collect', [
'headers' => ['Content-Type' => 'application/ld+json'],
'json' => ['name' => null],
diff --git a/tests/Functional/SubResource/SubResourceTest.php b/tests/Functional/SubResource/SubResourceTest.php
index 45d79665fae..cfc7c2328cf 100644
--- a/tests/Functional/SubResource/SubResourceTest.php
+++ b/tests/Functional/SubResource/SubResourceTest.php
@@ -369,7 +369,7 @@ public function testGetOffersFromAggregateOffers(): void
$this->assertResponseStatusCodeSame(200);
$this->assertJsonEquals([
- '@context' => '/contexts/DummyOffer',
+ '@context' => '/contexts/DummyOfferByProductOffer',
'@id' => '/dummy_products/2/offers/1/offers',
'@type' => 'hydra:Collection',
'hydra:member' => [[
@@ -391,7 +391,7 @@ public function testGetOffersFromAggregateOffersDirect(): void
$this->assertResponseStatusCodeSame(200);
$this->assertJsonEquals([
- '@context' => '/contexts/DummyOffer',
+ '@context' => '/contexts/DummyOfferByAggregate',
'@id' => '/dummy_aggregate_offers/1/offers',
'@type' => 'hydra:Collection',
'hydra:member' => [[
@@ -448,7 +448,7 @@ public function testPersonSentGreetings(): void
$this->assertResponseStatusCodeSame(200);
$this->assertResponseHeaderSame('Content-Type', 'application/ld+json; charset=utf-8');
$this->assertJsonEquals([
- '@context' => '/contexts/Greeting',
+ '@context' => '/contexts/GreetingBySender',
'@id' => '/people/1/sent_greetings',
'@type' => 'hydra:Collection',
'hydra:member' => [[
diff --git a/tests/Functional/SubResource/SubResourceWithoutGetTest.php b/tests/Functional/SubResource/SubResourceWithoutGetTest.php
new file mode 100644
index 00000000000..cc8e42880f4
--- /dev/null
+++ b/tests/Functional/SubResource/SubResourceWithoutGetTest.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace ApiPlatform\Tests\Functional\SubResource;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\Event;
+use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue5722\ItemLog;
+use ApiPlatform\Tests\RecreateSchemaTrait;
+use ApiPlatform\Tests\SetupClassResourcesTrait;
+use Doctrine\Common\Collections\ArrayCollection;
+use Ramsey\Uuid\Uuid;
+
+final class SubResourceWithoutGetTest extends ApiTestCase
+{
+ use RecreateSchemaTrait;
+ use SetupClassResourcesTrait;
+
+ protected static ?bool $alwaysBootKernel = false;
+
+ /**
+ * @return class-string[]
+ */
+ public static function getResources(): array
+ {
+ return [Event::class, ItemLog::class];
+ }
+
+ public function testGetSubresourceFromInverseSideWithoutItemOperation(): void
+ {
+ if ($this->isMongoDB()) {
+ $this->markTestSkipped('Not tested with mongodb.');
+ }
+
+ $this->recreateSchema([Event::class, ItemLog::class]);
+
+ $manager = $this->getManager();
+ $event = new Event();
+ $event->logs = new ArrayCollection([new ItemLog(), new ItemLog()]);
+ $event->uuid = Uuid::fromString('03af3507-271e-4cca-8eee-6244fb06e95b');
+ $manager->persist($event);
+ foreach ($event->logs as $log) {
+ $log->item = $event;
+ $manager->persist($log);
+ }
+ $manager->flush();
+
+ self::createClient()->request('GET', '/events/03af3507-271e-4cca-8eee-6244fb06e95b/logs', [
+ 'headers' => ['Accept' => 'application/ld+json'],
+ ]);
+
+ $this->assertResponseIsSuccessful();
+ }
+}
diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php
index a5f53cb9326..24511eec159 100644
--- a/tests/RecreateSchemaTrait.php
+++ b/tests/RecreateSchemaTrait.php
@@ -31,7 +31,7 @@ private function recreateSchema(array $classes = []): void
$schemaManager = $manager->getSchemaManager();
$firstDocumentClass = null;
foreach ($classes as $c) {
- $class = str_contains($c, 'Entity') ? str_replace('Entity', 'Document', $c) : $c;
+ $class = str_contains($c, '\\Entity\\') ? str_replace('\\Entity\\', '\\Document\\', $c) : $c;
$firstDocumentClass ??= $class;
$schemaManager->dropDocumentCollection($class);
}
diff --git a/tests/SetupClassResourcesTrait.php b/tests/SetupClassResourcesTrait.php
index 32f08efc2f9..16b61907db7 100644
--- a/tests/SetupClassResourcesTrait.php
+++ b/tests/SetupClassResourcesTrait.php
@@ -26,6 +26,7 @@ public static function setUpBeforeClass(): void
public static function tearDownAfterClass(): void
{
+ static::ensureKernelShutdown();
static::removeResources();
$reflectionClass = new \ReflectionClass(Router::class);
$reflectionClass->setStaticPropertyValue('cache', []);
diff --git a/tests/TestSuiteConfigCache.php b/tests/TestSuiteConfigCache.php
index e26db511af0..94a97673507 100644
--- a/tests/TestSuiteConfigCache.php
+++ b/tests/TestSuiteConfigCache.php
@@ -48,6 +48,8 @@ public function write(string $content, ?array $metadata = null): void
private function getHash(): string
{
- return hash_file('xxh3', __DIR__.'/Fixtures/app/var/resources.php');
+ $file = __DIR__.'/Fixtures/app/var/resources.php';
+
+ return is_file($file) ? hash_file('xxh3', $file) : '';
}
}
diff --git a/tests/WithResourcesTrait.php b/tests/WithResourcesTrait.php
index 464c653c263..aa8ee610b49 100644
--- a/tests/WithResourcesTrait.php
+++ b/tests/WithResourcesTrait.php
@@ -13,6 +13,8 @@
namespace ApiPlatform\Tests;
+use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
+
trait WithResourcesTrait
{
/**
@@ -21,10 +23,53 @@ trait WithResourcesTrait
protected static function writeResources(array $resources): void
{
file_put_contents(__DIR__.'/Fixtures/app/var/resources.php', \sprintf(' $v.'::class', $resources))));
+ self::invalidateMetadataPools();
}
protected static function removeResources(): void
{
file_put_contents(__DIR__.'/Fixtures/app/var/resources.php', 'hasProperty('valuesCache')) {
+ $property = $reflection->getProperty('valuesCache');
+ $property->setValue(null, []);
+ }
+ }
+
+ private static function removeDirectory(string $dir): void
+ {
+ if (!is_dir($dir)) {
+ return;
+ }
+
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
+ \RecursiveIteratorIterator::CHILD_FIRST
+ );
+
+ foreach ($iterator as $file) {
+ $file->isDir() ? @rmdir($file->getPathname()) : @unlink($file->getPathname());
+ }
+
+ @rmdir($dir);
}
}