diff --git a/.env.dist b/.env.dist new file mode 100644 index 0000000..27ed36e --- /dev/null +++ b/.env.dist @@ -0,0 +1,7 @@ +# Basic Wordpress Data + +DB_HOST=mysql +DB_PORT=3306 +DB_PASSWORD=p@55w0rd +DB_NAME=wordpress +DB_USER=root diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index bc952ac..75df221 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -24,12 +24,13 @@ jobs: uses: php-actions/composer@v6 with: php_version: ${{ matrix.php-versions }} + command: update - name: Validate Composer run: composer validate --strict - name: PHP Lint - run: composer run lint + run: composer lint - name: PHP test - run: composer run test + run: composer test:unit \ No newline at end of file diff --git a/.gitignore b/.gitignore index 61c9e30..6e7640d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ vendor yarn-error.log .phpunit.result.cache .DS_Store +testingdb +.vscode +.env \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index 4985860..fdbbd24 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,5 +2,5 @@ . "$(dirname -- "$0")/_/husky.sh" yarn lint:js && yarn test:js -composer run lint -composer run test +composer lint +composer test:unit diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..26836f7 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --ignore-externals --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --ignore-externals --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 087827e..f4925df 100644 --- a/composer.json +++ b/composer.json @@ -1,37 +1,49 @@ { - "name": "mr/feature-flags", - "description": "Allows developers to enable / disable features based on flags.", - "type": "wordpress-plugin", - "license": "proprietary", - "require-dev": { - "phpunit/phpunit": "^9.4", - "brain/monkey": "^2.6", - "newsuk/nuk-wp-phpcs-config": "^0.1.0", - "newsuk/nuk-wp-phpstan-config": "^0.1.0", - "newsuk/nuk-wp-phpmd-config": "^0.1.0" - }, - "autoload": { - "psr-4": { - "MR\\FeatureFlags\\": ["includes/"] - } - }, - "autoload-dev": { - "psr-4": { - "MR\\FeatureFlags\\Tests\\": "tests/unit/" - } - }, + "name": "mr/feature-flags", + "description": "Allows developers to enable / disable features based on flags.", + "type": "wordpress-plugin", + "license": "proprietary", + "repositories": [ + { + "type": "vcs", + "url": "https://git@github.com/WordPress/wordpress-develop" + } + ], + "require-dev": { + "wordpress/wordpress": "^6.4", + "phpunit/phpunit": "^9.4", + "brain/monkey": "^2.6", + "newsuk/nuk-wp-phpcs-config": "^0.1.0", + "newsuk/nuk-wp-phpstan-config": "^0.1.0", + "newsuk/nuk-wp-phpmd-config": "^0.1.0", + "yoast/wp-test-utils": "^1.2" + }, + "autoload": { + "psr-4": { + "MR\\FeatureFlags\\": [ + "includes/" + ] + } + }, + "autoload-dev": { + "psr-4": { + "MR\\FeatureFlags\\Tests\\": "tests/unit/" + } + }, "config": { - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "phpstan/extension-installer": true - } - }, - "scripts": { - "lint": "phpcs .", - "lint:fix": "phpcbf .", - "test": "phpunit --dont-report-useless-tests --configuration ./phpunit.xml --testdox", - "phpstan": "phpstan analyse --memory-limit=2048M", - "phpstan-baseline": "phpstan analyse -b --allow-empty-baseline --memory-limit=2048M", - "phpmd": "phpmd plugin.php,includes text phpmd.xml.dist --color" - } + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + } + }, + "scripts": { + "lint": "phpcs .", + "lint:fix": "phpcbf .", + "test:unit": "phpunit --dont-report-useless-tests --configuration ./phpunit.xml --testsuite unit --testdox --coverage-text", + "test:integration": "phpunit --dont-report-useless-tests --configuration ./phpunit-integration.xml --testsuite integration --testdox --coverage-text", + "test:multisite": "phpunit --dont-report-useless-tests --configuration ./phpunit-integration-multisite.xml --testsuite integration --testdox --coverage-text", + "phpstan": "phpstan analyse --memory-limit=2048M", + "phpstan-baseline": "phpstan analyse -b --allow-empty-baseline --memory-limit=2048M", + "phpmd": "phpmd plugin.php,includes text phpmd.xml.dist --color" + } } diff --git a/composer.lock b/composer.lock index 77f8f56..8aa3dc8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "10c64252fc2a1288e32dd26c50255f8b", + "content-hash": "1afbaeb104a5222a664f1628b3f4668e", "packages": [], "packages-dev": [ { "name": "antecedent/patchwork", - "version": "2.1.27", + "version": "2.1.28", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "16a1ab81559aabf14acb616141e801b32777f085" + "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/16a1ab81559aabf14acb616141e801b32777f085", - "reference": "16a1ab81559aabf14acb616141e801b32777f085", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/6b30aff81ebadf0f2feb9268d3e08385cebcc08d", + "reference": "6b30aff81ebadf0f2feb9268d3e08385cebcc08d", "shasum": "" }, "require": { @@ -51,9 +51,9 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.27" + "source": "https://github.com/antecedent/patchwork/tree/2.1.28" }, - "time": "2023-12-03T18:46:49+00:00" + "time": "2024-02-06T09:26:11+00:00" }, { "name": "automattic/vipwpcs", @@ -3873,16 +3873,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -3896,9 +3896,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -3935,7 +3932,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -3951,20 +3948,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -3978,9 +3975,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4018,7 +4012,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -4034,20 +4028,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5" + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fe2f306d1d9d346a7fee353d0d5012e401e984b5", - "reference": "fe2f306d1d9d346a7fee353d0d5012e401e984b5", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", "shasum": "" }, "require": { @@ -4055,9 +4049,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4097,7 +4088,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" }, "funding": [ { @@ -4113,7 +4104,7 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/service-contracts", @@ -4384,6 +4375,61 @@ ], "time": "2023-11-20T00:12:19+00:00" }, + { + "name": "wordpress/wordpress", + "version": "6.4.3", + "source": { + "type": "git", + "url": "https://git@github.com/WordPress/wordpress-develop", + "reference": "9e9559d6d6bd2327dee822c305b2905b65a91ef2" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpcompatibility/phpcompatibility-wp": "~2.1.3", + "squizlabs/php_codesniffer": "3.7.2", + "wp-coding-standards/wpcs": "~3.0.1", + "yoast/phpunit-polyfills": "^1.1.0" + }, + "suggest": { + "ext-dom": "*" + }, + "type": "library", + "scripts": { + "compat": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source" + ], + "format": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source" + ], + "lint": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --report=summary,source" + ], + "lint:errors": [ + "@lint -n" + ], + "test": [ + "Composer\\Config::disableProcessTimeout", + "@php ./vendor/phpunit/phpunit/phpunit" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", + "homepage": "https://wordpress.org", + "keywords": [ + "blog", + "cms", + "wordpress", + "wp" + ], + "support": { + "issues": "https://core.trac.wordpress.org/" + }, + "time": "2024-01-30T19:25:29+00:00" + }, { "name": "wp-coding-standards/wpcs", "version": "3.0.1", @@ -4449,6 +4495,135 @@ } ], "time": "2023-09-14T07:06:09+00:00" + }, + { + "name": "yoast/phpunit-polyfills", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", + "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/224e4a1329c03d8bad520e3fc4ec980034a4b212", + "reference": "224e4a1329c03d8bad520e3fc4ec980034a4b212", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + }, + "require-dev": { + "yoast/yoastcs": "^2.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "files": [ + "phpunitpolyfills-autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills/graphs/contributors" + } + ], + "description": "Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit cross-version compatible tests", + "homepage": "https://github.com/Yoast/PHPUnit-Polyfills", + "keywords": [ + "phpunit", + "polyfill", + "testing" + ], + "support": { + "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", + "source": "https://github.com/Yoast/PHPUnit-Polyfills" + }, + "time": "2023-08-19T14:25:08+00:00" + }, + { + "name": "yoast/wp-test-utils", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/Yoast/wp-test-utils.git", + "reference": "2e0f62e0281e4859707c5f13b7da1422aa1c8f7b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yoast/wp-test-utils/zipball/2e0f62e0281e4859707c5f13b7da1422aa1c8f7b", + "reference": "2e0f62e0281e4859707c5f13b7da1422aa1c8f7b", + "shasum": "" + }, + "require": { + "brain/monkey": "^2.6.1", + "php": ">=5.6", + "yoast/phpunit-polyfills": "^1.1.0" + }, + "require-dev": { + "yoast/yoastcs": "^2.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "1.x-dev", + "dev-main": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "exclude-from-classmap": [ + "/src/WPIntegration/TestCase.php", + "/src/WPIntegration/TestCaseNoPolyfills.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Team Yoast", + "email": "support@yoast.com", + "homepage": "https://yoast.com" + }, + { + "name": "Contributors", + "homepage": "https://github.com/Yoast/wp-test-utils/graphs/contributors" + } + ], + "description": "PHPUnit cross-version compatibility layer for testing plugins and themes build for WordPress", + "homepage": "https://github.com/Yoast/wp-test-utils/", + "keywords": [ + "brainmonkey", + "integration-testing", + "phpunit", + "testing", + "unit-testing", + "wordpress" + ], + "support": { + "issues": "https://github.com/Yoast/wp-test-utils/issues", + "source": "https://github.com/Yoast/wp-test-utils" + }, + "time": "2023-09-27T10:25:08+00:00" } ], "aliases": [], diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..155b23a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.7" + +services: + php: + build: + context: . + dockerfile: docker/php/Dockerfile + user: 1000:1000 + env_file: .env + environment: + WORDPRESS_DB_HOST: ${DB_HOST} + WORDPRESS_DB_PASSWORD: ${DB_PASSWORD} + WORDPRESS_DB_NAME: ${DB_NAME} + volumes: + - .:/usr/src + - ~/.composer:/var/cache/composer + links: + - mysql + depends_on: + - mysql + mysql: + image: mysql:8 + env_file: .env + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_NAME} + volumes: + - ./testingdb:/var/lib/mysql +volumes: + testingdb: diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..6c1a60c --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,38 @@ +FROM php:8.3 + +# Container + PHP Setup +ARG XDEBUG_INI="/usr/local/etc/php/conf.d/xdebug.ini" +# Install, enable and configure xdebug +RUN pecl install xdebug \ + && docker-php-ext-enable xdebug \ + && echo "[xdebug]" > $XDEBUG_INI \ + && echo "xdebug.mode = coverage" >> $XDEBUG_INI + +RUN docker-php-ext-install mysqli + +# Install Composer +RUN curl -s https://getcomposer.org/installer | php && \ + mv composer.phar /usr/local/bin/composer + +RUN mkdir /var/cache/composer +VOLUME /var/cache/composer +RUN chown -R www-data:www-data /var/cache/composer +ENV COMPOSER_HOME=/var/cache/composer + +# WP CLI Setup +ENV WORDPRESS_CLI_VERSION 2.9.0 + +RUN set -ex; \ + curl -o /usr/local/bin/wp -fSL "https://github.com/wp-cli/wp-cli/releases/download/v${WORDPRESS_CLI_VERSION}/wp-cli-${WORDPRESS_CLI_VERSION}.phar"; \ + curl -o /usr/local/bin/wp.sha512 -fSL "https://github.com/wp-cli/wp-cli/releases/download/v${WORDPRESS_CLI_VERSION}/wp-cli-${WORDPRESS_CLI_VERSION}.phar.sha512"; \ + \ + echo "$(cat /usr/local/bin/wp.sha512) /usr/local/bin/wp" | sha512sum -c -; \ + chmod +x /usr/local/bin/wp; \ + \ + wp --allow-root --version + +ENV WP_TESTS_DIR /usr/src/vendor/wordpress/wordpress/tests/phpunit +ENV WP_TESTS_CONFIG_FILE_PATH /usr/src/tests/functional + +VOLUME /usr/src +WORKDIR /usr/src diff --git a/includes/Api/Flags.php b/includes/Api/Flags.php index 4f48f6f..1fd5438 100644 --- a/includes/Api/Flags.php +++ b/includes/Api/Flags.php @@ -32,35 +32,41 @@ class Flags { /** * Register feature flag endpoints. * - * @return void * @since 1.0.0 */ - public function register_flags_endpoints() { + public function register(): void { add_action( 'rest_api_init', - function () { - register_rest_route( - 'feature-flags/v1', - 'flags', - [ - [ - 'methods' => WP_REST_Server::READABLE, - 'callback' => [ $this, 'get_all_flags' ], - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, - ], - [ - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'post_flags' ], - 'permission_callback' => function () { - return current_user_can( 'manage_options' ); - }, - 'validate_callback' => [ $this, 'validate_flag_input' ], - ], - ] - ); - } + [ $this, 'register_routes' ] + ); + } + + /** + * Register routes. + * + * * @since 1.0.0 + */ + public function register_routes(): void { + register_rest_route( + 'feature-flags/v1', + 'flags', + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_all_flags' ], + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ], + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'post_flags' ], + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + 'validate_callback' => [ $this, 'validate_flag_input' ], + ], + ] ); } @@ -71,7 +77,6 @@ function () { */ public function get_all_flags() { $flags = get_option( self::$option_name, [] ); - return rest_ensure_response( $flags ); } @@ -84,10 +89,10 @@ public function get_all_flags() { * @phpstan-param WP_REST_Request $request */ public function post_flags( WP_REST_Request $request ) { - $flags = $request->get_json_params(); + $input_data = $request->get_json_params(); - if ( count( $flags ) > 0 ) { - update_option( self::$option_name, $flags ); + if ( is_array( $input_data['flags'] ) ) { + update_option( self::$option_name, $input_data['flags'] ); return rest_ensure_response( array( 'status' => 200, @@ -102,25 +107,41 @@ public function post_flags( WP_REST_Request $request ) { /** * Validates flag input from POST method. * - * @param WP_REST_Request $param Request object. + * @param WP_REST_Request $request Request object. * * @return bool */ - public function validate_flag_input( $param ) { - $input_data = $param->get_json_params(); + public function validate_flag_input( $request ) { + $input_data = $request->get_json_params(); + + if ( ! isset( $input_data['flags'] ) || gettype( $input_data['flags'] ) !== 'array' ) { + return false; + } $valid_keys = [ 'id', 'name', 'enabled' ]; - if ( 0 === count( $input_data ) ) { + // handle delete all feature flags. + if ( 0 === count( $input_data['flags'] ) ) { return true; } - foreach ( $input_data as $flag ) { - foreach ( $valid_keys as $value ) { - if ( ! array_key_exists( $value, $flag ) ) { - return false; - } + foreach ( $input_data['flags'] as $flag ) { + // validate if the input contains allowed values. + if ( count( array_diff( $valid_keys, array_keys( $flag ) ) ) > 0 ) { + return false; + } + + foreach ( $valid_keys as $key ) { + $value = isset( $flag[ $key ] ) ? $flag[ $key ] : null; + + match ( $key ) { + 'id' => is_int( $value ), + 'name' => is_string( $value ), + 'enabled' => is_bool( $value ), + default => false, + }; } } + return true; } } diff --git a/jest.config.js b/jest.config.js index b9904ca..c981e6f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,5 +3,6 @@ module.exports = { moduleNameMapper: { '@wordpress/(.*)$': '/node_modules/@wordpress/$1', }, + modulePathIgnorePatterns: ['/vendor/'], testEnvironment: 'jsdom', }; diff --git a/local b/local new file mode 100755 index 0000000..4ecfcde --- /dev/null +++ b/local @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [ ! -f ".env" ]; then + cp .env.dist .env +fi + +docker-compose -p plugin-testing up -d mysql +docker-compose -p plugin-testing run --entrypoint bash php diff --git a/phpcs.xml b/phpcs.xml index d9ac96e..395ec0b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -8,4 +8,5 @@ + /vendor \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f4993c4..76aeced 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,4 +5,6 @@ parameters: level: max paths: - plugin.php - - includes/ \ No newline at end of file + - includes/ + excludePaths: + - tests/* \ No newline at end of file diff --git a/phpunit-integration-multisite.xml b/phpunit-integration-multisite.xml new file mode 100644 index 0000000..259f198 --- /dev/null +++ b/phpunit-integration-multisite.xml @@ -0,0 +1,12 @@ + + + + + + + + + ./tests/integration/ + + + diff --git a/phpunit-integration.xml b/phpunit-integration.xml new file mode 100644 index 0000000..ba53656 --- /dev/null +++ b/phpunit-integration.xml @@ -0,0 +1,13 @@ + + + + + ./includes/Api + + + + + ./tests/integration/ + + + diff --git a/phpunit.xml b/phpunit.xml index c9abd89..09777e5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,13 @@ - - + + + + ./includes + + - ./tests/Unit/ + ./tests/units/ diff --git a/plugin.php b/plugin.php index 6d356d4..5724cfa 100644 --- a/plugin.php +++ b/plugin.php @@ -124,7 +124,7 @@ function mr_feature_flags_scripts_enqueue(): void { // Registers feature flags API's. $mr_feature_flags_register_api = new Flags(); -$mr_feature_flags_register_api->register_flags_endpoints(); +$mr_feature_flags_register_api->register(); // Displays setting page link in plugin page. diff --git a/tests/Unit/FlagsTest.php b/tests/Unit/FlagsTest.php deleted file mode 100644 index c981626..0000000 --- a/tests/Unit/FlagsTest.php +++ /dev/null @@ -1,103 +0,0 @@ -1, 'name'=>'Test','enabled'=>true]]; - - \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); - \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); - - $flags = new Flags(); - $result = $flags->get_all_flags(); - $this->assertEquals($result, $mock_option_value); - } - - public function test_get_all_flags_method_should_return_empty_array_if_value_is_not_set() { - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - $mock_option_value = ''; - - \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); - \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); - - $flags = new Flags(); - $result = $flags->get_all_flags(); - $this->assertEquals($result, []); - } - - public function test_get_all_flags_method_should_return_multiple_flags_from_options_table() { - $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>true],['id'=>2, 'name'=>'Test2','enabled'=>false]]; - - \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); - \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); - - $flags = new Flags(); - $result = $flags->get_all_flags(); - $this->assertEquals($result, $mock_option_value); - } - - public function test_post_flags_methods_should_return_success_if_input_is_array() { - - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - $request_mock = \Mockery::mock('WP_Request'); - $request_mock->shouldReceive('get_json_params')->andReturn(['param1' => 'value1']); - - \Brain\Monkey\Functions\when('update_option')->justReturn(true); - \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); - - global $wp; - $wp = new \stdClass(); - $wp->request = $request_mock; - - $flags = new Flags(); - $result = $flags->post_flags($request_mock); - - $this->assertEquals(['status'=>200, 'success' => true], $result); - - unset($GLOBALS['wp']); - } - - public function test_post_flags_methods_should_throw_error_if_input_is_not_an_array() { - $this->markTestIncomplete( - 'This test has not been implemented yet.' - ); - $request_mock = \Mockery::mock('WP_Request'); - $request_mock->shouldReceive('get_json_params')->andReturn('test'); - - global $wp; - $wp = new \stdClass(); - $wp->request = $request_mock; - - $error_mock = \Mockery::mock('WP_Error'); - - \Brain\Monkey\Functions\expect('post_flags')->andReturn($error_mock); - - - $flags = new Flags(); - $result = $flags->post_flags($request_mock); - - $this->assertInstanceOf('WP_Error', $result); - - } - - -} diff --git a/tests/integration/FlagsApiTest.php b/tests/integration/FlagsApiTest.php new file mode 100644 index 0000000..92154b1 --- /dev/null +++ b/tests/integration/FlagsApiTest.php @@ -0,0 +1,196 @@ +user->create( + array( + 'role' => 'editor', + ) + ); + + self::$admin = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public static function wpTearDownAfterClass() { + wp_delete_user( self::$editor ); + delete_option( Flags::$option_name ); + } + + public function set_up() { + parent::set_up(); + $this->instance = new Flags(); + $this->instance->register(); + } + + public function test_register() { + $this->assertSame( 10, has_action( 'rest_api_init', array( $this->instance, 'register_routes' ) ) ); + } + + + public function test_register_routes() { + $this->instance->register_routes(); + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( self::$api_endpoint, $routes ); + } + + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', self::$api_endpoint ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); + + wp_set_current_user( self::$editor ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden', $response, 403 ); + } + + public function test_get_items_as_admin_returns_200() { + wp_set_current_user( self::$admin ); + $request = new WP_REST_Request( 'GET', self::$api_endpoint ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertEquals([], $response->get_data()); + } + + public function test_get_items() { + wp_set_current_user( self::$admin ); + $flags = [['id'=>1, 'name'=>'test', 'enabled'=>true], ['id'=>2, 'name'=>'test2', 'enabled'=>false]]; + update_option( Flags::$option_name, $flags ); + + $request = new WP_REST_Request( 'GET', self::$api_endpoint ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame($flags, $response->get_data()); + } + + public function flagsDataProvider() { + return [ + 'invalid input' => [['invalid' => []], false], + 'valid empty input' => [['flags' => []], true], + 'valid input' => [['flags' => [['id'=>1, 'name'=>'test', 'enabled'=>true], ['id'=>2, 'name'=>'test2', 'enabled'=>false]]], true], + ]; + } + + /** + * @dataProvider flagsDataProvider + */ + public function testValidateFlags($inputData, $expectedResult) { + wp_set_current_user(self::$admin); + + $request = new WP_REST_Request('POST', self::$api_endpoint); + $request->add_header('Content-Type', 'application/json'); + $request->set_body(wp_json_encode($inputData)); + + $result = $this->instance->validate_flag_input($request); + + $this->assertSame($expectedResult, $result); + } + + public function test_create_item() { + wp_set_current_user( self::$admin ); + $flags = [['id'=>1, 'name'=>'test', 'enabled'=>true], ['id'=>2, 'name'=>'test2', 'enabled'=>false]]; + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( ['flags' => $flags] ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertTrue( $response->get_data()['success'] ); + + $options = get_option(Flags::$option_name); + $this->assertSame($options, $flags); + } + + public function test_create_item_with_empty_array() { + wp_set_current_user( self::$admin ); + $flags = []; + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( ['flags' => $flags] ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertTrue( $response->get_data()['success'] ); + + $options = get_option(Flags::$option_name); + $this->assertSame($options, $flags); + } + + public function test_create_item_with_invalid_input_array() { + wp_set_current_user( self::$admin ); + $flags = [['id'=>1, 'name'=>'test', 'enabled'=>true]]; + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( ['invalid' => $flags] ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_params', $response, 400 ); + + } + + public function test_create_item_without_input() { + wp_set_current_user( self::$admin ); + + $request = new WP_REST_Request( 'POST', self::$api_endpoint ); + $request->add_header( 'Content-Type', 'application/json' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_params', $response, 400 ); + + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() {} + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() {} + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() {} + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() {} +} diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php new file mode 100644 index 0000000..28fa251 --- /dev/null +++ b/tests/integration/bootstrap.php @@ -0,0 +1,34 @@ +1, 'name'=>'Test','enabled'=>true]]; @@ -26,9 +16,8 @@ public function test_is_enabled_method_should_return_true_if_flag_name_present_a } public function test_is_enabled_method_should_return_false_if_no_flags_exist() { - $mock_option_value = ''; - \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('get_option')->justReturn([]); $result = Flag::is_enabled('Test'); $this->assertFalse($result); @@ -43,7 +32,7 @@ public function test_is_enabled_method_should_return_false_if_flag_name_present_ $this->assertFalse($result); } - public function test_is_enabled_method_should_return_false_if_flag_name_nor_present() { + public function test_is_enabled_method_should_return_false_if_flag_name_not_exist() { $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>false]]; \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); diff --git a/tests/units/FlagsTest.php b/tests/units/FlagsTest.php new file mode 100644 index 0000000..ec43a99 --- /dev/null +++ b/tests/units/FlagsTest.php @@ -0,0 +1,41 @@ +1, 'name'=>'Test','enabled'=>true]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, $mock_option_value); + } + + public function test_get_all_flags_method_should_return_empty_array_if_value_is_not_set() { + + \Brain\Monkey\Functions\when('get_option')->justReturn([]); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, []); + } + + public function test_get_all_flags_method_should_return_multiple_flags_from_options_table() { + $mock_option_value = [['id'=>1, 'name'=>'Test','enabled'=>true],['id'=>2, 'name'=>'Test2','enabled'=>false]]; + + \Brain\Monkey\Functions\when('get_option')->justReturn($mock_option_value); + \Brain\Monkey\Functions\when('rest_ensure_response')->returnArg(); + + $flags = new Flags(); + $result = $flags->get_all_flags(); + $this->assertEquals($result, $mock_option_value); + } + +} diff --git a/tests/Unit/HelperTest.php b/tests/units/HelperTest.php similarity index 73% rename from tests/Unit/HelperTest.php rename to tests/units/HelperTest.php index 0978f77..d73b97a 100644 --- a/tests/Unit/HelperTest.php +++ b/tests/units/HelperTest.php @@ -1,19 +1,9 @@