diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..bdb5be1 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,109 @@ +workflows: + version: 2 + main: + jobs: + - php56-build + - php70-build + - php71-build + - php72-build + - php73-build + - php74-build + +version: 2 + +job-references: + mysql_image: &mysql_image + circleci/mysql:5.6 + + setup_environment: &setup_environment + name: "Setup Environment Variables" + command: | + echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV + source /home/circleci/.bashrc + + install_dependencies: &install_dependencies + name: "Install Dependencies" + command: | + sudo apt-get update && sudo apt-get install subversion + sudo -E docker-php-ext-install mysqli + sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" + sudo apt-get update && sudo apt-get install mysql-client-5.7 + + php_job: &php_job + environment: + - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" + - WP_CORE_DIR: "/tmp/wordpress/" + steps: + - checkout + - run: *setup_environment + - run: *install_dependencies + - run: + name: "Run Tests" + command: | + composer global require "phpunit/phpunit=5.7.*" + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + phpcs + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest + phpunit + WP_MULTISITE=1 phpunit + +jobs: + php56-build: + <<: *php_job + docker: + - image: circleci/php:5.6 + - image: *mysql_image + steps: + - checkout + - run: *setup_environment + - run: *install_dependencies + - run: + name: "Run Tests" + command: | + composer global require "phpunit/phpunit=5.7.*" + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + phpcs + SKIP_DB_CREATE=false + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest $SKIP_DB_CREATE + phpunit + WP_MULTISITE=1 phpunit + SKIP_DB_CREATE=true + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 trunk $SKIP_DB_CREATE + phpunit + WP_MULTISITE=1 phpunit + SKIP_DB_CREATE=true + + php70-build: + <<: *php_job + docker: + - image: circleci/php:7.0 + - image: *mysql_image + + php71-build: + <<: *php_job + docker: + - image: circleci/php:7.1 + - image: *mysql_image + + php72-build: + <<: *php_job + docker: + - image: circleci/php:7.2 + - image: *mysql_image + + php73-build: + <<: *php_job + docker: + - image: circleci/php:7.3 + - image: *mysql_image + + php74-build: + <<: *php_job + docker: + - image: circleci/php:7.4 + - image: *mysql_image diff --git a/.distignore b/.distignore index d4a97d2..d9b1119 100644 --- a/.distignore +++ b/.distignore @@ -1,15 +1,23 @@ +/.circleci /.git /.github /.wordpress-org /assets/_src +/bin /node_modules +/tests +/vendor .distignore .eslintrc.json .gitattributes .gitignore +.phpcs.xml.dist +composer.json +composer.lock LICENSE package.json package-lock.json +phpunit.xml.dist tsconfig.json webpack.config.js diff --git a/.gitattributes b/.gitattributes index 713bcdf..949d0c8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,16 +1,25 @@ # Directories +/.circleci export-ignore /.git export-ignore /.github export-ignore /.wordpress-org export-ignore /assets/_src export-ignore +/bin export-ignore /node_modules export-ignore +/tests export-ignore +/vendor export-ignore # Files /.distignore export-ignore +/.eslintrc.json export-ignore /.gitattributes export-ignore /.gitignore export-ignore +/.phpcs.xml.dist export-ignore +/composer.json export-ignore +/composer.lock export-ignore /LICENSE export-ignore /package.json export-ignore /package-lock.json export-ignore +/phpunit.xml.dist export-ignore /tsconfig.json export-ignore /webpack.config.js export-ignore diff --git a/.gitignore b/.gitignore index ee70956..11bb515 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .DS_Store .idea +.phpunit.result.cache node_modules +vendor +composer.lock package-lock.json assets/css assets/js diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..952b9f5 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,47 @@ + + + Generally-applicable sniffs for WordPress plugins. + + + . + /vendor/ + /node_modules/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..c6f53dc --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,181 @@ +#!/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-trunk + rm -rf $TMPDIR/wordpress-trunk/* + svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress + mv $TMPDIR/wordpress-trunk/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.githubusercontent.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 + rm -rf $WP_TESTS_DIR/{includes,data} + svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn export --quiet --ignore-externals 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:__DIR__ . '/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 + +} + +recreate_db() { + shopt -s nocasematch + if [[ $1 =~ ^(y|yes)$ ]] + then + mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA + create_db + echo "Recreated the database ($DB_NAME)." + else + echo "Leaving the existing database ($DB_NAME) in place." + fi + shopt -u nocasematch +} + +create_db() { + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +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 + if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] + then + echo "Reinstalling will delete the existing test database ($DB_NAME)" + read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB + recreate_db $DELETE_EXISTING_DB + else + create_db + fi +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e44c2e9 --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "require-dev": { + "phpunit/phpunit": "^9", + "yoast/phpunit-polyfills": "^3.0" + } +} diff --git a/package.json b/package.json index 863697a..317e8d9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "scripts": { "watch": "webpack --watch", "build": "webpack --progress", - "translate": "wp i18n make-pot . languages/cf-images.pot" + "translate": "wp i18n make-pot . languages/cf-images.pot", + "install-unit-tests": "bash bin/install-wp-tests.sh wordpress_test root root localhost latest" }, "dependencies": { "@mdi/js": "^7.4.47", diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..50421ec --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + ./tests/ + ./tests/test-sample.php + + + diff --git a/tests/assets/test-image.jpg b/tests/assets/test-image.jpg new file mode 100644 index 0000000..e689746 Binary files /dev/null and b/tests/assets/test-image.jpg differ diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..40d0759 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,38 @@ +attachment->create_upload_object( __DIR__ . '/assets/test-image.jpg' ); + } + + /** + * Runs the routine after all tests have been run. + */ + public static function tear_down_after_class() { + wp_delete_attachment( self::$attachment_id, true ); + delete_post_meta( self::$attachment_id, '_cloudflare_image_id' ); // Just in case. + delete_site_option( 'cf-images-hash' ); + + parent::tear_down_after_class(); + } + + /** + * Set up before each test. + */ + public function set_up() { + parent::set_up(); + + // Instantiate the module. + $this->cf_images = $this->getMockBuilder( Cloudflare_Images::class ) + ->disableOriginalConstructor() + ->onlyMethods( array( 'can_run', 'is_module_enabled' ) ) + ->getMock(); + + $this->cf_images->init(); + + remove_filter( 'wp_get_attachment_url', array( $this->cf_images, 'get_attachment_url' ) ); + + $this->cf_images + ->expects( $this->once() ) + ->method( 'can_run' ) + ->willReturn( true ); + } + + /** + * Get original image object. + * + * @param string $size WordPress attachment size. + * + * @return array + */ + private function get_original_image_object( string $size = 'medium' ): array { + $year = gmdate( 'Y' ); + $month = gmdate( 'm' ); + + switch ( $size ) { + case 'thumbnail': + $image = array( "http://example.org/wp-content/uploads/$year/$month/test-image-150x150.jpg", 150, 150, true ); + break; + case 'medium': + default: + $image = array( "http://example.org/wp-content/uploads/$year/$month/test-image-300x200.jpg", 300, 200, true ); + break; + case 'large': + $image = array( "http://example.org/wp-content/uploads/$year/$month/test-image-1024x683.jpg", 1024, 1024, false ); + break; + case 'scaled': + $image = array( "http://example.org/wp-content/uploads/$year/$month/test-image-scaled.jpg", 2560, 0, false ); + break; + case 'full': + case 'original': + $image = array( "http://example.org/wp-content/uploads/$year/$month/test-image.jpg", 2400, 1600, false ); + break; + } + + return $image; + } + + /** + * Add Cloudflare image ID and hash. + */ + private function add_cf_image_id_and_hash() { + add_post_meta( self::$attachment_id, '_cloudflare_image_id', 'CLOUDFLARE_IMAGE_ID' ); + update_site_option( 'cf-images-hash', 'CF_IMAGES_HASH' ); + } + + /** + * Test: Returns the original image if `can_run()` is false. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_returns_original_if_can_run_is_false() { + $original = $this->get_original_image_object(); + + // Force `can_run()` to return false. + $this->cf_images + ->expects( $this->once() ) + ->method( 'can_run' ) + ->willReturn( false ); + + $image = wp_get_attachment_image_src( self::$attachment_id, 'medium' ); + $this->assertSame( $original, $image, 'Should return original image if can_run() is false.' ); + } + + /** + * Test: Returns original if Cloudflare image ID is missing. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_returns_original_if_missing_cloudflare_image_id() { + $original = $this->get_original_image_object(); + + $image = wp_get_attachment_image_src( self::$attachment_id, 'medium' ); + $this->assertSame( $original, $image, 'Should return original image if Cloudflare image ID is missing.' ); + } + + /** + * Test: Returns original if Cloudflare hash is missing. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_returns_original_if_missing_cloudflare_hash() { + $original = $this->get_original_image_object(); + + add_post_meta( self::$attachment_id, '_cloudflare_image_id', 'CLOUDFLARE_IMAGE_ID' ); + + $image = wp_get_attachment_image_src( self::$attachment_id, 'medium' ); + $this->assertSame( $original, $image, 'Should return original image if Cloudflare hash is missing.' ); + } + + /** + * Test: Known crop image with a named size. + * + * @see Cloudflare_Images::$registered_sizes + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_known_crop_image_size() { + $this->cf_images->populate_image_sizes(); + $this->add_cf_image_id_and_hash(); + + $image = wp_get_attachment_image_src( self::$attachment_id ); // Default thumbnail size is cropped 150x150. + $this->assertStringEndsWith( 'w=150,h=150,fit=crop', $image[0], 'Should return offloaded image.' ); + } + + /** + * Test: Image with defined dimensions [width, height]. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_defined_dimensions() { + $this->add_cf_image_id_and_hash(); + + $image = wp_get_attachment_image_src( self::$attachment_id, 'medium' ); + $this->assertStringEndsWith( 'w=300,h=200', $image[0], 'Should return offloaded image.' ); + } + + /** + * Test: Image with `-x` in the filename. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_variant_image_filename() { + $original = $this->get_original_image_object( 'large' ); + + $this->cf_images->populate_image_sizes(); + $this->add_cf_image_id_and_hash(); + + // Pass in just an image link without the sizes. + $image = $this->cf_images->get_attachment_image_src( array( $original[0] ), self::$attachment_id, null ); + $this->assertStringEndsWith( 'w=1024,h=683', $image[0], 'Should detect file suffix from file name.' ); + } + + /** + * Test: Image with `-x` in the filename, and width = height. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_variant_image_filename_with_crop() { + $original = $this->get_original_image_object( 'thumbnail' ); + + $this->cf_images->populate_image_sizes(); + $this->add_cf_image_id_and_hash(); + + // Pass in just an image link without the sizes. + $image = $this->cf_images->get_attachment_image_src( array( $original[0] ), self::$attachment_id, null ); + $this->assertStringEndsWith( 'w=150,h=150,fit=crop', $image[0], 'Should detect file suffix and apply cropping if matched in arrays.' ); + } + + /** + * Test: `$size` is an integer => /w=$size + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_size_is_int() { + $original = $this->get_original_image_object( 'original' ); + + $this->add_cf_image_id_and_hash(); + + $image = $this->cf_images->get_attachment_image_src( array( $original[0] ), self::$attachment_id, 2400 ); + $this->assertStringEndsWith( '/w=2400', $image[0], 'Should add /w=2400 if $size is an integer.' ); + } + + /** + * Test: `-scaled` image handling. + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_scaled_image_handling() { + $original = $this->get_original_image_object( 'scaled' ); + + $this->add_cf_image_id_and_hash(); + + $image = $this->cf_images->get_attachment_image_src( array( $original[0] ), self::$attachment_id, null ); + $this->assertStringEndsWith( '/w=2560', $image[0], 'Should add /w=2560 if image is scaled.' ); + } + + /** + * Test: No size prefix, no $image[1], => default to /w=9999 + * + * @covers Cloudflare_Images::get_attachment_image_src() + */ + public function test_no_size_prefix_and_no_width_property() { + $original = $this->get_original_image_object( 'original' ); + + $this->add_cf_image_id_and_hash(); + + $image = $this->cf_images->get_attachment_image_src( array( $original[0] ), self::$attachment_id, null ); + $this->assertStringContainsString( '/w=9999', $image[0], 'Should default to /w=9999 if no size is found.' ); + } +} diff --git a/tests/test-sample.php b/tests/test-sample.php new file mode 100644 index 0000000..0b42481 --- /dev/null +++ b/tests/test-sample.php @@ -0,0 +1,20 @@ +assertTrue( true ); + } +}