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); } }