diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2b89d65 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +/.github +/docs +/docker +/docker/data +/docker-compose.yml +!/docker/php/etc +!/docker/php/etc/* +/magento +/test_dir diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..de736b1 --- /dev/null +++ b/.env.dist @@ -0,0 +1,2 @@ +MAGENTO_PUBLIC_KEY=xxx +MAGENTO_SECRET_KEY=xxx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61b14c6..e4a2b9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,49 +5,54 @@ on: push: branches: - "master" - - "2.x" - - "3.x" jobs: behat: name: "Acceptance Tests" - runs-on: "ubuntu-latest" + strategy: + matrix: + php-version: + - "8.1" + - "8.2" env: M2_INSTANCE_ROOT_DIR: ${{ github.workspace }}/magento services: mysql: - image: mariadb:10.4 + image: mariadb:10.6 env: MYSQL_USER: magento MYSQL_PASSWORD: magento - MYSQL_DATABASE: magentodb + MYSQL_DATABASE: magento MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 options: --tmpfs /tmp:rw --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" - elasticsearch: - image: elasticsearch:7.12.1 - env: - ES_JAVA_OPTS: -Xms512m -Xmx512m - discovery.type: single-node + opensearch: + image: opensearchproject/opensearch:2.7.0 ports: - 9200:9200 + env: + discovery.type: single-node + cluster.name: opensearch-cluster + node.name: opensearch-node + bootstrap.memory_lock: true + OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m + DISABLE_INSTALL_DEMO_CONFIG: true + plugins.security.disabled: true + plugins.security.ssl.http.enabled: false + options: --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10 - steps: - - name: "Checkout" - uses: "actions/checkout@v2" - with: - path: 'behat-magento2-extension' + steps: - name: "Install PHP" uses: "shivammathur/setup-php@v2" with: coverage: none - php-version: 7.4 + php-version: "${{ matrix.php-version }}" extensions: bcmath, ctype, curl, dom, gd, hash, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, xsl, zip, sockets ini-values: memory_limit=-1 tools: composer:v2, cs2pr @@ -63,40 +68,121 @@ jobs: path: | ~/.composer/cache magento - key: "magento-2.4.2-with-php-7.4" + key: "magento-2.4.6-with-php-${{ matrix.php-version }}" - - name: "Create Magento 2.4.2 project with testing dependencies" + - name: "Create Magento 2.4.6 project" run: | - composer create-project --repository=https://repo.magento.com/ magento/project-community-edition=2.4.2 magento - cd magento - composer require tkotosz/test-area-magento2 - composer require --dev behat/behat friends-of-behat/mink-extension behat/mink-goutte-driver - cd - + composer create-project --no-progress --no-install --repository=https://repo.magento.com/ magento/project-community-edition=2.4.6 magento if: hashFiles('magento/composer.json') == '' + - name: "Add testing dependencies" + run: | + composer config --no-plugins allow-plugins.magento/* true + composer config --no-plugins allow-plugins.php-http/discovery true + composer config --no-plugins allow-plugins.laminas/laminas-dependency-plugin true + composer config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + composer require --no-progress behat/behat tkotosz/test-area-magento2 + composer install --no-progress + working-directory: 'magento' + + - name: "Checkout" + uses: "actions/checkout@v2" + with: + path: 'magento/vendor/seec/behat-magento2-extension' + - name: "Install Magento" run: | rm -f app/etc/env.php mkdir -p pub/static pub/media - bin/magento setup:install --admin-email "kotosy.magento@gmail.com" --admin-firstname "admin" --admin-lastname "admin" --admin-password "admin123" --admin-user "admin" --backend-frontname admin --base-url "http://magento.test" --db-host 127.0.0.1 --db-name magentodb --db-user magento --db-password magento --session-save files --use-rewrites 1 --use-secure 0 --search-engine=elasticsearch7 --elasticsearch-host=127.0.0.1 --elasticsearch-port=9200 -vvv - bin/magento setup:upgrade + composer require --dev behat/behat tkotosz/test-area-magento2 + bin/magento setup:install \ + --admin-email="magento@magento.com" \ + --admin-firstname="admin" \ + --admin-lastname="admin" \ + --admin-password="admin123!#" \ + --admin-user="admin" \ + --backend-frontname="admin" \ + --base-url="http://magento.test" \ + --cleanup-database \ + --db-host="127.0.0.1" \ + --db-name="magento" \ + --db-password="magento" \ + --db-user="magento" \ + --opensearch-host="127.0.0.1" \ + --opensearch-port=9200 \ + --search-engine="opensearch" \ + --session-save="files" \ + --skip-db-validation \ + --timezone="Europe/Amsterdam" \ + --use-rewrites=1 \ + --use-secure-admin=0 \ + --use-secure=0 + bin/magento --quiet deploy:mode:set developer + bin/magento --quiet setup:upgrade working-directory: 'magento' - - name: "Install Behat Magento 2 Extension in the Magento 2 Test Environment" + - name: "Install BehatMagento2 Extension" run: | - composer config repositories.behat-m2-extension path ../behat-magento2-extension - composer require --dev bex/behat-magento2-extension:@dev + composer config minimum-stability dev + composer config repositories.behat-m2-extension path vendor/seec/behat-magento2-extension + composer require seec/behat-magento2-extension:@dev working-directory: 'magento' - - name: "Install Behat Magento 2 Extension's testing dependencies" - run: | - composer install - working-directory: 'behat-magento2-extension' + - name: "Install BehatMagento2 Extension dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" + working-directory: 'magento/vendor/seec/behat-magento2-extension' + + - name: "Run phpstan for src/ and feature/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle src/ features/ --level=8 | cs2pr" + working-directory: 'magento/vendor/seec/behat-magento2-extension' + + - name: "Run phpstan for tests/" + run: "vendor/bin/phpstan analyse --error-format=checkstyle tests/ --level=6 | cs2pr" + working-directory: 'magento/vendor/seec/behat-magento2-extension' + + - name: "Run Behat tests" + run: "vendor/bin/behat --stop-on-failure --config behat.yml.dist --tags=@virtual" + working-directory: 'magento/vendor/seec/behat-magento2-extension' + + code-style: + name: "CodeStyle + UnitTests" + runs-on: "ubuntu-latest" + strategy: + matrix: + php-version: + - "8.1" + - "8.2" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Setup Composer Auth" + run: "echo $COMPOSER_AUTH_JSON > ~/.composer/auth.json" + env: + COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }} + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: none + php-version: "${{ matrix.php-version }}" + ini-values: memory_limit=-1 + tools: composer:v2, cs2pr + + - name: "Cache dependencies" + uses: "actions/cache@v2" + with: + path: | + ~/.composer/cache + vendor + key: "php-${{ matrix.php-version }}" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress --no-suggest" - - name: "Run tests without compiled DI" - run: "bin/behat -swithout_compiled_di" - working-directory: 'behat-magento2-extension' + - name: "EasyCodingStandards for Src, Features and Tests" + run: "vendor/bin/ecs check src/ features/ tests/ --no-interaction --no-progress-bar" - - name: "Run tests with compiled DI" - run: "bin/behat -swith_compiled_di" - working-directory: 'behat-magento2-extension' + - name: "Run unit tests" + run: "vendor/bin/phpunit tests/" diff --git a/.gitignore b/.gitignore index 12520a9..adf74e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -# Composer packages directory /vendor - -# Composer executable files directory -/bin - -# Installed Composer packages versions /composer.lock - -# documentation local build directory -docs/_build +/docs/_build +/.env +/behat.yml +/php_error.log +/.phpunit.result.cache +/.idea +/magento +/test_dir +/docker/data diff --git a/LICENSE b/LICENSE index 83086e9..486a742 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2019 Tibor Kotosz +Copyright (c) 2023 Maximilian Graf Schimmelmann Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index cb51940..bc9e2ed 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ -BehatMagento2Extension +Behat Magento2 Extension ====================== -[![License](https://poser.pugx.org/bex/behat-magento2-extension/license)](https://packagist.org/packages/bex/behat-magento2-extension) -[![Latest Stable Version](https://poser.pugx.org/bex/behat-magento2-extension/version)](https://packagist.org/packages/bex/behat-magento2-extension) -![Build Status](https://github.com/tkotosz/BehatMagento2Extension/actions/workflows/ci.yml/badge.svg) +[![License](https://poser.pugx.org/nopenopenope/behat-magento2-extension/license)](https://packagist.org/packages/nopenopenope/behat-magento2-extension) +[![Latest Stable Version](https://poser.pugx.org/nopenopenope/behat-magento2-extension/version)](https://packagist.org/packages/nopenopenope/behat-magento2-extension) +![Build Status](https://github.com/nopenopenope/BehatMagento2Extension/actions/workflows/ci.yml/badge.svg) -The `BehatMagento2Extension` provides a custom service container for Behat which allows to inject Magento services into Behat Contexts and Behat helper services. +This is a fork of the [BehatMagentoExtension](https://github.com/tkotosz/BehatMagento2Extension), which is +compatible with PHP8.1 and greater. This should ensure successful end-to-end testing of Magento 2 projects. + +The `BehatMagento2Extension` provides a custom service container for Behat which allows to inject Magento services into +Behat Contexts and Behat helper services. Installation ------------ @@ -13,7 +17,48 @@ Installation The recommended installation method is through [Composer](https://getcomposer.org): ```bash -composer require --dev bex/behat-magento2-extension +composer require seec/behat-magento2-extension +``` + +Usage +----- + +In order to bootstrap Magento2 into your Behat suite, some modifications to the used behat.yml are required. + +**Note**: If you use the Hooks provided by this package, your Magento Database will be purged and refilled with your +fixtures after each individual test. +This adds extra time to the execution but leaves your database also with DUMMY data. Do *not* use the hooks if you want +to keep your database intact. Do *not* use it on a production server if you don't know what you are doing. + + +Testing +------- + +If you want to contribute to this module, make sure to run tests locally before committing. Docker Compose Containers +are set-up to run all tests for all PHP versions automatically, so testing is very easy. + +```bash +$ cp .env.dist .env // make sure to add your keys to the .env file otherwise testing will not work! +$ docker compose build +$ docker compose up -d +$ docker compose exec php sh +$ install-magento +$ install-extension +$ cd /var/www/html/vendor/seec/behat-magento2-extension +$ php vendor/bin/behat +``` + +Code Quality +------------ + +We aim for a unified code style; thus we enforce ECS and PHPStan onto our code. Make sure to run the following commands +before committing: + +```bash +$ php vendor/bin/ecs check src/ tests/ features/ --fix +$ php vendor/bin/phpstan analyse src/ --level=8 +$ php vendor/bin/phpstan analyse features/ --level=8 +$ php vendor/bin/phpstan analyse tests/ --level=5 ``` Documentation diff --git a/behat.yml b/behat.yml deleted file mode 100644 index d660718..0000000 --- a/behat.yml +++ /dev/null @@ -1,9 +0,0 @@ -default: - suites: - without_compiled_di: - contexts: - - Bex\Behat\Magento2Extension\Acceptance\Context\WithoutCompiledDITestRunnerContext - - with_compiled_di: - contexts: - - Bex\Behat\Magento2Extension\Acceptance\Context\WithCompiledDITestRunnerContext diff --git a/behat.yml.dist b/behat.yml.dist index 448f4b2..1aab7a1 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -1,3 +1,9 @@ default: - extensions: - Bex\Behat\Magento2Extension: ~ + formatters: + progress: true + + suites: + default: + contexts: + - SEEC\Behat\Magento2Extension\Features\Bootstrap\Context\TestRunnerContext + - SEEC\BehatTestRunner\Context\TestRunnerContext diff --git a/composer.json b/composer.json index 16993e1..cbd1e27 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,9 @@ { - "name": "bex/behat-magento2-extension", + "name": "seec/behat-magento2-extension", "type": "behat-extension", "description": "Magento2 extension for Behat", "keywords": ["magento", "magento2", "tdd","bdd","behat"], - "homepage": "https://github.com/tkotosz/BehatMagento2Extension", + "homepage": "https://github.com/nopenopenope/BehatMagento2Extension", "license": "MIT", "authors": [ { @@ -11,34 +11,60 @@ "email": "kotosy@gmail.com", "homepage": "https://github.com/tkotosz", "role": "Developer" + }, + { + "name": "Maximilian Graf Schimmelmann", + "email": "max@schimmelmann.org", + "homepage": "https://www.schimmelmann.org", + "role": "Developer" } ], + "minimum-stability": "dev", + "prefer-stable": true, "require": { - "php": ">=7.1", - "behat/behat": "^3.5.0", - "magento/framework": ">=100.1", + "php": "^8.1", + "behat/behat": "^3.7", + "magento/framework": "103.0.6", + "magento/module-store": "101.1.6", "container-interop/container-interop": "^1.2", - "symfony/dependency-injection": ">=2.0", - "symfony/event-dispatcher": ">=2.0", + "symfony/dependency-injection": "^6", + "symfony/event-dispatcher": "^6", "magento/module-authorization": "*", "magento/module-user": "*", - "magento/module-backend": "*" + "magento/module-backend": "*", + "friends-of-behat/page-object-extension": "^0.3.2", + "friends-of-behat/suite-settings-extension": "^1.1", + "seec/behat-test-runner": "dev-master", + "friends-of-behat/symfony-extension": "^2.0", + "react/promise": "~2.0" }, "require-dev": { + "pdepend/pdepend": "^2.10", + "phpmd/phpmd": "^2.12", + "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^9.5", - "bex/behat-test-runner": "dev-master" + "sebastian/phpcpd": "^6.0", + "symfony/finder": "^5.4", + "symplify/easy-coding-standard": "^11.3", + "symplify/config-transformer": "^12.0", + "phpstan/phpstan-symfony": "^1.3", + "phpstan/phpstan-webmozart-assert": "1.2.x-dev" }, "config": { - "bin-dir": "bin" + "allow-plugins": { + "magento/composer-dependency-version-audit-plugin": true, + "phpstan/extension-installer": true + } }, "autoload": { - "psr-0": { - "Bex\\Behat\\Magento2Extension": "src/" + "psr-4": { + "SEEC\\Behat\\Magento2Extension\\": "src/", + "SEEC\\Behat\\Magento2Extension\\Features\\Bootstrap\\": "features/bootstrap" } }, "autoload-dev": { "psr-4": { - "Bex\\Behat\\Magento2Extension\\Acceptance\\": "features/bootstrap" + "SEEC\\Behat\\Magento2Extension\\Tests\\Unit\\": "tests" } }, "repositories": [ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d7b3e5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +version: '3.9' + +services: + php: + build: + context: . + dockerfile: ./docker/php/Dockerfile + args: + XDEBUG_VERSION: xdebug + PHP_VERSION: php:cli-alpine3.18 + MAGENTO_PUBLIC_KEY: ${MAGENTO_PUBLIC_KEY} + MAGENTO_SECRET_KEY: ${MAGENTO_SECRET_KEY} + volumes: + - ./src:/var/www/html/vendor/seec/behat-magento2-extension/src + - ./features:/var/www/html/vendor/seec/behat-magento2-extension/features + - ./tests:/var/www/html/vendor/seec/behat-magento2-extension/tests + - ./behat.yml.dist:/var/www/html/vendor/seec/behat-magento2-extension/behat.yml.dist + - ./ecs.php:/var/www/html/vendor/seec/behat-magento2-extension/ecs.php + - ./phpstan.neon:/var/www/html/vendor/seec/behat-magento2-extension/phpstan.neon + - ./composer.json:/var/www/html/vendor/seec/behat-magento2-extension/composer.json + - ./composer.lock:/var/www/html/vendor/seec/behat-magento2-extension/composer.lock + - ./vendor:/var/www/html/vendor/seec/behat-magento2-extension/vendor + - ./magento:/var/www/html/vendor/magento-ext + environment: + PHP_IDE_CONFIG: serverName=magento2-behat-extension + extra_hosts: + - "host.docker.internal:host-gateway" + env_file: + - .env + + mysql: + image: mariadb:10.6 + environment: + MYSQL_USER: magento + MYSQL_PASSWORD: magento + MYSQL_DATABASE: magento + MYSQL_ROOT_PASSWORD: magento + volumes: + - ./docker/data/database/:/var/lib/mysql/ + ports: + - "9906:3306" + + opensearch: + image: opensearchproject/opensearch:2.7.0 + environment: + discovery.type: single-node + cluster.name: opensearch-cluster + node.name: opensearch-node + bootstrap.memory_lock: true + OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m + DISABLE_INSTALL_DEMO_CONFIG: true + DISABLE_SECURITY_PLUGIN: true + ports: + - "8892:9200" diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..e5fbcd2 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,96 @@ +ARG PHP_VERSION=php:cli-alpine3.18 +ARG XDEBUG_VERSION=xdebug +FROM $PHP_VERSION +ARG XDEBUG_VERSION +ARG MAGENTO_PUBLIC_KEY=1 +ARG MAGENTO_SECRET_KEY=1 + +ENV COMPOSER_ALLOW_SUPERUSER=1 +ENV DOCKER_BUILDKIT=1 + +WORKDIR /var/www/ + +RUN apk add --update --no-cache linux-headers +RUN set -xe \ + && apk update \ + && apk add -u \ + git \ + vim \ + nano \ + gpg-agent \ + openssh-client \ + nano \ + wget \ + gnupg \ + unzip \ + autoconf \ + gcc \ + gettext \ + libxml2-dev \ + zip \ + gd + +RUN apk add --no-cache mysql-client msmtp perl procps shadow libzip libpng libjpeg-turbo libwebp freetype icu icu-data-full libxslt-dev libgcrypt-dev libxml2-dev pcre-dev ${PHPIZE_DEPS} +RUN apk add --no-cache --virtual build-essentials \ + icu-dev icu-libs zlib-dev g++ make automake autoconf libzip-dev \ + libpng-dev libwebp-dev libjpeg-turbo-dev freetype-dev && \ + docker-php-ext-configure gd --enable-gd --with-freetype --with-jpeg --with-webp && \ + docker-php-ext-install gd && \ + docker-php-ext-install mysqli && \ + docker-php-ext-install pdo_mysql && \ + docker-php-ext-install intl && \ + docker-php-ext-install opcache && \ + docker-php-ext-install exif && \ + docker-php-ext-install xml && \ + docker-php-ext-install zip && \ + docker-php-ext-install bcmath && \ + docker-php-ext-install soap && \ + docker-php-ext-install sockets && \ + docker-php-ext-install xsl && \ + pecl install $XDEBUG_VERSION && \ + docker-php-ext-enable xdebug && \ + apk del build-essentials pcre-dev ${PHPIZE_DEPS} && rm -rf /usr/src/php* + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +COPY ./docker/php/etc/auth.json.tmpl /tmp/auth.json.tmpl +COPY ./docker/php/etc/999-php-custom.ini /usr/local/etc/php/conf.d/999-php-custom.ini +COPY ./docker/php/etc/entrypoint.sh /usr/local/bin/entrypoint +COPY ./docker/php/etc/test-magento.sh /usr/local/bin/test-magento +COPY ./docker/php/etc/run-ecs.sh /usr/local/bin/run-ecs +COPY ./docker/php/etc/run-phpstan.sh /usr/local/bin/run-phpstan +COPY ./docker/php/etc/run-tests.sh /usr/local/bin/run-tests +COPY ./docker/php/etc/fetch-magento.sh /usr/local/bin/fetch-magento +COPY ./docker/php/etc/install-magento.sh /usr/local/bin/install-magento +COPY ./docker/php/etc/install-extension.sh /usr/local/bin/install-extension +COPY ./docker/php/etc/create-auth-file.sh /usr/local/bin/create-auth-file + +RUN mkdir -p ~/.composer +RUN chmod +x /usr/local/bin/entrypoint +RUN chmod +x /usr/local/bin/test-magento +RUN chmod +x /usr/local/bin/run-ecs +RUN chmod +x /usr/local/bin/run-phpstan +RUN chmod +x /usr/local/bin/run-tests +RUN chmod +x /usr/local/bin/install-magento +RUN chmod +x /usr/local/bin/fetch-magento +RUN chmod +x /usr/local/bin/install-extension +RUN chmod +x /usr/local/bin/create-auth-file + +RUN create-auth-file +RUN rm -rf /var/www/html/* +RUN fetch-magento + +COPY ./src /var/www/html/vendor/seec/behat-magento2-extension/src +COPY ./features /var/www/html/vendor/seec/behat-magento2-extension/features +COPY ./tests /var/www/html/vendor/seec/behat-magento2-extension/tests +COPY ./behat.yml.dist /var/www/html/vendor/seec/behat-magento2-extension/behat.yml.dist +COPY ./ecs.php /var/www/html/vendor/seec/behat-magento2-extension/ecs.php +COPY ./phpstan.neon /var/www/html/vendor/seec/behat-magento2-extension/phpstan.neon +COPY ./composer.json /var/www/html/vendor/seec/behat-magento2-extension/composer.json + +RUN install-extension + +WORKDIR /var/www/html/vendor/seec/behat-magento2-extension + +ENTRYPOINT ["/usr/local/bin/entrypoint"] +CMD ["tail", "-F", "/var/www/html/php_error.log"] diff --git a/docker/php/etc/999-php-custom.ini b/docker/php/etc/999-php-custom.ini new file mode 100644 index 0000000..498432f --- /dev/null +++ b/docker/php/etc/999-php-custom.ini @@ -0,0 +1,26 @@ +xdebug.idekey=PHPSTORM +xdebug.var_display_max_depth=200 +xdebug.mode=debug +xdebug.client_port=9000 +xdebug.client_host=host.docker.internal +xdebug.start_with_request=yes +xdebug.discover_client_host=1 +xdebug.show_error_trace = 1 +xdebug.max_nesting_level=250 +xdebug.log_level=1 + +magic_quotes_gpc = Off; +register_globals = Off; +file_uploads = On; +default_charset = UTF-8; +memory_limit = 4G; +max_execution_time = 600; +upload_max_filesize = 999M; +safe_mode = Off; +mysql.connect_timeout = 20; +allow_url_fopen = true; +display_errors = 1; +error_reporting = E_ALL; +date.timezone = "Europe/Berlin" +error_log=/var/www/html/php_error.log +pm.max_children = 25 diff --git a/docker/php/etc/auth.json.tmpl b/docker/php/etc/auth.json.tmpl new file mode 100644 index 0000000..efa6999 --- /dev/null +++ b/docker/php/etc/auth.json.tmpl @@ -0,0 +1,8 @@ +{ + "http-basic": { + "repo.magento.com": { + "username": "$MAGENTO_PUBLIC_KEY", + "password": "$MAGENTO_SECRET_KEY" + } + } +} diff --git a/docker/php/etc/create-auth-file.sh b/docker/php/etc/create-auth-file.sh new file mode 100644 index 0000000..5a68bd7 --- /dev/null +++ b/docker/php/etc/create-auth-file.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +echo "Create auth.json" +/usr/bin/envsubst < /tmp/auth.json.tmpl > ~/.composer/auth.json +cat ~/.composer/auth.json diff --git a/docker/php/etc/entrypoint.sh b/docker/php/etc/entrypoint.sh new file mode 100644 index 0000000..94e1930 --- /dev/null +++ b/docker/php/etc/entrypoint.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +create-auth-file +install-magento + +exec "$@" diff --git a/docker/php/etc/fetch-magento.sh b/docker/php/etc/fetch-magento.sh new file mode 100644 index 0000000..66516d6 --- /dev/null +++ b/docker/php/etc/fetch-magento.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +if [ ! -f /var/www/html/composer.json ]; then + rm -rf /var/www/html + composer create-project --no-install --repository=https://repo.magento.com/ magento/project-community-edition=2.4.6 /var/www/html/ + cd /var/www/html + composer config --no-plugins allow-plugins.magento/* true + composer config --no-plugins allow-plugins.php-http/discovery true + composer config --no-plugins allow-plugins.laminas/laminas-dependency-plugin true + composer config --no-plugins allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + composer require --dev behat/behat tkotosz/test-area-magento2 + composer install +fi diff --git a/docker/php/etc/install-extension.sh b/docker/php/etc/install-extension.sh new file mode 100644 index 0000000..60e5454 --- /dev/null +++ b/docker/php/etc/install-extension.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +cd /var/www/html +echo "Installing Behat Magento 2 Extension in Developer Magento Installation" +composer config repositories.behat-m2-extension path vendor/seec/behat-magento2-extension +composer require seec/behat-magento2-extension:@dev +cd /var/www/html/vendor/seec/behat-magento2-extension +echo "Installing Extension Composer Dependencies" +composer install +cd /var/www/html +composer dump-autoload -o diff --git a/docker/php/etc/install-magento.sh b/docker/php/etc/install-magento.sh new file mode 100644 index 0000000..4657e1b --- /dev/null +++ b/docker/php/etc/install-magento.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh + +cd /var/www/html +rm -f app/etc/env.php +mkdir -p pub/static pub/media +$(which php) bin/magento setup:install \ + --admin-email "magento@magento.com" \ + --admin-firstname "admin" \ + --admin-lastname "admin" \ + --admin-password "admin123!#" \ + --admin-user "admin" \ + --backend-frontname admin \ + --base-url "http://magento.test" \ + --db-host mysql \ + --db-name magento \ + --db-user root \ + --db-password magento \ + --session-save files \ + --use-rewrites 1 \ + --use-secure 0 \ + --search-engine="opensearch" \ + --opensearch-host="opensearch" \ + --opensearch-port="9200" \ + --timezone="Europe/Amsterdam" \ + --skip-db-validation \ + --cleanup-database \ + -vvv +$(which php) bin/magento deploy:mode:set developer +composer dump-autoload +$(which php) bin/magento setup:upgrade +composer config minimum-stability dev diff --git a/docker/php/etc/run-ecs.sh b/docker/php/etc/run-ecs.sh new file mode 100644 index 0000000..553e1fb --- /dev/null +++ b/docker/php/etc/run-ecs.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +cd /var/www/html/vendor/seec/behat-magento2-extension +php vendor/bin/ecs check src/ --fix +php vendor/bin/ecs check features/ --fix +php vendor/bin/ecs check tests/ --fix +chown -R 1000:1000 . diff --git a/docker/php/etc/run-phpstan.sh b/docker/php/etc/run-phpstan.sh new file mode 100644 index 0000000..79763fe --- /dev/null +++ b/docker/php/etc/run-phpstan.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +cd /var/www/html/vendor/seec/behat-magento2-extension +php vendor/bin/phpstan analyse src/ --level=8 +php vendor/bin/phpstan analyse features/ --level=6 +php vendor/bin/phpstan analyse tests/ --level=6 diff --git a/docker/php/etc/run-tests.sh b/docker/php/etc/run-tests.sh new file mode 100644 index 0000000..7426109 --- /dev/null +++ b/docker/php/etc/run-tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +cd /var/www/html/vendor/seec/behat-magento2-extension +php vendor/bin/phpunit tests/ +php vendor/bin/behat --stop-on-failure diff --git a/docker/php/etc/test-magento.sh b/docker/php/etc/test-magento.sh new file mode 100644 index 0000000..0eb0800 --- /dev/null +++ b/docker/php/etc/test-magento.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +create-auth-file +install-magento +install-extension +run-ecs +run-phpstan +run-tests diff --git a/docs/conf.py b/docs/conf.py index 92ff60a..00b0e1d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,8 +18,8 @@ # -- Project information ----------------------------------------------------- project = 'Behat Magento 2 Extension' -copyright = '2020, Tibor Kotosz' -author = 'Tibor Kotosz' +copyright = '2023, Tibor Kotosz, Maximilian Graf Schimmelmann' +author = 'Maximilian Graf Schimmelmann' # -- General configuration --------------------------------------------------- @@ -55,4 +55,4 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file +html_static_path = ['_static'] diff --git a/docs/guide/configuration.rst b/docs/guide/configuration.rst index 572b128..8fe2213 100644 --- a/docs/guide/configuration.rst +++ b/docs/guide/configuration.rst @@ -10,7 +10,7 @@ You can enable the extension in your ``behat.yml`` in following way: default: extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ Configure the Service Container ------------------------------- @@ -21,7 +21,7 @@ In order to be able to access the Magento 2 services from your Behat Contexts yo default: suites: yoursuite: - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' With the above configuration Behat will use the service container provided by this extension which makes all services defined in the Magento 2 DI available to inject into any Context. @@ -34,9 +34,9 @@ Note that you need to pass over the dependencies to your Contexts manually like yoursuite: contexts: - YourContext: - - '@Magento\Catalog\Api\ProductRepositoryInterface' + - '@Magento\Catalog\Api\ProductRepositoryInterface' - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' Enable Autowiring for Contexts ------------------------------ @@ -54,7 +54,7 @@ You can enable this feature by adding ``autowire: true`` to the behat config of contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' Note that the argument resolver is able to autowire services for: - constructor arguments @@ -78,7 +78,7 @@ You can configure the required area in the following way: contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: adminhtml @@ -156,7 +156,7 @@ In order to load this custom DI configuration during the test run the test area contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: test @@ -193,7 +193,7 @@ But don't worry this extension allows you to register your helper services in a default: extensions: - Bex\Behat\Magento2Extension: + SEEC\Behat\Magento2Extension: services: features/bootstrap/config/services.yml Note: You can use ``yml``, ``xml`` or ``php`` format. For more information see the official documentation of the `Symfony DI component `_. @@ -225,7 +225,7 @@ For more information see the official documentation of the `Symfony DI component - YourContext: - '@Magento\Catalog\Api\ProductRepositoryInterface' - '@SharedService' - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' Alternatively if you are using autowiring (see Enable Autowiring for Contexts section) then you can skip this step since the context arguments will be autowired even from this custom Symfony service container. @@ -314,5 +314,5 @@ If your Magento ``bootstrap.php`` is not available in the default ``app/bootstra default: extensions: - Bex\Behat\Magento2Extension: - bootstrap: path/to/your/bootstrap.php # by default app/bootstrap.php \ No newline at end of file + SEEC\Behat\Magento2Extension: + bootstrap: path/to/your/bootstrap.php # by default app/bootstrap.php diff --git a/docs/guide/installation.rst b/docs/guide/installation.rst index d44d094..4d96267 100644 --- a/docs/guide/installation.rst +++ b/docs/guide/installation.rst @@ -15,4 +15,4 @@ The recommended installation method is through `Composer `_ @@ -19,7 +19,7 @@ Similarly you can install the extension via composer: .. code-block:: bash - $ composer require --dev bex/behat-magento2-extension + $ composer require --dev seec/behat-magento2-extension For more information see the the :doc:`installation section of this documentation `. @@ -32,7 +32,7 @@ You need to enable the extension in the Behat configuration and configure your B default: extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ suites: application: @@ -41,7 +41,7 @@ You need to enable the extension in the Behat configuration and configure your B contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' With the above configuration: - The extension is enabled diff --git a/docs/guide/usage.rst b/docs/guide/usage.rst index 0c11eaa..870568f 100644 --- a/docs/guide/usage.rst +++ b/docs/guide/usage.rst @@ -9,3 +9,5 @@ Usage Examples usage/inject-service-to-behat-context-as-behat-step-argument usage/inject-service-to-behat-context-as-behat-step-argument-transformer-argument usage/mocking-dependency + purge-database-after-every-scenario-to-ensure-data-correctness.rst + diff --git a/docs/guide/usage/automatically-inject-service-to-behat-context-as-constructor-argument.rst b/docs/guide/usage/automatically-inject-service-to-behat-context-as-constructor-argument.rst index 1baca01..436ab05 100644 --- a/docs/guide/usage/automatically-inject-service-to-behat-context-as-constructor-argument.rst +++ b/docs/guide/usage/automatically-inject-service-to-behat-context-as-constructor-argument.rst @@ -76,4 +76,4 @@ You can enable the `Behat service autowiring feature contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' diff --git a/docs/guide/usage/inject-service-to-behat-context-as-behat-step-argument.rst b/docs/guide/usage/inject-service-to-behat-context-as-behat-step-argument.rst index 196c5c4..92a5c42 100644 --- a/docs/guide/usage/inject-service-to-behat-context-as-behat-step-argument.rst +++ b/docs/guide/usage/inject-service-to-behat-context-as-behat-step-argument.rst @@ -51,4 +51,4 @@ The `Behat service autowiring feature contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' diff --git a/docs/guide/usage/manually-inject-service-to-behat-context-as-constructor-argument.rst b/docs/guide/usage/manually-inject-service-to-behat-context-as-constructor-argument.rst index 8675a8f..1dd0eb5 100644 --- a/docs/guide/usage/manually-inject-service-to-behat-context-as-constructor-argument.rst +++ b/docs/guide/usage/manually-inject-service-to-behat-context-as-constructor-argument.rst @@ -74,6 +74,6 @@ If you didn't enable the Behat autowire feature then you need to provide your Be contexts: - YourContext: - '@Magento\Catalog\Api\ProductRepositoryInterface' - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' That's all. With the above the Product Repository will be injected to your Behat Context. diff --git a/docs/guide/usage/mocking-dependency.rst b/docs/guide/usage/mocking-dependency.rst index 3ffdf76..75cd471 100644 --- a/docs/guide/usage/mocking-dependency.rst +++ b/docs/guide/usage/mocking-dependency.rst @@ -28,8 +28,7 @@ And you have an implementation for this service: class ConfigProvider implements ConfigProviderInterface { - /** @var ScopeConfigInterface */ - private $scopeConfig; + private ScopeConfigInterface $scopeConfig; public function __construct(ScopeConfigInterface $scopeConfig) { @@ -189,7 +188,7 @@ In order to load this custom DI configuration during the test run the test area contexts: - YourContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: test diff --git a/docs/guide/usage/purge-database-after-every-scenario-to-ensure-data-correctness.rst b/docs/guide/usage/purge-database-after-every-scenario-to-ensure-data-correctness.rst new file mode 100644 index 0000000..bcb1bd6 --- /dev/null +++ b/docs/guide/usage/purge-database-after-every-scenario-to-ensure-data-correctness.rst @@ -0,0 +1,47 @@ +Purge the Database after each scenario +====================================== + +It can be beneficial to purge the database after each scenario in order to work with the data you want, which will +allow you correct test cases in the long run. Please be aware that this will totally truncate each table. Do not use this feature on any kind of +production application, as it will completely wipe your database and will add fixture data to it. + +You can make usage of the Database Hook with the following `behat.yml` configuration: + +.. code-block:: yaml + + default: + suites: + yoursuite: + autowire: true + + contexts: + - YourContext + - SEEC\Behat\Magento2Extension\Features\Bootstrap\Context\Hook\DatabaseHook + + services: '@seec.magento2_extension.service_container' + +With this Hook, Behat will always purge the database before you run a scenario. There are some exceptions to it, as not all +tables are purged to ensure that the Behat Test suite can continue and run properly. The following tables are not purged by the code: + +.. code-block:: php + + $purger->purge($connection, [ + 'core_config_data', + 'eav_attribute', + 'eav_attribute_group', + 'eav_attribute_label', + 'eav_attribute_option', + 'eav_attribute_option_swatch', + 'eav_attribute_option_value', + 'eav_attribute_set', + 'eav_entity_type', + ]); + +Also, the hook will automatically create a default Stock, Website and Store Group and Store View for you. This is necessary +to use various Repositories throughout the testing scenarios, as you can inject them into your Contexts. It may still +happen that various Repositories do not want to get autowired or injected, but you can use always the `ObjectManager` to +get new classes. In Magento2 development, this is strictly discouraged, but for testing purposes its the right way to go; +in the end we want fresh or singleton instances of classes, which are not affected by other tests. + +However: if you see yourself in a situation where you control a class yourself that does not want to get autowired, consider rewriting the class +rather than use the `ObjectManager`. diff --git a/docs/index.rst b/docs/index.rst index 8f590db..eaaf173 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,9 @@ Behat Magento 2 Extension ========================= This Behat extension provides provides a custom service container for Behat which allows to inject Magento services into Behat Contexts and Behat helper services. +The module ships some default actions, but users are highly encouraged to create their own logic. + +This module is tested for Magento 2.4.x and PHP 7.4 and above. In the future, support for PHP7.4 will be dropped. Guide ----- @@ -17,8 +20,8 @@ Guide References ---------- -- Github Repository: https://github.com/tkotosz/BehatMagento2Extension +- Github Repository: https://github.com/nopenopenope/BehatMagento2Extension -- Packagist: https://packagist.org/packages/bex/behat-magento2-extension +- Packagist: https://packagist.org/packages/seec/behat-magento2-extension -- Behat Official Documentaion: https://docs.behat.org \ No newline at end of file +- Behat Official Documentaion: https://docs.behat.org diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..db10619 --- /dev/null +++ b/ecs.php @@ -0,0 +1,284 @@ +parallel(); + $ecsConfig->sets([SetList::PSR_12]); + $ecsConfig->rule(CastSpacesFixer::class); + $ecsConfig->rule(ClassAttributesSeparationFixer::class); + $ecsConfig->rule(EncodingFixer::class); + $ecsConfig->rule(EregToPregFixer::class); + $ecsConfig->rule(LowercaseCastFixer::class); + $ecsConfig->rule(LowerCaseConstantSniff::class); + $ecsConfig->rule(LowercaseKeywordsFixer::class); + $ecsConfig->rule(LowercaseStaticReferenceFixer::class); + $ecsConfig->rule(MagicConstantCasingFixer::class); + $ecsConfig->rule(ModernizeTypesCastingFixer::class); + $ecsConfig->rule(NativeFunctionCasingFixer::class); + $ecsConfig->rule(NoAliasFunctionsFixer::class); + $ecsConfig->rule(NoBlankLinesAfterClassOpeningFixer::class); + $ecsConfig->rule(NoMultilineWhitespaceAroundDoubleArrowFixer::class); + $ecsConfig->rule(NonPrintableCharacterFixer::class); + $ecsConfig->rule(NoNullPropertyInitializationFixer::class); + $ecsConfig->rule(NoPhp4ConstructorFixer::class); + $ecsConfig->rule(NormalizeIndexBraceFixer::class); + $ecsConfig->rule(NoShortBoolCastFixer::class); + $ecsConfig->rule(NoUnneededFinalMethodFixer::class); + $ecsConfig->rule(NoWhitespaceBeforeCommaInArrayFixer::class); + $ecsConfig->rule(PowToExponentiationFixer::class); + $ecsConfig->rule(ProtectedToPrivateFixer::class); + $ecsConfig->rule(SelfAccessorFixer::class); + $ecsConfig->rule(ShortScalarCastFixer::class); + $ecsConfig->rule(SingleClassElementPerStatementFixer::class); + $ecsConfig->rule(TrimArraySpacesFixer::class); + $ecsConfig->rule(WhitespaceAfterCommaInArrayFixer::class); + $ecsConfig->rule(NoEmptyCommentFixer::class); + $ecsConfig->rule(NoTrailingWhitespaceInCommentFixer::class); + $ecsConfig->rule(CombineConsecutiveIssetsFixer::class); + $ecsConfig->rule(CombineConsecutiveUnsetsFixer::class); + $ecsConfig->rule(DeclareEqualNormalizeFixer::class); + $ecsConfig->rule(DirConstantFixer::class); + $ecsConfig->rule(ElseifFixer::class); + $ecsConfig->rule(FunctionDeclarationFixer::class); + $ecsConfig->rule(FunctionToConstantFixer::class); + $ecsConfig->rule(FunctionTypehintSpaceFixer::class); + $ecsConfig->rule(IncludeFixer::class); + $ecsConfig->rule(IsNullFixer::class); + $ecsConfig->rule(MethodArgumentSpaceFixer::class); + $ecsConfig->rule(NativeConstantInvocationFixer::class); + $ecsConfig->rule(NoBreakCommentFixer::class); + $ecsConfig->rule(NoLeadingImportSlashFixer::class); + $ecsConfig->rule(NoSpacesAfterFunctionNameFixer::class); + $ecsConfig->rule(NoSuperfluousElseifFixer::class); + $ecsConfig->rule(NoUnneededControlParenthesesFixer::class); + $ecsConfig->rule(NoUnneededCurlyBracesFixer::class); + $ecsConfig->rule(NoUnusedImportsFixer::class); + $ecsConfig->rule(NoUselessElseFixer::class); + $ecsConfig->rule(OrderedImportsFixer::class); + $ecsConfig->rule(ReturnTypeDeclarationFixer::class); + $ecsConfig->rule(SingleImportPerStatementFixer::class); + $ecsConfig->rule(SingleLineAfterImportsFixer::class); + $ecsConfig->rule(SwitchCaseSemicolonToColonFixer::class); + $ecsConfig->rule(SwitchCaseSpaceFixer::class); + $ecsConfig->rule(BinaryOperatorSpacesFixer::class); + $ecsConfig->rule(BlankLineAfterNamespaceFixer::class); + $ecsConfig->rule(NoHomoglyphNamesFixer::class); + $ecsConfig->rule(NoLeadingNamespaceWhitespaceFixer::class); + $ecsConfig->rule(BlankLineAfterOpeningTagFixer::class); + $ecsConfig->rule(BlankLineBeforeStatementFixer::class); + $ecsConfig->rule(DeclareStrictTypesFixer::class); + $ecsConfig->rule(FullOpeningTagFixer::class); + $ecsConfig->rule(IndentationTypeFixer::class); + $ecsConfig->rule(LineEndingFixer::class); + $ecsConfig->rule(NewWithBracesFixer::class); + $ecsConfig->rule(NoBlankLinesAfterPhpdocFixer::class); + $ecsConfig->rule(NoClosingTagFixer::class); + $ecsConfig->rule(NoEmptyPhpdocFixer::class); + $ecsConfig->rule(NoEmptyStatementFixer::class); + $ecsConfig->rule(NoSinglelineWhitespaceBeforeSemicolonsFixer::class); + $ecsConfig->rule(NoSpacesAroundOffsetFixer::class); + $ecsConfig->rule(NoSpacesInsideParenthesisFixer::class); + $ecsConfig->rule(NoTrailingWhitespaceFixer::class); + $ecsConfig->rule(NoWhitespaceInBlankLineFixer::class); + $ecsConfig->rule(ObjectOperatorWithoutWhitespaceFixer::class); + $ecsConfig->rule(PhpdocIndentFixer::class); + $ecsConfig->rule(PhpdocNoAccessFixer::class); + $ecsConfig->rule(PhpdocNoAliasTagFixer::class); + $ecsConfig->rule(PhpdocNoEmptyReturnFixer::class); + $ecsConfig->rule(PhpdocNoPackageFixer::class); + $ecsConfig->rule(PhpdocNoUselessInheritdocFixer::class); + $ecsConfig->rule(PhpdocReturnSelfReferenceFixer::class); + $ecsConfig->rule(PhpdocScalarFixer::class); + $ecsConfig->rule(PhpdocSeparationFixer::class); + $ecsConfig->rule(PhpdocSingleLineVarSpacingFixer::class); + $ecsConfig->rule(PhpdocTrimFixer::class); + $ecsConfig->rule(PhpdocTypesFixer::class); + $ecsConfig->rule(PhpdocVarWithoutNameFixer::class); + $ecsConfig->rule(PhpUnitDedicateAssertFixer::class); + $ecsConfig->rule(PhpUnitFqcnAnnotationFixer::class); + $ecsConfig->rule(SingleBlankLineAtEofFixer::class); + $ecsConfig->rule(SingleQuoteFixer::class); + $ecsConfig->rule(SpaceAfterSemicolonFixer::class); + $ecsConfig->rule(StandardizeNotEqualsFixer::class); + $ecsConfig->rule(TernaryOperatorSpacesFixer::class); + $ecsConfig->rule(TernaryToNullCoalescingFixer::class); + $ecsConfig->rule(UnaryOperatorSpacesFixer::class); + $ecsConfig->ruleWithConfiguration(TrailingCommaInMultilineFixer::class, [ + 'elements' => [ + 'arguments', + 'parameters', + ], + ]); + $ecsConfig->ruleWithConfiguration(NoMixedEchoPrintFixer::class, ['use' => 'echo']); + $ecsConfig->ruleWithConfiguration(ArraySyntaxFixer::class, ['syntax' => 'short']); + $ecsConfig->ruleWithConfiguration(ClassDefinitionFixer::class, [ + 'single_item_single_line' => true, + 'multi_line_extends_each_single_line' => true, + ]); + $ecsConfig->ruleWithConfiguration(VisibilityRequiredFixer::class, [ + 'elements' => [ + 'const', + 'property', + 'method', + ], + ]); + $ecsConfig->ruleWithConfiguration(SingleLineCommentStyleFixer::class, [ + 'comment_types' => [ + 'hash', + ] + ]); + $ecsConfig->ruleWithConfiguration(ListSyntaxFixer::class, [ + 'syntax' => 'short', + ]); + $ecsConfig->ruleWithConfiguration(ConcatSpaceFixer::class, [ + 'spacing' => 'one', + ]); + $ecsConfig->ruleWithConfiguration(IncrementStyleFixer::class, [ + 'style' => 'pre' + ]); + $ecsConfig->ruleWithConfiguration(NoExtraBlankLinesFixer::class, [ + 'tokens' => [ + 'break', + 'case', + 'continue', + 'curly_brace_block', + 'default', + 'extra', + 'parenthesis_brace_block', + 'return', + 'square_brace_block', + 'switch', + 'throw', + 'use', + ], + ]); + $ecsConfig->ruleWithConfiguration(PhpdocTypesOrderFixer::class, [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'none', + ]); + $ecsConfig->ruleWithConfiguration(NoSuperfluousPhpdocTagsFixer::class, [ + 'allow_mixed' => true, + ]); + + $parameters = $ecsConfig->parameters(); + $parameters->set(Option::PARALLEL_TIMEOUT_IN_SECONDS, 120); + $parameters->set('skip', [ + VisibilityRequiredFixer::class => ['*Spec.php'], + ]); +}; diff --git a/features/bootstrap/Context/AbstractMagentoContext.php b/features/bootstrap/Context/AbstractMagentoContext.php new file mode 100644 index 0000000..d207a19 --- /dev/null +++ b/features/bootstrap/Context/AbstractMagentoContext.php @@ -0,0 +1,16 @@ +getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * @Given I have a customer with this data: + * @Given there is a customer with this data; + * @Given there is a customer in store with code :storeCode with this data: + */ + public function thereIsACustomerWIthThisData(TableNode $node, ?string $storeCode = null): void + { + /** @var CustomerInterface $customer */ + $customer = $this->getObjectManager()->create(CustomerInterface::class); + $password = null; + $data = $node->getRowsHash(); + + /** @var StoreInterface $store */ + $store = $storeCode === null + ? $this->sharedStorage->get('store') + : $this->storeRepository->get($storeCode); + + $customer->setStoreId($store->getId()); + $customer->setWebsiteId($store->getWebsiteId()); + + foreach ($data as $key => $value) { + if ($key === 'password') { + $password = $this->encryptor->getHash($value, true); + + continue; + } + $method = sprintf('set%s', ucfirst($key)); + Assert::true(method_exists($customer, $method), sprintf('Method %s does not exist in class %s', $method, get_class($customer))); + $customer->$method($value); + } + + $this->getCustomerRepository()->save($customer, $password); + $this->sharedStorage->set('customer', $customer); + } + + /** + * @Given there is :amount customer existing + * @Given there are :amount customers existing + */ + public function thereAreXCustomerExisting(int $amount): void + { + $searchCriteria = $this->searchCriteriaBuilder->create(); + $collection = $this->getCustomerRepository()->getList($searchCriteria)->getItems(); + $total = count($collection); + Assert::same($total, $amount, sprintf('Expected %s customers, got %s', $amount, $total)); + } + + /** + * @Given there is a customer with email :email existing + */ + public function thereIsACustomerWithEmailExisting(string $email): void + { + $customer = $this->getCustomerRepository()->get($email); + Assert::notNull($customer); + } +} diff --git a/features/bootstrap/Context/Components/ProcessFactory/Input/MagentoCommandInput.php b/features/bootstrap/Context/Components/ProcessFactory/Input/MagentoCommandInput.php new file mode 100644 index 0000000..5d88f2d --- /dev/null +++ b/features/bootstrap/Context/Components/ProcessFactory/Input/MagentoCommandInput.php @@ -0,0 +1,24 @@ +setExecutor((new PhpExecutableFinder())->find() ?: null); + $this->setExecutorParameters('-dmemory_limit=-1'); + $this->setCommand('bin/magento'); + $this->setParameters($command); + $this->setExtraParameters($commandParameters); + $this->setDirectory($workingDirectory); + } +} diff --git a/features/bootstrap/Context/Components/Stock/StockContext.php b/features/bootstrap/Context/Components/Stock/StockContext.php new file mode 100644 index 0000000..63a83a1 --- /dev/null +++ b/features/bootstrap/Context/Components/Stock/StockContext.php @@ -0,0 +1,43 @@ +searchCriteriaBuilder->addFilter('name', $name)->create(); + $existingStock = $this->stockRepository->getList($filter); + if ($existingStock->getTotalCount() > 0) { + $stock = $existingStock->getItems()[0]; + } else { + /** @var StockINterface $stock */ + $stock = $this->getObjectManager()->create(StockInterface::class); + $stock->setName($name); + $this->stockRepository->save($stock); + } + + Assert::isInstanceOf($stock, StockInterface::class); + $this->sharedStorage->set('stock', $existingStock); + } +} diff --git a/features/bootstrap/Context/Components/Store/StoreContext.php b/features/bootstrap/Context/Components/Store/StoreContext.php new file mode 100644 index 0000000..26b0eb7 --- /dev/null +++ b/features/bootstrap/Context/Components/Store/StoreContext.php @@ -0,0 +1,44 @@ +resourceConnection = $resourceConnection->getConnection(); + } + + /** + * @Given a frontend store-view exists + * @Given a frontend store-view exists with code :code + * @Given a frontend store-view exists with code :code and name :name + */ + public function aFrontendStoreViewExistsWithCodeAndName(string $code = 'test_code', string $name = 'Test Name'): void + { + $this->fixtureFactory->setSharedStorage($this->sharedStorage); + $this->fixtureFactory->createDefaults($this->resourceConnection, $code, $name); + } + + /** + * @Given a backend store-view exists + */ + public function aBackendStoreViewExists(): void + { + $this->fixtureFactory->setSharedStorage($this->sharedStorage); + $this->fixtureFactory->createDefaults($this->resourceConnection, 'admin', 'Admin'); + } +} diff --git a/features/bootstrap/Context/Hook/DatabaseHook.php b/features/bootstrap/Context/Hook/DatabaseHook.php new file mode 100644 index 0000000..f5c24ad --- /dev/null +++ b/features/bootstrap/Context/Hook/DatabaseHook.php @@ -0,0 +1,60 @@ +resource = $resource; + $this->fixturesManager = $defaultFixtures ?? new DefaultFixtures(); + $this->cacheCleaner = $cacheCleaner ?? new CacheCleaner(); + $this->purger = $purger ?? new Purger(); + } + + /** + * @BeforeScenario + */ + public function purgeAndPrefillWithFixtures(BeforeScenarioScope $scope): void + { + $connection = $this->resource->getConnection(); + + $this->cacheCleaner->clean(); + $this->purger->purge($connection, [ + 'core_config_data', + 'eav_attribute', + 'eav_attribute_group', + 'eav_attribute_label', + 'eav_attribute_option', + 'eav_attribute_option_swatch', + 'eav_attribute_option_value', + 'eav_attribute_set', + 'eav_entity_type', + ]); + $this->fixturesManager->createDefaults($connection); + } +} diff --git a/features/bootstrap/Context/Hook/DatabaseHookInterface.php b/features/bootstrap/Context/Hook/DatabaseHookInterface.php new file mode 100644 index 0000000..96526cd --- /dev/null +++ b/features/bootstrap/Context/Hook/DatabaseHookInterface.php @@ -0,0 +1,12 @@ +magentoPathProvider = $magentoPathProvider ?? new MagentoPathProvider(); + $this->fileSystem = $filesystem ?? new Filesystem(); + $this->finder = $finder ?? new Finder(); + } + + public function clean(bool $cleanObjectManager = true): void + { + $directory = $this->magentoPathProvider->getMagentoRootDirectory(); + $this->finder->directories(); + $cacheFolder = $this->finder->in(sprintf('%s/var/cache', $directory)); + $this->fileSystem->remove($cacheFolder); + + if ($cleanObjectManager) { + /** @var Config $objectManager */ + $objectManager = ObjectManager::getInstance()->get(Config::class); + $objectManager->clean(); + } + } +} diff --git a/features/bootstrap/Context/Tasks/CacheCleanerInterface.php b/features/bootstrap/Context/Tasks/CacheCleanerInterface.php new file mode 100644 index 0000000..4253b26 --- /dev/null +++ b/features/bootstrap/Context/Tasks/CacheCleanerInterface.php @@ -0,0 +1,10 @@ +sharedStorage = $sharedStorage; + } + + private function getSharedStorage(): ?SharedStorageInterface + { + return $this->sharedStorage; + } + + private function getRepository(string $class): object + { + Assert::interfaceExists($class); + + return $this->getObjectManager()->create($class); + } + + public function createDefaults(AdapterInterface $connection, string $code = 'test_code', string $name = 'Test Name'): void + { + $stock = $this->getOrCreateStock($name, $connection); + $website = $this->getOrCreateWebsite($code, $name); + $group = $this->getOrCreateGroup($code, $name, $website, $connection); + $store = $this->getOrCreateStore($code, $name, $website, $group); + + if ($this->getSharedStorage() instanceof SharedStorage) { + $this->getSharedStorage()->set('stock', $stock); + $this->getSharedStorage()->set('website', $website); + $this->getSharedStorage()->set('group', $group); + $this->getSharedStorage()->set('store', $store); + } + } + + private function getObjectManager(): ObjectManager + { + return ObjectManager::getInstance(); + } + + private function getOrCreateWebsite(string $code, string $name): WebsiteInterface + { + $existing = $this->getExistingEntity(WebsiteInterface::class, $code); + if ($existing !== null) { + Assert::isInstanceOf($existing, WebsiteInterface::class); + + return $existing; + } + + /** @var WebsiteInterface $website */ + $website = $this->getObjectManager()->create(WebsiteInterface::class); + $website->setCode($code); + $website->setName($name); + $website->setDefaultGroupId(1); + $website->setData('is_default', true); /** @phpstan-ignore-line */ + $this->getObjectManager()->create(Website::class)->save($website); + Assert::isInstanceOf($website, WebsiteInterface::class); + + return $website; + } + + private function getOrCreateGroup(string $code, string $name, WebsiteInterface $website, AdapterInterface $connection): GroupInterface + { + $existing = $this->getExistingEntity(GroupInterface::class, $code); + if ($existing !== null) { + Assert::isInstanceOf($existing, GroupInterface::class); + + return $existing; + } + + $group = null; + + try { + /** @var GroupInterface $group */ + $group = $this->getObjectManager()->create(GroupInterface::class); + $group->setCode($code); + $group->setName($name); + $group->setRootCategoryId(1); + $group->setDefaultStoreId(1); + $group->setWebsiteId($website->getId()); + + $this->getObjectManager()->create(Group::class)->save($group); + } catch (NoSuchEntityException $e) { + } catch (Throwable $e) { + $connection->insert('store_group', [ + 'group_id' => 1, + 'website_id' => $website->getId(), + 'code' => $code, + 'name' => $name, + 'root_category_id' => 1, + 'default_store_id' => 1, + ]); + /** @var GroupRepositoryInterface $repository */ + $repository = $this->getObjectManager()->create(GroupRepositoryInterface::class); + $group = $repository->get(1); + } + + Assert::isInstanceOf($group, GroupInterface::class); + + $website->setDefaultGroupId($group->getId()); + $this->getObjectManager()->create(Website::class)->save($website); + + return $group; + } + + private function getOrCreateStore(string $code, string $name, WebsiteInterface $website, GroupInterface $group): StoreInterface + { + $existing = $this->getExistingEntity(StoreInterface::class, $code); + if ($existing !== null) { + Assert::isInstanceOf($existing, StoreInterface::class); + + return $existing; + } + + /** @var StoreInterface $store */ + $store = $this->getObjectManager()->create(StoreInterface::class); + $store->setCode($code); + $store->setName($name); + $store->setWebsiteId($website->getId()); + $store->setStoreGroupId($group->getId()); + $store->setIsActive(1); + + $this->getObjectManager()->create(Store::class)->save($store); + Assert::isInstanceOf($store, StoreInterface::class); + + $group->setDefaultStoreId($store->getId()); + $this->getObjectManager()->create(Group::class)->save($group); + + return $store; + } + + public function getOrCreateStock(string $name, AdapterInterface $connection): StockInterface + { + $existing = $this->getExistingEntity(StockInterface::class, $name); + if ($existing !== null) { + Assert::isInstanceOf($existing, StockInterface::class); + + return $existing; + } + + /** @var StockRepositoryInterface $repo */ + $repo = $this->getRepository(StockRepositoryInterface::class); + + try { + /** @var StockInterface $stock */ + $stock = $this->getObjectManager()->create(StockInterface::class); + $stock->setName($name); + $repo->save($stock); + } catch (DomainException|CouldNotSaveException $e) { + echo sprintf( + 'Could not create stock regularly, retry with direct injection. Error: %s, File: %s:%s', + $e->getPrevious()?->getMessage() ?? $e->getMessage(), + $e->getFile(), + $e->getLine(), + ) . \PHP_EOL; + $connection->insert('inventory_stock', ['stock_id' => 1, 'name' => $name]); + $stock = $repo->get(1); + } + + Assert::isInstanceOf($stock, StockInterface::class); + + return $stock; + } + + private function getExistingEntity(string $interface, string $identifier): null|GroupInterface|StoreInterface|WebsiteInterface|StockInterface + { + $checkBy = 'getCode'; + switch ($interface) { + case WebsiteInterface::class: + /** @var WebsiteRepositoryInterface $repository */ + $repository = $this->getRepository(WebsiteRepositoryInterface::class); + $repository->clean(); + + break; + case GroupInterface::class: + /** @var GroupRepositoryInterface $repository */ + $repository = $this->getRepository(GroupRepositoryInterface::class); + $repository->clean(); + + break; + case StoreInterface::class: + /** @var StoreRepositoryInterface $repository */ + $repository = $this->getRepository(StoreRepositoryInterface::class); + $repository->clean(); + + break; + case StockInterface::class: + /** @var StockRepositoryInterface $repository */ + $repository = $this->getRepository(StockRepositoryInterface::class); + $checkBy = 'getName'; + + break; + default: + throw new \InvalidArgumentException('Interface not found'); + } + + $existingList = is_array($repository->getList()) + ? $repository->getList() + : $repository->getList()->getItems(); + + foreach ($existingList as $existing) { + if ($existing->$checkBy() === $identifier) { + Assert::isInstanceOf($existing, $interface); + + return $existing; + } + } + + return null; + } +} diff --git a/features/bootstrap/Context/Tasks/DefaultFixturesInterface.php b/features/bootstrap/Context/Tasks/DefaultFixturesInterface.php new file mode 100644 index 0000000..8d058f4 --- /dev/null +++ b/features/bootstrap/Context/Tasks/DefaultFixturesInterface.php @@ -0,0 +1,13 @@ +query('SET FOREIGN_KEY_CHECKS = 0'); + $tables = $connection->getTables(); + foreach ($tables as $table) { + if (in_array($table, $excludedTables)) { + continue; + } + + $count = $connection->select()->from($table, 'COUNT(*)'); + $result = (int) $connection->fetchOne($count); + if ($result === 0) { + continue; + } + + $sql = sprintf('TRUNCATE TABLE %s', $table); + $connection->query($sql); + } + $connection->query('SET FOREIGN_KEY_CHECKS = 1'); + } +} diff --git a/features/bootstrap/Context/Tasks/PurgerInterface.php b/features/bootstrap/Context/Tasks/PurgerInterface.php new file mode 100644 index 0000000..b0517b3 --- /dev/null +++ b/features/bootstrap/Context/Tasks/PurgerInterface.php @@ -0,0 +1,12 @@ +filesystem = $fileSystem ?: new Filesystem(); + $this->processFactory = $processFactory ?: new ProcessFactory(); + $this->magentoPathProvider = $magentoPathProvider ?? new MagentoPathProvider(); + $this->cacheCleaner = $cacheCleaner ?? new CacheCleaner($this->magentoPathProvider); + parent::__construct($fileSystem, $processFactory, $workingDirectoryService, $workingDirectory, $finder); } - /** - * @Then I should see the tests passing - */ - public function iShouldSeeTheTestsPassing() + public function createWorkingDirectory(): void { - $this->iShouldNotSeeAFailingTest(); + $this->determineFreshWorkingDirectoryFlag(); + parent::createWorkingDirectory(); + + $this->filesystem->copy( + sprintf('%s/app/etc/config.php', $this->getMagentoRootDirectory()), + '/tmp/config.php.backup', + true, + ); } - public function createWorkingDirectory() + public function clearWorkingDirectory(): void { - parent::createWorkingDirectory(); + parent::clearWorkingDirectory(); + $this->removeEmptyWorkingDirectory(); + $this->removeModuleFolders(); + $this->revertMagentoConfig(); + } - $this->filesystem->copy($this->workingDirectory . '/app/etc/config.php', '/tmp/config.php.backup', true); + private function setModulePath(string $modulePath): void + { + $this->modulePath = $modulePath; } - public function clearWorkingDirectory() + private function getModulePath(): ?string { - parent::clearWorkingDirectory(); + return $this->modulePath; + } - $this->filesystem->copy('/tmp/config.php.backup', $this->workingDirectory . '/app/etc/config.php', true); - $this->filesystem->remove('/tmp/config.php.backup'); - $this->runMagentoCommand('cache:clear'); + /** + * @Given I have no Magento module called :moduleName + */ + public function iHaveNoMagentoModuleCalledX(string $moduleName): void + { + [$vendor, $module] = explode('_', $moduleName); + $modulePath = sprintf('%s/app/code/%s/%s', $this->getMagentoRootDirectory(), $vendor, $module); + + $this->filesystem->remove($modulePath); + Assert::false($this->filesystem->exists($modulePath)); } /** * @Given I have a Magento module called :moduleName */ - public function iHaveAMagentoModuleCalled(string $moduleName) + public function iHaveAMagentoModuleCalled(string $moduleName): void { [$vendor, $module] = explode('_', $moduleName); - $this->modulePath = $this->workingDirectory . '/app/code/' . $vendor . '/' . $module; - $registrationFile = $this->modulePath . '/registration.php'; - $moduleFile = $this->modulePath . '/etc/module.xml'; + $modulePath = sprintf('%s/app/code/%s/%s', $this->getMagentoRootDirectory(), $vendor, $module); + $registrationFile = sprintf('%s/registration.php', $modulePath); + $moduleFile = sprintf('%s/etc/module.xml', $modulePath); + $this->setModulePath($modulePath); $registrationFileContent = <<filesystem->dumpFile($registrationFile, $registrationFileContent); - $this->filesystem->dumpFile($moduleFile, $moduleFileContent); - - $this->files[] = $registrationFile; - $this->files[] = $moduleFile; - - $this->runMagentoCommand('module:enable', $moduleName); + $this->createFile($registrationFile, $registrationFileContent); + $this->createFile($moduleFile, $moduleFileContent); + //$this->runMagentoCommand('module:enable', $moduleName); } /** * @Given I have an interface :fqcn defined in this module: * @Given I have a class :fqcn defined in this module: */ - public function iHaveAnInterfaceDefinedInThisModule(string $fqcn, PyStringNode $content) + public function iHaveAnInterfaceDefinedInThisModule(string $fqcn, PyStringNode $content): void { - $file = $this->workingDirectory . '/app/code/' . str_replace('\\', '/', $fqcn) . '.php'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/app/code/%s.php', $this->getMagentoRootDirectory(), str_replace('\\', '/', $fqcn)); + $this->createFile($file, $content->getRaw()); } /** * @Given I have a global Magento DI configuration in this module: */ - public function iHaveAGlobalMagentoDiConfigurationInThisModule(PyStringNode $content) + public function iHaveAGlobalMagentoDiConfigurationInThisModule(PyStringNode $content): void { - $file = $this->modulePath . '/etc/di.xml'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/etc/di.xml', $this->getModulePath()); + $this->createFile($file, $content->getRaw()); $this->runMagentoCommand('cache:clean'); } @@ -114,70 +153,141 @@ public function iHaveAGlobalMagentoDiConfigurationInThisModule(PyStringNode $con /** * @Given I have a frontend Magento DI configuration in this module: */ - public function iHaveAFrontendMagentoDIConfigurationInThisModule(PyStringNode $content) + public function iHaveAFrontendMagentoDIConfigurationInThisModule(PyStringNode $content): void { - $file = $this->modulePath . '/etc/frontend/di.xml'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/etc/frontend/di.xml', $this->getModulePath()); + $this->createFile($file, $content->getRaw()); } /** * @Given I have an adminhtml Magento DI configuration in this module: */ - public function iHaveAnAdminhtmlMagentoDIConfigurationInThisModule(PyStringNode $content) + public function iHaveAnAdminhtmlMagentoDIConfigurationInThisModule(PyStringNode $content): void { - $file = $this->modulePath . '/etc/adminhtml/di.xml'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/etc/adminhtml/di.xml', $this->modulePath); + $this->createFile($file, $content->getRaw()); } /** * @Given I have a test Magento DI configuration in this module: */ - public function iHaveATestMagentoDiConfigurationInThisModule(PyStringNode $content) + public function iHaveATestMagentoDiConfigurationInThisModule(PyStringNode $content): void { - $file = $this->modulePath . '/etc/test/di.xml'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/etc/test/di.xml', $this->modulePath); + $this->createFile($file, $content->getRaw()); } /** * @Given I have the helper service configuration: */ - public function iHaveTheHelperServiceConfiguration(PyStringNode $content) + public function iHaveTheHelperServiceConfiguration(PyStringNode $content): void { - $file = $this->workingDirectory . '/features/bootstrap/config/services.yml'; - - $this->filesystem->dumpFile($file, $content->getRaw()); - - $this->files[] = $file; + $file = sprintf('%s/features/bootstrap/config/services.yml', $this->getWorkingDirectory()); + $this->createFile($file, $content->getRaw()); } /** * @Given /^the behat helper service class file "([^"]*)" contains:$/ */ - public function theBehatHelperServiceClassFileContains(string $className, PyStringNode $content) + public function theBehatHelperServiceClassFileContains(string $className, PyStringNode $content): void { - $file = $this->workingDirectory . '/features/bootstrap/' . str_replace('\\', '/', $className) . '.php'; + $file = sprintf( + '%s/features/bootstrap/%s.php', + $this->getWorkingDirectory(), + str_replace('\\', '/', $className), + ); + $this->createFile($file, $content->getRaw()); + } - $this->filesystem->dumpFile($file, $content->getRaw()); + /** + * @Given I compile the DI + */ + public function iCompileTheDi(): void + { + $this->iRunTheMagentoCommand('setup:di:compile'); + } - $this->files[] = $file; + /** + * @Given I :clearOrFlush the cache + */ + public function iCleanOrFlushTheCache(string $cleanOrFlush): void + { + Assert::inArray($cleanOrFlush, ['clean', 'flush'], 'Can only clean or flush the cache'); + $this->iRunTheMagentoCommand(sprintf('cache:%s', $cleanOrFlush)); + } + + /** + * @Given I run the Magento command :command + * @Given I run the Magento command :command with arguments :arguments + */ + public function iRunTheMagentoCommand(string $command, ?string $arguments = null): void + { + $this->runMagentoCommand($command, $arguments); } - protected function runMagentoCommand(string $command, string $arguments = '') + protected function runMagentoCommand(?string $command = null, ?string $arguments = null): void { - $magentoProcess = new Process( - sprintf('%s %s %s', 'bin/magento', $command, !empty($arguments) ? escapeshellarg($arguments) : ''), - $this->workingDirectory + $magentoProcess = $this->processFactory->createFromInput( + new MagentoCommandInput($command, $arguments, $this->getMagentoRootDirectory()), ); - $magentoProcess->setTimeout(120); + + $this->addProcess($magentoProcess); + $magentoProcess->setTimeout(600); $magentoProcess->run(); + Assert::same( + $magentoProcess->getExitCode(), + 0, + sprintf( + 'Expected Exit Code of Magento Process to be 0, got %d with message %s', + $magentoProcess->getExitCode(), + $magentoProcess->getErrorOutput(), + ), + ); + } + + private function getMagentoRootDirectory(): string + { + if ($this->magentoRootDirectory === null) { + $this->magentoRootDirectory = $this->magentoPathProvider->getMagentoRootDirectory(); + } + + return $this->magentoRootDirectory; + } + + private function revertMagentoConfig(): void + { + if ($this->filesystem->exists('/tmp/config.php.backup')) { + $this->filesystem->copy( + '/tmp/config.php.backup', + sprintf('%s/app/etc/config.php', $this->getMagentoRootDirectory()), + true, + ); + $this->filesystem->remove('/tmp/config.php.backup'); + $this->cacheCleaner->clean(false); + } + } + + private function removeModuleFolders(): void + { + $modulePath = $this->getModulePath(); + if ($modulePath && $this->filesystem->exists($modulePath)) { + $this->filesystem->remove($modulePath); + } + } + + private function determineFreshWorkingDirectoryFlag(): void + { + $directory = $this->getWorkingDirectory(); + Assert::string($directory, 'Working directory is not a string'); + $this->isFreshWorkingDirectory = $this->filesystem->exists($directory) === false; + } + + private function removeEmptyWorkingDirectory(): void + { + $directory = $this->getWorkingDirectory(); + Assert::string($directory, 'Working directory is not a string'); + if ($this->isFreshWorkingDirectory === true) { + $this->filesystem->remove($directory); + } } } diff --git a/features/bootstrap/Context/WithCompiledDITestRunnerContext.php b/features/bootstrap/Context/WithCompiledDITestRunnerContext.php deleted file mode 100644 index fd38b42..0000000 --- a/features/bootstrap/Context/WithCompiledDITestRunnerContext.php +++ /dev/null @@ -1,12 +0,0 @@ -runMagentoCommand('setup:di:compile'); - parent::iRunBehat($parameters, $phpParameters); - } -} diff --git a/features/bootstrap/Context/WithoutCompiledDITestRunnerContext.php b/features/bootstrap/Context/WithoutCompiledDITestRunnerContext.php deleted file mode 100644 index af60bd7..0000000 --- a/features/bootstrap/Context/WithoutCompiledDITestRunnerContext.php +++ /dev/null @@ -1,12 +0,0 @@ -runMagentoCommand('cache:clear'); - parent::iRunBehat($parameters, $phpParameters); - } -} diff --git a/features/helper_services.feature b/features/helper_services.feature index 019badd..2beadcf 100644 --- a/features/helper_services.feature +++ b/features/helper_services.feature @@ -1,16 +1,19 @@ +@virtual Feature: Using helper services to access services outside of Magento As a developer In order to write Behat tests easily I should be able to inject services from an additional helper service container - Scenario: Inject simple helper service to Context + Background: Given I have the feature: """ Feature: My awesome feature Scenario: Given a helper service has been successfully injected as argument to this step """ - And I have the context: + + Scenario: Inject simple helper service to Context + Given I have the context: """ another()); Assert::assertInstanceOf(OrderRepositoryInterface::class, $sharedService->orderRepository()); - Assert::assertInstanceOf(Mink::class, $sharedService->mink()); Assert::assertNotEmpty($sharedService->basePath()); } } @@ -104,32 +99,23 @@ Feature: Using helper services to access services outside of Magento """ anotherSharedService = $anotherSharedService; $this->orderRepository = $orderRepository; - $this->mink = $mink; $this->basePath = $basePath; } @@ -143,11 +129,6 @@ Feature: Using helper services to access services outside of Magento return $this->orderRepository; } - public function mink(): Mink - { - return $this->mink; - } - public function basePath(): string { return $this->basePath; @@ -180,7 +161,6 @@ Feature: Using helper services to access services outside of Magento arguments: - '@AnotherSharedService' - '@Magento\Sales\Api\OrderRepositoryInterface' - - '@mink' - '%paths.base%' """ And I have the configuration: @@ -191,35 +171,23 @@ Feature: Using helper services to access services outside of Magento autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: + SEEC\Behat\Magento2Extension: services: features/bootstrap/config/services.yml - Behat\MinkExtension: - base_url: 'http://example.com' - sessions: - default: - goutte: ~ """ When I run Behat Then I should see the tests passing Scenario: Autowire helper service dependencies - Given I have the feature: - """ - Feature: My awesome feature - Scenario: - Given a helper service has been successfully injected as argument to this step - """ - And I have the context: + Given I have the context: """ another()); Assert::assertInstanceOf(OrderRepositoryInterface::class, $sharedService->orderRepository()); - Assert::assertInstanceOf(Mink::class, $sharedService->mink()); Assert::assertNotEmpty($sharedService->basePath()); } } @@ -246,27 +213,19 @@ Feature: Using helper services to access services outside of Magento class SharedService { - /** @var AnotherSharedService */ - private $anotherSharedService; - - /** @var OrderRepositoryInterface */ - private $orderRepository; + private AnotherSharedService $anotherSharedService; - /** @var Mink */ - private $mink; + private OrderRepositoryInterface $orderRepository; - /** @var string */ - private $basePath; + private string $basePath; public function __construct( AnotherSharedService $anotherSharedService, OrderRepositoryInterface $orderRepository, - Mink $mink, string $basePath ) { $this->anotherSharedService = $anotherSharedService; $this->orderRepository = $orderRepository; - $this->mink = $mink; $this->basePath = $basePath; } @@ -280,11 +239,6 @@ Feature: Using helper services to access services outside of Magento return $this->orderRepository; } - public function mink(): Mink - { - return $this->mink; - } - public function basePath(): string { return $this->basePath; @@ -316,7 +270,6 @@ Feature: Using helper services to access services outside of Magento SharedService: class: SharedService arguments: - $mink: '@mink' $basePath: '%paths.base%' """ And I have the configuration: @@ -327,16 +280,11 @@ Feature: Using helper services to access services outside of Magento autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: + SEEC\Behat\Magento2Extension: services: features/bootstrap/config/services.yml - Behat\MinkExtension: - base_url: 'http://example.com' - sessions: - default: - goutte: ~ """ When I run Behat Then I should see the tests passing diff --git a/features/injecting_service_through_a_step_argument.feature b/features/injecting_service_through_a_step_argument.feature index 2a0bba8..ff52053 100644 --- a/features/injecting_service_through_a_step_argument.feature +++ b/features/injecting_service_through_a_step_argument.feature @@ -1,3 +1,4 @@ +@virtual Feature: Injecting service from Magento DI to Behat Context through a Behat Step argument As a developer In order to write Behat tests easily @@ -37,10 +38,11 @@ Feature: Injecting service from Magento DI to Behat Context through a Behat Step autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + - SEEC\Behat\Magento2Extension\Features\Bootstrap\Context\Hook\DatabaseHook + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing diff --git a/features/injecting_service_through_a_transformer_argument.feature b/features/injecting_service_through_a_transformer_argument.feature index bdf403e..cbfd7f9 100644 --- a/features/injecting_service_through_a_transformer_argument.feature +++ b/features/injecting_service_through_a_transformer_argument.feature @@ -1,3 +1,4 @@ +@virtual Feature: Injecting service from Magento DI to Behat Context through a Transformer argument As a developer In order to write Behat tests easily @@ -52,10 +53,10 @@ Feature: Injecting service from Magento DI to Behat Context through a Transforme autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing diff --git a/features/injecting_service_through_the_context_constructor.feature b/features/injecting_service_through_the_context_constructor.feature index 009632a..b7e2264 100644 --- a/features/injecting_service_through_the_context_constructor.feature +++ b/features/injecting_service_through_the_context_constructor.feature @@ -1,3 +1,4 @@ +@virtual Feature: Injecting service from Magento DI to Behat Context through the constructor As a developer In order to write Behat tests easily @@ -47,10 +48,10 @@ Feature: Injecting service from Magento DI to Behat Context through the construc contexts: - FeatureContext: - '@Magento\Catalog\Api\ProductRepositoryInterface' - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -64,10 +65,10 @@ Feature: Injecting service from Magento DI to Behat Context through the construc autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should not see a failing test diff --git a/features/mering_di_configurations.feature b/features/merging_di_configurations.feature similarity index 82% rename from features/mering_di_configurations.feature rename to features/merging_di_configurations.feature index bda7ff1..d3168ae 100644 --- a/features/mering_di_configurations.feature +++ b/features/merging_di_configurations.feature @@ -1,7 +1,18 @@ +@virtual @merging Feature: Merging DI configurations Background: - Given I have a Magento module called "Acme_FooBar" + Given I have the feature: + """ + Feature: FooBar + + Scenario: Fake Foo with Real Bar + Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" + And The bar service is "Acme\FooBar\Service\Bar" + Then The merge is correct + """ + And I have no Magento module called "Acme_FooBar" + And I have a Magento module called "Acme_FooBar" And I have an interface "Acme\FooBar\Service\FooInterface" defined in this module: """ foo); } /** @Given The bar service is :expected */ - public function checkBar($expected, FooBar $foobar) + public function checkBar($expected, FooBar $foobar): void { Assert::assertInstanceof($expected, $foobar->bar); } /** @Then The merge is correct */ - public function yay() {} + public function yay(): void {} } """ + And I flush the cache + And I run the Magento command "module:enable" with arguments "Acme_FooBar" Scenario: Merging global and test area correctly Given I have a global Magento DI configuration in this module: @@ -139,15 +149,6 @@ Feature: Merging DI configurations """ - And I have the feature: - """ - Feature: FooBar - - Scenario: Fake Foo with Real Bar - Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" - And The bar service is "Acme\FooBar\Service\Bar" - Then The merge is correct - """ And I have the configuration: """ default: @@ -156,12 +157,12 @@ Feature: Merging DI configurations autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: test extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -190,15 +191,6 @@ Feature: Merging DI configurations """ - And I have the feature: - """ - Feature: FooBar - - Scenario: Fake Foo with Real Bar - Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" - And The bar service is "Acme\FooBar\Service\Bar" - Then The merge is correct - """ And I have the configuration: """ default: @@ -207,12 +199,12 @@ Feature: Merging DI configurations autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [frontend, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -241,15 +233,6 @@ Feature: Merging DI configurations """ - And I have the feature: - """ - Feature: FooBar - - Scenario: Fake Foo with Real Bar - Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" - And The bar service is "Acme\FooBar\Service\Bar" - Then The merge is correct - """ And I have the configuration: """ default: @@ -258,12 +241,12 @@ Feature: Merging DI configurations autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [adminhtml, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -298,15 +281,6 @@ Feature: Merging DI configurations """ - And I have the feature: - """ - Feature: FooBar - - Scenario: Fake Foo with Real Bar - Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" - And The bar service is "Acme\FooBar\Service\Bar" - Then The merge is correct - """ And I have the configuration: """ default: @@ -315,12 +289,12 @@ Feature: Merging DI configurations autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [adminhtml, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -351,15 +325,6 @@ Feature: Merging DI configurations """ - And I have the feature: - """ - Feature: FooBar - - Scenario: Fake Foo with Real Bar - Given The foo service is "Acme\FooBar\Test\Service\FakeFoo" - And The bar service is "Acme\FooBar\Service\Bar" - Then The merge is correct - """ And I have the configuration: """ default: @@ -368,12 +333,12 @@ Feature: Merging DI configurations autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [adminhtml, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat - Then I should see the tests passing \ No newline at end of file + Then I should see the tests passing diff --git a/features/mocking.feature b/features/mocking.feature index 25a8add..c93b951 100644 --- a/features/mocking.feature +++ b/features/mocking.feature @@ -1,3 +1,4 @@ +@virtual @mocking Feature: Mocking As a developer In order to write Behat tests easily @@ -220,6 +221,7 @@ Feature: Mocking } } """ + And I run the Magento command "module:enable" with arguments "Acme_Awesome" Scenario: Override global service dependency using preference Given I have a global Magento DI configuration in this module: @@ -244,12 +246,12 @@ Feature: Mocking autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: test extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -281,12 +283,12 @@ Feature: Mocking autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: test extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -314,12 +316,12 @@ Feature: Mocking autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [frontend, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat Then I should see the tests passing @@ -351,12 +353,12 @@ Feature: Mocking autowire: true contexts: - FeatureContext - services: '@bex.magento2_extension.service_container' + services: '@seec.magento2_extension.service_container' magento: area: [frontend, test] extensions: - Bex\Behat\Magento2Extension: ~ + SEEC\Behat\Magento2Extension: ~ """ When I run Behat - Then I should see the tests passing \ No newline at end of file + Then I should see the tests passing diff --git a/features/purging_feature.feature b/features/purging_feature.feature new file mode 100644 index 0000000..de3a4f6 --- /dev/null +++ b/features/purging_feature.feature @@ -0,0 +1,87 @@ +@purge @fixtureCreation @virtual +Feature: Using helper services to access services outside of Magento + As a developer + In order to write Behat tests easily + I should be able to inject services from an additional helper service container + + Background: + Given I have the context: + """ + foo()); + } + } + """ + And the behat helper service class file "SharedService" contains: + """ + basePath = $basePath; - } - - public function create(Config $config, array $symfonyServiceContainers): DelegatingSymfonyServiceContainer - { - $container = new DelegatingSymfonyServiceContainer($symfonyServiceContainers); - - if (($file = $config->getServicesPath()) !== null) { - $fileLocator = new FileLocator([$this->basePath]); - $loader = new DelegatingLoader( - new LoaderResolver([ - new XmlFileLoader($container, $fileLocator), - new YamlFileLoader($container, $fileLocator), - new PhpFileLoader($container, $fileLocator), - ]) - ); - $loader->load($file); - } - - $container->compile(); - - return $container; - } -} diff --git a/src/Bex/Behat/Magento2Extension/HelperContainer/Magento2SymfonyServiceContainer.php b/src/Bex/Behat/Magento2Extension/HelperContainer/Magento2SymfonyServiceContainer.php deleted file mode 100644 index 73636ce..0000000 --- a/src/Bex/Behat/Magento2Extension/HelperContainer/Magento2SymfonyServiceContainer.php +++ /dev/null @@ -1,48 +0,0 @@ -magentoObjectManager = $magentoObjectManager; - } - - public function has($id) - { - try { - $this->magentoObjectManager->get($id); - return true; - } catch (\Exception $e) { - return false; - } - } - - public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) - { - try { - return $this->magentoObjectManager->get($id); - } catch (\Exception $e) { - throw new ServiceNotFoundException($id, null, $e); - } - } - - public function getDefinition($id) - { - return (new Definition($id, [$id]))->setFactory([new Reference('magento2.object_manager'), 'get']); - } -} diff --git a/src/Bex/Behat/Magento2Extension/Listener/MagentoObjectManagerInitializer.php b/src/Bex/Behat/Magento2Extension/Listener/MagentoObjectManagerInitializer.php deleted file mode 100644 index 1f601a0..0000000 --- a/src/Bex/Behat/Magento2Extension/Listener/MagentoObjectManagerInitializer.php +++ /dev/null @@ -1,138 +0,0 @@ -config = $config; - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() - { - return [ - SuiteTested::BEFORE => 'initApplication' - ]; - } - - public function initApplication(BeforeSuiteTested $event) - { - $areas = $event->getSuite()->getSettings()['magento']['area'] ?? Area::AREA_GLOBAL; - - if (is_string($areas)) { - $areas = [$areas]; - } - - $bootstrapPath = $this->config->getMagentoBootstrapPath(); - - if (!file_exists($bootstrapPath)) { - throw new \RuntimeException(sprintf("Magento's bootstrap file was not found at path '%s'", $bootstrapPath)); - } - - include $bootstrapPath; - - $params = $_SERVER; - - // TODO Can we remove this? - $params[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [ - DirectoryList::PUB => [DirectoryList::URL_PATH => ''], - DirectoryList::MEDIA => [DirectoryList::URL_PATH => 'media'], - DirectoryList::STATIC_VIEW => [DirectoryList::URL_PATH => 'static'], - DirectoryList::UPLOAD => [DirectoryList::URL_PATH => 'media/upload'], - ]; - - $bootstrap = Bootstrap::create(BP, $params); - $magentoObjectManager = ObjectManager::getInstance(); - - $configLoader = $magentoObjectManager->get(ConfigLoaderInterface::class); - - $mainArea = array_shift($areas); - $config = $configLoader->load($mainArea); - foreach ($areas as $area) { - $config = array_replace_recursive( - $config, - $this->arrayRecursiveDiff($configLoader->load($area), $configLoader->load(Area::AREA_GLOBAL)) - ); - } - - $bootstrap = Bootstrap::create(BP, $params); - $magentoObjectManager = ObjectManager::getInstance(); - - $magentoObjectManager->configure($config); - - $appState = $magentoObjectManager->get(State::class); - $appState->setAreaCode($mainArea); - - // TODO can we remove this? - if ($appState->getAreaCode() === Area::AREA_ADMINHTML) { - $registry = $magentoObjectManager->get(Registry::class); - $registry->register('isSecureArea', true); - $roleCollection = $magentoObjectManager->get(Collection::class); - $roleCollection->setRolesFilter(); - - $adminRole = $roleCollection->getFirstItem(); - - $userFactory = $magentoObjectManager->get(UserFactory::class); - $user = $userFactory->create(); - - $reflectedUser = new \ReflectionObject($user); - $aclRoleProperty = $reflectedUser->getProperty('_role'); - $aclRoleProperty->setAccessible(true); - $aclRoleProperty->setValue($user, $adminRole); - - $session = $magentoObjectManager->get(Session::class); - $session->setUser($user); - } - } - - // TODO replace this with one of the nice array diff packages :D - // copied from http://php.net/manual/en/function.array-diff.php#91756 - private function arrayRecursiveDiff($original, $excluded): array - { - $aReturn = []; - - foreach ($original as $key => $value) { - if (array_key_exists($key, $excluded)) { - if (is_array($value)) { - $recursiveDiff = $this->arrayRecursiveDiff($value, $excluded[$key]); - if (count($recursiveDiff)) { $aReturn[$key] = $recursiveDiff; } - } else { - if ($value != $excluded[$key]) { - $aReturn[$key] = $value; - } - } - } else { - $aReturn[$key] = $value; - } - } - - return $aReturn; - } -} diff --git a/src/Bex/Behat/Magento2Extension/Service/MagentoObjectManager.php b/src/Bex/Behat/Magento2Extension/Service/MagentoObjectManager.php deleted file mode 100644 index 25346aa..0000000 --- a/src/Bex/Behat/Magento2Extension/Service/MagentoObjectManager.php +++ /dev/null @@ -1,18 +0,0 @@ -get($id); - } - - public function create(string $id) - { - return ObjectManager::getInstance()->create($id); - } -} diff --git a/src/Bex/Behat/Magento2Extension/ServiceContainer/Magento2Extension.php b/src/Bex/Behat/Magento2Extension/ServiceContainer/Magento2Extension.php deleted file mode 100644 index 5adb93d..0000000 --- a/src/Bex/Behat/Magento2Extension/ServiceContainer/Magento2Extension.php +++ /dev/null @@ -1,53 +0,0 @@ -children() - ->scalarNode(Config::CONFIG_KEY_MAGENTO_BOOTSTRAP) - ->defaultValue(getcwd() . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'bootstrap.php') - ->end() - ->scalarNode(Config::CONFIG_KEY_SERVICES) - ->defaultValue(null) - ->end() - ->end(); - } - - public function load(ContainerBuilder $container, array $config) - { - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/config')); - $loader->load('services.xml'); - $extensionConfig = new Config($config); - $container->set(self::SERVICE_ID_EXTENSION_CONFIG, $extensionConfig); - $container->set('bex.behat_service_container', $container); - } - - public function process(ContainerBuilder $container) - { - // nothing to do here - } -} diff --git a/src/Bex/Behat/Magento2Extension/ServiceContainer/config/services.xml b/src/Bex/Behat/Magento2Extension/ServiceContainer/config/services.xml deleted file mode 100644 index 8887b93..0000000 --- a/src/Bex/Behat/Magento2Extension/ServiceContainer/config/services.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - %paths.base% - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Components/SharedStorage/SharedStorage.php b/src/Components/SharedStorage/SharedStorage.php new file mode 100644 index 0000000..a0d2442 --- /dev/null +++ b/src/Components/SharedStorage/SharedStorage.php @@ -0,0 +1,34 @@ +data[$key] = $value; + } + + public function get(string $key) + { + Assert::keyExists($this->data, $key); + + return $this->data[$key]; + } + + public function has(string $key): bool + { + return isset($this->data[$key]); + } + + public function setClipboard(array $clipboard): void + { + $this->data = array_merge($this->data, $clipboard); + } +} diff --git a/src/Components/SharedStorage/SharedStorageAwareInterface.php b/src/Components/SharedStorage/SharedStorageAwareInterface.php new file mode 100644 index 0000000..32ce620 --- /dev/null +++ b/src/Components/SharedStorage/SharedStorageAwareInterface.php @@ -0,0 +1,10 @@ +fallbackContainers = $symfonyServiceContainers; } - public function has($id) + public function has(string $id): bool { - if (!$this->isSupportedServiceId($id)) { + if ($this->isPageObject($id)) { return false; } @@ -38,49 +39,45 @@ public function has($id) return false; } - public function get($id, $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE) + public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object { - if (!$this->isSupportedServiceId($id)) { + if ($this->isPageObject($id)) { return null; } try { return parent::get($id); } catch (ServiceNotFoundException $e) { - // no-op continue } foreach ($this->fallbackContainers as $serviceContainer) { try { return $serviceContainer->get($id); - } catch (\Exception $e) { - // no-op continue + } catch (Throwable $e) { } } throw new ServiceNotFoundException($id); } - public function getDefinition($id) + public function getDefinition(string $id): Definition { try { return parent::getDefinition($id); } catch (ServiceNotFoundException $e) { - // no-op continue } foreach ($this->fallbackContainers as $serviceContainer) { try { return $serviceContainer->getDefinition($id); } catch (ServiceNotFoundException $e) { - // no-op continue } } throw new ServiceNotFoundException($id); } - public function compile(bool $resolveEnvPlaceholders = false) + public function compile(bool $resolveEnvPlaceholders = false): void { foreach ($this->fallbackContainers as $serviceContainer) { $this->parameterBag->add($serviceContainer->getParameterBag()->all()); @@ -89,26 +86,10 @@ public function compile(bool $resolveEnvPlaceholders = false) parent::compile($resolveEnvPlaceholders); } - private function isSupportedServiceId($id) + private function isPageObject(string $id): bool { - if (is_null($id)) { - return false; - } + Assert::notNull($id); - // If the Page Object Extension is used then let it handle the autowiring - // @see \SensioLabs\Behat\PageObjectExtension\Context\Argument\PageObjectArgumentResolver::resolveArguments - if ($this->isPageObject($id)) { - return false; - } - - return true; - } - - private function isPageObject($id) - { - return ( - is_subclass_of($id, '\SensioLabs\Behat\PageObjectExtension\PageObject\Page') || - is_subclass_of($id, '\SensioLabs\Behat\PageObjectExtension\PageObject\Element') - ); + return is_subclass_of($id, Page::class) || is_subclass_of($id, Element::class); } } diff --git a/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactory.php b/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactory.php new file mode 100644 index 0000000..430f3c3 --- /dev/null +++ b/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactory.php @@ -0,0 +1,30 @@ +getServicesPath()) !== null) { + $this->loaderHelper->loadFiles($container, $file, $this->basePath); + } + + $container->compile(); + + return $container; + } +} diff --git a/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryInterface.php b/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryInterface.php new file mode 100644 index 0000000..f78a2f1 --- /dev/null +++ b/src/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryInterface.php @@ -0,0 +1,13 @@ +load($file); + } +} diff --git a/src/HelperContainer/Loader/DelegatingLoaderHelperInterface.php b/src/HelperContainer/Loader/DelegatingLoaderHelperInterface.php new file mode 100644 index 0000000..bd966ce --- /dev/null +++ b/src/HelperContainer/Loader/DelegatingLoaderHelperInterface.php @@ -0,0 +1,12 @@ +get($id); + + return true; + } catch (\Throwable $e) { + return false; + } + } + + public function get(string $id, int $invalidBehavior = self::EXCEPTION_ON_INVALID_REFERENCE): ?object + { + try { + return $this->magentoObjectManager->get($id); + } catch (\Throwable $e) { + throw new ServiceNotFoundException($id, null, $e); + } + } + + public function getDefinition(string $id): Definition + { + return (new Definition($id, [$id]))->setFactory([new Reference('magento2.object_manager'), 'get']); + } +} diff --git a/src/HelperContainer/ServiceContainerInterface.php b/src/HelperContainer/ServiceContainerInterface.php new file mode 100644 index 0000000..71ba8bd --- /dev/null +++ b/src/HelperContainer/ServiceContainerInterface.php @@ -0,0 +1,12 @@ + 'initApplication', + ]; + } + + public function initApplication(SuiteTested $event): void + { + $areas = $this->getAreas($event); + + // fix issues with Target component + $target = new Target(self::class); + + $bootstrapPath = $this->config->getMagentoBootstrapPath(); + Assert::fileExists($bootstrapPath, sprintf("Magento's bootstrap file was not found at path '%s'", $bootstrapPath)); + include $bootstrapPath; + + $_SERVER[Bootstrap::INIT_PARAM_FILESYSTEM_DIR_PATHS] = [ + DirectoryList::PUB => [DirectoryListAlias::URL_PATH => ''], + DirectoryList::MEDIA => [DirectoryListAlias::URL_PATH => 'media'], + DirectoryList::STATIC_VIEW => [DirectoryListAlias::URL_PATH => 'static'], + DirectoryList::UPLOAD => [DirectoryListAlias::URL_PATH => 'media/upload'], + ]; + + Bootstrap::create(BP, $_SERVER); /** @phpstan-ignore-line */ + $magentoObjectManager = ObjectManager::getInstance(); + + $configLoader = $magentoObjectManager->get(ConfigLoaderInterface::class); + + $mainArea = array_shift($areas); + $config = $configLoader->load($mainArea); + foreach ($areas as $area) { + $config = array_replace_recursive( + $config, + $this->arrayRecursiveDiff($configLoader->load($area), $configLoader->load(Area::AREA_GLOBAL)), + ); + } + + Bootstrap::create(BP, $_SERVER); /** @phpstan-ignore-line */ + $magentoObjectManager = ObjectManager::getInstance(); + + $magentoObjectManager->configure($config); + + $appState = $magentoObjectManager->get(State::class); + $appState->setAreaCode($mainArea); + + $this->handleAdminAreaBootstrapping($appState, $magentoObjectManager); + } + + public function arrayRecursiveDiff(array $original, array $excluded): array + { + $aReturn = []; + foreach ($original as $key => $value) { + if (array_key_exists($key, $excluded)) { + if (is_array($value)) { + $recursiveDiff = $this->arrayRecursiveDiff($value, $excluded[$key]); + if (count($recursiveDiff) > 0) { + $aReturn[$key] = $recursiveDiff; + } + } elseif ($value !== $excluded[$key]) { + $aReturn[$key] = $value; + } + } else { + $aReturn[$key] = $value; + } + } + + return $aReturn; + } + + private function getAreas(SuiteTested $event): array + { + $areas = $event->getSuite()->getSettings()['magento']['area'] ?? Area::AREA_GLOBAL; + + if (is_string($areas)) { + $areas = [$areas]; + } + + if (!is_array($areas)) { + $areas = [$areas]; + } + + return $areas; + } + + public function handleAdminAreaBootstrapping(State $appState, ObjectManager $magentoObjectManager): void + { + if ($appState->getAreaCode() === Area::AREA_ADMINHTML) { + $registry = $magentoObjectManager->get(Registry::class); + $registry->register('isSecureArea', true); + $roleCollection = $magentoObjectManager->get(Collection::class); + $roleCollection->setRolesFilter(); + + /** @var Role $adminRole */ + $adminRole = $roleCollection->getFirstItem(); + + /** @var User $user */ + $user = $magentoObjectManager->get(User::class); + $user->setRoleId($adminRole->getId()); /** @phpstan-ignore-line */ + + /** @var Session $session */ + $session = $magentoObjectManager->get(Session::class); + $session->setUser($user); + } + } +} diff --git a/src/Listener/MagentoObjectManagerInitListenerInterface.php b/src/Listener/MagentoObjectManagerInitListenerInterface.php new file mode 100644 index 0000000..6355b28 --- /dev/null +++ b/src/Listener/MagentoObjectManagerInitListenerInterface.php @@ -0,0 +1,19 @@ +get($id); + } + + public function create(string $id): object + { + return ObjectManager::getInstance()->create($id); + } +} diff --git a/src/Service/MagentoObjectManagerInterface.php b/src/Service/MagentoObjectManagerInterface.php new file mode 100644 index 0000000..3109067 --- /dev/null +++ b/src/Service/MagentoObjectManagerInterface.php @@ -0,0 +1,12 @@ +magentoBootstrapPath = $config[self::CONFIG_KEY_MAGENTO_BOOTSTRAP]; diff --git a/src/ServiceContainer/ConfigInterface.php b/src/ServiceContainer/ConfigInterface.php new file mode 100644 index 0000000..4cf22cc --- /dev/null +++ b/src/ServiceContainer/ConfigInterface.php @@ -0,0 +1,16 @@ +children() + ->scalarNode(ConfigInterface::CONFIG_KEY_MAGENTO_BOOTSTRAP) + ->defaultValue($this->getMagentoBootstrapPath()) + ->end() + ->scalarNode(ConfigInterface::CONFIG_KEY_SERVICES) + ->defaultValue(null) + ->end() + ->end(); + } + + public function load(ContainerBuilder $container, array $config): void + { + $locator = new FileLocator(__DIR__ . '/config'); + $loader = new PhpFileLoader($container, $locator); + Assert::fileExists($locator->locate('services.php')); + $loader->load('services.php'); + $extensionConfig = new Config($config); + $container->addCompilerPass(new RegisterListenersPass()); + $container->set(self::SERVICE_ID_EXTENSION_CONFIG, $extensionConfig); + $container->set(self::BEHAT_CONTAINER_KEY, $container); + } + + public function process(TaggedContainerInterface $container): void + { + } + + private function getMagentoBootstrapPath(): string + { + $magentoPathProvider = new MagentoPathProvider(); + + return sprintf('%s/app/bootstrap.php', $magentoPathProvider->getMagentoRootDirectory()); + } +} diff --git a/src/ServiceContainer/Magento2ExtensionInterface.php b/src/ServiceContainer/Magento2ExtensionInterface.php new file mode 100644 index 0000000..363b89a --- /dev/null +++ b/src/ServiceContainer/Magento2ExtensionInterface.php @@ -0,0 +1,19 @@ +services(); + + $services->set('seec.magento2_extension.config', Config::class); + + $services->set('magento2.object_manager', MagentoObjectManager::class) + ->public(); + + $services->set( + 'seec.behat.magento2_extension.helper_container.loader.delegating_loader_helper', + DelegatingLoaderHelper::class, + )->public(); + + $services->set( + 'seec.behat.magento2_extension.delegating_symfony_service_container_factory', + DelegatingSymfonyServiceContainerFactory::class, + ) + ->public() + ->args([ + '%paths.base%', + service('seec.behat.magento2_extension.helper_container.loader.delegating_loader_helper'), + ]); + + $services->set('seec.magento2_extension.magento2_service_container', Magento2SymfonyServiceContainer::class) + ->public() + ->args([ + service('magento2.object_manager'), + ]) + ->share(false); + + $services->set('seec.behat_service_container', ContainerBuilder::class); + + $services->set('seec.magento2_extension.service_container', DelegatingSymfonyServiceContainer::class) + ->public() + ->tag('helper_container.container') + ->args([ + service('seec.magento2_extension.config'), + [ + service('seec.behat_service_container'), + service('seec.magento2_extension.magento2_service_container'), + ], + ]) + ->factory([ + service('seec.behat.magento2_extension.delegating_symfony_service_container_factory'), + 'create', + ]) + ->share(false); + + $services->set( + 'seec.magento2_extension.object_manager_initializer_listener', + MagentoObjectManagerInitListener::class, + ) + ->tag('event_dispatcher.subscriber') + ->args([service('seec.magento2_extension.config')]); + + $services->set('seec.magento2_extension.shared_storage', SharedStorage::class) + ->public() + ->share(); +}; diff --git a/tests/Context/Tasks/CacheCleanerTest.php b/tests/Context/Tasks/CacheCleanerTest.php new file mode 100644 index 0000000..d80c2c6 --- /dev/null +++ b/tests/Context/Tasks/CacheCleanerTest.php @@ -0,0 +1,50 @@ +pathProvider = $this->createMock(MagentoPathProviderInterface::class); + $this->fileSystem = $this->createMock(Filesystem::class); + $this->finder = $this->createMock(Finder::class); + $this->cacheCleaner = new CacheCleaner($this->pathProvider, $this->fileSystem, $this->finder); + } + + public function test_it_will_attempt_to_clear_the_cache_correctly(): void + { + $this->pathProvider->expects($this->once()) + ->method('getMagentoRootDirectory') + ->willReturn('/var/www/html'); + + $this->finder->expects($this->once()) + ->method('in') + ->with('/var/www/html/var/cache') + ->willReturnSelf(); + + $this->fileSystem->expects($this->once()) + ->method('remove') + ->with($this->finder); + + $this->cacheCleaner->clean(false); + } +} diff --git a/tests/Context/Tasks/PurgerTest.php b/tests/Context/Tasks/PurgerTest.php new file mode 100644 index 0000000..e11a30c --- /dev/null +++ b/tests/Context/Tasks/PurgerTest.php @@ -0,0 +1,56 @@ +purger = new Purger(); + } + + public function test_it_will_attempt_to_purge_all_tables(): void + { + $mockConnection = $this->createMock(AdapterInterface::class); + $mockConnection->expects($this->once()) + ->method('getTables') + ->willReturn(['table1', 'table2', 'table3']); + $mockConnection->expects($this->exactly(3)) + ->method('query') + ->with(...$this->withConsecutive( + ['SET FOREIGN_KEY_CHECKS = 0'], + ['TRUNCATE TABLE table1'], + ['SET FOREIGN_KEY_CHECKS = 1'], + )); + + $mockSelect = $this->createMock(Select::class); + $mockConnection->expects($this->exactly(2)) + ->method('select') + ->willReturn($mockSelect); + + $mockSelect->expects($this->exactly(2)) + ->method('from') + ->with(...$this->withConsecutive( + ['table1', 'COUNT(*)'], + ['table2', 'COUNT(*)'], + )); + $mockConnection->expects($this->exactly(2)) + ->method('fetchOne') + ->willReturnOnConsecutiveCalls('999', '0'); + + $this->purger->purge($mockConnection, ['table3']); + } +} diff --git a/tests/HelperContainer/DelegatingSymfonyServiceContainerTest.php b/tests/HelperContainer/DelegatingSymfonyServiceContainerTest.php new file mode 100644 index 0000000..a31b02a --- /dev/null +++ b/tests/HelperContainer/DelegatingSymfonyServiceContainerTest.php @@ -0,0 +1,175 @@ +container = new DelegatingSymfonyServiceContainer([]); + } + + public function test_has_returns_true_if_service_exists(): void + { + $mockSession = $this->createMock(Session::class); + $this->container->set('test_class', new TestClass($mockSession, [])); + $this->assertTrue($this->container->has('test_class')); + $this->assertInstanceOf(TestClass::class, $this->container->get('test_class')); + } + + public function test_has_returns_false_if_service_does_not_exist(): void + { + $this->assertFalse($this->container->has('my_service')); + } + + public function test_get_returns_service_if_it_exists(): void + { + $mockSession = $this->createMock(Session::class); + $service = new TestClass($mockSession, []); + $this->container->set('test_class', $service); + $this->assertSame($service, $this->container->get('test_class')); + } + + public function test_it_throws_an_error_if_service_does_not_exist(): void + { + $this->expectException(ServiceNotFoundException::class); + $this->assertNull($this->container->get('my_service')); + } + + public function test_getDefinition_returns_definition_if_service_exists(): void + { + $definition = $this->createMock(Definition::class); + $this->container->setDefinition(TestClass::class, $definition); + + $this->assertSame($definition, $this->container->getDefinition(TestClass::class)); + } + + public function test_getDefinition_throws_exception_if_service_does_not_exist(): void + { + $this->expectException(ServiceNotFoundException::class); + + $this->container->getDefinition('my_service'); + } + + public function test_compile_merges_parameter_bags_of_fallback_containers(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackContainer1->setParameter('param1', 'value1'); + + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackContainer2->setParameter('param2', 'value2'); + + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + $container->compile(); + + $this->assertSame(['param1' => 'value1', 'param2' => 'value2'], $container->getParameterBag()->all()); + } + + public function test_get_returns_service_from_fallback_containers(): void + { + $mockSession = $this->createMock(Session::class); + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackService1 = new TestClass($mockSession); + $fallbackContainer1->set('some_other_class', $fallbackService1); + + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackService2 = new TestClass($mockSession); + $fallbackContainer2->set('test_class', $fallbackService2); + + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + + $this->assertSame($fallbackService2, $container->get('test_class')); + } + + public function test_get_returns_null_if_service_does_not_exist_in_fallback_containers(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackContainer2 = new SymfonyServiceContainer(); + + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('You have requested a non-existent service "test_class".'); + $this->assertNull($container->get('test_class')); + } + + public function test_getDefinition_returns_definition_from_fallback_containers(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackDefinition1 = new Definition(\stdClass::class); + $fallbackContainer1->setDefinition('my_service', $fallbackDefinition1); + + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackDefinition2 = new Definition(\stdClass::class); + $fallbackContainer2->setDefinition('my_service', $fallbackDefinition2); + + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + + $this->assertSame($fallbackDefinition1, $container->getDefinition('my_service')); + } + + public function test_getDefinition_throws_exception_if_definition_does_not_exist_in_fallback_containers(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + + $this->expectException(ServiceNotFoundException::class); + $container->getDefinition('my_service'); + } + + public function test_has_returns_false_if_service_does_not_exist_in_any_container(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + + $this->assertFalse($container->has(TestClass::class)); + } + + public function test_has_returns_true_if_service_exists_in_fallback_containers(): void + { + $fallbackContainer1 = new SymfonyServiceContainer(); + $fallbackContainer2 = new SymfonyServiceContainer(); + $fallbackContainers = [$fallbackContainer1, $fallbackContainer2]; + $container = new DelegatingSymfonyServiceContainer($fallbackContainers); + $fallbackContainer1->set('test_class', new \stdClass()); + + $this->assertTrue($container->has('test_class')); + } + + public function test_it_returns_null_when_class_should_be_handled_by_autowiring(): void + { + $mockSession = $this->createMock(Session::class); + $service = new TestClass($mockSession, []); + $this->container->set('test_class', $service); + $this->assertNull($this->container->get(TestClass::class)); + } +} diff --git a/tests/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryTest.php b/tests/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryTest.php new file mode 100644 index 0000000..2942ded --- /dev/null +++ b/tests/HelperContainer/Factory/DelegatingSymfonyServiceContainerFactoryTest.php @@ -0,0 +1,56 @@ +basePath = '/path/to/base'; + $this->helper = $this->createMock(DelegatingLoaderHelperInterface::class); + $this->factory = new DelegatingSymfonyServiceContainerFactory($this->basePath, $this->helper); + } + + public function test_it_will_not_configure_created_class_when_config_contains_nothing_to_use(): void + { + /** @var ConfigInterface|MockObject $config */ + $config = $this->createMock(ConfigInterface::class); + $symfonyServiceContainers = []; + $container = $this->factory->create($config, $symfonyServiceContainers); + $this->helper->expects($this->never())->method('loadFiles'); + $this->assertInstanceOf(DelegatingSymfonyServiceContainer::class, $container); + } + + public function test_it_will_correctly_create_new_class(): void + { + /** @var ConfigInterface|MockObject $config */ + $config = $this->createMock(ConfigInterface::class); + $symfonyServiceContainers = []; + $config->expects($this->once()) + ->method('getServicesPath') + ->willReturn('/var/log/test.yml'); + + $this->helper->expects($this->once()) + ->method('loadFiles') + ->with($this->isInstanceOf(DelegatingSymfonyServiceContainer::class), '/var/log/test.yml', $this->basePath); + + $container = $this->factory->create($config, $symfonyServiceContainers); + $this->assertInstanceOf(DelegatingSymfonyServiceContainer::class, $container); + } +} diff --git a/tests/HelperContainer/Helper/DelegatingLoaderHelperTest.php b/tests/HelperContainer/Helper/DelegatingLoaderHelperTest.php new file mode 100644 index 0000000..675cbd0 --- /dev/null +++ b/tests/HelperContainer/Helper/DelegatingLoaderHelperTest.php @@ -0,0 +1,22 @@ +createMock(ContainerBuilder::class); + $file = 'test.yml'; + $basePath = __DIR__; + $helper = new DelegatingLoaderHelper(); + $helper->loadFiles($container, $file, $basePath); + $this->assertTrue(true); + } +} diff --git a/tests/HelperContainer/Helper/test.yml b/tests/HelperContainer/Helper/test.yml new file mode 100644 index 0000000..e69de29 diff --git a/tests/HelperContainer/Magento2SymfonyServiceContainerTest.php b/tests/HelperContainer/Magento2SymfonyServiceContainerTest.php new file mode 100644 index 0000000..e250b10 --- /dev/null +++ b/tests/HelperContainer/Magento2SymfonyServiceContainerTest.php @@ -0,0 +1,63 @@ +magentoObjectManager = $this->createMock(MagentoObjectManagerInterface::class); + $this->container = new Magento2SymfonyServiceContainer($this->magentoObjectManager); + } + + public function test_it_is_a_container(): void + { + $this->assertInstanceOf(ContainerInterface::class, $this->container); + } + + public function serviceProvider(): array + { + return [ + 'it has the service available' => [true], + 'it has the service not available' => [false], + ]; + } + + /** @dataProvider serviceProvider */ + public function test_it_can_evaluate_if_it_has_an_service(bool $expectation): void + { + if ($expectation) { + $this->magentoObjectManager->expects($this->once()) + ->method('get') + ->with('some_service') + ->willReturn(new stdClass()); + } else { + $this->magentoObjectManager->expects($this->once()) + ->method('get') + ->with('some_service') + ->willThrowException(new \Exception()); + } + + $this->assertSame($expectation, $this->container->has('some_service')); + } + + public function test_it_can_correctly_get_the_definition(): void + { + $this->assertInstanceOf(Definition::class, $this->container->getDefinition('some_service')); + } +} diff --git a/tests/Listener/MagentoObjectManagerInitListenerTest.php b/tests/Listener/MagentoObjectManagerInitListenerTest.php new file mode 100644 index 0000000..cd2b1e8 --- /dev/null +++ b/tests/Listener/MagentoObjectManagerInitListenerTest.php @@ -0,0 +1,165 @@ +config = $this->createMock(ConfigInterface::class); + $this->listener = new MagentoObjectManagerInitListener($this->config); + } + + public function test_it_is_an_event_subscriber(): void + { + $this->assertInstanceOf(EventSubscriberInterface::class, $this->listener); + } + + public function test_it_is_subscribed_to_the_correct_events(): void + { + $this->assertSame(['tester.suite_tested.before' => 'initApplication'], $this->listener::getSubscribedEvents()); + } + + public function test_it_will_throw_an_error_when_bootstrap_file_cannot_be_found(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Magento's bootstrap file was not found at path 'some_path'"); + $this->config->expects($this->once()) + ->method('getMagentoBootstrapPath') + ->willReturn('some_path'); + + $mockEvent = $this->createMock(SuiteTested::class); + $mockSuite = $this->createMock(Suite::class); + $mockSuite->expects($this->once()) + ->method('getSettings') + ->willReturn(['magento' => ['area' => 'test']]); + $mockEvent->expects($this->once()) + ->method('getSuite') + ->willReturn($mockSuite); + + $this->listener->initApplication($mockEvent); + } + + public function test_it_will_create_an_admin_user_on_demand_when_bootstrapping_admin_area(): void + { + $objectManager = $this->createMock(ObjectManager::class); + $appState = $this->createMock(State::class); + $appState->expects($this->once()) + ->method('getAreaCode') + ->willReturn('adminhtml'); + $registryMock = $this->getMockBuilder(Registry::class) + ->disableOriginalConstructor() + ->getMock(); + $collectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + $user = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->addMethods(['setRoleId']) + ->getMock(); + + $sessionMock = $this->getMockBuilder(Session::class) + ->addMethods(['setUser']) + ->disableOriginalConstructor() + ->getMock(); + + $objectManager->expects($this->exactly(4)) + ->method('get') + ->with(...$this->withConsecutive( + [Registry::class], + [Collection::class], + ['Magento\User\Model\User'], + [Session::class], + )) + ->willReturnOnConsecutiveCalls( + $registryMock, + $collectionMock, + $user, + $sessionMock, + ); + + $registryMock->expects($this->once()) + ->method('register') + ->with('isSecureArea', true); + $collectionMock->expects($this->once()) + ->method('setRolesFilter'); + + $roleMock = $this->createMock(Role::class); + $collectionMock->expects($this->once()) + ->method('getFirstItem') + ->willReturn($roleMock); + $roleMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $sessionMock->expects($this->once()) + ->method('setUser') + ->with($user); + + $this->listener->handleAdminAreaBootstrapping($appState, $objectManager); + } + + public function testArrayRecursiveDiff(): void + { + $original = [ + 'key1' => 'value1', + 'key2' => 'value2', + 'key3' => [ + 'key4' => 'value4', + 'key5' => 'value5', + ], + 'key6' => [ + 'key7' => 'value7', + 'key8' => 'value8', + ], + ]; + + $excluded = [ + 'key1' => 'value1', + 'key3' => [ + 'key5' => 'value5', + ], + 'key6' => [ + 'key8' => 'value8', + ], + ]; + + $result = $this->listener->arrayRecursiveDiff($original, $excluded); + $this->assertEquals([ + 'key2' => 'value2', + 'key3' => [ + 'key4' => 'value4', + ], + 'key6' => [ + 'key7' => 'value7', + ], + ], $result); + } +} diff --git a/tests/ServiceContainer/ConfigTest.php b/tests/ServiceContainer/ConfigTest.php new file mode 100644 index 0000000..ac8b81a --- /dev/null +++ b/tests/ServiceContainer/ConfigTest.php @@ -0,0 +1,32 @@ +config = new Config([ + ConfigInterface::CONFIG_KEY_SERVICES => 'services', + ConfigInterface::CONFIG_KEY_MAGENTO_BOOTSTRAP => 'bootstrap', + ]); + } + + public function test_it_can_get_service_parameter(): void + { + $this->assertSame('services', $this->config->getServicesPath()); + } + + public function test_it_can_get_bootstrap_parameter(): void + { + $this->assertSame('bootstrap', $this->config->getMagentoBootstrapPath()); + } +} diff --git a/tests/ServiceContainer/Magento2ExtensionTest.php b/tests/ServiceContainer/Magento2ExtensionTest.php new file mode 100644 index 0000000..4d9d8c8 --- /dev/null +++ b/tests/ServiceContainer/Magento2ExtensionTest.php @@ -0,0 +1,67 @@ +extension = new Magento2Extension(); + } + + public function test_it_can_get_config_key(): void + { + $this->assertSame('seec_magento2', $this->extension->getConfigKey()); + } + + public function test_it_can_configure(): void + { + $builder = new ArrayNodeDefinition('root', new NodeBuilder()); + $this->extension->configure($builder); + $children = $builder->getChildNodeDefinitions(); + $this->assertArrayHasKey(ConfigInterface::CONFIG_KEY_MAGENTO_BOOTSTRAP, $children); + $this->assertArrayHasKey(ConfigInterface::CONFIG_KEY_SERVICES, $children); + } + + public function test_it_has_noop_functions(): void + { + $manager = new ExtensionManager([]); + $this->extension->initialize($manager); + $containerBuilder = $this->createMock(TaggedContainerInterface::class); + $this->extension->process($containerBuilder); + $this->assertTrue(true); + } + + public function test_it_can_correctly_load_config_into_container(): void + { + $containerBuilder = $this->createMock(ContainerBuilder::class); + $containerBuilder->expects($this->exactly(2)) + ->method('set') + ->with(...$this->withConsecutive( + ['seec.magento2_extension.config', $this->isInstanceOf(ConfigInterface::class)], + ['seec.behat_service_container', $containerBuilder], + )); + + $this->extension->load($containerBuilder, [ + 'bootstrap' => 'bootstrap', + 'services' => 'services', + ]); + } +}