From 384ea838eb0ae2569196996d03e82ac4261176f7 Mon Sep 17 00:00:00 2001 From: Christer Edvartsen Date: Sun, 2 Apr 2017 08:28:07 +0200 Subject: [PATCH] Update and improve Behat tests * Bump Behat-version and add imbo/behat-api-extension * Move behat.yml to the default location * Add composer script for starting PHPs built in httpd for the Behat tests * Rename Behat configuration file to .dist so developers can have local configuration files * Install micheh/psr7-cache to easily check if a response is cacheable or not. This functionality was earlier present in Guzzle, but has been removed. * Move the PHPUnit configuration files to root. Resolves #507 --- .gitignore | 4 +- .travis.yml | 8 +- CONTRIBUTING.md | 10 +- behat.yml.dist | 11 + composer.json | 19 +- composer.lock | 1240 +++++--- .../phpunit.xml.dist => phpunit.xml.dist | 15 +- .../phpunit.xml.travis => phpunit.xml.travis | 6 +- tests/.gitignore | 1 - tests/behat/README.md | 98 + tests/behat/behat.yml | 31 - tests/behat/bootstrap/ImboContext.php | 739 ----- tests/behat/bootstrap/RESTContext.php | 554 ---- .../features/access-control-keys.feature | 271 +- .../features/access-control-mutable.feature | 32 +- tests/behat/features/access-control.feature | 64 +- tests/behat/features/access-token.feature | 59 +- .../authenticate-write-operations.feature | 72 +- .../autorotate-event-listener.feature | 13 +- .../features/bootstrap/FeatureContext.php | 1787 +++++++++++ .../features/border-transformation.feature | 76 +- tests/behat/features/client-caching.feature | 69 +- .../features/content-negotiation.feature | 60 +- .../features/cors-event-listener.feature | 126 +- .../features/custom-event-listeners.feature | 12 +- tests/behat/features/custom-resource.feature | 6 +- .../features/draw-pois-transformation.feature | 50 +- tests/behat/features/etags.feature | 41 +- .../exif-metadata-event-listener.feature | 70 +- tests/behat/features/global-images.feature | 149 +- tests/behat/features/group.feature | 59 +- tests/behat/features/groups.feature | 64 +- tests/behat/features/head.feature | 84 +- .../image-identifier-generator-md5.feature | 40 +- ...mage-transformation-cache-listener.feature | 99 +- .../features/image-transformations.feature | 278 +- tests/behat/features/image-variations.feature | 82 +- tests/behat/features/image.feature | 80 +- tests/behat/features/images.feature | 284 +- tests/behat/features/index.feature | 34 +- .../features/level-transformation.feature | 20 +- .../max-image-size-event-listener.feature | 51 +- tests/behat/features/metadata.feature | 223 +- tests/behat/features/shorturls.feature | 55 +- .../features/smartsize-transformation.feature | 111 +- tests/behat/features/stats.feature | 119 +- tests/behat/features/status.feature | 71 +- .../strip-exif-transformation.feature | 10 +- tests/behat/features/user.feature | 35 +- .../features/varnish-hashtwo-listener.feature | 27 +- .../features/watermark-transformation.feature | 32 +- .../behat/fixtures/access-control-mutable.php | 2 + tests/behat/imbo-configs/access-control.php | 6 +- tests/behat/imbo-configs/config.testing.php | 26 +- .../stats-access-and-custom-stats.php | 29 +- tests/behat/imbo-configs/status.php | 39 +- tests/behat/router.php | 74 +- tests/phpunit/bootstrap.php | 4 + tests/phpunit/unit/FeatureContextTest.php | 2760 +++++++++++++++++ 59 files changed, 7043 insertions(+), 3448 deletions(-) create mode 100644 behat.yml.dist rename tests/phpunit/phpunit.xml.dist => phpunit.xml.dist (67%) rename tests/phpunit/phpunit.xml.travis => phpunit.xml.travis (81%) delete mode 100644 tests/.gitignore create mode 100644 tests/behat/README.md delete mode 100644 tests/behat/behat.yml delete mode 100644 tests/behat/bootstrap/ImboContext.php delete mode 100644 tests/behat/bootstrap/RESTContext.php create mode 100644 tests/behat/features/bootstrap/FeatureContext.php create mode 100644 tests/phpunit/unit/FeatureContextTest.php diff --git a/.gitignore b/.gitignore index b5adc0313..e245da84d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ docs/_build .lintcache *.log public/.htaccess -.idea \ No newline at end of file +.idea +behat.yml +phpunit.xml diff --git a/.travis.yml b/.travis.yml index 7ebb1723b..749958008 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,12 +37,16 @@ before_install: before_script: - phpenv config-rm xdebug.ini + - echo 'always_populate_raw_post_data = -1' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - composer self-update + - composer lint + - composer prepare - composer install --prefer-dist + - composer start-httpd-for-behat-tests script: - - ./vendor/bin/phpunit --verbose -c tests/phpunit/phpunit.xml.travis - - ./vendor/bin/behat --strict --profile no-cc --config tests/behat/behat.yml + - ./vendor/bin/phpunit --verbose -c phpunit.xml.travis --stop-on-failure + - ./vendor/bin/behat --strict --stop-on-failure after_failure: - echo "Tests failed - httpd log follows" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 984383d61..b7398f491 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,28 +8,24 @@ If you wish to contribute to Imbo, please read the following resources first: ## Running tests -Imbo has both [Behat](http://docs.behat.org/en/v2.5/) and [PHPUnit](https://phpunit.de/) tests, and when adding new features or fixing bugs you are required to add relevant test cases. Remember to install the development requirements using [Composer](https://getcomposer.org/) before running the tests: +Imbo has both [Behat](http://behat.org) and [PHPUnit](https://phpunit.de/) tests, and when adding new features or fixing bugs you are required to add relevant test cases. Remember to install dependencies before running the tests: ``` -composer install --dev +composer install ``` ### Behat ``` -./vendor/bin/behat --strict --profile no-cc --config tests/behat/behat.yml +./vendor/bin/behat --strict ``` -The `--profile no-cc` arguments will disable the generation of code coverage. If you skip these arguments you will get code covarage of the Behat tests. - ### PHPUnit ``` ./vendor/bin/phpunit --verbose -c tests/phpunit ``` -Include `--coverage-html ` if you want to generate code coverage report of the tests. - ## Writing documentation Imbo uses [Read the docs](https://readthedocs.org/projects/imbo/) for documentation, and all docs are located in the `docs` dir. The docs are written using [Sphinx](http://sphinx-doc.org/), and if you are contributing new features please add relevant docs. diff --git a/behat.yml.dist b/behat.yml.dist new file mode 100644 index 000000000..cba413d19 --- /dev/null +++ b/behat.yml.dist @@ -0,0 +1,11 @@ +default: + autoload: + '': %paths.base%/tests/behat/features/bootstrap + suites: + default: + paths: [%paths.base%/tests/behat/features] + + extensions: + Imbo\BehatApiExtension: + apiClient: + base_uri: http://localhost:8080 diff --git a/composer.json b/composer.json index d76646408..955b28d10 100644 --- a/composer.json +++ b/composer.json @@ -36,15 +36,16 @@ "require-dev": { "mikey179/vfsStream": "^1.5.0", "phpunit/phpunit": "^5.6", - "behat/behat": "^2.0", - "guzzle/guzzle": "^3.9.3", + "behat/behat": "^3.2", "phploc/phploc": "^3.0", "sebastian/phpcpd": "^2.0", "phpmd/phpmd": "^2.4", "squizlabs/php_codesniffer": "^2.7", "imbo/imbo-phpcs-standard": "^1.4", "phpdocumentor/phpdocumentor": "^2.9", - "cwhite92/b2-sdk-php": "^1.2" + "cwhite92/b2-sdk-php": "^1.2", + "imbo/behat-api-extension": "^2.0", + "micheh/psr7-cache": "^0.5" }, "suggest": { "ext-mongo": "Enables usage of MongoDB and GridFS as database and store. Recommended version: >=1.4.0", @@ -77,12 +78,11 @@ "mkdir build", "mkdir build/coverage", "mkdir build/logs", - "mkdir build/docs", - "mkdir build/code-browser", - "mkdir build/pdepend" + "mkdir build/docs" ], - "test-phpunit": "vendor/bin/phpunit --verbose -c tests/phpunit", - "test-behat": "vendor/bin/behat --strict --config tests/behat/behat.yml", + "test-phpunit": "vendor/bin/phpunit --verbose", + "test-phpunit-coverage": "vendor/bin/phpunit --verbose --coverage-html build/coverage", + "test-behat": "vendor/bin/behat --strict", "test": [ "@test-phpunit", "@test-behat" @@ -107,6 +107,7 @@ "docs": [ "cd docs; make spelling", "cd docs; make html" - ] + ], + "start-httpd-for-behat-tests": "php -S localhost:8080 -t ./public tests/behat/router.php > build/logs/httpd.log 2>&1 &" } } diff --git a/composer.lock b/composer.lock index 3bd8414be..b023df8e8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,27 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "f5e4f1b8e72be96f21803f1da372f1a2", - "content-hash": "95787b38a1dc3d0b153d90033c727e5a", + "hash": "f3daa97656b8ef4d8a24d13a8b9fb3d5", + "content-hash": "4e031f4ae9d0a1f2595609904961a44b", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.19.25", + "version": "3.25.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b3657b6d2519bc8838be072c759ca5e272527877" + "reference": "cd362e4dda55d6c3466e60070dad6527f6938d5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b3657b6d2519bc8838be072c759ca5e272527877", - "reference": "b3657b6d2519bc8838be072c759ca5e272527877", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cd362e4dda55d6c3466e60070dad6527f6938d5d", + "reference": "cd362e4dda55d6c3466e60070dad6527f6938d5d", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^5.3.1|^6.2.1", "guzzlehttp/promises": "~1.0", - "guzzlehttp/psr7": "~1.3.1", + "guzzlehttp/psr7": "^1.4.1", "mtdowling/jmespath.php": "~2.2", "php": ">=5.5" }, @@ -85,20 +85,20 @@ "s3", "sdk" ], - "time": "2016-11-14 22:45:48" + "time": "2017-03-31 19:42:09" }, { "name": "doctrine/annotations", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "30e07cf03edc3cd3ef579d0dd4dd8c58250799a5" + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/30e07cf03edc3cd3ef579d0dd4dd8c58250799a5", - "reference": "30e07cf03edc3cd3ef579d0dd4dd8c58250799a5", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", "shasum": "" }, "require": { @@ -107,7 +107,7 @@ }, "require-dev": { "doctrine/cache": "1.*", - "phpunit/phpunit": "^5.6.1" + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { @@ -153,7 +153,7 @@ "docblock", "parser" ], - "time": "2016-10-24 11:45:47" + "time": "2017-02-24 16:22:25" }, { "name": "doctrine/cache", @@ -227,28 +227,29 @@ }, { "name": "doctrine/collections", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -289,20 +290,20 @@ "collections", "iterator" ], - "time": "2015-04-14 22:21:58" + "time": "2017-01-03 10:49:41" }, { "name": "doctrine/common", - "version": "v2.6.1", + "version": "v2.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "a579557bc689580c19fee4e27487a67fe60defc0" + "reference": "930297026c8009a567ac051fd545bf6124150347" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/a579557bc689580c19fee4e27487a67fe60defc0", - "reference": "a579557bc689580c19fee4e27487a67fe60defc0", + "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", + "reference": "930297026c8009a567ac051fd545bf6124150347", "shasum": "" }, "require": { @@ -311,10 +312,10 @@ "doctrine/collections": "1.*", "doctrine/inflector": "1.*", "doctrine/lexer": "1.*", - "php": "~5.5|~7.0" + "php": "~5.6|~7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0" + "phpunit/phpunit": "^5.4.6" }, "type": "library", "extra": { @@ -362,24 +363,24 @@ "persistence", "spl" ], - "time": "2015-12-25 13:18:31" + "time": "2017-01-13 14:02:13" }, { "name": "doctrine/dbal", - "version": "v2.5.5", + "version": "v2.5.12", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9" + "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/7b9e911f9d8b30d43b96853dab26898c710d8f44", + "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44", "shasum": "" }, "require": { - "doctrine/common": ">=2.4,<2.7-dev", + "doctrine/common": ">=2.4,<2.8-dev", "php": ">=5.3.2" }, "require-dev": { @@ -433,7 +434,7 @@ "persistence", "queryobject" ], - "time": "2016-09-09 19:13:33" + "time": "2017-02-08 12:53:47" }, { "name": "doctrine/inflector", @@ -558,21 +559,21 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.2.2", + "version": "6.2.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60" + "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ebf29dee597f02f09f4d5bbecc68230ea9b08f60", - "reference": "ebf29dee597f02f09f4d5bbecc68230ea9b08f60", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006", + "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006", "shasum": "" }, "require": { "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.3.1", + "guzzlehttp/psr7": "^1.4", "php": ">=5.5" }, "require-dev": { @@ -616,32 +617,32 @@ "rest", "web service" ], - "time": "2016-10-08 15:01:37" + "time": "2017-02-28 22:50:30" }, { "name": "guzzlehttp/promises", - "version": "1.2.0", + "version": "v1.3.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579" + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/c10d860e2a9595f8883527fa0021c7da9e65f579", - "reference": "c10d860e2a9595f8883527fa0021c7da9e65f579", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", "shasum": "" }, "require": { "php": ">=5.5.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -667,20 +668,20 @@ "keywords": [ "promise" ], - "time": "2016-05-18 16:56:05" + "time": "2016-12-20 10:07:11" }, { "name": "guzzlehttp/psr7", - "version": "1.3.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b" + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", - "reference": "5c6447c9df362e8f8093bda8f5d8873fe5c7f65b", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/f5b8a8512e2b58b0071a7280e39f14f72e05d87c", + "reference": "f5b8a8512e2b58b0071a7280e39f14f72e05d87c", "shasum": "" }, "require": { @@ -716,16 +717,23 @@ "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" } ], - "description": "PSR-7 message implementation", + "description": "PSR-7 message implementation that also provides common utility methods", "keywords": [ "http", "message", + "request", + "response", "stream", - "uri" + "uri", + "url" ], - "time": "2016-06-24 23:00:38" + "time": "2017-03-20 17:10:46" }, { "name": "ircmaxell/password-compat", @@ -771,16 +779,16 @@ }, { "name": "mongodb/mongodb", - "version": "1.0.3", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "3c742f3ceffc4dc67fee02dc6ebfc2e7cb403b7c" + "reference": "2f99156b29bc85582415d6a32bc31010d61a0a71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/3c742f3ceffc4dc67fee02dc6ebfc2e7cb403b7c", - "reference": "3c742f3ceffc4dc67fee02dc6ebfc2e7cb403b7c", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/2f99156b29bc85582415d6a32bc31010d61a0a71", + "reference": "2f99156b29bc85582415d6a32bc31010d61a0a71", "shasum": "" }, "require": { @@ -822,20 +830,20 @@ "mongodb", "persistence" ], - "time": "2016-09-23 19:38:29" + "time": "2017-02-16 18:35:09" }, { "name": "mtdowling/jmespath.php", - "version": "2.3.0", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "192f93e43c2c97acde7694993ab171b3de284093" + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/192f93e43c2c97acde7694993ab171b3de284093", - "reference": "192f93e43c2c97acde7694993ab171b3de284093", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", "shasum": "" }, "require": { @@ -877,20 +885,20 @@ "json", "jsonpath" ], - "time": "2016-01-05 18:25:05" + "time": "2016-12-03 22:08:25" }, { "name": "paragonie/random_compat", - "version": "v2.0.4", + "version": "v2.0.10", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e" + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", - "reference": "a9b97968bcde1c4de2a5ec6cbd06a0f6c919b46e", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/634bae8e911eefa89c1abfbf1b66da679ac8f54d", + "reference": "634bae8e911eefa89c1abfbf1b66da679ac8f54d", "shasum": "" }, "require": { @@ -925,7 +933,7 @@ "pseudorandom", "random" ], - "time": "2016-11-07 23:38:38" + "time": "2017-03-13 16:27:32" }, { "name": "psr/http-message", @@ -1026,33 +1034,35 @@ }, { "name": "ramsey/uuid", - "version": "3.5.1", + "version": "3.6.1", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "a07797b986671b0dc823885a81d5e3516b931599" + "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/a07797b986671b0dc823885a81d5e3516b931599", - "reference": "a07797b986671b0dc823885a81d5e3516b931599", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e", + "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e", "shasum": "" }, "require": { "paragonie/random_compat": "^1.0|^2.0", - "php": ">=5.4" + "php": "^5.4 || ^7.0" }, "replace": { "rhumsaa/uuid": "self.version" }, "require-dev": { "apigen/apigen": "^4.1", - "codeception/aspect-mock": "1.0.0", - "goaop/framework": "1.0.0-alpha.2", + "codeception/aspect-mock": "^1.0 | ^2.0", + "doctrine/annotations": "~1.2.0", + "goaop/framework": "1.0.0-alpha.2 | ^1.0 | ^2.1", "ircmaxell/random-lib": "^1.1", "jakub-onderka/php-parallel-lint": "^0.9.0", "mockery/mockery": "^0.9.4", "moontoast/math": "^1.1", + "php-mock/php-mock-phpunit": "^0.3|^1.1", "phpunit/phpunit": "^4.7|>=5.0 <5.4", "satooshi/php-coveralls": "^0.6.1", "squizlabs/php_codesniffer": "^2.3" @@ -1102,25 +1112,25 @@ "identifier", "uuid" ], - "time": "2016-10-02 15:51:17" + "time": "2017-03-26 20:37:53" }, { "name": "symfony/console", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7350016c8abcab897046f1aead2b766b84d3eff8" + "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7350016c8abcab897046f1aead2b766b84d3eff8", - "reference": "7350016c8abcab897046f1aead2b766b84d3eff8", + "url": "https://api.github.com/repos/symfony/console/zipball/81508e6fac4476771275a3f4f53c3fee9b956bfa", + "reference": "81508e6fac4476771275a3f4f53c3fee9b956bfa", "shasum": "" }, "require": { "php": ">=5.3.9", - "symfony/debug": "~2.7,>=2.7.2|~3.0.0", + "symfony/debug": "^2.7.2|~3.0.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { @@ -1163,7 +1173,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-10-06 01:43:09" + "time": "2017-03-04 11:00:12" }, { "name": "symfony/debug", @@ -1224,16 +1234,16 @@ }, { "name": "symfony/http-foundation", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c" + "reference": "88af747e7af17d8d7d439ad4639dc3e23ddd3edd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a6e6c34d337f3c74c39b29c5f54d33023de8897c", - "reference": "a6e6c34d337f3c74c39b29c5f54d33023de8897c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/88af747e7af17d8d7d439ad4639dc3e23ddd3edd", + "reference": "88af747e7af17d8d7d439ad4639dc3e23ddd3edd", "shasum": "" }, "require": { @@ -1275,7 +1285,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-10-24 15:52:36" + "time": "2017-03-04 12:20:59" }, { "name": "symfony/polyfill-mbstring", @@ -1452,33 +1462,93 @@ } ], "packages-dev": [ + { + "name": "beberlei/assert", + "version": "v2.7.4", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "3ee3bc468a3ce4bbfc3d74f53c6cdb5242d39d1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/3ee3bc468a3ce4bbfc3d74f53c6cdb5242d39d1a", + "reference": "3ee3bc468a3ce4bbfc3d74f53c6cdb5242d39d1a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.1.1", + "phpunit/phpunit": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Assert\\": "lib/Assert" + }, + "files": [ + "lib/Assert/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "time": "2017-03-14 18:06:52" + }, { "name": "behat/behat", - "version": "v2.5.5", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120" + "reference": "15a3a1857457eaa29cdf41564a5e421effb09526" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/c1e48826b84669c97a1efa78459aedfdcdcf2120", - "reference": "c1e48826b84669c97a1efa78459aedfdcdcf2120", + "url": "https://api.github.com/repos/Behat/Behat/zipball/15a3a1857457eaa29cdf41564a5e421effb09526", + "reference": "15a3a1857457eaa29cdf41564a5e421effb09526", "shasum": "" }, "require": { - "behat/gherkin": "~2.3.0", - "php": ">=5.3.1", - "symfony/config": "~2.3", - "symfony/console": "~2.0", - "symfony/dependency-injection": "~2.0", - "symfony/event-dispatcher": "~2.0", - "symfony/finder": "~2.0", - "symfony/translation": "~2.3", - "symfony/yaml": "~2.0" + "behat/gherkin": "^4.4.4", + "behat/transliterator": "~1.0", + "container-interop/container-interop": "^1.1", + "ext-mbstring": "*", + "php": ">=5.3.3", + "symfony/class-loader": "~2.1||~3.0", + "symfony/config": "~2.3||~3.0", + "symfony/console": "~2.5||~3.0", + "symfony/dependency-injection": "~2.1||~3.0", + "symfony/event-dispatcher": "~2.1||~3.0", + "symfony/translation": "~2.3||~3.0", + "symfony/yaml": "~2.1||~3.0" }, "require-dev": { - "phpunit/phpunit": "~3.7.19" + "herrera-io/box": "~1.6.1", + "phpunit/phpunit": "~4.5", + "symfony/process": "~2.5|~3.0" }, "suggest": { "behat/mink-extension": "for integration with Mink testing framework", @@ -1489,9 +1559,15 @@ "bin/behat" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, "autoload": { "psr-0": { - "Behat\\Behat": "src/" + "Behat\\Behat": "src/", + "Behat\\Testwork": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1508,44 +1584,50 @@ "description": "Scenario-oriented BDD framework for PHP 5.3", "homepage": "http://behat.org/", "keywords": [ + "Agile", "BDD", - "Behat", - "Symfony2" + "ScenarioBDD", + "Scrum", + "StoryBDD", + "User story", + "business", + "development", + "documentation", + "examples", + "symfony", + "testing" ], - "time": "2015-06-01 09:37:55" + "time": "2016-12-25 13:43:52" }, { "name": "behat/gherkin", - "version": "v2.3.5", + "version": "v4.4.5", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "2b33963da5525400573560c173ab5c9c057e1852" + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/2b33963da5525400573560c173ab5c9c057e1852", - "reference": "2b33963da5525400573560c173ab5c9c057e1852", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/5c14cff4f955b17d20d088dec1bde61c0539ec74", + "reference": "5c14cff4f955b17d20d088dec1bde61c0539ec74", "shasum": "" }, "require": { - "php": ">=5.3.1", - "symfony/finder": "~2.0" + "php": ">=5.3.1" }, "require-dev": { - "symfony/config": "~2.0", - "symfony/translation": "~2.0", - "symfony/yaml": "~2.0" + "phpunit/phpunit": "~4.5|~5", + "symfony/phpunit-bridge": "~2.7|~3", + "symfony/yaml": "~2.3|~3" }, "suggest": { - "symfony/config": "If you want to use Config component to manage resources", - "symfony/translation": "If you want to use Symfony2 translations adapter", "symfony/yaml": "If you want to parse features, represented in YAML files" }, "type": "library", "extra": { "branch-alias": { - "dev-develop": "2.2-dev" + "dev-master": "4.4-dev" } }, "autoload": { @@ -1569,11 +1651,52 @@ "keywords": [ "BDD", "Behat", + "Cucumber", "DSL", - "Symfony2", + "gherkin", "parser" ], - "time": "2013-10-15 11:22:17" + "time": "2016-10-30 11:50:56" + }, + { + "name": "behat/transliterator", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/Behat/Transliterator.git", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003", + "reference": "868e05be3a9f25ba6424c2dd4849567f50715003", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-0": { + "Behat\\Transliterator": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Artistic-1.0" + ], + "description": "String transliterator", + "keywords": [ + "i18n", + "slug", + "transliterator" + ], + "time": "2015-09-28 16:26:35" }, { "name": "cilex/cilex", @@ -1695,18 +1818,21 @@ }, { "name": "container-interop/container-interop", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/container-interop/container-interop.git", - "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e" + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/container-interop/container-interop/zipball/fc08354828f8fd3245f77a66b9e23a6bca48297e", - "reference": "fc08354828f8fd3245f77a66b9e23a6bca48297e", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", "shasum": "" }, + "require": { + "psr/container": "^1.0" + }, "type": "library", "autoload": { "psr-4": { @@ -1718,7 +1844,8 @@ "MIT" ], "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", - "time": "2014-12-30 15:22:37" + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14 19:40:03" }, { "name": "cwhite92/b2-sdk-php", @@ -1826,16 +1953,16 @@ }, { "name": "erusev/parsedown", - "version": "1.6.1", + "version": "1.6.2", "source": { "type": "git", "url": "https://github.com/erusev/parsedown.git", - "reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb" + "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/20ff8bbb57205368b4b42d094642a3e52dac85fb", - "reference": "20ff8bbb57205368b4b42d094642a3e52dac85fb", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/1bf24f7334fe16c88bf9d467863309ceaf285b01", + "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01", "shasum": "" }, "require": { @@ -1864,115 +1991,19 @@ "markdown", "parser" ], - "time": "2016-11-02 15:56:58" - }, - { - "name": "guzzle/guzzle", - "version": "v3.9.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle3.git", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": "~2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" - }, - "require-dev": { - "doctrine/cache": "~1.3", - "monolog/monolog": "~1.0", - "phpunit/phpunit": "3.7.*", - "psr/log": "~1.0", - "symfony/class-loader": "~2.1", - "zendframework/zend-cache": "2.*,<2.3", - "zendframework/zend-log": "2.*,<2.3" - }, - "suggest": { - "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.9-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" - } - ], - "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "abandoned": "guzzlehttp/guzzle", - "time": "2015-03-18 18:23:50" + "time": "2017-03-29 16:04:15" }, { "name": "herrera-io/json", "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/kherge-abandoned/php-json.git", + "url": "https://github.com/kherge-php/json.git", "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kherge-abandoned/php-json/zipball/60c696c9370a1e5136816ca557c17f82a6fa83f1", + "url": "https://api.github.com/repos/kherge-php/json/zipball/60c696c9370a1e5136816ca557c17f82a6fa83f1", "reference": "60c696c9370a1e5136816ca557c17f82a6fa83f1", "shasum": "" }, @@ -2020,6 +2051,7 @@ "schema", "validate" ], + "abandoned": "kherge/json", "time": "2013-10-30 16:51:34" }, { @@ -2077,8 +2109,67 @@ "phar", "update" ], + "abandoned": true, "time": "2013-10-30 17:23:01" }, + { + "name": "imbo/behat-api-extension", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/imbo/behat-api-extension.git", + "reference": "fcd9e6e9f24c982987f309e33d54a93041bc1a4a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/imbo/behat-api-extension/zipball/fcd9e6e9f24c982987f309e33d54a93041bc1a4a", + "reference": "fcd9e6e9f24c982987f309e33d54a93041bc1a4a", + "shasum": "" + }, + "require": { + "beberlei/assert": "^2.1", + "behat/behat": "^3.0", + "guzzlehttp/guzzle": "^6.0", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "^5.5", + "silex/silex": "^2.0", + "symfony/process": "^3.1", + "symfony/security": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Imbo\\BehatApiExtension\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christer Edvartsen", + "email": "cogo@starzinger.net", + "homepage": "https://github.com/christeredvartsen" + }, + { + "name": "Contributors", + "homepage": "https://github.com/imbo/behat-api-extension/graphs/contributors" + } + ], + "description": "API extension for Behat", + "homepage": "https://github.com/imbo/behat-api-extension", + "keywords": [ + "Behat", + "api", + "http", + "rest", + "testing" + ], + "time": "2017-04-01 08:20:00" + }, { "name": "imbo/imbo-phpcs-standard", "version": "v1.4.0", @@ -2119,23 +2210,24 @@ }, { "name": "jms/metadata", - "version": "1.5.1", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/metadata.git", - "reference": "22b72455559a25777cfd28c4ffda81ff7639f353" + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/22b72455559a25777cfd28c4ffda81ff7639f353", - "reference": "22b72455559a25777cfd28c4ffda81ff7639f353", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/6a06970a10e0a532fb52d3959547123b84a3b3ab", + "reference": "6a06970a10e0a532fb52d3959547123b84a3b3ab", "shasum": "" }, "require": { "php": ">=5.3.0" }, "require-dev": { - "doctrine/cache": "~1.0" + "doctrine/cache": "~1.0", + "symfony/cache": "~3.1" }, "type": "library", "extra": { @@ -2150,14 +2242,12 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache" + "Apache-2.0" ], "authors": [ { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" } ], "description": "Class/method/property metadata management in PHP", @@ -2167,7 +2257,7 @@ "xml", "yaml" ], - "time": "2014-07-12 07:13:19" + "time": "2016-12-05 10:18:33" }, { "name": "jms/parser-lib", @@ -2206,16 +2296,16 @@ }, { "name": "jms/serializer", - "version": "1.4.2", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/serializer.git", - "reference": "f39d8b4660d5cef43b0c3265ce642173d9b2c58b" + "reference": "5e9008cbb0bac2986979d905a87ae48efcf578c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/f39d8b4660d5cef43b0c3265ce642173d9b2c58b", - "reference": "f39d8b4660d5cef43b0c3265ce642173d9b2c58b", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/5e9008cbb0bac2986979d905a87ae48efcf578c4", + "reference": "5e9008cbb0bac2986979d905a87ae48efcf578c4", "shasum": "" }, "require": { @@ -2228,6 +2318,7 @@ "phpoption/phpoption": "^1.1" }, "conflict": { + "jms/serializer-bundle": "<1.2.1", "twig/twig": "<1.12" }, "require-dev": { @@ -2237,20 +2328,23 @@ "jackalope/jackalope-doctrine-dbal": "^1.1.5", "phpunit/phpunit": "^4.8|^5.0", "propel/propel1": "~1.7", + "symfony/expression-language": "^2.6|^3.0", "symfony/filesystem": "^2.1", - "symfony/form": "~2.1", - "symfony/translation": "^2.1", - "symfony/validator": "^2.2", - "symfony/yaml": "^2.1", + "symfony/form": "~2.1|^3.0", + "symfony/translation": "^2.1|^3.0", + "symfony/validator": "^2.2|^3.0", + "symfony/yaml": "^2.1|^3.0", "twig/twig": "~1.12|~2.0" }, "suggest": { + "doctrine/cache": "Required if you like to use cache functionality.", + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", "symfony/yaml": "Required if you'd like to serialize data to YAML format." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -2277,7 +2371,7 @@ "serialization", "xml" ], - "time": "2016-11-13 10:20:11" + "time": "2017-03-24 13:11:23" }, { "name": "justinrainbow/json-schema", @@ -2385,8 +2479,57 @@ ], "description": "A parsing and comparison library for semantic versioning.", "homepage": "http://github.com/kherge/Version", + "abandoned": true, "time": "2012-08-16 17:13:03" }, + { + "name": "micheh/psr7-cache", + "version": "0.5", + "source": { + "type": "git", + "url": "https://github.com/micheh/psr7-cache.git", + "reference": "b99e8ae6d024d8c70945084b5b979a62cfd05df4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/micheh/psr7-cache/zipball/b99e8ae6d024d8c70945084b5b979a62cfd05df4", + "reference": "b99e8ae6d024d8c70945084b5b979a62cfd05df4", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0||~5.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Micheh\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Cache and conditional request helpers for PSR-7 HTTP Messages", + "keywords": [ + "cache", + "conditional", + "http", + "http-message", + "if-match", + "if-modified-since", + "if-none-match", + "if-unmodified-since", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-04-15 16:31:14" + }, { "name": "mikey179/vfsStream", "version": "v1.6.4", @@ -2435,16 +2578,16 @@ }, { "name": "monolog/monolog", - "version": "1.21.0", + "version": "1.22.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952" + "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f42fbdfd53e306bda545845e4dbfd3e72edb4952", - "reference": "f42fbdfd53e306bda545845e4dbfd3e72edb4952", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1e044bc4b34e91743943479f1be7a1d5eb93add0", + "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0", "shasum": "" }, "require": { @@ -2455,7 +2598,7 @@ "psr/log-implementation": "1.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9", + "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", "graylog2/gelf-php": "~1.0", "jakub-onderka/php-parallel-lint": "0.9", @@ -2509,20 +2652,20 @@ "logging", "psr-3" ], - "time": "2016-07-29 03:23:52" + "time": "2017-03-13 07:08:03" }, { "name": "myclabs/deep-copy", - "version": "1.5.5", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108" + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe", + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe", "shasum": "" }, "require": { @@ -2551,7 +2694,7 @@ "object", "object graph" ], - "time": "2016-10-31 17:19:45" + "time": "2017-01-26 22:05:40" }, { "name": "nikic/php-parser", @@ -2600,16 +2743,16 @@ }, { "name": "pdepend/pdepend", - "version": "2.2.4", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/pdepend/pdepend.git", - "reference": "b086687f3a01dc6bb92d633aef071d2c5dd0db06" + "reference": "0c50874333149c0dad5a2877801aed148f2767ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pdepend/pdepend/zipball/b086687f3a01dc6bb92d633aef071d2c5dd0db06", - "reference": "b086687f3a01dc6bb92d633aef071d2c5dd0db06", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/0c50874333149c0dad5a2877801aed148f2767ff", + "reference": "0c50874333149c0dad5a2877801aed148f2767ff", "shasum": "" }, "require": { @@ -2636,7 +2779,7 @@ "BSD-3-Clause" ], "description": "Official version of pdepend to be handled with Composer", - "time": "2016-03-10 15:15:04" + "time": "2017-01-19 14:23:36" }, { "name": "phpcollection/phpcollection", @@ -3017,21 +3160,22 @@ }, { "name": "phpmd/phpmd", - "version": "2.4.3", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/phpmd/phpmd.git", - "reference": "2b9c2417a18696dfb578b38c116cd0ddc19b256e" + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpmd/phpmd/zipball/2b9c2417a18696dfb578b38c116cd0ddc19b256e", - "reference": "2b9c2417a18696dfb578b38c116cd0ddc19b256e", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/4e9924b2c157a3eb64395460fcf56b31badc8374", + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374", "shasum": "" }, "require": { - "pdepend/pdepend": "^2.0.4", - "php": ">=5.3.0" + "ext-xml": "*", + "pdepend/pdepend": "^2.5", + "php": ">=5.3.9" }, "require-dev": { "phpunit/phpunit": "^4.0", @@ -3078,7 +3222,7 @@ "phpmd", "pmd" ], - "time": "2016-04-04 11:52:04" + "time": "2017-01-20 14:41:10" }, { "name": "phpoption/phpoption", @@ -3132,27 +3276,28 @@ }, { "name": "phpspec/prophecy", - "version": "v1.6.1", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0" + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/58a8137754bc24b25740d4281399a4a3596058e0", - "reference": "58a8137754bc24b25740d4281399a4a3596058e0", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", + "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", - "sebastian/comparator": "^1.1", - "sebastian/recursion-context": "^1.0" + "sebastian/comparator": "^1.1|^2.0", + "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { - "phpspec/phpspec": "^2.0" + "phpspec/phpspec": "^2.5|^3.2", + "phpunit/phpunit": "^4.8 || ^5.6.5" }, "type": "library", "extra": { @@ -3190,39 +3335,39 @@ "spy", "stub" ], - "time": "2016-06-07 08:13:47" + "time": "2017-03-02 20:05:34" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.2", + "version": "4.0.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d" + "reference": "09e2277d14ea467e5a984010f501343ef29ffc69" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6cba06ff75a1a63a71033e1a01b89056f3af1e8d", - "reference": "6cba06ff75a1a63a71033e1a01b89056f3af1e8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/09e2277d14ea467e5a984010f501343ef29ffc69", + "reference": "09e2277d14ea467e5a984010f501343ef29ffc69", "shasum": "" }, "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "~1.3", - "phpunit/php-text-template": "~1.2", - "phpunit/php-token-stream": "^1.4.2", - "sebastian/code-unit-reverse-lookup": "~1.0", + "phpunit/php-file-iterator": "^1.3", + "phpunit/php-text-template": "^1.2", + "phpunit/php-token-stream": "^1.4.2 || ^2.0", + "sebastian/code-unit-reverse-lookup": "^1.0", "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "~1.0|~2.0" + "sebastian/version": "^1.0 || ^2.0" }, "require-dev": { - "ext-xdebug": ">=2.1.4", - "phpunit/phpunit": "^5.4" + "ext-xdebug": "^2.1.4", + "phpunit/phpunit": "^5.7" }, "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.4.0", - "ext-xmlwriter": "*" + "ext-xdebug": "^2.5.1" }, "type": "library", "extra": { @@ -3253,20 +3398,20 @@ "testing", "xunit" ], - "time": "2016-11-01 05:06:24" + "time": "2017-03-01 09:12:17" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", "shasum": "" }, "require": { @@ -3300,7 +3445,7 @@ "filesystem", "iterator" ], - "time": "2015-06-21 13:08:43" + "time": "2016-10-03 07:40:28" }, { "name": "phpunit/php-text-template", @@ -3345,25 +3490,30 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.8", + "version": "1.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", - "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", + "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4|~5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, "autoload": { "classmap": [ "src/" @@ -3385,20 +3535,20 @@ "keywords": [ "timer" ], - "time": "2016-05-12 18:03:57" + "time": "2017-02-26 11:10:40" }, { "name": "phpunit/php-token-stream", - "version": "1.4.8", + "version": "1.4.11", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", - "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", + "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", "shasum": "" }, "require": { @@ -3434,20 +3584,20 @@ "keywords": [ "tokenizer" ], - "time": "2015-09-15 10:49:45" + "time": "2017-02-27 10:12:30" }, { "name": "phpunit/phpunit", - "version": "5.6.3", + "version": "5.7.17", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a9de0dbafeb6b1391b391fbb034734cb0af9f67c" + "reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a9de0dbafeb6b1391b391fbb034734cb0af9f67c", - "reference": "a9de0dbafeb6b1391b391fbb034734cb0af9f67c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/68752b665d3875f9a38a357e3ecb35c79f8673bf", + "reference": "68752b665d3875f9a38a357e3ecb35c79f8673bf", "shasum": "" }, "require": { @@ -3458,20 +3608,20 @@ "ext-xml": "*", "myclabs/deep-copy": "~1.3", "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.3.1", - "phpunit/php-code-coverage": "^4.0.1", + "phpspec/prophecy": "^1.6.2", + "phpunit/php-code-coverage": "^4.0.4", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "~1.1", + "sebastian/comparator": "^1.2.4", "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3 || ^2.0", - "sebastian/exporter": "~1.2", - "sebastian/global-state": "~1.0", - "sebastian/object-enumerator": "~1.0", + "sebastian/environment": "^1.3.4 || ^2.0", + "sebastian/exporter": "~2.0", + "sebastian/global-state": "^1.1", + "sebastian/object-enumerator": "~2.0", "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0|~2.0", + "sebastian/version": "~1.0.3|~2.0", "symfony/yaml": "~2.1|~3.0" }, "conflict": { @@ -3490,7 +3640,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.6.x-dev" + "dev-master": "5.7.x-dev" } }, "autoload": { @@ -3516,27 +3666,27 @@ "testing", "xunit" ], - "time": "2016-11-14 06:39:40" + "time": "2017-03-19 16:52:12" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2" + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", - "reference": "238d7a2723bce689c79eeac9c7d5e1d623bb9dc2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.6 || ^7.0", "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2" + "sebastian/exporter": "^1.2 || ^2.0" }, "conflict": { "phpunit/phpunit": "<5.4.0" @@ -3575,7 +3725,7 @@ "mock", "xunit" ], - "time": "2016-10-09 07:01:45" + "time": "2016-12-08 20:27:08" }, { "name": "pimple/pimple", @@ -3624,24 +3774,73 @@ "time": "2013-11-22 08:30:29" }, { - "name": "sebastian/code-unit-reverse-lookup", + "name": "psr/container", "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14 16:28:37" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe" + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/c36f5e7cfce482fde5bf8d10d41a53591e0198fe", - "reference": "c36f5e7cfce482fde5bf8d10d41a53591e0198fe", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", "shasum": "" }, "require": { - "php": ">=5.6" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "^5.7 || ^6.0" }, "type": "library", "extra": { @@ -3666,26 +3865,26 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2016-02-13 06:45:14" + "time": "2017-03-04 06:30:41" }, { "name": "sebastian/comparator", - "version": "1.2.0", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", - "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", "shasum": "" }, "require": { "php": ">=5.3.3", "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2" + "sebastian/exporter": "~1.2 || ~2.0" }, "require-dev": { "phpunit/phpunit": "~4.4" @@ -3730,7 +3929,7 @@ "compare", "equality" ], - "time": "2015-07-26 15:48:44" + "time": "2017-01-29 09:50:25" }, { "name": "sebastian/diff", @@ -3786,28 +3985,28 @@ }, { "name": "sebastian/environment", - "version": "1.3.8", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", - "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8 || ^5.0" + "phpunit/phpunit": "^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -3832,25 +4031,25 @@ "environment", "hhvm" ], - "time": "2016-08-18 05:49:44" + "time": "2016-11-26 07:53:53" }, { "name": "sebastian/exporter", - "version": "1.2.2", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", - "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", "shasum": "" }, "require": { "php": ">=5.3.3", - "sebastian/recursion-context": "~1.0" + "sebastian/recursion-context": "~2.0" }, "require-dev": { "ext-mbstring": "*", @@ -3859,7 +4058,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -3899,7 +4098,7 @@ "export", "exporter" ], - "time": "2016-06-17 09:04:28" + "time": "2016-11-19 08:54:04" }, { "name": "sebastian/finder-facade", @@ -3942,16 +4141,16 @@ }, { "name": "sebastian/git", - "version": "2.1.3", + "version": "2.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/git.git", - "reference": "5100bc50cd9e70f424c643618e142214225024f3" + "reference": "815bbbc963cf35e5413df195aa29df58243ecd24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/git/zipball/5100bc50cd9e70f424c643618e142214225024f3", - "reference": "5100bc50cd9e70f424c643618e142214225024f3", + "url": "https://api.github.com/repos/sebastianbergmann/git/zipball/815bbbc963cf35e5413df195aa29df58243ecd24", + "reference": "815bbbc963cf35e5413df195aa29df58243ecd24", "shasum": "" }, "require": { @@ -3983,7 +4182,7 @@ "keywords": [ "git" ], - "time": "2016-06-15 09:30:19" + "time": "2017-01-23 20:57:12" }, { "name": "sebastian/global-state", @@ -4038,21 +4237,21 @@ }, { "name": "sebastian/object-enumerator", - "version": "1.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26" + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d4ca2fb70344987502567bc50081c03e6192fb26", - "reference": "d4ca2fb70344987502567bc50081c03e6192fb26", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", + "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", "shasum": "" }, "require": { "php": ">=5.6", - "sebastian/recursion-context": "~1.0" + "sebastian/recursion-context": "~2.0" }, "require-dev": { "phpunit/phpunit": "~5" @@ -4060,7 +4259,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4080,7 +4279,7 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2016-01-28 13:25:10" + "time": "2017-02-18 15:18:39" }, { "name": "sebastian/phpcpd", @@ -4135,16 +4334,16 @@ }, { "name": "sebastian/recursion-context", - "version": "1.0.4", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "938df7a6478e72795e5f8266cff24d06e3136f2e" + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/938df7a6478e72795e5f8266cff24d06e3136f2e", - "reference": "938df7a6478e72795e5f8266cff24d06e3136f2e", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", "shasum": "" }, "require": { @@ -4156,7 +4355,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -4184,7 +4383,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-15 06:55:36" + "time": "2016-11-19 07:33:16" }, { "name": "sebastian/resource-operations", @@ -4230,16 +4429,16 @@ }, { "name": "sebastian/version", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", - "reference": "c829badbd8fdf16a0bad8aa7fa7971c029f1b9c5", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, "require": { @@ -4269,25 +4468,28 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-02-04 12:56:52" + "time": "2016-10-03 07:35:21" }, { "name": "seld/jsonlint", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "19495c181d6d53a0a13414154e52817e3b504189" + "reference": "791f8c594f300d246cdf01c6b3e1e19611e301d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/19495c181d6d53a0a13414154e52817e3b504189", - "reference": "19495c181d6d53a0a13414154e52817e3b504189", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/791f8c594f300d246cdf01c6b3e1e19611e301d8", + "reference": "791f8c594f300d246cdf01c6b3e1e19611e301d8", "shasum": "" }, "require": { "php": "^5.3 || ^7.0" }, + "require-dev": { + "phpunit/phpunit": "^4.5" + }, "bin": [ "bin/jsonlint" ], @@ -4315,20 +4517,20 @@ "parser", "validator" ], - "time": "2016-11-14 17:59:58" + "time": "2017-03-06 16:42:24" }, { "name": "squizlabs/php_codesniffer", - "version": "2.7.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "571e27b6348e5b3a637b2abc82ac0d01e6d7bbed" + "reference": "d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/571e27b6348e5b3a637b2abc82ac0d01e6d7bbed", - "reference": "571e27b6348e5b3a637b2abc82ac0d01e6d7bbed", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d", + "reference": "d7cf0d894e8aa4c73712ee4a331cc1eaa37cdc7d", "shasum": "" }, "require": { @@ -4393,26 +4595,85 @@ "phpcs", "standards" ], - "time": "2016-09-01 23:53:02" + "time": "2017-03-01 22:17:45" + }, + { + "name": "symfony/class-loader", + "version": "v3.2.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/class-loader.git", + "reference": "c29a5bc6ca14cfff1f5e3d7781ed74b6e898d2b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/c29a5bc6ca14cfff1f5e3d7781ed74b6e898d2b9", + "reference": "c29a5bc6ca14cfff1f5e3d7781ed74b6e898d2b9", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/finder": "~2.8|~3.0", + "symfony/polyfill-apcu": "~1.1" + }, + "suggest": { + "symfony/polyfill-apcu": "For using ApcClassLoader on HHVM" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\ClassLoader\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony ClassLoader Component", + "homepage": "https://symfony.com", + "time": "2017-02-18 17:28:00" }, { "name": "symfony/config", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde" + "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/f8b1922bbda9d2ac86aecd649399040bce849fde", - "reference": "f8b1922bbda9d2ac86aecd649399040bce849fde", + "url": "https://api.github.com/repos/symfony/config/zipball/06ce6bb46c24963ec09323da45d0f4f85d3cecd2", + "reference": "06ce6bb46c24963ec09323da45d0f4f85d3cecd2", "shasum": "" }, "require": { "php": ">=5.3.9", "symfony/filesystem": "~2.3|~3.0.0" }, + "require-dev": { + "symfony/yaml": "~2.7|~3.0.0" + }, "suggest": { "symfony/yaml": "To use the yaml reference dumper" }, @@ -4446,32 +4707,32 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2016-09-14 20:31:12" + "time": "2017-03-01 18:13:50" }, { "name": "symfony/dependency-injection", - "version": "v2.8.13", + "version": "v3.2.6", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "3d61c765daa1a5832f1d7c767f48886b8d8ea64c" + "reference": "74e0935e414ad33d5e82074212c0eedb4681a691" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/3d61c765daa1a5832f1d7c767f48886b8d8ea64c", - "reference": "3d61c765daa1a5832f1d7c767f48886b8d8ea64c", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/74e0935e414ad33d5e82074212c0eedb4681a691", + "reference": "74e0935e414ad33d5e82074212c0eedb4681a691", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.5.9" }, "conflict": { - "symfony/expression-language": "<2.6" + "symfony/yaml": "<3.2" }, "require-dev": { - "symfony/config": "~2.2|~3.0.0", - "symfony/expression-language": "~2.6|~3.0.0", - "symfony/yaml": "~2.3.42|~2.7.14|~2.8.7|~3.0.7" + "symfony/config": "~2.8|~3.0", + "symfony/expression-language": "~2.8|~3.0", + "symfony/yaml": "~3.2" }, "suggest": { "symfony/config": "", @@ -4482,7 +4743,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -4509,20 +4770,20 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2016-10-24 15:52:36" + "time": "2017-03-05 00:06:55" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934" + "reference": "bb4ec47e8e109c1c1172145732d0aa468d967cd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", - "reference": "25c576abd4e0f212e678fe8b2bd9a9a98c7ea934", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bb4ec47e8e109c1c1172145732d0aa468d967cd0", + "reference": "bb4ec47e8e109c1c1172145732d0aa468d967cd0", "shasum": "" }, "require": { @@ -4530,7 +4791,7 @@ }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5|~3.0.0", + "symfony/config": "^2.0.5|~3.0.0", "symfony/dependency-injection": "~2.6|~3.0.0", "symfony/expression-language": "~2.6|~3.0.0", "symfony/stopwatch": "~2.3|~3.0.0" @@ -4569,7 +4830,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-10-13 01:43:15" + "time": "2017-02-21 08:33:48" }, { "name": "symfony/filesystem", @@ -4622,16 +4883,16 @@ }, { "name": "symfony/finder", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691" + "reference": "5fc4b5cab38b9d28be318fcffd8066988e7d9451" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/bc24c8f5674c6f6841f2856b70e5d60784be5691", - "reference": "bc24c8f5674c6f6841f2856b70e5d60784be5691", + "url": "https://api.github.com/repos/symfony/finder/zipball/5fc4b5cab38b9d28be318fcffd8066988e7d9451", + "reference": "5fc4b5cab38b9d28be318fcffd8066988e7d9451", "shasum": "" }, "require": { @@ -4667,20 +4928,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-09-28 00:10:16" + "time": "2017-02-21 08:33:48" }, { "name": "symfony/process", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f" + "reference": "41336b20b52f5fd5b42a227e394e673c8071118f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", - "reference": "024de37f8a6b9e5e8244d9eb3fcf3e467dd2a93f", + "url": "https://api.github.com/repos/symfony/process/zipball/41336b20b52f5fd5b42a227e394e673c8071118f", + "reference": "41336b20b52f5fd5b42a227e394e673c8071118f", "shasum": "" }, "require": { @@ -4716,20 +4977,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-09-29 14:03:54" + "time": "2017-03-04 12:20:59" }, { "name": "symfony/stopwatch", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "35bae476693150728b0eb51647faac82faf9aaca" + "reference": "9e4369666d02ee9b8830da878b7f6a769eb96f4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/35bae476693150728b0eb51647faac82faf9aaca", - "reference": "35bae476693150728b0eb51647faac82faf9aaca", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/9e4369666d02ee9b8830da878b7f6a769eb96f4b", + "reference": "9e4369666d02ee9b8830da878b7f6a769eb96f4b", "shasum": "" }, "require": { @@ -4765,34 +5026,34 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2016-06-29 05:29:29" + "time": "2017-02-18 17:06:33" }, { "name": "symfony/translation", - "version": "v2.8.13", + "version": "v3.0.9", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "cca6ff892355876534b01a927f789bac9601c935" + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/cca6ff892355876534b01a927f789bac9601c935", - "reference": "cca6ff892355876534b01a927f789bac9601c935", + "url": "https://api.github.com/repos/symfony/translation/zipball/eee6c664853fd0576f21ae25725cfffeafe83f26", + "reference": "eee6c664853fd0576f21ae25725cfffeafe83f26", "shasum": "" }, "require": { - "php": ">=5.3.9", + "php": ">=5.5.9", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/config": "<2.7" + "symfony/config": "<2.8" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8", - "symfony/intl": "~2.4|~3.0.0", - "symfony/yaml": "~2.2|~3.0.0" + "symfony/config": "~2.8|~3.0", + "symfony/intl": "~2.8|~3.0", + "symfony/yaml": "~2.8|~3.0" }, "suggest": { "psr/log": "To use logging capability in translator", @@ -4802,7 +5063,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -4829,20 +5090,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-10-18 04:28:30" + "time": "2016-07-30 07:22:48" }, { "name": "symfony/validator", - "version": "v2.8.13", + "version": "v2.8.18", "source": { "type": "git", "url": "https://github.com/symfony/validator.git", - "reference": "987bf3a7d585680773bc79f6cf3d9e78340cb02c" + "reference": "8d4bfa7ec24e70ebc28d0cea5f2702d3f1257a63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/validator/zipball/987bf3a7d585680773bc79f6cf3d9e78340cb02c", - "reference": "987bf3a7d585680773bc79f6cf3d9e78340cb02c", + "url": "https://api.github.com/repos/symfony/validator/zipball/8d4bfa7ec24e70ebc28d0cea5f2702d3f1257a63", + "reference": "8d4bfa7ec24e70ebc28d0cea5f2702d3f1257a63", "shasum": "" }, "require": { @@ -4853,13 +5114,13 @@ "require-dev": { "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", - "egulias/email-validator": "~1.2,>=1.2.1", + "egulias/email-validator": "^1.2.1", "symfony/config": "~2.2|~3.0.0", "symfony/expression-language": "~2.4|~3.0.0", "symfony/http-foundation": "~2.3|~3.0.0", - "symfony/intl": "~2.7.4|~2.8|~3.0.0", + "symfony/intl": "~2.7.25|^2.8.18|~3.2.5", "symfony/property-access": "~2.3|~3.0.0", - "symfony/yaml": "~2.0,>=2.0.5|~3.0.0" + "symfony/yaml": "^2.0.5|~3.0.0" }, "suggest": { "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.", @@ -4902,29 +5163,35 @@ ], "description": "Symfony Validator Component", "homepage": "https://symfony.com", - "time": "2016-10-19 22:37:24" + "time": "2017-02-28 02:24:56" }, { "name": "symfony/yaml", - "version": "v2.8.13", + "version": "v3.2.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "396784cd06b91f3db576f248f2402d547a077787" + "reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/396784cd06b91f3db576f248f2402d547a077787", - "reference": "396784cd06b91f3db576f248f2402d547a077787", + "url": "https://api.github.com/repos/symfony/yaml/zipball/093e416ad096355149e265ea2e4cc1f9ee40ab1a", + "reference": "093e416ad096355149e265ea2e4cc1f9ee40ab1a", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.2-dev" } }, "autoload": { @@ -4951,7 +5218,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-10-21 20:59:10" + "time": "2017-03-07 16:47:02" }, { "name": "theseer/fdomdocument", @@ -4995,29 +5262,30 @@ }, { "name": "twig/twig", - "version": "v1.27.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "3c6c0033fd3b5679c6e1cb60f4f9766c2b424d97" + "reference": "05cf49921b13f6f01d3cfdf9018cfa7a8086fd5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/3c6c0033fd3b5679c6e1cb60f4f9766c2b424d97", - "reference": "3c6c0033fd3b5679c6e1cb60f4f9766c2b424d97", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/05cf49921b13f6f01d3cfdf9018cfa7a8086fd5a", + "reference": "05cf49921b13f6f01d3cfdf9018cfa7a8086fd5a", "shasum": "" }, "require": { "php": ">=5.2.7" }, "require-dev": { + "psr/container": "^1.0", "symfony/debug": "~2.7", - "symfony/phpunit-bridge": "~2.7" + "symfony/phpunit-bridge": "~3.3@dev" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.27-dev" + "dev-master": "1.33-dev" } }, "autoload": { @@ -5052,20 +5320,20 @@ "keywords": [ "templating" ], - "time": "2016-10-25 19:17:17" + "time": "2017-03-22 15:40:09" }, { "name": "zendframework/zend-cache", - "version": "2.7.1", + "version": "2.7.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-cache.git", - "reference": "2c68def8f96ce842d2f2a9a69e2f3508c2f5312d" + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/2c68def8f96ce842d2f2a9a69e2f3508c2f5312d", - "reference": "2c68def8f96ce842d2f2a9a69e2f3508c2f5312d", + "url": "https://api.github.com/repos/zendframework/zend-cache/zipball/c98331b96d3b9d9b24cf32d02660602edb34d039", + "reference": "c98331b96d3b9d9b24cf32d02660602edb34d039", "shasum": "" }, "require": { @@ -5075,9 +5343,9 @@ "zendframework/zend-stdlib": "^2.7 || ^3.0" }, "require-dev": { - "fabpot/php-cs-fixer": "1.7.*", "phpbench/phpbench": "^0.10.0", - "phpunit/phpunit": "^4.5", + "phpunit/phpunit": "^4.8", + "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-serializer": "^2.6", "zendframework/zend-session": "^2.6.2" }, @@ -5121,7 +5389,7 @@ "cache", "zf2" ], - "time": "2016-05-12 21:47:55" + "time": "2016-12-16 11:35:47" }, { "name": "zendframework/zend-config", @@ -5181,26 +5449,26 @@ }, { "name": "zendframework/zend-eventmanager", - "version": "3.0.1", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-eventmanager.git", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e" + "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/5c80bdee0e952be112dcec0968bad770082c3a6e", - "reference": "5c80bdee0e952be112dcec0968bad770082c3a6e", + "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/c3bce7b7d47c54040b9ae51bc55491c72513b75d", + "reference": "c3bce7b7d47c54040b9ae51bc55491c72513b75d", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0" + "php": "^5.6 || ^7.0" }, "require-dev": { "athletic/athletic": "^0.1", "container-interop/container-interop": "^1.1.0", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "^2.0", + "phpunit/phpunit": "^5.6", + "zendframework/zend-coding-standard": "~1.0.0", "zendframework/zend-stdlib": "^2.7.3 || ^3.0" }, "suggest": { @@ -5210,8 +5478,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev", - "dev-develop": "3.1-dev" + "dev-master": "3.1-dev", + "dev-develop": "3.2-dev" } }, "autoload": { @@ -5231,7 +5499,7 @@ "events", "zf2" ], - "time": "2016-02-18 20:53:00" + "time": "2016-12-19 21:47:12" }, { "name": "zendframework/zend-filter", @@ -5527,16 +5795,16 @@ }, { "name": "zendframework/zend-servicemanager", - "version": "2.7.7", + "version": "2.7.8", "source": { "type": "git", "url": "https://github.com/zendframework/zend-servicemanager.git", - "reference": "eeecb78945c2a9f653caded36ba5274e8a8f5468" + "reference": "2ae3b6e4978ec2e9ff52352e661946714ed989f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/eeecb78945c2a9f653caded36ba5274e8a8f5468", - "reference": "eeecb78945c2a9f653caded36ba5274e8a8f5468", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/2ae3b6e4978ec2e9ff52352e661946714ed989f9", + "reference": "2ae3b6e4978ec2e9ff52352e661946714ed989f9", "shasum": "" }, "require": { @@ -5575,7 +5843,7 @@ "servicemanager", "zf2" ], - "time": "2016-09-01 19:03:15" + "time": "2016-12-19 19:14:29" }, { "name": "zendframework/zend-stdlib", diff --git a/tests/phpunit/phpunit.xml.dist b/phpunit.xml.dist similarity index 67% rename from tests/phpunit/phpunit.xml.dist rename to phpunit.xml.dist index 018ff3989..2118734ed 100644 --- a/tests/phpunit/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,11 +1,11 @@ - + - unit + tests/phpunit/unit - integration + tests/phpunit/integration @@ -25,11 +25,12 @@ - ../../src + src + tests/behat/features/bootstrap/FeatureContext.php - ../../src - ../../src/Exception.php - ../../src/Version.php + src + src/Exception.php + src/Version.php diff --git a/tests/phpunit/phpunit.xml.travis b/phpunit.xml.travis similarity index 81% rename from tests/phpunit/phpunit.xml.travis rename to phpunit.xml.travis index 3d6c4fc08..a8745c5d4 100644 --- a/tests/phpunit/phpunit.xml.travis +++ b/phpunit.xml.travis @@ -1,11 +1,11 @@ - + - unit + tests/phpunit/unit - integration + tests/phpunit/integration diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index ce3aa6521..000000000 --- a/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -phpunit.xml diff --git a/tests/behat/README.md b/tests/behat/README.md new file mode 100644 index 000000000..ea8cf8d28 --- /dev/null +++ b/tests/behat/README.md @@ -0,0 +1,98 @@ +# Behat features +The `features` directory contains all the feature files that is tested with Behat. [imbo/behat-api-extension](https://github.com/imbo/behat-api-extension) is used to test the API, and the [FeatureContext](features/bootstrap/FeatureContext.php) class contains a series of steps that can be used to test Imbo-specific features. It also contains steps for priming Imbo with given images and metadata. + +## Custom steps + +The following is a list of steps implemented in the `FeatureContext` class: + +```gherkin +Given Imbo uses the :configFile configuration +Given the stats are allowed by :mask +Given the storage is down +Given the database is down +Given I sign the request using HTTP headers +Given I sign the request +Given I include an access token in the query string +Given I include an access token in the query string for all requests +Given :imagePath exists for user :user +Given :imagePath exists for user :user with the following metadata: +Given the client IP is :ip +Given I specify :transformation as transformation +Given I specify the following transformations: +Given I prime the database with :fixture +Given I authenticate using :method +Given I use :publicKey and :privateKey for public and private keys +Given the query string parameter :name is set to :value +Given the query string parameter :param is set to the image identifier of :path +Given I generate a short URL for :path with the following parameters: +Given I use :localPath as the watermark image +Given I use :localPath as the watermark image with :params as parameters + +When I request the previously added image +When I request the previously added image using HTTP :method +When I request the previously added image as a :extension +When I request the previously added image as a :extension using HTTP :method +When I replay the last request +When I replay the last request using HTTP :method +When I request the metadata of the previously added image +When I request the metadata of the previously added image using HTTP :method +When I request the image resource for :path +When I request the image resource for :path using HTTP :method +When I request the image resource for :path as a (png|gif|jpg) +When I request the image resource for :path as a (png|gif|jpg) using HTTP :method +When I request: +When I request the image using the generated short URL + +Then the Imbo error message is :message +Then the Imbo error message is :message and the error code is :code +Then the image width is :width +Then the image height is :height +Then the image dimension is :dimension +Then the pixel at coordinate :coordinates has a color of :color +Then the pixel at coordinate :coordinates has an alpha of :alpha +Then the ACL rule under public key :publicKey with ID :aclId no longer exists +Then the :publicKey public key no longer exists +Then the response can be cached +Then the response can not be cached +Then the response has a max-age of :max seconds +Then the response has a :directive cache-control directive +Then the response does not have a :directive cache-control directive +Then the last :num :headerName response headers are the same +Then the last :num :headerName response headers are not the same +Then the last responses match: +Then the image should not have any :prefix properties +Then the response body size is :expectedSize +``` + +## Run tests + +To run the complete testsuite, execute the following command from the project root: + + ./vendor/bin/behat --strict + +or + + composer test-behat + +For the tests to run you need to have an HTTPD running that hosts the Imbo installation. A composer script has been created for this purpose: + + composer start-httpd-for-behat-tests + +which simply executes: + + php -S localhost:8080 -t ./public tests/behat/router.php > build/logs/httpd.log 2>&1 & + +## Configuration +The `behat.yml.dist` file in the project root specifies the `base_uri` for the Imbo installation, and by default it is set to `http://localhost:8080`. If you wish to run the testsuite using a different host and/or port you will need to create a `behat.yml` configuration file in the project root that specifies the host/port combination you want to use. Remember to use the router script specified above for the tests to work as expected. This script makes sure that Imbo uses the correct configuration with regards to testing, and is also responsible for adding custom configuration based on steps defined in the FeatureContext class. + +### Authentication + +The test configuration specifies the following authentication information that is used in the tests: + +| Public key | Private key | Access to | +| --------------- | ------------- | -------------------- | +| `publickey` | `privatekey` | `user`, `other-user` | +| `unpriviledged` | `privatekey` | `user` | +| `wildcard` | `*` | `*` | + +Feel free to add more authentication information if you create tests that needs a different set of keys / users. diff --git a/tests/behat/behat.yml b/tests/behat/behat.yml deleted file mode 100644 index d0e0929cf..000000000 --- a/tests/behat/behat.yml +++ /dev/null @@ -1,31 +0,0 @@ -default: - paths: - features: features - bootstrap: bootstrap - - context: - class: ImboContext - parameters: - # URL to use for the functional tests with the built in httpd in php-5.4 - url: http://localhost:8888 - - # Document root and router used by the httpd - documentRoot: public - router: %behat.paths.base%/router.php - - # httpd log file location - httpdLog: %behat.paths.base%/../../build/logs/httpd.log - - # Timeout when trying to connect to the built in httpd - timeout: 5 - - enableCodeCoverage: true - coveragePath: %behat.paths.base%/../../build/behat-coverage - whitelist: - - %behat.paths.base%/../../src - - %behat.paths.base%/../../public - -no-cc: - context: - parameters: - enableCodeCoverage: false diff --git a/tests/behat/bootstrap/ImboContext.php b/tests/behat/bootstrap/ImboContext.php deleted file mode 100644 index 3be6abf80..000000000 --- a/tests/behat/bootstrap/ImboContext.php +++ /dev/null @@ -1,739 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file that was - * distributed with this source code. - */ - -use Behat\Behat\Event\FeatureEvent, - Behat\Behat\Event\SuiteEvent, - Behat\Behat\Exception\PendingException, - Behat\Gherkin\Node\PyStringNode, - Behat\Behat\Context\Step\Given; - -// Use the RESTContext -require 'RESTContext.php'; - -/** - * Imbo Context - * - * @author Christer Edvartsen - * @package Test suite\Functional tests - */ -class ImboContext extends RESTContext { - /** - * The user used by the client - * - * @var string - */ - private $user = 'user'; - - /** - * The public key used by the client - * - * @var string - */ - private $publicKey; - - /** - * The private key used by the client - * - * @var string - */ - private $privateKey; - - /** - * Holds the configuration file specified in the current feature - * - * @var string - */ - private $currentConfig; - - /** - * An array of urls for added images, keyed by local file path - * - * @var array - */ - private $imageUrls = []; - - /** - * An array of image identifiers for added images, keyed by local file path - * - * @var array - */ - private $imageIdentifiers = []; - - /** - * Holds the current image target URL - used when the same image is requested - * over several tests in the same feature - * - * @var string - */ - private static $testImageUrl; - - /** - * Holds the image identifier of the current testing target - * - * @var string - */ - private static $testImageIdentifier; - - /** - * Holds the path to the image currently used as the testing target - * - * @var string - */ - private static $testImagePath; - - /** - * Holds the current feature for this test image - * - * @var string - */ - private static $testImageFeature; - - /** - * @BeforeFeature - */ - public static function prepare(FeatureEvent $event) { - // Drop mongo test collection which stores information regarding images, and the images - // themselves - $mongo = new MongoClient(); - $mongo->imbo_testing->drop(); - - $cachePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'imbo-behat-image-transformation-cache'; - - if (is_dir($cachePath)) { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($cachePath), - RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($iterator as $file) { - $name = $file->getPathname(); - - if (substr($name, -1) === '.') { - continue; - } - - if ($file->isDir()) { - // Remove dir - rmdir($name); - } else { - // Remove file - unlink($name); - } - } - - // Remove the directory itself - rmdir($cachePath); - } - } - - /** - * @Given /^the (storage|database) is down$/ - */ - public function forceAdapterFailure($adapter) { - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) use ($adapter) { - $event['request']->getQuery()->set($adapter . 'Down', 1); - }); - } - - /** - * @Given /^the database and the storage is down$/ - */ - public function forceBothAdapterFailure() { - return [ - new Given('the storage is down'), - new Given('the database is down'), - ]; - } - - /** - * @Given /^I use "([^"]*)" and "([^"]*)" for public and private keys$/ - */ - public function setClientAuth($publicKey, $privateKey) { - $this->publicKey = $publicKey; - $this->privateKey = $privateKey; - - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) { - $request = $event['request']; - $request->addHeaders([ - 'X-Imbo-PublicKey' => $this->publicKey - ]); - }, -100); - } - - /** - * @Given /^I do not specify a public and private key$/ - */ - public function removeClientAuth() { - $this->publicKey = null; - $this->privateKey = null; - } - - /** - * @Given /^I authenticate using "(.*?)"$/ - */ - public function authenticateRequest($method) { - if ($method == 'access-token') { - return new Given('I include an access token in the query'); - } - - if ($method == 'signature') { - return new Given('I sign the request'); - } - - throw new \Exception('Unknown authentication method: ' . $method); - } - - /** - * @Given /^I include an access token in the query$/ - */ - public function appendAccessToken() { - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) { - $request = $event['request']; - $query = $request->getQuery(); - - if (!$query->get('publicKey')) { - $query->set('publicKey', $this->publicKey); - } - - $query->remove('accessToken'); - $accessToken = hash_hmac('sha256', urldecode($request->getUrl()), $this->privateKey); - $query->set('accessToken', $accessToken); - }, -100); - } - - /** - * @Given /^I sign the request( using HTTP headers)?$/ - */ - public function signRequest($useHeaders = false) { - $useHeaders = (boolean) $useHeaders; - - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) use ($useHeaders) { - $request = $event['request']; - - // Remove headers and query params that should not be present at this time - $request->removeHeader('X-Imbo-PublicKey'); - $request->removeHeader('X-Imbo-Authenticate-Signature'); - $request->removeHeader('X-Imbo-Authenticate-Timestamp'); - $query = $request->getQuery(); - $query->remove('accessToken'); - $query->remove('signature'); - $query->remove('timestamp'); - - // Add public key to query if we're told not to use headers - if (!$useHeaders) { - $query->set('publicKey', $this->publicKey); - } - - $method = $request->getHeader('X-Http-Method-Override') ?: $request->getMethod(); - - $timestamp = gmdate('Y-m-d\TH:i:s\Z'); - $data = $method . '|' . urldecode($request->getUrl()) . '|' . $this->publicKey . '|' . $timestamp; - - // Generate signature - $signature = hash_hmac('sha256', $data, $this->privateKey); - - if ($useHeaders) { - $request->addHeaders([ - 'X-Imbo-PublicKey' => $this->publicKey, - 'X-Imbo-Authenticate-Signature' => $signature, - 'X-Imbo-Authenticate-Timestamp' => $timestamp, - ]); - } else { - $query->set('signature', $signature); - $query->set('timestamp', $timestamp); - } - }, -100); - } - - /** - * @Given /^I specify "([^"]*)" as transformation$/ - */ - public function applyTransformation($transformation) { - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) use ($transformation) { - $event['request']->getQuery()->set('t', [$transformation]); - }); - } - - /** - * @Given /^the (width|height) of the image is "([^"]*)"$/ - */ - public function theWidthOfTheImageIs($value, $size) { - $image = (string) $this->getLastResponse()->getBody(); - $size = (int) $size; - - $info = getimagesizefromstring($image); - - if ($value === 'width') { - $index = 0; - } else { - $index = 1; - } - - assertSame($size, $info[$index], 'Incorrect ' . $value . ', expected ' . $size . ', got ' . $info[$index]); - } - - /** - * @Given /^the Imbo error message is "([^"]*)"(?: and the error code is "([^"]*)")?$/ - */ - public function assertImboError($message, $code = null) { - $response = $this->getLastResponse(); - $contentType = $response->getContentType(); - - try { - if ($contentType === 'application/json') { - $data = $response->json(); - $errorMessage = $data['error']['message']; - $errorCode = $data['error']['imboErrorCode']; - } else if ($contentType === 'application/xml') { - $data = $response->xml(); - $errorMessage = (string) $data->error->message; - $errorCode = $data->error->imboErrorCode; - } - } catch (\Exception $e) { - throw new RuntimeException( - "Unable to parse response: \n" . - $response->getMessage() . "\n\n" . - $e->getMessage() - ); - } - - assertSame($message, $errorMessage, 'Expected "' . $message. '", got "' . $errorMessage . '"'); - - if ($code !== null) { - $expected = (int) $code; - $actual = (int) $errorCode; - - assertSame($expected, $actual, 'Expected "' . $expected . '", got "' . $actual . '"'); - } - } - - /** - * @Given /^"([^"]*)" exists in Imbo$/ - */ - public function addImageToImbo($imagePath) { - $this->addUserImageToImbo($imagePath, 'user'); - } - - /** - * @Given /^"([^"]*)" exists for user "([^"]*)" in Imbo$/ - */ - public function addUserImageToImbo($imagePath, $user) { - $this->setClientAuth('publickey', 'privatekey'); - $this->signRequest(); - $this->attachFileToRequestBody($imagePath); - $this->request('/users/' . $user . '/images/', 'POST'); - $this->imageUrls[$imagePath] = $this->getPreviouslyAddedImageUrl(); - $this->imageIdentifiers[$imagePath] = $this->getPreviouslyAddedImageIdentifier(); - } - - /** - * @Given /^I specify the following transformations:$/ - */ - public function applyTransformations(PyStringNode $transformations) { - foreach ($transformations->getLines() as $t) { - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) use ($t) { - $event['request']->getQuery()->add('t', $t); - }); - } - } - - /** - * {@inheritdoc} - */ - public function setRequestHeader($header, $value) { - if ($value === 'current-timestamp') { - $value = gmdate('Y-m-d\TH:i:s\Z'); - } - - parent::setRequestHeader($header, $value); - } - - /** - * @Given /^"([^"]*)" is used as the test image( for the "([^"]*)" feature)?$/ - */ - public function imageIsUsedAsTestImage($testImagePath, $forFeature = null, $feature = null) { - if (self::$testImagePath === $testImagePath && $feature && - self::$testImageFeature === $feature) { - return; - } - - $this->addImageToImbo($testImagePath); - self::$testImageIdentifier = $this->getPreviouslyAddedImageIdentifier(); - self::$testImageUrl = $this->getPreviouslyAddedImageUrl(); - self::$testImagePath = $testImagePath; - self::$testImageFeature = $feature; - } - - /** - * @When /^I request the test image(?: as a "([^"]*)")?$/ - */ - public function requestTestImage($format = null) { - $url = self::$testImageUrl . ($format ? '.' . $format : ''); - return [ - new Given('I request "' . $url . '" using HTTP "GET"'), - ]; - } - - /** - * @When /^I request the test image using HTTP "([^"]*)"$/ - */ - public function requestTestImageUsingHttpMethod($method) { - $url = self::$testImageUrl; - return [ - new Given('I request "' . $url . '" using HTTP "' . $method . '"'), - ]; - } - - /** - * @When /^I request the metadata of the test image(?: using HTTP "(.*?)")?$/ - */ - public function requestMetadataOfTestImage($method = 'GET') { - $url = self::$testImageUrl . '/meta'; - return [ - new Given('I request "' . $url . '" using HTTP "' . $method . '"'), - ]; - } - - /** - * @When /^I request the metadata of the test image as "(xml|json)"$/ - */ - public function requestMetadataOfTestImageInFormat($format = null) { - $url = self::$testImageUrl . '/meta' . ($format ? '.' . $format : ''); - return [ - new Given('I request "' . $url . '" using HTTP "GET"'), - ]; - } - - /** - * @When /^I request the (?:previously )?added image(?: with the query string "([^"]*)")?$/ - */ - public function requestPreviouslyAddedImage($queryParams = '') { - $url = $this->getPreviouslyAddedImageUrl() . $queryParams; - return [ - new Given('I request "' . $url . '" using HTTP "GET"'), - ]; - } - - /** - * @When /^I request the (?:previously )?added image using HTTP "([^"]*)"$/ - */ - public function requestPreviouslyAddedImageWithHttpMethod($method) { - $url = $this->getPreviouslyAddedImageUrl(); - return [ - new Given('I request "' . $url . '" using HTTP "' . $method . '"'), - ]; - } - - - /** - * @When /^I request the (?:previously )?added image as a "(jpg|png|gif)"$/ - */ - public function requestTheAddedImage($extension) { - $url = $this->getPreviouslyAddedImageUrl() . '.' . $extension; - return [ - new Given('I request "' . $url . '" using HTTP "GET"'), - ]; - } - - /** - * @When /^I request the metadata of the previously added image as "(xml|json)"$/ - */ - public function requestMetadataOfPreviouslyAddedImageInFormat($format = null) { - $url = $this->getPreviouslyAddedImageUrl() . '/meta' . ($format ? '.' . $format : ''); - return [ - new Given('I request "' . $url . '" using HTTP "GET"'), - ]; - } - - /** - * @When /^I request the metadata of the previously added image(?: using HTTP "(.*?)")?$/ - */ - public function requestMetadataOfPreviouslyAddedImage($method = 'GET') { - $url = $this->getPreviouslyAddedImageUrl() . '/meta'; - return [ - new Given('I request "' . $url . '" using HTTP "' . $method . '"'), - ]; - } - - /** - * @When /^I request the image resource for "([^"]*)"(?: as a "(png|gif|jpg)")?(?: using HTTP "([^"]*)")?$/ - */ - public function requestImageResourceForLocalImage($imagePath, $format = null, $method = 'GET') { - if (!isset($this->imageUrls[$imagePath])) { - throw new RuntimeException('Image URL for "' . $imagePath . '" not found'); - } - - $url = $this->imageUrls[$imagePath]; - if ($format) { - $url .= '.' . $format; - } - - return [ - new Given('I request "' . $url . '" using HTTP "' . $method . '"'), - ]; - } - - /** - * @Given /^I append a query string parameter, "([^"]*)" with the image identifier of "([^"]*)"$/ - */ - public function appendQueryStringParamWithImageIdentifierForLocalImage($queryParam, $imagePath) { - if (!isset($this->imageIdentifiers[$imagePath])) { - throw new RuntimeException('Image identifier for "' . $imagePath . '" not found'); - } - - $this->appendQueryStringParameter($queryParam, $this->imageIdentifiers[$imagePath]); - } - - /** - * @Given /^the image is deleted$/ - */ - public function deleteImage() { - $identifier = $this->getLastResponse()->getHeaders()->toArray()['X-Imbo-ImageIdentifier'][0]; - - $this->setClientAuth('publickey', 'privatekey'); - $this->signRequest(); - $this->request('/users/user/images/' . $identifier, 'DELETE'); - } - - /** - * @Given /^the client IP is "([^"]*)"$/ - */ - public function setClientIp($ip) { - $this->addHeaderToNextRequest('X-Client-Ip', $ip); - } - - /** - * @Given /^the image should not have any "([^"]*)" properties$/ - */ - public function assertImageProperties($tag) { - $imagick = new \Imagick(); - $imagick->readImageBlob((string) $this->getLastResponse()->getBody()); - - foreach ($imagick->getImageProperties() as $key => $value) { - assertStringStartsNotWith($tag, $key, 'Properties exist that should have been stripped'); - } - } - - /** - * @Given /^the pixel at coordinate "([^"]*)" should have a color of "#([^"]*)"$/ - */ - public function assertImagePixelColor($coordinates, $expectedColor) { - $info = $this->getImagePixelInfo($coordinates); - $expectedColor = strtolower($expectedColor); - - assertSame( - $expectedColor, - $info['color'], - 'Incorrect color at coordinate ' . $coordinates . - ', expected ' . $expectedColor . ', got ' . $info['color'] - ); - } - - /** - * @Given /^the pixel at coordinate "([^"]*)" should have an alpha of "([^"]*)"$/ - */ - public function assertImagePixelAlpha($coordinates, $expectedAlpha) { - $info = $this->getImagePixelInfo($coordinates); - $expectedAlpha = (float) $expectedAlpha; - - assertSame( - $expectedAlpha, - $info['alpha'], - 'Incorrect alpha value at coordinate ' . $coordinates . - ', expected ' . $expectedAlpha . ', got ' . $info['alpha'] - ); - } - - /** - * @Given /^Imbo uses the "([^"]*)" configuration$/ - */ - public function setImboConfigHeader($config) { - $this->currentConfig = $config; - $this->addHeaderToNextRequest('X-Imbo-Test-Config', $config); - } - - /** - * @Given /^the checksum of the image is "([^"]*)"$/ - */ - public function assertImageChecksum($checksum) { - assertSame($checksum, md5((string) $this->getLastResponse()->getBody()), 'Checksum of the image in the last response did not match the expected checksum'); - } - - /** - * @Given /^I generate a short URL with the following parameters:$/ - */ - public function generateShortImageUrl(PyStringNode $params) { - $lastResponse = $this->getLastResponse(); - - preg_match('/\/users\/([^\/]+)/', $lastResponse->getInfo('url'), $matches); - $user = $matches[1]; - - $imageIdentifier = $lastResponse->json()['imageIdentifier']; - $params = array_merge(json_decode((string) $params, true), [ - 'imageIdentifier' => $imageIdentifier, - ]); - - return [ - new Given('the request body contains:', new PyStringNode(json_encode($params))), - new Given('I request "/users/' . $user . '/images/' . $imageIdentifier . '/shorturls" using HTTP "POST"'), - ]; - } - - /** - * @When /^I request the image using the generated short URL$/ - */ - public function requestImageUsingShortUrl() { - $shortUrlId = $this->getLastResponse()->json()['id']; - - return [ - new Given('the "Accept" request header is "image/*"'), - new Given('I request "/s/' . $shortUrlId . '"'), - ]; - } - - /** - * @Given /^I prime the database with "([^"]*)"$/ - */ - public function iPrimeTheDatabaseWith($fixture) { - $fixturePath = implode(DIRECTORY_SEPARATOR, [ - dirname(__DIR__), - 'fixtures', - $fixture - ]); - - if (!$fixturePath = realpath($fixturePath)) { - throw new RuntimeException('Path "' . $fixturePath . '" is invalid'); - } - - $mongo = (new MongoClient())->imbo_testing; - - $fixtures = require $fixturePath; - foreach ($fixtures as $collection => $data) { - $mongo->$collection->drop(); - - if ($data) { - $mongo->$collection->batchInsert($data); - } - } - } - - /** - * @Given /^the ACL rule under public key "([^"]*)" with ID "([^"]*)" should not exist( anymore)?$/ - */ - public function aclRuleWithIdShouldNotExist($publicKey, $aclId) { - if ($this->currentConfig) { - $this->addHeaderToNextRequest('X-Imbo-Test-Config', $this->currentConfig); - } - - $url = '/keys/' . $publicKey . '/access/' . $aclId; - return [ - new Given('I use "acl-checker" and "foobar" for public and private keys'), - new Given('I include an access token in the query'), - new Given('I request "' . $url . '" using HTTP "GET"'), - new Given('I should get a response with "404 Access rule not found"') - ]; - } - - /** - * @Given /^the "([^"]*)" public key should not exist( anymore)?$/ - */ - public function publicKeyShouldNotExist($publicKey) { - if ($this->currentConfig) { - $this->addHeaderToNextRequest('X-Imbo-Test-Config', $this->currentConfig); - } - - $url = '/keys/' . $publicKey; - return [ - new Given('I use "acl-creator" and "someprivkey" for public and private keys'), - new Given('I include an access token in the query'), - new Given('I request "' . $url . '" using HTTP "HEAD"'), - new Given('I should get a response with "404 Public key not found"') - ]; - } - - /** - * @Given /^Imbo starts with an empty database$/ - */ - public function imboStartsWithEmptyDatabase() { - $mongo = new MongoClient(); - $mongo->imbo_testing->drop(); - } - - /** - * @Given /^I use "([^"]*)" as the watermark image with "([^"]*)" as parameters$/ - */ - public function specifyAsTheWatermarkImage($watermarkPath, $parameters = '') { - $this->addImageToImbo($watermarkPath); - $imageIdentifier = $this->getPreviouslyAddedImageIdentifier(); - $params = empty($parameters) ? '' : ',' . $parameters; - $transformation = 'watermark:img=' . $imageIdentifier . $params; - - return [ - new Given('I specify "' . $transformation . '" as transformation') - ]; - } - - /** - * Get the previously added image identifier - * - * @throws RuntimeException If previous response did not include image identifier - * @return string - */ - private function getPreviouslyAddedImageIdentifier() { - $response = $this->getLastResponse()->json(); - if (!isset($response['imageIdentifier'])) { - throw new RuntimeException( - 'Image identifier was not present in previous response, response: ' . - $this->getLastResponse()->getBody(true) - ); - } - - return $response['imageIdentifier']; - } - - /** - * Get the previously added image URL - * - * @throws RuntimeException If previous response did not include image identifier - * @return string - */ - private function getPreviouslyAddedImageUrl() { - $identifier = $this->getPreviouslyAddedImageIdentifier(); - return '/users/' . $this->user . '/images/' . $identifier; - } - - /** - * Get the pixel info for given coordinates, from the image returned in the previous response - * - * @param string $coordinates - * @return array - */ - private function getImagePixelInfo($coordinates) { - $coordinates = array_map('trim', explode(',', $coordinates)); - $coordinates = array_map('intval', $coordinates); - - $imagick = new \Imagick(); - $imagick->readImageBlob((string) $this->getLastResponse()->getBody()); - - $pixel = $imagick->getImagePixelColor($coordinates[0], $coordinates[1]); - $color = $pixel->getColor(); - - $toHex = function($col) { - return str_pad(dechex($col), 2, '0', STR_PAD_LEFT); - }; - - $hexColor = $toHex($color['r']) . $toHex($color['g']) . $toHex($color['b']); - - return [ - 'color' => $hexColor, - 'alpha' => (float) $pixel->getColorValue(\Imagick::COLOR_ALPHA), - ]; - } -} diff --git a/tests/behat/bootstrap/RESTContext.php b/tests/behat/bootstrap/RESTContext.php deleted file mode 100644 index c3674ecba..000000000 --- a/tests/behat/bootstrap/RESTContext.php +++ /dev/null @@ -1,554 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file that was - * distributed with this source code. - */ - -use Behat\Behat\Context\BehatContext, - Behat\Behat\Exception\PendingException, - Behat\Behat\Event\SuiteEvent, - Behat\Gherkin\Node\PyStringNode, - Guzzle\Http\Client, - Guzzle\Http\Message\Request, - Guzzle\Http\Message\Response, - SebastianBergmann\CodeCoverage, - SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlReport; - -// Require PHPUnit assertions manually since we're using it outside of PHPUnit -require __DIR__ . '/../../../vendor/phpunit/phpunit/src/Framework/Assert/Functions.php'; - -/** - * REST context for Behat tests - * - * @author Christer Edvartsen - * @package Test suite\Functional tests - */ -class RESTContext extends BehatContext { - /** - * Guzzle client used to make requests against the httpd - * - * @var Client - */ - protected $client; - - /** - * The current response objects used by the client (populated when the request is sent) - * - * @var Response[] - */ - protected $responses = []; - - /** - * Headers for the request - * - * @var array - */ - protected $requestHeaders = []; - - /** - * Query parameters for the request - * - * @var array - */ - protected $queryParams = []; - - /** - * Optional request body to add to the request - * - * @var string - */ - protected $requestBody; - - /** - * The previously requested path - * - * @var string - */ - private $prevRequestedPath; - - /** - * The current test session id - * - * @var string - */ - private static $testSessionId; - - /** - * Parameters from the configuration - * - * @var array - */ - private $params; - - /** - * Class constructor - * - * @param array $parameters Context parameters - */ - public function __construct(array $parameters) { - $this->params = $parameters; - $this->createClient(); - } - - /** - * Returns a list of HTTP verbs that we need to do an override of in order - * to bypass limitations in the built-in PHP HTTP server. - * - * The returned list contains the verb to use override for, and what verb - * to use when overriding. For instance POST could be used when we want to - * perform a SEARCH request as a payload is expected while GET could be used - * if we want to test something using the LINK method. - */ - private function getOverrideVerbs() { - return [ - 'SEARCH' => 'POST' - ]; - } - - /** - * Create a new HTTP client - */ - private function createClient() { - $this->client = new Client($this->params['url']); - - $defaultHeaders = [ - 'X-Test-Session-Id' => self::$testSessionId, - ]; - - if ($this->params['enableCodeCoverage']) { - $defaultHeaders['X-Enable-Coverage'] = 1; - } - - $this->client->setDefaultHeaders($defaultHeaders); - } - - /** - * Start up the built in httpd in php-5.4 - * - * @BeforeSuite - */ - public static function setUp(SuiteEvent $event) { - $params = $event->getContextParameters(); - $url = parse_url($params['url']); - $port = !empty($url['port']) ? $url['port'] : 80; - - if (self::canConnectToHttpd($url['host'], $port)) { - throw new RuntimeException('Something is already running on ' . $params['url'] . '. Aborting tests.'); - } - - $pid = self::startBuiltInHttpd( - $url['host'], - $port, - $params['documentRoot'], - $params['router'], - $params['httpdLog'] - ); - - if (!$pid) { - // Could not start the httpd for some reason - throw new RuntimeException('Could not start the web server'); - } - - // Try to connect - $start = microtime(true); - $connected = false; - - while (microtime(true) - $start <= (int) $params['timeout']) { - if (self::canConnectToHttpd($url['host'], $port)) { - $connected = true; - break; - } - } - - if (!$connected) { - throw new RuntimeException( - sprintf( - 'Could not connect to the web server within the given timeframe (%d second(s))', - $params['timeout'] - ) - ); - } - - // Register a shutdown function that will automatically shut down the httpd - register_shutdown_function(function() use ($pid) { - exec('kill ' . $pid); - }); - - self::$testSessionId = uniqid('', true); - } - - /** - * Collect code coverage after the suite has been run - * - * @AfterSuite - */ - public static function tearDown(SuiteEvent $event) { - $parameters = $event->getContextParameters(); - - if ($parameters['enableCodeCoverage']) { - $client = new Client($parameters['url']); - $response = $client->get('/', [ - 'X-Enable-Coverage' => 1, - 'X-Test-Session-Id' => self::$testSessionId, - 'X-Collect-Coverage' => 1, - ])->send(); - - $data = unserialize((string) $response->getBody()); - - $filter = new CodeCoverage\Filter(); - - foreach ($parameters['whitelist'] as $dir) { - $filter->addDirectoryToWhitelist($dir); - } - - $coverage = new CodeCoverage\CodeCoverage(null, $filter); - $coverage->append($data, 'behat-suite'); - - $report = new HtmlReport(); - $report->process($coverage, $parameters['coveragePath']); - } - } - - /** - * Set method override header used to fake non-standard HTTP verbs - * - * @param string $method Override method - */ - public function setOverrideMethodHeader($method) { - $this->setRequestHeader('X-Http-Method-Override', $method); - } - - /** - * @Given /^the "([^"]*)" request header is "([^"]*)"$/ - */ - public function setRequestHeader($header, $value) { - $this->requestHeaders[$header] = $value; - } - - /** - * @Given /^I append a query string parameter, "([^"]*)" with the value "([^"]*)"$/ - */ - public function appendQueryStringParameter($queryParam, $value) { - $this->queryParams[] = $queryParam . '=' . $value; - } - - /** - * @When /^I request "([^"]*)" with the given query string$/ - */ - public function performRequestWithGivenQueryString($path) { - $this->request($path . '?' . implode('&', $this->queryParams)); - } - - /** - * @When /^I request "([^"]*)"(?: using HTTP "([^"]*)")?$/ - */ - public function request($path, $method = 'GET') { - $this->prevRequestedPath = $path; - - if (empty($this->requestHeaders['Accept'])) { - $this->requestHeaders['Accept'] = 'application/json'; - } - - // Add override method header if specified in the list of override verbs - if (array_key_exists($method, $this->getOverrideVerbs())) { - $this->setOverrideMethodHeader($method); - $method = $this->getOverrideVerbs()[$method]; - } - - $request = $this->client->createRequest($method, $path, $this->requestHeaders); - - if ($this->requestBody) { - $request->setBody($this->requestBody); - $this->requestBody = null; - } - - try { - $response = $request->send(); - } catch (Exception $e) { - $response = $e->getResponse(); - } - - $this->responses[] = $response; - } - - /** - * @Given /^make the same request using HTTP "([^"]*)"$/ - */ - public function makeSameRequest($method) { - $this->appendAccessToken(); - $this->request($this->prevRequestedPath, $method); - } - - /** - * @Then /^the following response headers should be the same:$/ - */ - public function assertEqualResponseHeaders(PyStringNode $list) { - if (count($this->responses) < 2) { - throw new \Exception('Need more than one response'); - } - - $headersToMatch = $list->getLines(); - $numResponses = count($this->responses); - - $latestResponse = $this->responses[$numResponses - 1]; - $previousResponse = $this->responses[$numResponses - 2]; - - foreach ($headersToMatch as $header) { - assertTrue($latestResponse->hasHeader($header), 'Header "' . $header . '" is missing'); - assertTrue($previousResponse->hasHeader($header), 'Header "' . $header . '" is missing'); - assertSame((string) $latestResponse->getHeader($header), (string) $previousResponse->getHeader($header), 'Header "' . $header . '" does not match'); - } - } - - /** - * @Given /^the following response headers should not be present:$/ - */ - public function assertMissingHeaders(PyStringNode $list) { - $headers = $list->getLines(); - - foreach ($headers as $header) { - assertFalse($this->getLastResponse()->hasHeader($header), 'Header "' . $header . '" should not be present'); - } - } - - /** - * @Given /^the following response headers should be present:$/ - */ - public function assertExistingHeaders(PyStringNode $list) { - $headers = $list->getLines(); - - foreach ($headers as $header) { - assertTrue($this->getLastResponse()->hasHeader($header), 'Header "' . $header . '" should be present'); - } - } - - /** - * @Given /^the response is (not )?cacheable$/ - */ - public function assertResponseIsCacheable($cacheable = true) { - if ($cacheable !== true) { - $cacheable = false; - } - - assertSame($cacheable, $this->getLastResponse()->canCache()); - } - - /** - * @Given /^the response has a max age of (\d+) seconds$/ - */ - public function assertMaxAge($seconds) { - $cacheControl = $this->getPreviousCacheControlHeader(); - $maxAge = $cacheControl->getDirective('max-age'); - - if ($maxAge === null) { - throw new \Exception('No `max-age` directive present in `cache-control`'); - } - - assertSame((int) $seconds, (int) $maxAge); - } - - /** - * @Given /^the response (does not )?(?:has|have) a must-revalidate directive$/ - */ - public function assertMustRevalidate($doesNotHave = false) { - $cacheControl = $this->getPreviousCacheControlHeader(); - - if ($doesNotHave) { - assertFalse($cacheControl->hasDirective('must-revalidate')); - } else { - assertTrue($cacheControl->hasDirective('must-revalidate')); - } - } - - /** - * @Then /^I should get a response with "([^"]*)"$/ - */ - public function assertResponseStatus($status) { - $response = $this->getLastResponse(); - $actual = $response->getStatusCode() . ' ' . $response->getReasonPhrase(); - assertSame($status, $actual, 'Expected "' . $status . '", got "' . $actual . '"'); - } - - /** - * @Given /^the "([^"]*)" response header (is|contains|matches) "(.*?)"$/ - */ - public function assertResponseHeader($header, $match, $value) { - $response = $this->getLastResponse(); - $actual = (string) $response->getHeader($header); - - if ($match === 'is') { - assertSame($value, $actual, 'Expected "' . $value . '", got "' . $actual . '"'); - } else if ($match === 'matches') { - assertRegExp('#^' . $value . '$#', $actual, $actual . ' does not match ' . $value); - } else { - assertContains($value, $actual, $actual . ' does not contain ' . $value); - } - } - - /** - * @Then /^the "([^"]*)" response header does not exist$/ - */ - public function assertHeaderDoesNotExist($header) { - $response = $this->getLastResponse(); - assertFalse($response->hasHeader($header), 'The "' . $header . '" response header should not exist'); - } - - /** - * @Given /^I attach "([^"]*)" to the request body$/ - */ - public function attachFileToRequestBody($path) { - if (!$fullPath = realpath($path)) { - throw new RuntimeException('Path "' . $path . '" is invalid'); - } - - $this->requestBody = file_get_contents($fullPath); - } - - /** - * @Given /^the request body contains:$/ - */ - public function setRequestBody(PyStringNode $body) { - $this->requestBody = (string) $body; - } - - /** - * @Given /^the response body should be empty$/ - */ - public function assertEmptyResponseBody() { - $response = $this->getLastResponse(); - assertEmpty((string) $response->getBody()); - } - - /** - * @Given /^the response body (contains|is|matches):$/ - */ - public function assertResponseBody($match, PyStringNode $expected) { - $expected = trim((string) $expected); - - $actual = trim((string) $this->getLastResponse()->getBody()); - - if ($match === 'is') { - assertSame($expected, $actual, sprintf('Expected %s, got %s', $expected, $actual)); - } else if ($match === 'matches') { - assertRegExp($expected, $actual); - } else { - assertContains($expected, $actual); - } - - } - - /** - * @Given /^the response body length is "([^"]*)"$/ - */ - public function assertResponseBodyLength($length) { - assertSame(strlen((string) $this->getLastResponse()->getBody()), (int) $length); - } - - /** - * @Then /^the "([^"]*)" response header is not the same for any of the requests$/ - */ - public function assertHeaderNotSameForPreviousRequests($header) { - $responses = array_slice($this->responses, 1); - - $headerValues = array_map(function($response) use ($header) { - return (string) $response->getHeader($header); - }, $responses); - - $totalValues = count($headerValues); - $uniqueValues = count(array_unique($headerValues)); - - assertSame($totalValues, $uniqueValues, 'Only ' . $uniqueValues . ' header(s) were unique, out of ' . $totalValues . ' total'); - } - - /** - * See if we have an httpd we can connect to - * - * @param string $host The hostname to connect to - * @param int $port The port to use - * @return boolean - */ - private static function canConnectToHttpd($host, $port) { - set_error_handler(function() { return true; }); - $sp = fsockopen($host, $port); - restore_error_handler(); - - if ($sp === false) { - return false; - } - - fclose($sp); - - return true; - } - - /** - * Start the built in httpd in php-5.4 - * - * @param string $host The hostname to use - * @param int $port The port to use - * @param string $documentRoot The document root - * @param string $router Path to an optional router - * @param string $httpdLog Path the httpd log should be written to - * @return int Returns the PID of the httpd - * @throws RuntimeException - */ - private static function startBuiltInHttpd($host, $port, $documentRoot, $router, $httpdLog) { - $logPath = dirname($httpdLog); - - if (!is_dir($logPath)) { - mkdir($logPath, 0777, true); - } - - $command = sprintf('php -S %s:%d -t %s %s >%s 2>&1 & echo $!', - $host, - $port, - $documentRoot, - $router, - $httpdLog); - - $output = []; - exec($command, $output); - - return (int) $output[0]; - } - - /** - * Get the response to the last request made by the Guzzle client - * - * @return Response - */ - protected function getLastResponse() { - return $this->responses[count($this->responses) - 1]; - } - - /** - * Add a request header for the next request - * - * @param string $key The name of the header - * @param mixed $value The value of the header - */ - protected function addHeaderToNextRequest($key, $value) { - $this->client->getEventDispatcher()->addListener('request.before_send', function($event) use ($key, $value) { - $event['request']->setHeader($key, $value); - }); - } - - /** - * Get the cache-control header for the previous response - * - * @throws Exception If no cache-control header is present in the previous response - * @return Guzzle\Http\Message\Header\CacheControl The Cache-Control header for the previous response - */ - protected function getPreviousCacheControlHeader() { - $cacheControl = $this->getLastResponse()->getHeader('Cache-Control'); - if (!$cacheControl) { - throw new \Exception('No `cache-control` header present'); - } - - return $cacheControl; - } -} diff --git a/tests/behat/features/access-control-keys.feature b/tests/behat/features/access-control-keys.feature index 03e759465..9586f7a5b 100644 --- a/tests/behat/features/access-control-keys.feature +++ b/tests/behat/features/access-control-keys.feature @@ -7,63 +7,100 @@ Feature: Imbo provides a keys endpoint Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" - Scenario Outline: Fetch access control rules for a public key + Scenario: Fetch access control rules for a public key Given I use "master-pubkey" and "master-privkey" for public and private keys - And I include an access token in the query - When I request "/keys/master-pubkey/access." - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^\[{"id":".*?","resources":\["keys\.put","keys\.head","keys\.delete","accessrule\.get","accessrule\.head","accessrule\.delete","accessrules\.get","accessrules\.head","accessrules\.post"],"users":\[]},{"id":".*?","group":"something","users":\["some-user"]}]$# | + And I include an access token in the query string + When I request "/keys/master-pubkey/access.json" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + [ + { + "id": "@regExp(/[a-z0-9]+/)", + "resources": + [ + "keys.put", + "keys.head", + "keys.delete", + "accessrule.get", + "accessrule.head", + "accessrule.delete", + "accessrules.get", + "accessrules.head", + "accessrules.post" + ], + "users": "@arrayLength(0)" + }, + { + "id": "@regExp(/[a-z0-9]+/)", + "group": "something", + "users": + [ + "some-user" + ] + } + ] + """ - Scenario Outline: Fetch access control rules for a public key that has access to all users + Scenario: Fetch access control rules for a public key that has access to all users Given I use "master-pubkey" and "master-privkey" for public and private keys - And I include an access token in the query - When I request "/keys/wildcarded/access." - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^\[{"id":".*?","group":"user-stats","users":"\*"}\]$# | + And I include an access token in the query string + When I request "/keys/wildcarded/access.json" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + [ + { + "id": "@regExp(/[a-z0-9]+/)", + "group": "user-stats", + "users": "*" + } + ] + """ - Scenario Outline: Fetch access control rules with expanded groups + Scenario: Fetch access control rules with expanded groups Given I use "master-pubkey" and "master-privkey" for public and private keys - And I include an access token in the query - When I request "/keys/group-based/access.?expandGroups=1" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^\[{"id":".*?","group":"user-stats","users":\["user1"\],"resources":\["user\.get","user\.head"]}]$# | + Given I include an access token in the query string + When I request "/keys/group-based/access.json?expandGroups=1" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + [ + { + "id": "@regExp(/[a-z0-9]+/)", + "group": "user-stats", + "users": + [ + "user1" + ], + "resources": + [ + "user.get", + "user.head" + ] + } + ] + """ Scenario: Create a public key Given I use "master-pubkey" and "master-privkey" for public and private keys - And the request body contains: - """ - {"privateKey":"the-private-key"} - """ And I sign the request + And the request body is: + """ + {"privateKey":"the-private-key"} + """ When I request "/keys/the-public-key" using HTTP "PUT" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" Scenario Outline: Check if a public key exist Given I use "acl-creator" and "someprivkey" for public and private keys - And I include an access token in the query + Given I include an access token in the query string When I request "/keys/" using HTTP "HEAD" - Then I should get a response with "" + Then the response status line is "" + Examples: | pubkey | status | | foobar | 200 OK | @@ -71,127 +108,137 @@ Feature: Imbo provides a keys endpoint Scenario: Update the private key for an existing public key Given I use "master-pubkey" and "master-privkey" for public and private keys - And the request body contains: - """ - {"privateKey":"new-private-key"} - """ And I sign the request + And the request body is: + """ + {"privateKey":"new-private-key"} + """ When I request "/keys/master-pubkey" using HTTP "PUT" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Create new public key without having access to the keys resource Given I use "foobar" and "barfoo" for public and private keys - And the request body contains: - """ - {"privateKey":"moo"} - """ And I sign the request + And the request body is: + """ + {"privateKey":"moo"} + """ When I request "/keys/some-new-pubkey" using HTTP "PUT" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario Outline: Add access rules for a public key Given I use "master-pubkey" and "master-privkey" for public and private keys - And the request body contains: - """ - - """ And I sign the request + And the request body is: + """ + + """ When I request "/keys/foobar/access" using HTTP "POST" - Then I should get a response with "" + Then the response status line is "" + Examples: - | body | status | - | | 400 No access rule data provided | - | {} | 400 Neither group nor resources found in rule | - | {"group":"existing-group"} | 400 Users not specified in rule | - | {"group":"non-existant-group"} | 400 Group 'non-existant-group' does not exist | - | {"resources":"images.get"} | 400 Illegal value in resources array. String array expected | - | {"resources":[123]} | 400 Illegal value in resources array. String array expected | - | {"resources":["images.get"]} | 400 Users not specified in rule | - | {"resources":["images.get"],"users":"foobar"} | 400 Illegal value for users property. Allowed: '*' or array with users | - | {"foo":"bar","bar":"foo"} | 400 Found unknown properties in rule: [foo, bar] | - | [{"resources":["images.get"],"users":["user1"]}] | 200 OK | - | {"group":"existing-group","users":["user1", "user5"]} | 200 OK | + | body | status | + | | 400 No access rule data provided | + | {} | 400 Neither group nor resources found in rule | + | {"group":"existing-group"} | 400 Users not specified in rule | + | {"group":"non-existant-group"} | 400 Group 'non-existant-group' does not exist | + | {"resources":"images.get"} | 400 Illegal value in resources array. String array expected | + | {"resources":[123]} | 400 Illegal value in resources array. String array expected | + | {"resources":["images.get"]} | 400 Users not specified in rule | + | {"resources":["images.get"],"users":"foobar"} | 400 Illegal value for users property. Allowed: '*' or array with users | + | {"foo":"bar","bar":"foo"} | 400 Found unknown properties in rule: [foo, bar] | + | [{"resources":["images.get"],"users":["user1"]}] | 200 OK | + | {"group":"existing-group","users":["user1", "user5"]} | 200 OK | Scenario: Get an access control rule - And I use "master-pubkey" and "master-privkey" for public and private keys - And I include an access token in the query + Given I use "master-pubkey" and "master-privkey" for public and private keys + And I include an access token in the query string When I request "/keys/foobar/access/100000000000000000001337" using HTTP "GET" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Delete an access control rule - And I use "master-pubkey" and "master-privkey" for public and private keys + Given I use "master-pubkey" and "master-privkey" for public and private keys And I sign the request When I request "/keys/foobar/access/100000000000000000001337" using HTTP "DELETE" - Then I should get a response with "200 OK" - And the ACL rule under public key "foobar" with ID "100000000000000000001337" should not exist + Then the response status line is "200 OK" + And the ACL rule under public key "foobar" with ID "100000000000000000001337" no longer exists Scenario: Delete a public key Given I use "master-pubkey" and "master-privkey" for public and private keys And I sign the request When I request "/keys/foobar" using HTTP "DELETE" - Then I should get a response with "200 OK" - And the "foobar" public key should not exist + Then the response status line is "200 OK" + And the "foobar" public key no longer exists Scenario Outline: The keys resource supports PUT, HEAD and DELETE only Given I use "master-pubkey" and "master-privkey" for public and private keys And I authenticate using "" - And the request body contains: - """ - - """ + And the request body is: + """ + + """ When I request "/keys/foobar" using HTTP "" - Then I should get a response with "" + Then the response status line is "" Examples: - | method | auth-method | body | status | - | GET | access-token | | 405 Method not allowed | - | HEAD | access-token | | 200 OK | - | POST | signature | | 405 Method not allowed | - | PUT | signature | {"privateKey":"the-private-key"} | 200 OK | - | DELETE | signature | | 200 OK | + | method | auth-method | body | status | + | GET | access-token | | 405 Method not allowed | + | HEAD | access-token | | 200 OK | + | POST | signature | | 405 Method not allowed | + | PUT | signature | {"privateKey":"the-private-key"} | 200 OK | + | DELETE | signature | | 200 OK | + | POST | signature (headers) | | 405 Method not allowed | + | PUT | signature (headers) | {"privateKey":"the-private-key"} | 200 OK | + | DELETE | signature (headers) | | 200 OK | Scenario Outline: The access rules resource supports GET, HEAD and POST only Given I use "master-pubkey" and "master-privkey" for public and private keys And I authenticate using "" - And the request body contains: - """ - - """ + And the request body is: + """ + + """ When I request "/keys/master-pubkey/access" using HTTP "" - Then I should get a response with "" + Then the response status line is "" Examples: - | method | auth-method | body | status | - | GET | access-token | | 200 OK | - | HEAD | access-token | | 200 OK | - | POST | signature | [{"resources":["images.get"],"users":["user1"]}] | 200 OK | - | PUT | signature | | 405 Method not allowed | - | DELETE | signature | | 405 Method not allowed | + | method | auth-method | body | status | + | GET | access-token | | 200 OK | + | HEAD | access-token | | 200 OK | + | POST | signature | [{"resources":["images.get"],"users":["user1"]}] | 200 OK | + | PUT | signature | | 405 Method not allowed | + | DELETE | signature | | 405 Method not allowed | + | POST | signature (headers) | [{"resources":["images.get"],"users":["user1"]}] | 200 OK | + | PUT | signature (headers) | | 405 Method not allowed | + | DELETE | signature (headers) | | 405 Method not allowed | Scenario Outline: The access rule resource supports GET, HEAD and POST only Given I use "master-pubkey" and "master-privkey" for public and private keys And I authenticate using "" When I request "/keys/foobar/access/100000000000000000001337" using HTTP "" - Then I should get a response with "" + Then the response status line is "" Examples: - | method | auth-method | body | status | - | GET | access-token | | 200 OK | - | HEAD | access-token | | 200 OK | - | POST | signature | [{"resources":[],"users":[]}] | 405 Method not allowed | - | PUT | signature | | 405 Method not allowed | - | DELETE | signature | | 200 OK | + | method | auth-method | body | status | + | GET | access-token | | 200 OK | + | HEAD | access-token | | 200 OK | + | POST | signature | [{"resources":[],"users":[]}] | 405 Method not allowed | + | PUT | signature | | 405 Method not allowed | + | DELETE | signature | | 200 OK | + | POST | signature (headers) | [{"resources":[],"users":[]}] | 405 Method not allowed | + | PUT | signature (headers) | | 405 Method not allowed | + | DELETE | signature (headers) | | 200 OK | Scenario Outline: Operations on an immutable access control provider Given Imbo uses the "access-control.php" configuration And I use "valid-pubkey" and "foobar" for public and private keys And I authenticate using "" - And the request body contains: - """ - - """ + And the request body is: + """ + + """ When I request "" using HTTP "" - Then I should get a response with "" + Then the response status line is "" Examples: | uri | method | auth-method | body | status | diff --git a/tests/behat/features/access-control-mutable.feature b/tests/behat/features/access-control-mutable.feature index d21d0a812..145487b44 100644 --- a/tests/behat/features/access-control-mutable.feature +++ b/tests/behat/features/access-control-mutable.feature @@ -9,24 +9,24 @@ Feature: Imbo features access control backed by a MongoDB database Scenario: Request an access-controlled resource under an unknown user When I request "/users/user1337.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request an access-controlled resource with no public key specified When I request "/users/foobar.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request an access-controlled resource with invalid public key specified Given I use "invalid" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/user1.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario Outline: Request an access-controlled resource with public key that does not have access to the user Given I use "foobar" and "barfoo" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "" - Then I should get a response with "" + Then the response status line is "" And the Imbo error message is "" and the error code is "" Examples: @@ -36,30 +36,30 @@ Feature: Imbo features access control backed by a MongoDB database Scenario: Request an access-controlled resource with valid public key specified Given I use "foobar" and "barfoo" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/barfoo/images.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request an access-controlled resource with group that does not contain the resource Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/user/images/9c554794f784778cd436064faa2ea24a" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request user information when access is granted through a group Given I use "group-based" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/user1" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request user information when resource is granted but not for the given user Given I use "group-based" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/other-user" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request user information when access is granted through a wildcard Given I use "wildcarded" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/users/random-user" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" diff --git a/tests/behat/features/access-control.feature b/tests/behat/features/access-control.feature index fb8d6d832..1fb9475bb 100644 --- a/tests/behat/features/access-control.feature +++ b/tests/behat/features/access-control.feature @@ -6,103 +6,107 @@ Feature: Imbo provides a way to access control resources on a per-public key bas Scenario: Request a resource under an unknown user Given Imbo uses the "access-control.php" configuration When I request "/users/user1337.json" - Then I should get a response with "400 Permission denied (public key)" + And the response status line is "400 Permission denied (public key)" Scenario: Request a resource with no public key specified Given Imbo uses the "access-control.php" configuration When I request "/users/user1.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request a resource with invalid public key specified Given I use "invalid" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/user1.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario: Request a resource with public key that does not have access to the user Given I use "valid-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/user2.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario: Request a resource with valid public key specified Given I use "valid-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/user1.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request a resource with a public key that has access to all users and the resource Given I use "valid-pubkey-with-wildcard" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/some-user.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request a resource with a public key that has access to all users but not requested resource Given I use "valid-pubkey-with-wildcard" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/some-user/images.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario: Request a resource with a public key that uses a resource group Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/user/images.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request a resource with group that does not contain the resource Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/users/user/images/9c554794f784778cd436064faa2ea24a" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request custom access-controlled resource with insufficient privileges Given I use "public" and "private" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/foobar" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request custom access-controlled resource that a different public key has access to - Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query + Given I use "valid-group-pubkey" and "private" for public and private keys + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/foobar" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario: Request custom access-controlled resource with sufficient privileges specified using wildcard - Given I use "valid-pubkey-with-wildcard" and "foobar" for public and private keys - And I include an access token in the query + Given I use "valid-pubkey-with-wildcard" and "private" for public and private keys + And I include an access token in the query string And Imbo uses the "access-control.php" configuration When I request "/foobar" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" + And the response body is: + """ + {"foo":[1,2,3]} + """ Scenario: Request user information when Imbo uses an alternative access control adapter Given I use "public" and "private" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "custom-access-control.php" configuration When I request "/users/public" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario Outline: Request open resources with default configuration Given the "Accept" request header is "application/json" When I request "" - Then I should get a response with "" - And the response body matches: + Then the response status line is "" + And the response body contains JSON: """ """ Examples: | url | status | response | - | / | 200 Hell Yeah | #^{.*}$# | - | /status.json | 200 OK | #^{"date":".*?","database":true,"storage":true}$# | + | / | 200 Hell Yeah | {"site": "http://imbo.io"} | + | /status.json | 200 OK | {"date": "@isDate()", "database": true, "storage": true} | diff --git a/tests/behat/features/access-token.feature b/tests/behat/features/access-token.feature index 24748da1e..67c955cc1 100644 --- a/tests/behat/features/access-token.feature +++ b/tests/behat/features/access-token.feature @@ -4,65 +4,62 @@ Feature: Imbo requires an access token for read operations I must specify an access token in the URI Background: - Given "tests/phpunit/Fixtures/image.png" exists in Imbo + Given "tests/phpunit/Fixtures/image.png" exists for user "user" Scenario: Request user information using the correct private key - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request user information using the wrong private key - Given I use "publickey" and "foobar" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "foobar" for public and private keys + And I include an access token in the query string When I request "/users/user" - Then I should get a response with "400 Incorrect access token" + Then the response status line is "400 Incorrect access token" And the Imbo error message is "Incorrect access token" and the error code is "0" Scenario: Request user information using a correct read-only private key Given I use "ro-pubkey" and "read-only-key" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "ro-rw-auth.php" configuration When I request "/users/someuser" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Request user information without a valid access token - Given I use "publickey" and "foobar" for public and private keys - When I request "/users/user?publicKey=publickey" - Then I should get a response with "400 Missing access token" + When I request "/users/user?publicKey=publicKey" + Then the response status line is "400 Missing access token" And the Imbo error message is "Missing access token" and the error code is "0" Scenario: Request image using no access token - Given I use "publickey" and "privatekey" for public and private keys - And the "Accept" request header is "*/*" - When I request "/users/user/images" - Then I should get a response with "400 Missing access token" + Given the "Accept" request header is "*/*" + When I request "/users/user/images?publicKey=publicKey" + Then the response status line is "400 Missing access token" Scenario: Can request a whitelisted transformation without access tokens - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And the "Accept" request header is "*/*" And Imbo uses the "access-token-whitelist-transformation.php" configuration - When I request the previously added image with the query string "?t[]=whitelisted" - Then I should get a response with "200 OK" - And the width of the image is "100" - And the height of the image is "50" + And I specify "whitelisted" as transformation + When I request the previously added image + Then the response status line is "200 OK" + Then the image dimension is "100x50" Scenario: Can not issue transformations that are not whitelisted without a valid access token - Given I use "publickey" and "privatekey" for public and private keys - And the "Accept" request header is "*/*" - When I request "/users/user/images/929db9c5fc3099f7576f5655207eba47?t[]=thumbnail" - Then I should get a response with "400 Missing access token" + Given the "Accept" request header is "*/*" + When I request "/users/user/images/929db9c5fc3099f7576f5655207eba47?publicKey=publicKey&t[]=thumbnail" + Then the response status line is "400 Missing access token" Scenario: Request user information using the correct private key and a superfluous public key query parameter - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user?publicKey=publickey" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user?publicKey=publicKeyy" + Then the response status line is "200 OK" Scenario: Request user information for a user with an incorrect public key specified as query parameter Given I use "rw-pubkey" and "read-only-key" for public and private keys - And I include an access token in the query + And I include an access token in the query string And Imbo uses the "ro-rw-auth.php" configuration When I request "/users/someuser?publicKey=rw-pubkey" - Then I should get a response with "400 Incorrect access token" + Then the response status line is "400 Incorrect access token" And the Imbo error message is "Incorrect access token" and the error code is "0" diff --git a/tests/behat/features/authenticate-write-operations.feature b/tests/behat/features/authenticate-write-operations.feature index e58b40d37..381b0ac5d 100644 --- a/tests/behat/features/authenticate-write-operations.feature +++ b/tests/behat/features/authenticate-write-operations.feature @@ -4,72 +4,94 @@ Feature: Imbo requires write operations to be signed I can specify a signature and timestamp as request headers or as query parameters Scenario: Authenticate using request headers - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request using HTTP headers - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" + And the response body contains JSON: + """ + { + "imageIdentifier": "@regExp(/[a-z0-9]+/i)", + "width": 599, + "height": 417, + "extension": "png" + } + """ Scenario: Authenticate using query parameters - Given I use "publickey" and "privatekey" for public and private keys - And "tests/phpunit/Fixtures/image.png" exists in Imbo + Given "tests/phpunit/Fixtures/image.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys And I sign the request When I request the previously added image using HTTP "DELETE" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" + And the response body contains JSON: + """ + {"imageIdentifier": "@regExp(/[a-z0-9]+/i)"} + """ Scenario: Add an image with no authentication information - Given I use "publickey" and "privatekey" for public and private keys - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + Given I use "publicKey" and "privateKey" for public and private keys + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "400 Missing authentication timestamp" + Then the response status line is "400 Missing authentication timestamp" And the Imbo error message is "Missing authentication timestamp" and the error code is "101" Scenario: Add an image with an invalid timestamp - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And the "X-Imbo-Authenticate-Timestamp" request header is "foobar" - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "400 Invalid timestamp: foobar" + Then the response status line is "400 Invalid timestamp: foobar" And the Imbo error message is "Invalid timestamp: foobar" and the error code is "102" Scenario: Add an image with an expired timestamp - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And the "X-Imbo-Authenticate-Timestamp" request header is "2010-02-03T01:02:03Z" - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "400 Timestamp has expired: 2010-02-03T01:02:03Z" + Then the response status line is "400 Timestamp has expired: 2010-02-03T01:02:03Z" And the Imbo error message is "Timestamp has expired: 2010-02-03T01:02:03Z" and the error code is "104" Scenario: Add an image with a missing signature - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And the "X-Imbo-Authenticate-Timestamp" request header is "current-timestamp" - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "400 Missing authentication signature" + Then the response status line is "400 Missing authentication signature" And the Imbo error message is "Missing authentication signature" and the error code is "101" Scenario: Add an image with an incorrect signature - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And the "X-Imbo-Authenticate-Timestamp" request header is "current-timestamp" And the "X-Imbo-Authenticate-Signature" request header is "foobar" - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "400 Signature mismatch" + Then the response status line is "400 Signature mismatch" And the Imbo error message is "Signature mismatch" and the error code is "103" Scenario: Authenticate a write operation with a read-only private key Given Imbo uses the "ro-rw-auth.php" configuration And I use "ro-pubkey" and "read-only-key" for public and private keys And I sign the request using HTTP headers - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/someuser/images" using HTTP "POST" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario: Authenticate using a read+write private key Given Imbo uses the "ro-rw-auth.php" configuration And I use "rw-pubkey" and "read+write-key" for public and private keys And I sign the request using HTTP headers - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/someuser/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" + And the response body contains JSON: + """ + { + "imageIdentifier": "@regExp(/[a-z0-9]+/i)", + "width": 599, + "height": 417, + "extension": "png" + } + """ diff --git a/tests/behat/features/autorotate-event-listener.feature b/tests/behat/features/autorotate-event-listener.feature index ebbbc5176..25c5eb4ba 100644 --- a/tests/behat/features/autorotate-event-listener.feature +++ b/tests/behat/features/autorotate-event-listener.feature @@ -5,13 +5,12 @@ Feature: Imbo provides an event listener for auto rotating images based on EXIF- Background: Given Imbo uses the "auto-rotate-added-images.php" configuration - And "tests/phpunit/Fixtures/640x160_rotated.jpg" exists in Imbo + And "tests/phpunit/Fixtures/640x160_rotated.jpg" exists for user "user" Scenario: Fetch the auto rotated image - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the added image as a "png" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request the previously added image as a "png" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/png" - And the width of the image is "640" - And the height of the image is "160" + And the image dimension is "640x160" diff --git a/tests/behat/features/bootstrap/FeatureContext.php b/tests/behat/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..1457ae52f --- /dev/null +++ b/tests/behat/features/bootstrap/FeatureContext.php @@ -0,0 +1,1787 @@ + + * + * For the full copyright and license information, please view the LICENSE file that was + * distributed with this source code. + */ + +use Imbo\BehatApiExtension\Context\ApiContext; +use Imbo\BehatApiExtension\ArrayContainsComparator; +use Imbo\BehatApiExtension\Exception\AssertionFailedException; +use Behat\Behat\Hook\Scope\BeforeScenarioScope; +use Behat\Gherkin\Node\PyStringNode; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Middleware; +use Psr\Http\Message\RequestInterface; +use Behat\Gherkin\Node\TableNode; +use Assert\Assertion; +use Micheh\Cache\CacheUtil; + +/** + * Imbo Context + * + * @author Christer Edvartsen + * @package Test suite\Functional tests + */ +class FeatureContext extends ApiContext { + /** + * Names for middlewares + * + * @var string + */ + const MIDDLEWARE_SIGN_REQUEST = 'imbo-behat-sign-request'; + const MIDDLEWARE_APPEND_ACCESS_TOKEN = 'imbo-behat-append-access-token'; + const MIDDLEWARE_HISTORY = 'imbo-behat-history'; + + /** + * @var CacheUtil + */ + private $cacheUtil; + + /** + * The public key used by the client + * + * @var string + */ + private $publicKey; + + /** + * The private key used by the client + * + * @var string + */ + private $privateKey; + + /** + * An array of urls for added images, keyed by local file path + * + * @var array + */ + private $imageUrls = []; + + /** + * An array of image identifiers for added images, keyed by local file path + * + * @var array + */ + private $imageIdentifiers = []; + + /** + * Array container for the history middleware + * + * @param array + */ + private $history = []; + + /** + * Keys for the users + * + * @var array + */ + private $keys = [ + 'user' => [ + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', + ], + 'other-user' => [ + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', + ], + ]; + + /** + * Whether or not the access token handler is currently active + * + * @var boolean + */ + private $accessTokenHandlerIsActive = false; + + /** + * Whether or not the authentication handler is currently active + * + * @var boolean + */ + private $authenticationHandlerIsActive = false; + + /** + * Class constructor + * + * @param CacheUtil $cacheUtil + */ + public function __construct(CacheUtil $cacheUtil = null) { + // @codeCoverageIgnoreStart + if ($cacheUtil === null) { + $cacheUtil = new CacheUtil(); + } + // @codeCoverageIgnoreEnd + + $this->cacheUtil = $cacheUtil; + } + + /** + * Manipulate the handler stack of the client for all tests + * + * - Add the history middleware to record all request / responses in the $this->history array + * + * {@inheritdoc} + */ + public function setClient(ClientInterface $client) { + $client->getConfig('handler')->push( + Middleware::history($this->history), + self::MIDDLEWARE_HISTORY + ); + + return parent::setClient($client); + } + + /** + * Add custom functions to the comparator + * + * The following functions are added and can be used with the + * `Then the response body contains JSON:` step: + * + * - @isDate(): Check if a field that is supposed to represent a date is property formatted + * + * {@inheritdoc} + */ + public function setArrayContainsComparator(ArrayContainsComparator $comparator) { + $comparator->addFunction('isDate', [$this, 'isDate']); + + return parent::setArrayContainsComparator($comparator); + } + + /** + * {@inheritdoc} + */ + public function setRequestHeader($header, $value) { + if ($value === 'current-timestamp') { + $value = gmdate('Y-m-d\TH:i:s\Z'); + } + + return parent::setRequestHeader($header, $value); + } + + /** + * Function for the array contains comparator to validate a date string + * + * Validates the following date format: + * + * 'D, d M Y H:i:s GMT' + * + * @param string $date The string to validate + * @throws InvalidArgumentException + * @return void + */ + public function isDate($date) { + if (!preg_match('/^[A-Z][a-z]{2}, [\d]{2} [A-Z][a-z]{2} [\d]{4} [\d]{2}:[\d]{2}:[\d]{2} GMT$/', $date)) { + throw new InvalidArgumentException(sprintf( + 'Date is not properly formatted: "%s".', + $date + )); + } + } + + /** + * Drop mongo test collection which stores information regarding images, and the images + * themselves + * + * @param BeforeScenarioScope $scope + * + * @BeforeScenario + * @codeCoverageIgnore + */ + public static function prepare(BeforeScenarioScope $scope) { + $mongo = new MongoClient(); + $mongo->imbo_testing->drop(); + + $cachePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'imbo-behat-image-transformation-cache'; + + if (is_dir($cachePath)) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($cachePath), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $file) { + $name = $file->getPathname(); + + if (substr($name, -1) === '.') { + continue; + } + + if ($file->isDir()) { + // Remove dir + rmdir($name); + } else { + // Remove file + unlink($name); + } + } + + // Remove the directory itself + rmdir($cachePath); + } + } + + /** + * Set a request header that will have Imbo load a custom configuration file + * + * @param string $configFile Custom configuration file to use for the next request (file must + * reside in the tests/behat/imbo-configs directory) + * @throws InvalidArgumentException + * @return self + * + * @Given Imbo uses the :configFile configuration + */ + public function setImboConfigHeader($configFile) { + $dir = __DIR__ . '/../../imbo-configs'; + + if (!is_file($dir . '/' . $configFile)) { + throw new InvalidArgumentException(sprintf( + 'Configuration file "%s" does not exist in the "%s" directory.', + $configFile, + $dir + )); + } + + return $this->setRequestHeader('X-Imbo-Test-Config-File', $configFile); + } + + /** + * Allow IPs to view stats by specifying a net mask + * + * This feature is implemented in the stats-access-and-custom-stats.php custom configuration + * file. + * + * @param string $mask Specify which subnet mask who are allowed to view stats + * @return self + * + * @Given the stats are allowed by :mask + */ + public function statsAllowedBy($mask) { + return $this->setRequestHeader('X-Imbo-Stats-Allowed-By', $mask); + } + + /** + * Send a request header with the next request, informing Imbo which adapter to take down + * + * This feature is implemented in the status.php custom configuration file. + * + * @param string $adapter Which adapter to take down + * @throws InvalidArgumentException + * @return self + * + * @Given /^the (storage|database) is down$/ + */ + public function forceAdapterFailure($adapter) { + if (!in_array($adapter, ['storage', 'database'])) { + throw new InvalidArgumentException(sprintf('Invalid adapter: "%s".', $adapter)); + } + + if ($adapter === 'storage') { + $header = 'X-Imbo-Status-Storage-Failure'; + } else { + $header = 'X-Imbo-Status-Database-Failure'; + } + + return $this->setRequestHeader($header, 1); + } + + /** + * Sign the request using HTTP headers + * + * @return self + * + * @Given I sign the request using HTTP headers + */ + public function signRequestUsingHttpHeaders() { + return $this->signRequest(true); + } + + /** + * Sign the request + * + * This step adds a "sign-request" middleware to the request. The middleware should be executed + * last. + * + * @param boolean $useHeaders Whether or not to put the signature in the request HTTP headers + * @return self + * + * @Given I sign the request + */ + public function signRequest($useHeaders = false) { + if ($this->authenticationHandlerIsActive) { + throw new RuntimeException( + 'The authentication handler is currently added to the stack. It can not be added more than once.' + ); + } else if ($this->accessTokenHandlerIsActive) { + throw new RuntimeException( + 'The access token handler is currently added to the stack. These handlers should not be added to the same request.' + ); + } + + // Set the token handler as active + $this->authenticationHandlerIsActive = true; + + $useHeaders = (boolean) $useHeaders; + + // Fetch the handler stack and push a signature function to it + $stack = $this->client->getConfig('handler'); + $stack->unshift(Middleware::mapRequest(function(RequestInterface $request) use ($useHeaders, $stack) { + // Add public key as a query parameter if we're told not to use headers. We do this + // before the signing below since this parameter needs to be a part of the data that + // will be used for signing + if (!$useHeaders) { + $request = $request->withUri(Uri::withQueryValue( + $request->getUri(), + 'publicKey', + $this->publicKey + )); + } + + // Fetch the HTTP method + $httpMethod = $request->getHeaderLine('X-Http-Method-Override') ?: $request->getMethod(); + + // Prepare the data that will be signed using the private key + $timestamp = gmdate('Y-m-d\TH:i:s\Z'); + $data = sprintf('%s|%s|%s|%s', + $httpMethod, + urldecode((string) $request->getUri()), + $this->publicKey, + $timestamp + ); + + // Generate signature + $signature = hash_hmac('sha256', $data, $this->privateKey); + + if ($useHeaders) { + $request = $request + ->withHeader('X-Imbo-PublicKey', $this->publicKey) + ->withHeader('X-Imbo-Authenticate-Signature', $signature) + ->withHeader('X-Imbo-Authenticate-Timestamp', $timestamp); + } else { + $request = $request->withUri( + Uri::withQueryValue( + Uri::withQueryValue( + $request->getUri(), + 'signature', + $signature + ), + 'timestamp', + $timestamp + ) + ); + } + + // Remove this middleware as we don't want the signing to happen more than once + $this->authenticationHandlerIsActive = false; + $stack->remove(self::MIDDLEWARE_SIGN_REQUEST); + + return $request; + }), self::MIDDLEWARE_SIGN_REQUEST); + + return $this; + } + + /** + * Append an access token as a query parameter + * + * @param boolean $allRequests Whether or not to keep the handler for all requests or not + * @throws RuntimeException Method can not be called if the handler is still active + * @return self + * + * @Given /^I include an access token in the query string( for all requests)?$/ + */ + public function appendAccessToken($allRequests = false) { + if ($this->accessTokenHandlerIsActive) { + throw new RuntimeException( + 'The access token handler is currently added to the stack. It can not be added more than once.' + ); + } else if ($this->authenticationHandlerIsActive) { + throw new RuntimeException( + 'The authentication handler is currently added to the stack. These handlers should not be added to the same request.' + ); + } + + // Set the token handler as active + $this->accessTokenHandlerIsActive = true; + + // Fetch the handler stack and push an access token function to it + $stack = $this->client->getConfig('handler'); + $stack->unshift(Middleware::mapRequest(function(RequestInterface $request) use ($stack, $allRequests) { + $uri = $request->getUri(); + + // Set the public key and remove a possible accessToken query parameter + $uri = Uri::withQueryValue($uri, 'publicKey', $this->publicKey); + $uri = Uri::withoutQueryValue($uri, 'accessToken'); + + // Generate the access token and append to the query + $accessToken = hash_hmac('sha256', urldecode((string) $uri), $this->privateKey); + $uri = Uri::withQueryValue($uri, 'accessToken', $accessToken); + + // Remove the middleware from the stack unless we want to keep adding the token + if (!$allRequests) { + // Deactivate the handler + $this->accessTokenHandlerIsActive = false; + $stack->remove(self::MIDDLEWARE_APPEND_ACCESS_TOKEN); + } + + // Return Uri with query string including the access token + return $request->withUri($uri); + }), self::MIDDLEWARE_APPEND_ACCESS_TOKEN); + + return $this; + } + + /** + * Add an image to Imbo for a given user + * + * This is a convenience step mostly used for backgrounds in tests. It combines a few other + * steps: + * + * - add an image to the request + * - sign the request + * - issue a POST + * + * The users, public keys and private keys are specified in the test configuration, and the + * map of keys exist in $this->keys. + * + * Since this method might be executed in between other steps we will not have a fresh instance + * of the client after this step is finished, so we need to clean up after we're done by + * resetting the request and request options. + * + * @param string $imagePath Path to the image, relative to the project root path + * @param string $user The user who will own the image + * @param PyStringNode $metadata Metadata to add to the image + * @throws InvalidArgumentException Throws an exception if the user specified does not have a + * set of keys. + * @return self + * + * @Given :imagePath exists for user :user + * @Given :imagePath exists for user :user with the following metadata: + */ + public function addUserImageToImbo($imagePath, $user, PyStringNode $metadata = null) { + if (!file_exists($imagePath)) { + throw new InvalidArgumentException(sprintf('File does not exist: "%s".', $imagePath)); + } + + // See if the user specified has a set of keys + if (!isset($this->keys[$user])) { + throw new InvalidArgumentException(sprintf('No keys exist for user "%s".', $user)); + } + + // Store the original request + $originalRequest = clone $this->request; + $originalRequestOptions = $this->requestOptions; + $existingPublicKey = $this->publicKey; + $existingPrivateKey = $this->privateKey; + + $this + // Attach the file to the request body + ->setRequestBody(fopen($imagePath, 'r')) + + // Sign the request + ->setPublicAndPrivateKey($this->keys[$user]['publicKey'], $this->keys[$user]['privateKey']) + ->signRequest() + + // Request the endpoint for adding the image + ->requestPath(sprintf('/users/%s/images', $user), 'POST'); + + // Store the mapping of path => image identifier and the image data + $responseBody = json_decode((string) $this->response->getBody(), true); + + if (empty($responseBody['imageIdentifier'])) { + throw new RuntimeException(sprintf( + 'Image was not successfully added. Response body: %s', + print_r($responseBody, true) + )); + } + + $imageIdentifier = $responseBody['imageIdentifier']; + $this->imageIdentifiers[$imagePath] = $imageIdentifier; + $this->imageUrls[$imagePath] = sprintf('/users/%s/images/%s', $user, $imageIdentifier); + + // Attach metadata + if ($metadata !== null) { + $this + // Attach the file to the request body + ->setRequestBody((string) $metadata) + + // Sign the request + ->setPublicAndPrivateKey($this->keys[$user]['publicKey'], $this->keys[$user]['privateKey']) + ->signRequest() + + // Request the endpoint for adding the image + ->requestPath(sprintf('/users/%s/images/%s/metadata', $user, $imageIdentifier), 'POST'); + } + + // Reset the request / response + $this->setPublicAndPrivateKey($existingPublicKey, $existingPrivateKey); + $this->request = $originalRequest; + $this->requestOptions = $originalRequestOptions; + $this->response = null; + + return $this; + } + + /** + * Set a request header that is picked up by the "stats-access-and-custom-stats.php" custom + * configuration file to test the access part of the event listener + * + * @param string $ip The IP address to set + * @return self + * + * @Given the client IP is :ip + */ + public function setClientIp($ip) { + return $this->setRequestHeader('X-Client-Ip', $ip); + } + + /** + * Add a transformation to the query parameter for the next request + * + * @param string $transformation The value of the transformation, for instance "border" + * @return self + * + * @Given I specify :transformation as transformation + */ + public function applyTransformation($transformation) { + return $this->setRequestQueryParameter('t[]', $transformation); + } + + /** + * Add one or more transformations to the query parameter for the next request + * + * @param PyStringNode $transformations + * @return self + * + * @Given I specify the following transformations: + */ + public function applyTransformations(PyStringNode $transformations) { + foreach (explode("\n", (string) $transformations) as $transformation) { + $this->applyTransformation(trim($transformation)); + } + + return $this; + } + + /** + * Prive the database with content from a PHP script + * + * @param string $fixture Name of a PHP file in the tests/behat/fixtures directory that returns + * an array. + * @throws InvalidArgumentException Throws an exception if $fixture does not exist, or if it + * does not return an array. + * @return self + * + * @codeCoverageIgnore + * @Given I prime the database with :fixture + */ + public function primeDatabase($fixture) { + $fixtureDir = realpath(implode(DIRECTORY_SEPARATOR, [ + dirname(dirname(__DIR__)), + 'fixtures' + ])); + $fixturePath = $fixtureDir . DIRECTORY_SEPARATOR . $fixture; + + if (!is_file($fixturePath)) { + throw new InvalidArgumentException(sprintf( + 'Fixture file "%s" does not exist in "%s".', + $fixture, + $fixtureDir + )); + } + + $mongo = (new MongoClient())->imbo_testing; + + $fixtures = require $fixturePath; + + if (!is_array($fixtures)) { + throw new InvalidArgumentException(sprintf( + 'Fixture "%s" does not return an array.', + $fixturePath + )); + } + + foreach ($fixtures as $collection => $data) { + $mongo->$collection->drop(); + + if ($data) { + $mongo->$collection->batchInsert($data); + } + } + } + + /** + * Authenticate the request using some authentication method + * + * @param string $method The method of authentication + * @throws InvalidArgumentException Throws an exception if an invalid method is used + * @return self + * + * @Given I authenticate using :method + */ + public function authenticateRequest($method) { + if ($method === 'access-token') { + return $this->appendAccessToken(); + } else if ($method === 'signature') { + return $this->signRequest(); + } else if ($method === 'signature (headers)') { + return $this->signRequestUsingHttpHeaders(); + } + + throw new InvalidArgumentException(sprintf( + 'Invalid authentication method: "%s".', + $method + )); + } + + /** + * Set the public and private keys to be used for signing the request / generating the access + * token + * + * @param string $publicKey The public key to set + * @param string $privateKey The private key to set + * @return self + * + * @Given I use :publicKey and :privateKey for public and private keys + */ + public function setPublicAndPrivateKey($publicKey, $privateKey) { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + + // Add the request header + $this->setRequestHeader('X-Imbo-PublicKey', $publicKey); + + return $this; + } + + /** + * Set a query string parameter + * + * @param string $name The name of the parameter + * @param mixed $value The value for the parameter + * @return self + * + * @Given the query string parameter :name is set to :value + */ + public function setRequestQueryParameter($name, $value) { + if (empty($this->requestOptions['query'])) { + $this->requestOptions['query'] = []; + } + + // If the name ends with [] we remove that from the name, and convert the value to an array + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + + if (isset($this->requestOptions['query'][$name]) && !is_array($this->requestOptions['query'][$name])) { + // The field already exists, but not as an array + throw new InvalidArgumentException(sprintf( + 'The "%s" query parameter already exists and it\'s not an array, so can\'t append more values to it.', + $name + )); + } else if (!isset($this->requestOptions['query'][$name])) { + // The field does not exist, set it to an empty array + $this->requestOptions['query'][$name] = []; + } + + // Append the value + $this->requestOptions['query'][$name][] = $value; + } else { + // Set the key => value + $this->requestOptions['query'][$name] = $value; + } + + return $this; + } + + /** + * Set a query parameter to the image identifier of a specific image already added to Imbo + * + * @param string $name Name of the query parameter + * @param string $path Path of a local image that exists in Imbo + * @throws InvalidArgumentException + * @return self + * + * @Given the query string parameter :name is set to the image identifier of :path + */ + public function setRequestParameterToImageIdentifier($name, $path) { + if (!isset($this->imageIdentifiers[$path])) { + throw new InvalidArgumentException(sprintf( + 'No image identifier exists for image: "%s".', + $path + )); + } + + return $this->setRequestQueryParameter($name, $this->imageIdentifiers[$path]); + } + + /** + * Generate a short URL with some parameters for a given image path + * + * @param string $path + * @param PyStringNode $params + * @throws InvalidArgumentException + * @return self + * + * @Given I generate a short URL for :path with the following parameters: + */ + public function generateShortImageUrl($path, PyStringNode $params) { + if (!isset($this->imageIdentifiers[$path])) { + throw new InvalidArgumentException(sprintf( + 'No image identifier exists for path: "%s".', $path + )); + } + + $imageIdentifier = $this->imageIdentifiers[$path]; + $user = explode('/', ltrim($this->imageUrls[$path], '/'))[1]; + $params = array_merge(json_decode((string) $params, true), [ + 'imageIdentifier' => $imageIdentifier, + ]); + + return $this + ->setRequestBody(json_encode($params)) + ->requestPath(sprintf( + '/users/%s/images/%s/shorturls', $user, $imageIdentifier + ), 'POST'); + } + + /** + * Specify a watermark image based on a local path + * + * @param string $localPath + * @param string $params + * @throws InvalidArgumentException + * @return self + * + * @Given I use :localPath as the watermark image + * @Given I use :localPath as the watermark image with :params as parameters + */ + public function specifyAsTheWatermarkImage($localPath, $params = null) { + if (!isset($this->imageIdentifiers[$localPath])) { + throw new InvalidArgumentException(sprintf( + 'No image exists for path: "%s".', $localPath + )); + } + + $imageIdentifier = $this->imageIdentifiers[$localPath]; + $transformation = 'watermark:img=' . $imageIdentifier . ($params ? ',' . $params : ''); + + return $this->applyTransformation($transformation); + } + + /** + * Make a request to the previously added image (in the same scenario) + * + * This method will loop through the history in reverse order and look for responses which + * contains image identifiers. The first one found will be requested. + * + * @param string $method The HTTP method to use + * @return self + * + * @When I request the previously added image + * @When I request the previously added image using HTTP :method + */ + public function requestPreviouslyAddedImage($method = 'GET') { + $image = $this->getUserAndImageIdentifierOfPreviouslyAddedImage(); + $path = sprintf('/users/%s/images/%s', $image['user'], $image['imageIdentifier']); + + return $this->requestPath($path, $method); + } + + /** + * Request the previously added image as a specific extension + * + * @param string $extension Extension of the image: jpg, gif or png + * @throws InvalidArgumentException Throws an extension if the given extension is invalid + * @return self + * + * @When I request the previously added image as a :extension + * @When I request the previously added image as a :extension using HTTP :method + */ + public function requestPreviouslyAddedImageAsType($extension, $method = 'GET') { + if (!in_array($extension, ['gif', 'png', 'jpg'])) { + throw new InvalidArgumentException(sprintf('Invalid extension: "%s".', $extension)); + } + + $image = $this->getUserAndImageIdentifierOfPreviouslyAddedImage(); + $path = sprintf('/users/%s/images/%s', $image['user'], $image['imageIdentifier']); + + if ($extension) { + $path .= '.' . $extension; + } + + return $this->requestPath($path, $method); + } + + /** + * Replay the last request + * + * This method can be used to replay a request, with or without a different HTTP method. If the + * public and private keys have been set the method will append an access token. + * + * @param string $method Optional HTTP method. If not set the HTTP method from the previous + * request will be used. + * @throws RuntimeException Throws an exception if no request have been made yet. + * @return self + * + * @When I replay the last request + * @When I replay the last request using HTTP :method + */ + public function makeSameRequest($method = null) { + if (!$this->response) { + throw new RuntimeException('No request has been made yet.'); + } + + $this->setRequestMethod($method ?: $this->request->getMethod()); + + if ($this->publicKey && $this->privateKey) { + $this->appendAccessToken(); + } + + return $this->sendRequest(); + } + + /** + * Request the metadata of the previously added image + * + * @param string $method The HTTP method to use when fetching the metadata + * @return self + * + * @When I request the metadata of the previously added image + * @When I request the metadata of the previously added image using HTTP :method + */ + public function requestMetadataOfPreviouslyAddedImage($method = 'GET') { + $image = $this->getUserAndImageIdentifierOfPreviouslyAddedImage(); + $path = sprintf('/users/%s/images/%s/metadata', $image['user'], $image['imageIdentifier']); + + return $this->requestPath($path, $method); + } + + /** + * Request an image using a local file path + * + * This method can be used to fetch images that has been added to Imbo earlier via the + * `addUserImageToImbo` method, that is triggered by `Given :imagePath exists for user :user`. + * + * @param string $localPath The local path for the image that was added earlier + * @param string $extension Optional extension of the image (png|gif|jpg) + * @param string $method Optional HTTP method to use + * @throws InvalidArgumentException + * @return self + * + * @When /^I request the image resource for "([^"]*)"(?: as a "(png|gif|jpg)")?(?: using HTTP "([^"]*)")?$/ + */ + public function requestImageResourceForLocalImage($localPath, $extension = null, $method = 'GET') { + if (!isset($this->imageUrls[$localPath])) { + throw new InvalidArgumentException(sprintf( + 'Image URL for image with path "%s" can not be found.', + $localPath + )); + } + + $url = $this->imageUrls[$localPath]; + + if ($extension) { + // Append extension if specified + $url .= '.' . $extension; + } + + return $this->requestPath($url, $method); + } + + /** + * Perform a series of requests + * + * The $table parameter must be a table with the following columns: + * + * - (string) path, required: The path to request. Some special values can be used for dynamic + * requests: + * - "previously added image": Request the previously added image + * - "metadata of the previously added image": Request the metadata + * of the previously added image + * - (string) method: The HTTP method to use, defaults to GET + * - (string) extension: Used to force a specific image type, for instance "jpg" + * - (string) transformation: An image transformation to add to the request + * - (string) access token: Set to "yes" to append an access token as a query parameter + * - (string) sign request: Set to "yes" to sign the request. Remember to specify public and + * private keys prior to running the request + * - (string) request body: Set the request body to this value + * + * @param TableNode $table Information about the requests to make + * @throws InvalidArgumentException + * @return self + * + * @When I request: + */ + public function requestPaths(TableNode $table) { + // Store these as they need to be reset bewteen each run for all requests to have the same + // starting point + $originalRequest = clone $this->request; + $originalRequestOptions = $this->requestOptions; + + foreach ($table as $row) { + if (empty($row['path'])) { + throw new InvalidArgumentException('Missing or empty "path" key.'); + } + + $method = !empty($row['method']) ? $row['method'] : 'GET'; + $path = $row['path']; + + if (!empty($row['transformation'])) { + $this->applyTransformation($row['transformation']); + } + + if ( + !empty($row['access token']) && $row['access token'] === 'yes' && + !empty($row['sign request']) && $row['sign request'] === 'yes' + ) { + throw new InvalidArgumentException( + 'Both "sign request" and "access token" can not be set to "yes".' + ); + } + + if (!empty($row['access token']) && $row['access token'] === 'yes') { + $this->appendAccessToken(); + } + + if (!empty($row['sign request']) && $row['sign request'] === 'yes') { + $this->signRequest(); + } + + if (!empty($row['request body'])) { + $this->setRequestBody($row['request body']); + } + + if ($path === 'previously added image') { + if (!empty($row['extension'])) { + $this->requestPreviouslyAddedImageAsType($row['extension'], $method); + } else { + $this->requestPreviouslyAddedImage($method); + } + } else if ($path === 'metadata of previously added image') { + $this->requestMetadataOfPreviouslyAddedImage($method); + } else { + $this->requestPath($path, $method); + } + + // Reset the request and request options between every run + $this->request = $originalRequest; + $this->requestOptions = $originalRequestOptions; + } + + return $this; + } + + /** + * Make a request to the short URL generated in the previous request + * + * @throws RuntimeException + * @return self + * + * @When I request the image using the generated short URL + */ + public function requestImageUsingShortUrl() { + $this->requireResponse(); + $body = json_decode((string) $this->response->getBody(), true); + + if (!is_array($body) || json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Invalid response body in the current response instance'); + } else if (empty($body['id'])) { + throw new RuntimeException(sprintf( + 'Missing "id" from body: "%s".', + (string) $this->response->getBody() + )); + } + + return $this->requestPath(sprintf('/s/%s', $body['id'])); + } + + /** + * Assert the contents of an imbo error message + * + * @param string $message The error message + * @param int $code The error code + * @throws InvalidArgumentException + * @return self + * + * @Then the Imbo error message is :message + * @Then the Imbo error message is :message and the error code is :code + */ + public function assertImboError($message, $code = null) { + $this->requireResponse(); + + if ($this->response->getStatusCode() < 400) { + throw new InvalidArgumentException( + 'The status code of the last response is lower than 400, so it is not considered an error.' + ); + } + + $body = json_decode((string) $this->response->getBody()); + $actualMessage = $body->error->message; + $actualCode = $body->error->imboErrorCode; + + Assertion::same( + $message, + $actualMessage, + sprintf('Expected error message "%s", got "%s".', $message, $actualMessage) + ); + + if ($code !== null) { + $code = (int) $code; + $actualCode = (int) $actualCode; + + Assertion::same( + $code, + $actualCode, + sprintf('Expected imbo error code "%d", got "%d".', $code, $actualCode) + ); + } + + return $this; + } + + /** + * Assert the width of the image in the current response + * + * @param int $width + * @return self + * + * @Then the image width is :width + */ + public function assertImageWidth($width) { + $this->requireResponse(); + + $width = (int) $width; + + list($actualWidth) = getimagesizefromstring((string) $this->response->getBody()); + + Assertion::same( + $width, + $actualWidth, + sprintf('Incorrect image width, expected %d, got %d.', $width, $actualWidth) + ); + + return $this; + } + + /** + * Assert the height of image in the current response + * + * @param int $height + * @return self + * + * @Then the image height is :height + */ + public function assertImageHeight($height) { + $this->requireResponse(); + + $height = (int) $height; + + list($actualWidth, $actualHeight) = getimagesizefromstring((string) $this->response->getBody()); + unset($actualWidth); + + Assertion::same( + $height, + $actualHeight, + sprintf('Incorrect image height, expected %d, got %d.', $height, $actualHeight) + ); + + return $this; + } + + /** + * Assert the dimensions of the image in the current response + * + * @param string $dimension Image dimension as "x", for instance "1024x768" + * @throws InvalidArgumentException + * @return self + * + * @Then the image dimension is :dimension + */ + public function assertImageDimension($dimension) { + $this->requireResponse(); + + $match = []; + preg_match('/^(?[\d]+)x(?[\d]+)$/', $dimension, $match); + + if (!$match) { + throw new InvalidArgumentException(sprintf( + 'Invalid dimension value: "%s". Specify "x".', + $dimension + )); + } + + $width = (int) $match['width']; + $height = (int) $match['height']; + + list($actualWidth, $actualHeight) = getimagesizefromstring((string) $this->response->getBody()); + + Assertion::same( + $width, + $actualWidth, + sprintf('Incorrect image width, expected %d, got %d.', $width, $actualWidth) + ); + + Assertion::same( + $height, + $actualHeight, + sprintf('Incorrect image height, expected %d, got %d.', $height, $actualHeight) + ); + + return $this; + } + + /** + * Assert the hex value of a given coordinate in the image found in the current response + * + * @param string $coordinates X and Y coordinates, separated by a comma + * @param string $color Hex color value + * @return self + * + * @Then the pixel at coordinate :coordinates has a color of :color + */ + public function assertImagePixelColor($coordinates, $color) { + $this->requireResponse(); + + $info = $this->getImagePixelInfo($coordinates); + $color = ltrim(strtolower($color), '#'); + + Assertion::same( + $color, + $info['color'], + sprintf( + 'Incorrect color at coordinate "%s", expected "%s", got "%s".', + $coordinates, + $color, + $info['color'] + ) + ); + + return $this; + } + + /** + * Assert the alpha value of a given coordinate in the image found in the current response + * + * @param string $coordinates X and Y coordinates, separated by a comma + * @param float $alpha Alpha value + * @return self + * + * @Then the pixel at coordinate :coordinates has an alpha of :alpha + */ + public function assertImagePixelAlpha($coordinates, $alpha) { + $this->requireResponse(); + + $info = $this->getImagePixelInfo($coordinates); + $alpha = (float) $alpha; + + Assertion::same( + $alpha, + $info['alpha'], + sprintf( + 'Incorrect alpha value at coordinate "%s", expected "%f", got "%f".', + $coordinates, + $alpha, + $info['alpha'] + ) + ); + + return $this; + } + + /** + * Make sure an ACL rule has been deleted + * + * @param string $publicKey The public key + * @param string $aclId The ACL ID to check + * @return self + * + * @Then the ACL rule under public key :publicKey with ID :aclId no longer exists + */ + public function assertAclRuleWithIdDoesNotExist($publicKey, $aclId) { + // Append an access token with the current public / private keys, and request the given + // ACL rule + $this + ->appendAccessToken() + ->requestPath(sprintf('/keys/%s/access/%s', $publicKey, $aclId)); + + $expectedStatusLine = '404 Access rule not found'; + $actualStatusLine = sprintf( + '%d %s', + $this->response->getStatusCode(), + $this->response->getReasonPhrase() + ); + + Assertion::same( + $expectedStatusLine, + $actualStatusLine, + sprintf( + 'ACL rule "%s" with public key "%s" still exists. Expected "%s", got "%s".', + $aclId, + $publicKey, + $expectedStatusLine, + $actualStatusLine + ) + ); + + return $this; + } + + /** + * Make sure a public does not exist + * + * @param string $publicKey The public key to check for + * @return self + * + * @Then the :publicKey public key no longer exists + */ + public function assertPublicKeyDoesNotExist($publicKey) { + // Append an access token with the current public / private keys, and request the given + // public key + $this + ->appendAccessToken() + ->requestPath(sprintf('/keys/%s', $publicKey), 'HEAD'); + + $expectedStatusLine = '404 Public key not found'; + $actualStatusLine = sprintf( + '%d %s', + $this->response->getStatusCode(), + $this->response->getReasonPhrase() + ); + + Assertion::same( + $expectedStatusLine, + $actualStatusLine, + sprintf( + 'Public key "%s" still exists. Expected "%s", got "%s".', + $publicKey, + $expectedStatusLine, + $actualStatusLine + ) + ); + + return $this; + } + + /** + * Check whether or not the response can be cached + * + * @param boolean $cacheable + * @return self + * + * @Then /^the response can (not )?be cached$/ + */ + public function assertCacheability($cacheable = true) { + $this->requireResponse(); + + if ($cacheable !== true) { + $cacheable = false; + } + + Assertion::same( + $cacheable, + $this->cacheUtil->isCacheable($this->response), + $cacheable ? + 'Response was supposed to be cacheble, but it\'s not.' : + 'Response was not supposed to be cacheable, but it is.' + ); + + return $this; + } + + /** + * Validate the max-age directive of the cache-control response header + * + * @param int $expected Expected max-age + * @throws RuntimeException + * @return self + * + * @Then the response has a max-age of :max seconds + */ + public function assertMaxAge($expected) { + $this->requireResponse(); + + $cacheControl = $this->response->getHeaderLine('cache-control'); + + if (!$cacheControl) { + throw new RuntimeException('Response does not have a cache-control header.'); + } + + $match = []; + preg_match('/max-age=(?[\d]+)/i', $cacheControl, $match); + + if (!$match) { + throw new RuntimeException(sprintf( + 'Response cache-control header does not include a max-age directive: "%s".', + $cacheControl + )); + } + + $expected = (int) $expected; + $maxAge = (int) $match['maxAge']; + + Assertion::same( + $expected, + $maxAge, + sprintf( + 'The max-age directive in the cache-control header is not correct. Expected %d, got %d. Complete cache-control header: "%s".', + $expected, + $maxAge, + $cacheControl + ) + ); + + return $this; + } + + /** + * Verify that the response has a specific cache-control directive + * + * @param string $directive + * @throws RuntimeException Throws an exception if the response does not have a cache-control + * directive, or if the validation fails + * @return self + * + * @Then the response has a :directive cache-control directive + */ + public function assertResponseHasCacheControlDirective($directive) { + $this->requireResponse(); + + $cacheControl = $this->response->getHeaderLine('cache-control'); + + if (!$cacheControl) { + throw new RuntimeException('Response does not have a cache-control header.'); + } + + Assertion::contains( + $cacheControl, + $directive, + sprintf( + 'The cache-control header does not contain the "%s" directive. Complete cache-control header: "%s".', + $directive, + $cacheControl + ) + ); + + return $this; + } + + /** + * Verify that the response does not have a given cache-control directive + * + * @param string $directive + * @throws RuntimeException Throws an exception if the response does not have a cache-control + * directive, or if the validation fails + * @return self + * + * @Then the response does not have a :directive cache-control directive + */ + public function assertResponseDoesNotHaveCacheControlDirective($directive) { + $this->requireResponse(); + + $cacheControl = $this->response->getHeaderLine('cache-control'); + + if (!$cacheControl) { + throw new RuntimeException('Response does not have a cache-control header.'); + } + + if (strpos($cacheControl, $directive) !== false) { + throw new RuntimeException( + sprintf( + 'The cache-control header contains the "%s" directive when it should not. Complete cache-control header: "%s".', + $directive, + $cacheControl + ) + ); + } + + return $this; + } + + /** + * Compare a specific header in the last $num responses + * + * @param int $num The number of responses to compare. Must be at least 2. + * @param string $header The response header to compare + * @param boolean $unique Whether or not the values should be unique + * @throws InvalidArgumentException|RuntimeException + * @return self + * + * @Then /^the last ([\d]+) "([^"]+)" response headers are (not )?the same$/ + */ + public function assertLastResponseHeaders($num, $header, $unique = false) { + $num = (int) $num; + + if ($num < 2) { + throw new InvalidArgumentException(sprintf( + 'Need to compare at least 2 responses.', + $num + )); + } + + $numResponses = count($this->history); + + if ($numResponses < $num) { + throw new InvalidArgumentException(sprintf( + 'Not enough responses in the history. Need at least %d, there are currently %d.', + $num, + $numResponses + )); + } + + $values = []; + + foreach (array_slice(array_reverse($this->history), 0, $num) as $transaction) { + $response = $transaction['response']; + + if (!$response->hasHeader($header)) { + throw new RuntimeException(sprintf( + 'The "%s" header is not present in all of the last %d response headers.', + $header, + $num + )); + } + + $values[] = $response->getHeaderLine($header); + } + + if ($unique) { + Assertion::count( + $uniqueValues = array_unique($values), + $num, + sprintf( + 'Expected %d unique values, got %d. Values compared: %s', + $num, + count($uniqueValues), + print_r($values, true) + ) + ); + } else { + Assertion::count( + array_unique($values), + 1, + sprintf( + 'Expected all values to be the same. Values compared: %s', + print_r($values, true) + ) + ); + } + + return $this; + } + + /** + * Match a series of responses against a data set represented as a TableNode + * + * This step can be used to match requests typically made with the `@When I request:` step. + * + * The $table parameter must be a table with the following columns: + * + * - (int) response, required: The number of the request, 1-based index where the lowest number + * is the oldest response. + * - (string) status line: Match the status line + * - (string) header name: Match a header (used with `header value`) + * - (string) header value: Match a header (used with `header name`) + * - (string) checksum: Match the MD5 checksum of the response body with this value + * - (int) image width: Match the width of the image in the request with this value + * - (int) image height: Match the height of the image in the reqeust with this value + * + * @param TableNode $table The data to match against + * @throws RuntimeException|InvalidArgumentException + * @return self + * + * @Then the last responses match: + */ + public function assertLastResponsesMatch(TableNode $table) { + $num = array_map(function($num) { + return (int) $num; + }, array_column($table->getColumnsHash(), 'response')); + + if (!$num) { + throw new InvalidArgumentException('Missing response column'); + } + + $num = max($num); + + if (count($this->history) < $num) { + throw new RuntimeException(sprintf( + 'Not enough transactions in the history. Needs at least %d, actual: %d.', + $num, + count($this->history)) + ); + } + + // First, reverse the history and slice $num elements off. Then reverse those, and pick + // only the response elements from the resulting array. + $reversedOrder = array_reverse($this->history); + $responses = array_column(array_reverse(array_slice($reversedOrder, 0, $num)), 'response'); + + // Valid keys for the rows in $table + $validKeys = [ + 'response', + 'status line', + 'body is', + 'header name', + 'header value', + 'checksum', + 'image width', + 'image height', + ]; + + foreach (array_keys($table->getColumnsHash()[0]) as $column) { + if (!in_array($column, $validKeys)) { + throw new InvalidArgumentException(sprintf( + 'Invalid column name: "%s".', + $column + )); + } + } + + foreach ($table as $i => $row) { + if (empty($row['response'])) { + throw new InvalidArgumentException( + 'Each row must refer to a response by using the "response" column.' + ); + } + + $index = $row['response'] - 1; + $response = $responses[$index]; + + if (!empty($row['status line'])) { + $actualStatusLine = sprintf( + '%d %s', + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + Assertion::same( + $row['status line'], + $actualStatusLine, sprintf( + 'Incorrect status line in response %d, expected "%s", got: "%s".', + $row['response'], + $row['status line'], + $actualStatusLine + ) + ); + } + + if (!empty($row['header name']) && !empty($row['header value'])) { + Assertion::true( + $response->hasHeader($row['header name']), + sprintf( + 'Expected response %d to have the "%s" header, but it does not.', + $row['response'], + $row['header name'] + ) + ); + + Assertion::same( + $row['header value'], + $headerValue = $response->getHeaderLine($row['header name']), + sprintf( + 'Incorrect "%s" header value in response %d, expected "%s", got: "%s".', + $row['header name'], + $row['response'], + $row['header value'], + $headerValue + ) + ); + } + + if (!empty($row['checksum'])) { + Assertion::same( + $row['checksum'], + $checksum = md5((string) $response->getBody()), + sprintf( + 'Incorrect checksum in response %d, expected "%s", got: "%s".', + $row['response'], + $row['checksum'], + $checksum + ) + ); + } + + if (!empty($row['image width']) || !empty($row['image height'])) { + list($actualWidth, $actualHeight) = getimagesizefromstring((string) $response->getBody()); + + if (!empty($row['image width'])) { + Assertion::same( + (int) $row['image width'], + $actualWidth, + sprintf( + 'Expected image in response %d to be %d pixel(s) wide, actual: %d.', + $row['response'], + $row['image width'], + $actualWidth + ) + ); + } + + if (!empty($row['image height'])) { + Assertion::same( + (int) $row['image height'], + $actualHeight, + sprintf( + 'Expected image in response %d to be %d pixel(s) high, actual: %d.', + $row['response'], + $row['image height'], + $actualHeight + ) + ); + } + } + + if (!empty($row['body is'])) { + Assertion::same( + $row['body is'], + $actualBody = (string) $response->getBody(), + sprintf( + 'Incorrect response body for request %d, expected "%s", got: "%s".', + $row['response'], + $row['body is'], + $actualBody + ) + ); + } + } + + return $this; + } + + /** + * Assert that the image does not have any properties with a specific prefix + * + * @param string $prefix + * @throws AssertionFailedException|RuntimeException + * @return self + * + * @Then the image should not have any :prefix properties + */ + public function assertImageProperties($prefix) { + $imagick = new Imagick(); + + try { + $imagick->readImageBlob((string) $this->response->getBody()); + } catch (ImagickException $e) { + throw new RuntimeException(sprintf( + 'Imagick could not read response body: "%s".', + $e->getMessage() + )); + } + + foreach ($imagick->getImageProperties() as $key => $value) { + if (strpos($key, $prefix) === 0) { + throw new AssertionFailedException(sprintf( + 'Image properties have not been properly stripped. Did not expect properties that starts with "%s", found: "%s".', + $prefix, + $key + )); + } + } + + return $this; + } + + /** + * Check the size of the response body (not the Content-Length response header) + * + * @param int $expetedSize The size we are expecting + * @return self + * + * @Then the response body size is :expectedSize + */ + public function assertResponseBodySize($expectedSize) { + $this->requireResponse(); + + Assertion::same( + $actualSize = strlen((string) $this->response->getBody()), + (int) $expectedSize, + sprintf('Expected response body size: %d, actual: %d.', $expectedSize, $actualSize) + ); + + return $this; + } + + /** + * Get the pixel info for given coordinates from the image in the current response + * + * @param string $coordinates + * @throws InvalidArgumentException Throws an exception if the coordinates value is invalid + * @return array Returns an array with two keys: + * - `color`: Hex color of the pixel. + * - `alpha`: Alpha value of the pixel. + */ + private function getImagePixelInfo($coordinates) { + $this->requireResponse(); + + $match = []; + preg_match('/^(?[\d]+),(?[\d]+)$/', $coordinates, $match); + + if (!$match) { + throw new InvalidArgumentException(sprintf( + 'Invalid coordinates: "%s". Format is "x", no spaces allowed.', + $coordinates + )); + } + + $x = (int) $match['x']; + $y = (int) $match['y']; + + $imagick = new Imagick(); + $imagick->readImageBlob((string) $this->response->getBody()); + + $pixel = $imagick->getImagePixelColor($x, $y); + $color = $pixel->getColor(); + + $toHex = function($col) { + return str_pad(dechex($col), 2, '0', STR_PAD_LEFT); + }; + + $hexColor = $toHex($color['r']) . $toHex($color['g']) . $toHex($color['b']); + + return [ + 'color' => $hexColor, + 'alpha' => (float) $pixel->getColorValue(Imagick::COLOR_ALPHA), + ]; + } + + /** + * Get the user and image identifier of the previoysly added image + * + * @throws RuntimeException + * @return array + */ + private function getUserAndImageIdentifierOfPreviouslyAddedImage() { + foreach (array_reverse($this->history) as $transaction) { + $request = $transaction['request']; + $response = $transaction['response']; + + $body = json_decode((string) $response->getBody()); + + if (!empty($body->imageIdentifier) && $request->getMethod() === 'POST') { + $match = []; + preg_match( + '|^/users/(?.+?)/images$|', + (string) $request->getUri()->getPath(), + $match + ); + + return [ + 'user' => $match['user'], + 'imageIdentifier' => $body->imageIdentifier + ]; + } + } + + // No hit + throw new RuntimeException( + 'Could not find any response in the history with an image identifier.' + ); + } +} diff --git a/tests/behat/features/border-transformation.feature b/tests/behat/features/border-transformation.feature index fff4893fa..e5ce7c67b 100644 --- a/tests/behat/features/border-transformation.feature +++ b/tests/behat/features/border-transformation.feature @@ -3,51 +3,47 @@ Feature: Imbo can apply border to images As an HTTP Client I can use the border transformation - Background: - Given "tests/phpunit/Fixtures/transparency.png" is used as the test image for the "border" feature - Scenario: Apply an outbound border to only top/bottom - Given I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/transparency.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And I specify "border:color=bf1942,height=100,width=0,mode=outbound" as transformation - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "512" - And the height of the image is "712" - And the pixel at coordinate "0,0" should have a color of "#bf1942" - And the pixel at coordinate "64,164" should have a color of "#225d10" - And the pixel at coordinate "0,164" should have a color of "#225d10" - And the pixel at coordinate "64,662" should have a color of "#bf1942" - And the pixel at coordinate "448,292" should have a color of "#588e00" - And the pixel at coordinate "448,292" should have an alpha of "1" - And the pixel at coordinate "192,164" should have an alpha of "0" + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "512x712" + And the pixel at coordinate "0,0" has a color of "#bf1942" + And the pixel at coordinate "64,164" has a color of "#225d10" + And the pixel at coordinate "0,164" has a color of "#225d10" + And the pixel at coordinate "64,662" has a color of "#bf1942" + And the pixel at coordinate "448,292" has a color of "#588e00" + And the pixel at coordinate "448,292" has an alpha of "1" + And the pixel at coordinate "192,164" has an alpha of "0" Scenario: Apply an outbound border to an image without an alpha channel - Given "tests/phpunit/Fixtures/512x512.png" is used as the test image - And I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/512x512.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And I specify "border:color=bf1942,height=100,width=0,mode=outbound" as transformation - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "512" - And the height of the image is "712" - And the pixel at coordinate "0,0" should have a color of "#bf1942" - And the pixel at coordinate "64,164" should have a color of "#225d10" - And the pixel at coordinate "0,164" should have a color of "#225d10" - And the pixel at coordinate "64,662" should have a color of "#bf1942" - And the pixel at coordinate "448,292" should have a color of "#588e00" + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "512x712" + And the pixel at coordinate "0,0" has a color of "#bf1942" + And the pixel at coordinate "64,164" has a color of "#225d10" + And the pixel at coordinate "0,164" has a color of "#225d10" + And the pixel at coordinate "64,662" has a color of "#bf1942" + And the pixel at coordinate "448,292" has a color of "#588e00" Scenario: Apply an inline border - Given I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/transparency.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And I specify "border:color=bf1942,height=100,width=100,mode=inline" as transformation - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "512" - And the height of the image is "512" - And the pixel at coordinate "0,0" should have a color of "#bf1942" - And the pixel at coordinate "114,114" should have a color of "#225d10" - And the pixel at coordinate "340,115" should have a color of "#95c400" - And the pixel at coordinate "100,411" should have a color of "#588e00" - And the pixel at coordinate "382,411" should have a color of "#95c400" - And the pixel at coordinate "413,413" should have a color of "#bf1942" + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "512x512" + And the pixel at coordinate "0,0" has a color of "#bf1942" + And the pixel at coordinate "114,114" has a color of "#225d10" + And the pixel at coordinate "340,115" has a color of "#95c400" + And the pixel at coordinate "100,411" has a color of "#588e00" + And the pixel at coordinate "382,411" has a color of "#95c400" + And the pixel at coordinate "413,413" has a color of "#bf1942" diff --git a/tests/behat/features/client-caching.feature b/tests/behat/features/client-caching.feature index dec29b70a..03bb836af 100644 --- a/tests/behat/features/client-caching.feature +++ b/tests/behat/features/client-caching.feature @@ -4,67 +4,58 @@ Feature: Imbo enables client caching using related response headers I can make look at the cache-related response headers set by Imbo Background: - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" Scenario: Request index page (not cacheable) When I request "/" - Then the response is not cacheable - And the following response headers should not be present: - """ - last-modified - etag - """ + Then the response can not be cached + And the "last-modified" response header does not exist + And the "etag" response header does not exist Scenario: Request status information (not cacheable) When I request "/status" - Then the response is not cacheable - And the following response headers should not be present: - """ - last-modified - etag - """ + Then the response can not be cached + And the "last-modified" response header does not exist + And the "etag" response header does not exist Scenario: Request stats information (not cacheable) - When I request "/stats=statsAllow=*" - Then the response is not cacheable - And the following response headers should not be present: - """ - last-modified - etag - """ + When I request "/stats?statsAllow=*" + Then the response can not be cached + And the "last-modified" response header does not exist + And the "etag" response header does not exist Scenario: Request user information - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user" - Then the response is cacheable + Then the response can be cached Scenario: Request user images - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images" - Then the response is cacheable - And the response has a max age of 0 seconds - And the response has a must-revalidate directive + Then the response can be cached + And the response has a max-age of 0 seconds + And the response has a "must-revalidate" cache-control directive Scenario: Request user images with custom caching configuration Given Imbo uses the "custom-http-cache.php" configuration - And I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images" - Then the response has a max age of 15 seconds - And the response does not have a must-revalidate directive + Then the response has a max-age of 15 seconds + And the response does not have a "must-revalidate" cache-control directive Scenario: Request user image - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "image/*" When I request the previously added image - Then the response is cacheable - And the response has a max age of 31536000 seconds + Then the response can be cached + And the response has a max-age of 31536000 seconds Scenario: Request user image metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request the metadata of the previously added image - Then the response is cacheable + Then the response can be cached diff --git a/tests/behat/features/content-negotiation.feature b/tests/behat/features/content-negotiation.feature index f3055c7ce..cc7f54d50 100644 --- a/tests/behat/features/content-negotiation.feature +++ b/tests/behat/features/content-negotiation.feature @@ -4,15 +4,16 @@ Feature: Imbo supports content negotiation I can specify the type I want in the Accept request header Background: - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo - And "tests/phpunit/Fixtures/image.jpg" exists in Imbo - And "tests/phpunit/Fixtures/image.gif" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And "tests/phpunit/Fixtures/image.jpg" exists for user "user" + And "tests/phpunit/Fixtures/image.gif" exists for user "user" Scenario Outline: Imbo's resources can respond with different content types using content negotiation Given the "Accept" request header is "" - And I include an access token in the query + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" Examples: @@ -26,9 +27,10 @@ Feature: Imbo supports content negotiation Scenario Outline: Imbo's metadata resource can respond with different content types using content negotiation Given the "Accept" request header is "" - And I include an access token in the query + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request the metadata of the previously added image - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" Examples: @@ -39,53 +41,61 @@ Feature: Imbo supports content negotiation Scenario: If the client includes an extension, the Accept header should be ignored Given the "Accept" request header is "application/xml" When I request "/status.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" Scenario: If the server responds with an error, and the client included a valid extension, that type should be returned Given the "Accept" request header is "application/xml" When I request "/users/foobar.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" And the "Content-Type" response header is "application/json" Scenario Outline: Imbo uses the Accept header when encountering errors to choose the error format - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "" When I request "/users/user/images/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - Then I should get a response with "" + Then the response status line is "" And the "Content-Type" response header is "application/json" Examples: - | accept | extension | reason | + | accept | extension | status-line | | */* | .png | 404 Image not found | | image/png | .png | 406 Not acceptable | | */* | | 404 Image not found | | image/png | | 406 Not acceptable | Scenario: Fetch an image when not accepting images - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "application/json" When I request the image resource for "tests/phpunit/Fixtures/image1.png" - Then I should get a response with "406 Not acceptable" + Then the response status line is "406 Not acceptable" And the "Content-Type" response header is "application/json" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" And the "X-Imbo-Originalheight" response header is "417" And the "X-Imbo-Originalmimetype" response header is "image/png" And the "X-Imbo-Originalwidth" response header is "599" - And the response body matches: + And the response body contains JSON: """ - /{"error":{"code":406,"message":"Not acceptable","date":"[^"]+","imboErrorCode":0},"imageIdentifier":"[^"]+"}/ + { + "error": { + "code": 406, + "message": "Not acceptable", + "date": "@isDate()", + "imboErrorCode": 0 + }, + "imageIdentifier": "@regExp(/[a-z0-9]+/)" + } """ Scenario Outline: Imbo uses the original mime type of the image if the client has no preferences - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "image/*" When I request the image resource for "" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" Examples: @@ -95,12 +105,12 @@ Feature: Imbo supports content negotiation | tests/phpunit/Fixtures/image.gif | image/gif | Scenario Outline: Imbo uses the original mime type of the image if configuration has disabled content negotiation for images - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-content-negotiation-disabled.php" configuration - And I include an access token in the query + Given Imbo uses the "image-content-negotiation-disabled.php" configuration + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "" When I request the image resource for "" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" Examples: diff --git a/tests/behat/features/cors-event-listener.feature b/tests/behat/features/cors-event-listener.feature index 0b651ad8b..3a49e8afa 100644 --- a/tests/behat/features/cors-event-listener.feature +++ b/tests/behat/features/cors-event-listener.feature @@ -7,114 +7,96 @@ Feature: Imbo provides an event listener for CORS Given the "Origin" request header is "http://allowedhost" And Imbo uses the "cors.php" configuration When I request "/" using HTTP "HEAD" - Then I should get a response with "200 Hell Yeah" + Then the response status line is "200 Hell Yeah" And the "Access-Control-Allow-Origin" response header is "http://allowedhost" - And the "Access-Control-Expose-Headers" response header contains "X-Imbo-ImageIdentifier" - And the "Access-Control-Expose-Headers" response header contains "X-Imbo-Version" - And the "Vary" response header contains "Origin" - And the "Allow" response header contains "GET" - And the "Allow" response header contains "HEAD" - And the "Allow" response header contains "OPTIONS" + And the "Access-Control-Expose-Headers" response header matches "/X-Imbo-ImageIdentifier/" + And the "Access-Control-Expose-Headers" response header matches "/X-Imbo-Version/" + And the "Vary" response header matches "/Origin/" + And the "Allow" response header matches "/GET/" + And the "Allow" response header matches "/HEAD/" + And the "Allow" response header matches "/OPTIONS/" Scenario: Request a resource using a non-allowed host Given the "Origin" request header is "http://imbo" And Imbo uses the "cors.php" configuration When I request "/" using HTTP "HEAD" - Then I should get a response with "200 Hell Yeah" - And the "Vary" response header contains "Origin" - And the "Allow" response header contains "GET" - And the "Allow" response header contains "HEAD" - And the "Allow" response header contains "OPTIONS" - And the following response headers should not be present: - """ - Access-Control-Allow-Origin - Access-Control-Expose-Headers - """ + Then the response status line is "200 Hell Yeah" + And the "Vary" response header matches "/Origin/" + And the "Allow" response header matches "/GET/" + And the "Allow" response header matches "/HEAD/" + And the "Allow" response header matches "/OPTIONS/" + And the "Access-Control-Allow-Origin" response header does not exist + And the "Access-Control-Expose-Headers" response header does not exist Scenario: Request a resource using HTTP OPTIONS using an allowed host Given the "Origin" request header is "http://allowedhost" And the "Access-Control-Request-Headers" request header is "x-imbo-something, x-imbo-signature" And Imbo uses the "cors.php" configuration When I request "/" using HTTP "OPTIONS" - Then I should get a response with "204 No Content" + Then the response status line is "204 No Content" And the "Access-Control-Allow-Origin" response header is "http://allowedhost" - And the "Access-Control-Allow-Methods" response header contains "GET" - And the "Access-Control-Allow-Methods" response header contains "HEAD" - And the "Access-Control-Allow-Methods" response header contains "OPTIONS" - And the "Access-Control-Allow-Headers" response header contains "Accept" - And the "Access-Control-Allow-Headers" response header contains "Content-Type" - And the "Access-Control-Allow-Headers" response header contains "X-Imbo-Signature" - And the "Access-Control-Allow-Headers" response header contains "X-Imbo-Something" + And the "Access-Control-Allow-Methods" response header matches "/GET/" + And the "Access-Control-Allow-Methods" response header matches "/HEAD/" + And the "Access-Control-Allow-Methods" response header matches "/OPTIONS/" + And the "Access-Control-Allow-Headers" response header matches "/Accept/" + And the "Access-Control-Allow-Headers" response header matches "/Content-Type/" + And the "Access-Control-Allow-Headers" response header matches "/X-Imbo-Signature/" + And the "Access-Control-Allow-Headers" response header matches "/X-Imbo-Something/" And the "Access-Control-Max-Age" response header is "1349" - And the "Vary" response header contains "Origin" - And the "Allow" response header contains "GET" - And the "Allow" response header contains "HEAD" - And the "Allow" response header contains "OPTIONS" + And the "Vary" response header matches "/Origin/" + And the "Allow" response header matches "/GET/" + And the "Allow" response header matches "/HEAD/" + And the "Allow" response header matches "/OPTIONS/" Scenario: Request a resource using HTTP OPTIONS using a non-allowed host Given the "Origin" request header is "http://imbo" And Imbo uses the "cors.php" configuration When I request "/" using HTTP "OPTIONS" - Then I should get a response with "204 No Content" - And the "Vary" response header contains "Origin" - And the "Allow" response header contains "GET" - And the "Allow" response header contains "HEAD" - And the "Allow" response header contains "OPTIONS" - And the following response headers should not be present: - """ - Access-Control-Allow-Origin - Access-Control-Allow-Methods - Access-Control-Allow-Headers - Access-Control-Max-Age - """ + Then the response status line is "204 No Content" + And the "Vary" response header matches "/Origin/" + And the "Allow" response header matches "/GET/" + And the "Allow" response header matches "/HEAD/" + And the "Allow" response header matches "/OPTIONS/" + And the "Access-Control-Allow-Origin" response header does not exist + And the "Access-Control-Allow-Methods" response header does not exist + And the "Access-Control-Allow-Headers" response header does not exist + And the "Access-Control-Max-Age" response header does not exist Scenario: Provides CORS headers when applications fails - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And Imbo uses the "cors.php" configuration And the "Origin" request header is "http://allowedhost" And I sign the request - And I attach "ChangeLog.markdown" to the request body + And the request body contains "ChangeLog.markdown" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "415 Unsupported image type: text/plain" - And the "Vary" response header contains "Origin" - And the following response headers should be present: - """ - Access-Control-Allow-Origin - """ + Then the response status line is "415 Unsupported image type: text/plain" + And the "Vary" response header matches "/Origin/" + And the "Access-Control-Allow-Origin" response header exists Scenario: Provides CORS headers when authentication fails Given I use "invalid-pubkey" and "invalid-privkey" for public and private keys And Imbo uses the "cors.php" configuration And the "Origin" request header is "http://allowedhost" When I request "/users/user/images" using HTTP "GET" - Then I should get a response with "400 Permission denied (public key)" - And the following response headers should be present: - """ - Access-Control-Allow-Origin - """ + Then the response status line is "400 Permission denied (public key)" + And the "Access-Control-Allow-Origin" response header exists Scenario: Request a resource using HTTP OPTIONS without an Origin header when all origins are accepted Given Imbo uses the "cors-wildcard.php" configuration When I request "/" using HTTP "OPTIONS" - Then I should get a response with "204 No Content" - And the "Vary" response header contains "Origin" - And the following response headers should not be present: - """ - Access-Control-Allow-Origin - Access-Control-Allow-Methods - Access-Control-Allow-Headers - Access-Control-Max-Age - """ + Then the response status line is "204 No Content" + And the "Vary" response header matches "/Origin/" + And the "Access-Control-Allow-Origin" response header does not exist + And the "Access-Control-Allow-Methods" response header does not exist + And the "Access-Control-Allow-Headers" response header does not exist + And the "Access-Control-Max-Age" response header does not exist Scenario: Request a resource without an Origin header when all origins are accepted Given Imbo uses the "cors-wildcard.php" configuration When I request "/" using HTTP "GET" - Then I should get a response with "200 Hell Yeah" - And the "Vary" response header contains "Origin" - And the following response headers should not be present: - """ - Access-Control-Allow-Origin - Access-Control-Allow-Methods - Access-Control-Allow-Headers - Access-Control-Max-Age - """ + Then the response status line is "200 Hell Yeah" + And the "Vary" response header matches "/Origin/" + And the "Access-Control-Allow-Origin" response header does not exist + And the "Access-Control-Allow-Methods" response header does not exist + And the "Access-Control-Allow-Headers" response header does not exist + And the "Access-Control-Max-Age" response header does not exist diff --git a/tests/behat/features/custom-event-listeners.feature b/tests/behat/features/custom-event-listeners.feature index a86d554d8..cc38367c2 100644 --- a/tests/behat/features/custom-event-listeners.feature +++ b/tests/behat/features/custom-event-listeners.feature @@ -7,15 +7,15 @@ Feature: Imbo supports custom event handlers in the configuration Given the "Accept" request header is "application/json" And Imbo uses the "custom-event-listeners.php" configuration When I request "/" - Then the "X-Imbo-SomeHandler" response header matches "\d+\.\d+" - And the "X-Imbo-SomeOtherHandler" response header matches "\d+\.\d+" + Then the "X-Imbo-SomeHandler" response header matches "/\d+\.\d+/" + And the "X-Imbo-SomeOtherHandler" response header matches "/\d+\.\d+/" Scenario: Register an event handler with multiple events Given the "Accept" request header is "application/json" And Imbo uses the "custom-event-listeners.php" configuration When I request "/" using HTTP "HEAD" Then the "X-Imbo-SomeHandler" response header does not exist - And the "X-Imbo-SomeOtherHandler" response header matches "\d+\.\d+" + And the "X-Imbo-SomeOtherHandler" response header matches "/\d+\.\d+/" Scenario: Register an event handler by specifying an implementation of an event listener Given the "Accept" request header is "application/json" @@ -25,15 +25,15 @@ Feature: Imbo supports custom event handlers in the configuration And the "X-Imbo-Value2" response header is "value2" Scenario: Register an event listener that will only trigger for some users - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And Imbo uses the "custom-event-listeners.php" configuration - And I include an access token in the query + And I include an access token in the query string When I request "/users/user.json" Then the "X-Imbo-CurrentUser" response header is "user" Scenario: Register an event listener that will only trigger for a given user and make a request to another key Given I use "user" and "key" for public and private keys And Imbo uses the "custom-event-listeners.php" configuration - And I include an access token in the query + And I include an access token in the query string When I request "/users/user.json" Then the "X-Imbo-CurrentUser" response header does not exist diff --git a/tests/behat/features/custom-resource.feature b/tests/behat/features/custom-resource.feature index 66e5815c7..fafcc5fd8 100644 --- a/tests/behat/features/custom-resource.feature +++ b/tests/behat/features/custom-resource.feature @@ -7,7 +7,7 @@ Feature: Imbo supports custom resources Given the "Accept" request header is "application/json" And Imbo uses the "custom-routes-and-resources.php" configuration When I request "/custom/1234567" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" And the response body is: """ @@ -17,7 +17,7 @@ Feature: Imbo supports custom resources Scenario: Request a custom route with a closure returning the resource in the configuration Given Imbo uses the "custom-routes-and-resources.php" configuration When I request "/custom.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" And the response body is: """ @@ -27,7 +27,7 @@ Feature: Imbo supports custom resources Scenario: Request a custom route with a closure returning the resource in the configuration using PUT Given Imbo uses the "custom-routes-and-resources.php" configuration When I request "/custom.json" using HTTP "PUT" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" And the response body is: """ diff --git a/tests/behat/features/draw-pois-transformation.feature b/tests/behat/features/draw-pois-transformation.feature index 314260751..e8e5919f9 100644 --- a/tests/behat/features/draw-pois-transformation.feature +++ b/tests/behat/features/draw-pois-transformation.feature @@ -4,49 +4,45 @@ Feature: Imbo can read POIs from metadata and draw them on images I can use the drawPois transformation Background: - Given "tests/behat/fixtures/faces.jpg" is used as the test image for the "drawpois" feature - And I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - { - "poi": [{ + Given "tests/behat/fixtures/faces.jpg" exists for user "user" with the following metadata: + """ + { + "poi": [{ "x": 362, "y": 80, "cx": 467, "cy": 203, "width": 210, "height": 245 - }, { + }, { "x": 74, "y": 237, "cx": 98, "cy": 263, "width": 48, "height": 51 - }, { + }, { "cx": 653, "cy": 185 - }] - } - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" + }] + } + """ Scenario Outline: Draw POIs on image Given I specify "" as transformation - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the pixel at coordinate "" should have a color of "" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the pixel at coordinate "" has a color of "" Examples: - | transformation | coord | color | - | drawPois | 362, 81 | #ff0000 | - | drawPois | 659, 187 | #ffcec9 | - | drawPois | 610, 187 | #ff0000 | - | drawPois:borderSize=10 | 65, 250 | #ff0000 | - | drawPois:borderSize=10 | 74, 250 | #ff0000 | - | drawPois:borderSize=10 | 75, 250 | #6b4b36 | - | drawPois:color=cc00cc | 362, 81 | #cc00cc | - | drawPois:pointSize=50,borderSize=5 | 608, 242 | #ff0000 | + | transformation | coord | color | + | drawPois | 362,81 | #ff0000 | + | drawPois | 659,187 | #ffcec9 | + | drawPois | 610,187 | #ff0000 | + | drawPois:borderSize=10 | 65,250 | #ff0000 | + | drawPois:borderSize=10 | 74,250 | #ff0000 | + | drawPois:borderSize=10 | 75,250 | #6b4b36 | + | drawPois:color=cc00cc | 362,81 | #cc00cc | + | drawPois:pointSize=50,borderSize=5 | 608,242 | #ff0000 | diff --git a/tests/behat/features/etags.feature b/tests/behat/features/etags.feature index 6daf928d0..6193c50c5 100644 --- a/tests/behat/features/etags.feature +++ b/tests/behat/features/etags.feature @@ -4,50 +4,49 @@ Feature: Imbo adds ETag's to some responses I can specify ETag's in some responses Background: - Given "tests/phpunit/Fixtures/image.png" is used as the test image for the "etags" feature - And I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/image.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys Scenario: Index resource does not contain any Etag header When I request "/" - Then I should get a response with "200 Hell Yeah" + Then the response status line is "200 Hell Yeah" And the "ETag" response header does not exist Scenario: Stats resource does not contain any Etag header When I request "/stats?statsAllow=*" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "ETag" response header does not exist Scenario: Status resource does not contain any Etag header When I request "/status" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "ETag" response header does not exist Scenario: User resource includes an Etag - Given I include an access token in the query + Given I include an access token in the query string When I request "/users/user" - Then I should get a response with "200 OK" - And the "ETag" response header matches ""[a-z0-9]{32}"" + Then the response status line is "200 OK" + And the "ETag" response header matches "/[a-z0-9]{32}/" Scenario: Images resource includes an Etag - Given I include an access token in the query + Given I include an access token in the query string When I request "/users/user/images" - Then I should get a response with "200 OK" - And the "ETag" response header matches ""[a-z0-9]{32}"" + Then the response status line is "200 OK" + And the "ETag" response header matches "/[a-z0-9]{32}/" Scenario: Different image formats result in different ETag's - Given I include an access token in the query - When I request the test image as a "png" - And I request the test image as a "jpg" - And I request the test image as a "gif" - Then the "etag" response header is not the same for any of the requests + Given I include an access token in the query string for all requests + When I request the previously added image as a "png" + And I request the previously added image as a "jpg" + And I request the previously added image as a "gif" + Then the last 3 "etag" response headers are not the same Scenario: Metadata resource includes an ETag - Given I include an access token in the query - When I request the metadata of the test image - Then I should get a response with "200 OK" - And the "ETag" response header matches ""[a-z0-9]{32}"" + Given I include an access token in the query string + When I request the metadata of the previously added image + Then the "ETag" response header matches "/[a-z0-9]{32}/" Scenario: Responses that is not 200 OK does not get ETags When I request "/users/user" - Then I should get a response with "400 Missing access token" + Then the response status line is "400 Missing access token" And the "ETag" response header does not exist diff --git a/tests/behat/features/exif-metadata-event-listener.feature b/tests/behat/features/exif-metadata-event-listener.feature index 610f71b0e..c2c4f9b7a 100644 --- a/tests/behat/features/exif-metadata-event-listener.feature +++ b/tests/behat/features/exif-metadata-event-listener.feature @@ -7,49 +7,39 @@ Feature: Imbo provides an event listener for turning EXIF data into metadata Given Imbo uses the "add-exif-data-as-metadata.php" configuration Scenario: Fetch the added metadata - Given "tests/phpunit/Fixtures/exif-logo.jpg" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given "tests/phpunit/Fixtures/exif-logo.jpg" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request the metadata of the previously added image - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body contains: - """ - "exif:Make":"SAMSUNG" - """ - And the response body contains: - """ - "exif:Model":"GT-I9100" - """ - And the response body contains: - """ - "exif:GPSAltitude":"254\/5" - """ - And the response body contains: - """ - "exif:GPSLatitude":"63\/1, 40\/1, 173857\/3507" - """ - And the response body contains: - """ - "exif:GPSLongitude":"9\/1, 5\/1, 38109\/12500" - """ - And the response body contains: - """ - "gps:location":[9.0841802,63.680437300003] - """ - And the response body contains: - """ - "gps:altitude":50.8 - """ + And the response body contains JSON: + """ + { + "exif:Make": "SAMSUNG", + "exif:Model": "GT-I9100", + "exif:GPSAltitude": "254\/5", + "exif:GPSLatitude": "63\/1, 40\/1, 173857\/3507", + "exif:GPSLongitude": "9\/1, 5\/1, 38109\/12500", + "gps:location": + [ + 9.0841802, + 63.680437300003 + ], + "gps:altitude": 50.8 + } + """ Scenario: Metadata is normalized - Given "tests/phpunit/Fixtures/logo-horizontal.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given "tests/phpunit/Fixtures/logo-horizontal.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request the metadata of the previously added image - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body contains: - """ - "png:IHDR:bit-depth":"8" - """ + And the response body contains JSON: + """ + { + "png:IHDR:bit-depth": "8" + } + """ diff --git a/tests/behat/features/global-images.feature b/tests/behat/features/global-images.feature index e7a9cfe9e..51b9cffb6 100644 --- a/tests/behat/features/global-images.feature +++ b/tests/behat/features/global-images.feature @@ -4,54 +4,137 @@ Feature: Imbo provides a global images endpoint I want to make requests against the images endpoint Background: - Given Imbo starts with an empty database - And "tests/phpunit/Fixtures/image1.png" exists for user "user" in Imbo - And "tests/phpunit/Fixtures/image.jpg" exists for user "user" in Imbo - And "tests/phpunit/Fixtures/image.gif" exists for user "other-user" in Imbo - And "tests/phpunit/Fixtures/1024x256.png" exists for user "other-user" in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And "tests/phpunit/Fixtures/image.jpg" exists for user "user" + And "tests/phpunit/Fixtures/image.gif" exists for user "other-user" + And "tests/phpunit/Fixtures/1024x256.png" exists for user "other-user" Scenario: Fetch images without specifying any users - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/images.json" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"search":{"hits":0# - """ + And the response body contains JSON: + """ + { + "search": + { + "hits": 0, + "page": 1, + "limit": 20, + "count": 0 + }, + + "images": "@arrayLength(0)" + } + """ Scenario: Fetch images for user with wildcard access Given I use "wildcard" and "*" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/images.json?users[]=user&users[]=other-user" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"search":{"hits":4# - """ + And the response body contains JSON: + """ + { + "search": + { + "hits": 4, + "page": 1, + "limit": 20, + "count": 4 + }, + "images": "@arrayLength(4)", + "images[0]": + { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "@regExp(/^[a-z0-9]{32}$/)", + "originalChecksum": "@regExp(/^[a-z0-9]{32}$/)", + "extension": "@regExp(/^(jpg|png|gif)$/)", + "size": "@variableType(int)", + "width": "@variableType(int)", + "height": "@variableType(int)", + "mime": "@regExp(#^image/(jpeg|gif|png)$#)", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "@regExp(/^(other-)?user$/)" + }, + "images[1]": { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "@regExp(/^[a-z0-9]{32}$/)", + "originalChecksum": "@regExp(/^[a-z0-9]{32}$/)", + "extension": "@regExp(/^(jpg|png|gif)$/)", + "size": "@variableType(int)", + "width": "@variableType(int)", + "height": "@variableType(int)", + "mime": "@regExp(#^image/(jpeg|gif|png)$#)", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "@regExp(/^(other-)?user$/)" + }, + "images[2]": { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "@regExp(/^[a-z0-9]{32}$/)", + "originalChecksum": "@regExp(/^[a-z0-9]{32}$/)", + "extension": "@regExp(/^(jpg|png|gif)$/)", + "size": "@variableType(int)", + "width": "@variableType(int)", + "height": "@variableType(int)", + "mime": "@regExp(#^image/(jpeg|gif|png)$#)", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "@regExp(/^(other-)?user$/)" + }, + "images[3]": { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "@regExp(/^[a-z0-9]{32}$/)", + "originalChecksum": "@regExp(/^[a-z0-9]{32}$/)", + "extension": "@regExp(/^(jpg|png|gif)$/)", + "size": "@variableType(int)", + "width": "@variableType(int)", + "height": "@variableType(int)", + "mime": "@regExp(#^image/(jpeg|gif|png)$#)", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "@regExp(/^(other-)?user$/)" + } + } + """ Scenario Outline: Fetch images specifying users - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/images.json" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/images.json" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - - """ + And the response body contains JSON: + """ + + """ Examples: - | queryString | response | - | ?users[]=user&users[]=other-user | #^{"search":{"hits":4,"page":1,"limit":20,"count":4}# | - | ?users[]=user | #^{"search":{"hits":2,"page":1,"limit":20,"count":2}# | - | ?users[]=other-user | #^{"search":{"hits":2,"page":1,"limit":20,"count":2}# | + | query-string | response | + | ?users[]=user&users[]=other-user | {"search":{"hits":4,"page":1,"limit":20,"count":4}, "images": "@arrayLength(4)"} | + | ?users[]=user | {"search":{"hits":2,"page":1,"limit":20,"count":2}, "images": "@arrayLength(2)"} | + | ?users[]=other-user | {"search":{"hits":2,"page":1,"limit":20,"count":2}, "images": "@arrayLength(2)"} | Scenario: Fetch images specifying users that the publickey does not have access to - Given I use "unpriviledged" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "unpriviledged" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/images.json?users[]=foo&users[]=bar" - Then I should get a response with "400 Public key does not have access to the users: [foo, bar]" + Then the response status line is "400 Public key does not have access to the users: [foo, bar]" And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + { + "error": + { + "code": 400, + "message": "Public key does not have access to the users: [foo, bar]", + "date": "@isDate()", + "imboErrorCode": 0 + } + } + """ diff --git a/tests/behat/features/group.feature b/tests/behat/features/group.feature index da5531f27..6f164c336 100644 --- a/tests/behat/features/group.feature +++ b/tests/behat/features/group.feature @@ -6,31 +6,32 @@ Feature: Imbo provides a group endpoint Background: Given Imbo uses the "access-control.php" configuration - Scenario Outline: Fetch resources of a group + Scenario: Fetch resources of a group Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query - When I request "/groups/images-read." - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^{"name":"images-read","resources":\["images\.get","images\.head"]}$# | + And I include an access token in the query string + When I request "/groups/images-read.json" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + { + "name": "images-read", + "resources": ["images.get", "images.head"] + } + """ Scenario Outline: Create a resource group with invalid data Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" And I use "acl-creator" and "someprivkey" for public and private keys - And the request body contains: + And I sign the request + And the request body is: """ """ - And I sign the request When I request "/groups/read-images" using HTTP "PUT" - Then I should get a response with "" + Then the response status line is "" + Examples: | data | response | | | 400 Invalid data. Array of resource strings is expected | @@ -43,25 +44,25 @@ Feature: Imbo provides a group endpoint Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" And I use "acl-creator" and "someprivkey" for public and private keys - And the request body contains: + And I sign the request + And the request body is: """ ["images.get"] """ - And I sign the request When I request "/groups/read-images" using HTTP "PUT" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" Scenario: Update a resource group Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" And I use "acl-creator" and "someprivkey" for public and private keys - And the request body contains: + And I sign the request + And the request body is: """ ["images.get", "images.head"] """ - And I sign the request When I request "/groups/existing-group" using HTTP "PUT" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Delete a resource group Given Imbo uses the "access-control-mutable.php" configuration @@ -69,33 +70,33 @@ Feature: Imbo provides a group endpoint And I use "acl-creator" and "someprivkey" for public and private keys And I sign the request When I request "/groups/existing-group" using HTTP "DELETE" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" Scenario: Delete a resource group that has access-control rules that depends on it Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" - And I use "acl-creator" and "someprivkey" for public and private keys + And I use "master-pubkey" and "master-privkey" for public and private keys And I sign the request When I request "/groups/user-stats" using HTTP "DELETE" - Then I should get a response with "200 OK" - And the ACL rule under public key "group-based" with ID "100000000000000000001942" should not exist anymore + Then the response status line is "200 OK" + And the ACL rule under public key "group-based" with ID "100000000000000000001942" no longer exists Scenario: Delete a resource group with an immutable access control adapter Given I use "valid-group-pubkey" and "foobar" for public and private keys And I sign the request When I request "/groups/groups-read" using HTTP "DELETE" - Then I should get a response with "405 Access control adapter is immutable" + Then the response status line is "405 Access control adapter is immutable" And the "Content-Type" response header is "application/json" And the Imbo error message is "Access control adapter is immutable" and the error code is "0" Scenario: Update a resource group with an immutable access control adapter Given I use "valid-group-pubkey" and "foobar" for public and private keys - And the request body contains: + And I sign the request + And the request body is: """ ["images.get"] """ - And I sign the request When I request "/groups/groups-read" using HTTP "PUT" - Then I should get a response with "405 Access control adapter is immutable" + Then the response status line is "405 Access control adapter is immutable" And the "Content-Type" response header is "application/json" And the Imbo error message is "Access control adapter is immutable" and the error code is "0" diff --git a/tests/behat/features/groups.feature b/tests/behat/features/groups.feature index c2011c079..375a83f42 100644 --- a/tests/behat/features/groups.feature +++ b/tests/behat/features/groups.feature @@ -6,29 +6,46 @@ Feature: Imbo provides a groups endpoint Background: Given Imbo uses the "access-control.php" configuration - Scenario Outline: Fetch list of groups + Scenario: Fetch list of groups Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query - When I request "/groups." - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^{"search":{"hits":2,"page":1,"limit":20,"count":2},"groups":\[{"name":"images-read","resources":\["images\.get","images\.head"]},{"name":"groups-read","resources":\["group\.get","group\.head","groups\.get","groups\.head"]}]}$# | + And I include an access token in the query string + When I request "/groups.json" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + { + "search": + { + "hits":2, + "page":1, + "limit":20, + "count":2 + }, + "groups": + [ + { + "name":"images-read", + "resources": ["images.get","images.head"] + }, + { + "name":"groups-read", + "resources": ["group.get","group.head","groups.get","groups.head"] + } + ] + } + """ Scenario Outline: Fetch a list of groups with limit + paging Given I use "valid-group-pubkey" and "foobar" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/groups.json?limit=1&page=" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the response body is: - """ - - """ + """ + + """ + Examples: | page | response | | 1 | {"search":{"hits":2,"page":1,"limit":1,"count":1},"groups":[{"name":"images-read","resources":["images.get","images.head"]}]} | @@ -38,15 +55,14 @@ Feature: Imbo provides a groups endpoint Given Imbo uses the "access-control-mutable.php" configuration And I prime the database with "access-control-mutable.php" And I use "acl-creator" and "someprivkey" for public and private keys - And I include an access token in the query + And I include an access token in the query string When I request "/groups.json?limit=2" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the response body is: - """ - {"search":{"hits":3,"page":1,"limit":2,"count":2},"groups":[{"name":"existing-group","resources":["group.get","group.head"]},{"name":"user-stats","resources":["user.get","user.head"]}]} - """ + """ + {"search":{"hits":3,"page":1,"limit":2,"count":2},"groups":[{"name":"existing-group","resources":["group.get","group.head"]},{"name":"user-stats","resources":["user.get","user.head"]}]} + """ Scenario: Fetch list of groups without specifying a public key - Given I do not specify a public and private key When I request "/groups.json" - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" diff --git a/tests/behat/features/head.feature b/tests/behat/features/head.feature index 723f29388..c4116e9a4 100644 --- a/tests/behat/features/head.feature +++ b/tests/behat/features/head.feature @@ -4,64 +4,52 @@ Feature: Imbo supports HTTP HEAD for all resources I can make requests using HTTP HEAD and get the same headers as if I did a GET Background: - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" Scenario: Request status information When I request "/status" using HTTP "HEAD" - And make the same request using HTTP "GET" - Then the following response headers should be the same: - """ - cache-control - allow - vary - content-type - content-length - """ + And I replay the last request using HTTP "GET" + Then the last 2 "cache-control" response headers are the same + Then the last 2 "allow" response headers are the same + Then the last 2 "vary" response headers are the same + Then the last 2 "content-type" response headers are the same + Then the last 2 "content-length" response headers are the same Scenario: Request user information - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user" using HTTP "HEAD" - And make the same request using HTTP "GET" - Then the following response headers should be the same: - """ - cache-control - allow - vary - content-type - content-length - """ + And I replay the last request using HTTP "GET" + Then the last 2 "cache-control" response headers are the same + Then the last 2 "allow" response headers are the same + Then the last 2 "vary" response headers are the same + Then the last 2 "content-type" response headers are the same + Then the last 2 "content-length" response headers are the same Scenario: Request user images using a valid access token - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images" using HTTP "HEAD" - And make the same request using HTTP "GET" - Then the following response headers should be the same: - """ - cache-control - allow - vary - content-type - content-length - """ + And I replay the last request using HTTP "GET" + Then the last 2 "cache-control" response headers are the same + Then the last 2 "allow" response headers are the same + Then the last 2 "vary" response headers are the same + Then the last 2 "content-type" response headers are the same + Then the last 2 "content-length" response headers are the same Scenario: Fetch image information - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "image/png" When I request the previously added image using HTTP "HEAD" - And make the same request using HTTP "GET" - Then the following response headers should be the same: - """ - cache-control - allow - vary - content-type - content-length - X-Imbo-Originalextension - X-Imbo-Originalfilesize - X-Imbo-Originalheight - X-Imbo-Originalmimetype - X-Imbo-Originalwidth - """ + And I replay the last request using HTTP "GET" + Then the last 2 "cache-control" response headers are the same + Then the last 2 "allow" response headers are the same + Then the last 2 "vary" response headers are the same + Then the last 2 "content-type" response headers are the same + Then the last 2 "content-length" response headers are the same + Then the last 2 "X-imbo-originalextension" response headers are the same + Then the last 2 "X-imbo-originalfilesize" response headers are the same + Then the last 2 "X-imbo-originalheight" response headers are the same + Then the last 2 "X-imbo-originalmimetype" response headers are the same + Then the last 2 "X-imbo-originalwidth" response headers are the same diff --git a/tests/behat/features/image-identifier-generator-md5.feature b/tests/behat/features/image-identifier-generator-md5.feature index c4eeb8c88..29d923b68 100644 --- a/tests/behat/features/image-identifier-generator-md5.feature +++ b/tests/behat/features/image-identifier-generator-md5.feature @@ -5,28 +5,38 @@ Feature: Imbo supports generation of md5 image identifiers Background: Given Imbo uses the "image-identifier-md5.php" configuration - And "tests/phpunit/Fixtures/image1.png" exists in Imbo Scenario: Add a new image - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/image.jpg" to the request body + And the request body contains "tests/phpunit/Fixtures/image.jpg" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - /{"imageIdentifier":"f3210f1bb34bfbfa432cc3560be40761".*}/ - """ + And the response body contains JSON: + """ + { + "imageIdentifier": "f3210f1bb34bfbfa432cc3560be40761", + "width": 665, + "height": 463, + "extension": "jpg" + } + """ Scenario: Add an image that already exists - Given I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/image1.png" to the request body + And the request body contains "tests/phpunit/Fixtures/image1.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - /{"imageIdentifier":"fc7d2d06993047a0b5056e8fac4462a2".*}/ - """ + And the response body contains JSON: + """ + { + "imageIdentifier": "fc7d2d06993047a0b5056e8fac4462a2", + "width": 599, + "height": 417, + "extension": "png" + } + """ diff --git a/tests/behat/features/image-transformation-cache-listener.feature b/tests/behat/features/image-transformation-cache-listener.feature index 0c0b5b9c6..ab7a377e6 100644 --- a/tests/behat/features/image-transformation-cache-listener.feature +++ b/tests/behat/features/image-transformation-cache-listener.feature @@ -4,55 +4,62 @@ Feature: Imbo enables caching of transformations I will cache and re-use transformed images Background: - Given Imbo uses the "image-transformation-cache.php" configuration - And I use "publickey" and "privatekey" for public and private keys - And "tests/phpunit/Fixtures/image1.png" is used as the test image for the "transformation cache" feature + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And Imbo uses the "image-transformation-cache.php" configuration + And I use "publicKey" and "privateKey" for public and private keys Scenario: Fetch uncached image, then fetch same image from cache - Given I include an access token in the query - When I request the test image as a "jpg" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/jpeg" - And the "X-Imbo-TransformationCache" response header is "Miss" - When I request the test image as a "jpg" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/jpeg" - And the "X-Imbo-TransformationCache" response header is "Hit" + When I request: + | path | extension | method | access token | + | previously added image | jpg | GET | yes | + | previously added image | jpg | GET | yes | + + Then the last responses match: + | response | status line | header name | header value | + | 1 | 200 OK | content-type | image/jpeg | + | 1 | | X-Imbo-TransformationCache | Miss | + | 2 | 200 OK | content-type | image/jpeg | + | 2 | | X-Imbo-TransformationCache | Hit | Scenario: Fetch the same image, but with a different extension - Given I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/png" - And the "X-Imbo-TransformationCache" response header is "Miss" - And the checksum of the image is "fc7d2d06993047a0b5056e8fac4462a2" - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/png" - And the "X-Imbo-TransformationCache" response header is "Hit" - And the checksum of the image is "fc7d2d06993047a0b5056e8fac4462a2" + When I request: + | path | extension | method | access token | + | previously added image | png | GET | yes | + | previously added image | png | GET | yes | + + Then the last responses match: + | response | status line | header name | header value | checksum | + | 1 | 200 OK | content-type | image/png | fc7d2d06993047a0b5056e8fac4462a2 | + | 1 | | X-Imbo-TransformationCache | Miss | | + | 2 | 200 OK | content-type | image/png | fc7d2d06993047a0b5056e8fac4462a2 | + | 2 | | X-Imbo-TransformationCache | Hit | | Scenario: Fetch image with extra transformations added - Given I include an access token in the query - And I specify "crop:width=50,height=60,x=1,y=10" as transformation - When I request the test image as a "jpg" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/jpeg" - And the "X-Imbo-TransformationCache" response header is "Miss" - And the width of the image is "50" - And the height of the image is "60" - When I request the test image as a "jpg" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "image/jpeg" - And the "X-Imbo-TransformationCache" response header is "Hit" - And the width of the image is "50" - And the height of the image is "60" - - Scenario: Delete an image, which will also delete the transformed images from the cache - Given I use "publickey" and "privatekey" for public and private keys - And I sign the request - When I request the test image using HTTP "DELETE" - Then I should get a response with "200 OK" - When I include an access token in the query - And I request the test image as a "jpg" - Then I should get a response with "404 Image not found" + When I request: + | path | transformation | extension | method | access token | + | previously added image | crop:width=50,height=60,x=1,y=10 | jpg | GET | yes | + | previously added image | crop:width=50,height=60,x=1,y=10 | jpg | GET | yes | + + Then the last responses match: + | response | status line | header name | header value | image width | image height | + | 1 | 200 OK | content-type | image/jpeg | 50 | 60 | + | 1 | | X-Imbo-TransformationCache | Miss | | | + | 2 | 200 OK | content-type | image/jpeg | 50 | 60 | + | 2 | | X-Imbo-TransformationCache | Hit | | | + + Scenario: Fetch an image to place it in the transformation cache, then delete it, and fetch it again + When I request: + | path | extension | method | sign request | access token | + | previously added image | jpg | GET | | yes | + | previously added image | jpg | GET | | yes | + | previously added image | | DELETE | yes | | + | previously added image | jpg | GET | | yes | + + Then the last responses match: + | response | status line | header name | header value | + | 1 | 200 OK | content-type | image/jpeg | + | 1 | | X-Imbo-TransformationCache | Miss | + | 2 | 200 OK | content-type | image/jpeg | + | 2 | | X-Imbo-TransformationCache | Hit | + | 3 | 200 OK | | | + | 4 | 404 Image not found | | | diff --git a/tests/behat/features/image-transformations.feature b/tests/behat/features/image-transformations.feature index 5f7f3a01a..2790074e9 100644 --- a/tests/behat/features/image-transformations.feature +++ b/tests/behat/features/image-transformations.feature @@ -4,95 +4,90 @@ Feature: Imbo enables dynamic transformations of images I can specify image transformations as query parameters Background: - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo - And "tests/phpunit/Fixtures/image.png" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + And Imbo uses the "image-transformation-presets.php" configuration Scenario Outline: Transform the image - Given I use "publickey" and "privatekey" for public and private keys - And I specify "" as transformation - And I include an access token in the query - And Imbo uses the "image-transformation-presets.php" configuration + Given I specify "" as transformation When I request the image resource for "tests/phpunit/Fixtures/image1.png" as a "png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/png" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" And the "X-Imbo-Originalheight" response header is "417" And the "X-Imbo-Originalmimetype" response header is "image/png" And the "X-Imbo-Originalwidth" response header is "599" - And the width of the image is "" - And the height of the image is "" + And the image dimension is "x" Examples: - | transformation | width | height | - | blur:radius=2,sigma=4 | 599 | 417 | - | blur:angle=5,type=radial | 599 | 417 | - | blur:radius=20,sigma=10,angle=70,type=motion | 599 | 417 | - | blur:radius=2,sigma=4,type=adaptive | 599 | 417 | - | border | 601 | 419 | - | border:width=4,height=5 | 607 | 427 | - | border:mode=inline,width=4,height=5 | 599 | 417 | - | canvas | 599 | 417 | - | canvas:width=700,height=600 | 700 | 600 | - | contrast | 599 | 417 | - | contrast:sharpen:-1 | 599 | 417 | - | contrast:sharpen:1 | 599 | 417 | - | crop:width=50,height=60,x=1,y=10 | 50 | 60 | - | crop:width=599,height=417,x=0,y=0 | 599 | 417 | - | crop:mode=center,width=100,height=100 | 100 | 100 | - | crop:mode=center,width=599,height=417 | 599 | 417 | - | crop:mode=center-x,y=10,width=123,height=20 | 123 | 20 | - | crop:mode=center-y,x=10,width=234,height=30 | 234 | 30 | - | desaturate | 599 | 417 | - | flipHorizontally | 599 | 417 | - | flipVertically | 599 | 417 | - | histogram | 256 | 158 | - | histogram:scale=2 | 512 | 316 | - | histogram:scale=2,ratio=2 | 512 | 256 | - | level | 599 | 417 | - | level:channel=r,amount=40 | 599 | 417 | - | level:channel=cm,amount=-30 | 599 | 417 | - | maxSize:width=200 | 200 | 139 | - | maxSize:height=200 | 287 | 200 | - | maxSize:width=100,height=100 | 100 | 70 | - | modulate | 599 | 417 | - | modulate:b=1,s=2 | 599 | 417 | - | modulate:b=1,s=2,h=3 | 599 | 417 | - | progressive | 599 | 417 | - | resize:width=100 | 100 | 70 | - | resize:height=200 | 288 | 200 | - | resize:width=100,height=100 | 100 | 100 | - | rotate:angle=90 | 417 | 599 | - | sepia | 599 | 417 | - | sharpen | 599 | 417 | - | sharpen:radius=2,sigma=1 | 599 | 417 | - | sharpen:radius=0,sigma=2 | 599 | 417 | - | sharpen:preset=light | 599 | 417 | - | sharpen:preset=moderate | 599 | 417 | - | sharpen:preset=strong | 599 | 417 | - | sharpen:preset=extreme | 599 | 417 | - | smartSize:width=250,height=400,poi=0\,0 | 250 | 400 | - | smartSize:width=700,height=300,poi=0\,0 | 700 | 300 | - | smartSize:width=300,height=300,poi=0\,0 | 300 | 300 | - | strip | 599 | 417 | - | thumbnail | 50 | 50 | - | thumbnail:width=40,height=30 | 40 | 30 | - | thumbnail:width=40,height=40,fit=inset | 40 | 27 | - | thumbnail:width=10,height=70,fit=inset | 10 | 6 | - | transpose | 417 | 599 | - | transverse | 417 | 599 | - | graythumb:width=40,height=40 | 40 | 40 | - | vignette | 599 | 417 | - | vignette:inner=bf1942,outer=ccc | 599 | 417 | - | vignette:inner=f00baa,outer=f0f0f0,scale=2.4 | 599 | 417 | + | transformation | width | height | + | blur:radius=2,sigma=4 | 599 | 417 | + | blur:angle=5,type=radial | 599 | 417 | + | blur:radius=20,sigma=10,angle=70,type=motion | 599 | 417 | + | blur:radius=2,sigma=4,type=adaptive | 599 | 417 | + | border | 601 | 419 | + | border:width=4,height=5 | 607 | 427 | + | border:mode=inline,width=4,height=5 | 599 | 417 | + | canvas | 599 | 417 | + | canvas:width=700,height=600 | 700 | 600 | + | contrast | 599 | 417 | + | contrast:sharpen:-1 | 599 | 417 | + | contrast:sharpen:1 | 599 | 417 | + | crop:width=50,height=60,x=1,y=10 | 50 | 60 | + | crop:width=599,height=417,x=0,y=0 | 599 | 417 | + | crop:mode=center,width=100,height=100 | 100 | 100 | + | crop:mode=center,width=599,height=417 | 599 | 417 | + | crop:mode=center-x,y=10,width=123,height=20 | 123 | 20 | + | crop:mode=center-y,x=10,width=234,height=30 | 234 | 30 | + | desaturate | 599 | 417 | + | flipHorizontally | 599 | 417 | + | flipVertically | 599 | 417 | + | histogram | 256 | 158 | + | histogram:scale=2 | 512 | 316 | + | histogram:scale=2,ratio=2 | 512 | 256 | + | level | 599 | 417 | + | level:channel=r,amount=40 | 599 | 417 | + | level:channel=cm,amount=-30 | 599 | 417 | + | maxSize:width=200 | 200 | 139 | + | maxSize:height=200 | 287 | 200 | + | maxSize:width=100,height=100 | 100 | 70 | + | modulate | 599 | 417 | + | modulate:b=1,s=2 | 599 | 417 | + | modulate:b=1,s=2,h=3 | 599 | 417 | + | progressive | 599 | 417 | + | resize:width=100 | 100 | 70 | + | resize:height=200 | 288 | 200 | + | resize:width=100,height=100 | 100 | 100 | + | rotate:angle=90 | 417 | 599 | + | sepia | 599 | 417 | + | sharpen | 599 | 417 | + | sharpen:radius=2,sigma=1 | 599 | 417 | + | sharpen:radius=0,sigma=2 | 599 | 417 | + | sharpen:preset=light | 599 | 417 | + | sharpen:preset=moderate | 599 | 417 | + | sharpen:preset=strong | 599 | 417 | + | sharpen:preset=extreme | 599 | 417 | + | smartSize:width=250,height=400,poi=0,0 | 250 | 400 | + | smartSize:width=700,height=300,poi=0,0 | 700 | 300 | + | smartSize:width=300,height=300,poi=0,0 | 300 | 300 | + | strip | 599 | 417 | + | thumbnail | 50 | 50 | + | thumbnail:width=40,height=30 | 40 | 30 | + | thumbnail:width=40,height=40,fit=inset | 40 | 27 | + | thumbnail:width=10,height=70,fit=inset | 10 | 6 | + | transpose | 417 | 599 | + | transverse | 417 | 599 | + | graythumb:width=40,height=40 | 40 | 40 | + | vignette | 599 | 417 | + | vignette:inner=bf1942,outer=ccc | 599 | 417 | + | vignette:inner=f00baa,outer=f0f0f0,scale=2.4 | 599 | 417 | Scenario Outline: Transform the image using HTTP HEAD - Given I use "publickey" and "privatekey" for public and private keys - And I specify "" as transformation - And I include an access token in the query - And Imbo uses the "image-transformation-presets.php" configuration + Given I specify "" as transformation When I request the image resource for "tests/phpunit/Fixtures/image1.png" as a "png" using HTTP "HEAD" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/png" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" @@ -101,67 +96,65 @@ Feature: Imbo enables dynamic transformations of images And the "X-Imbo-Originalwidth" response header is "599" Examples: - | transformation | - | blur:radius=2,sigma=4 | - | blur:angle=5,type=radial | - | blur:radius=20,sigma=10,angle=70,type=motion | - | blur:radius=2,sigma=4,type=adaptive | - | border | - | border:width=4,height=5 | - | border:mode=inline,width=4,height=5 | - | canvas | - | canvas:width=700,height=600 | - | contrast:sharpen=1 | - | crop:width=50,height=60,x=1,y=10 | - | crop:width=599,height=417,x=0,y=0 | - | desaturate | - | drawPois | - | flipHorizontally | - | flipVertically | - | histogram | - | histogram:scale=2 | - | histogram:scale=2,ratio=2 | - | level | - | level:channel=r | - | level:channel=cm,amount=-30 | - | maxSize:width=200 | - | maxSize:height=200 | - | maxSize:width=100,height=100 | - | modulate | - | modulate:b=1,s=2 | - | modulate:b=1,s=2,h=3 | - | progressive | - | resize:width=100 | - | resize:height=200 | - | resize:width=100,height=100 | - | rotate:angle=90 | - | sepia | - | sharpen | - | sharpen:radius=2,sigma=1 | - | sharpen:radius=0,sigma=2 | - | sharpen:preset=light | - | sharpen:preset=moderate | - | sharpen:preset=strong | - | sharpen:preset=extreme | - | smartSize:width=300,height=300,poi=0\,0 | - | strip | - | thumbnail | - | thumbnail:width=40,height=30 | - | thumbnail:width=40,height=40,fit=inset | - | thumbnail:width=10,height=70,fit=inset | - | transpose | - | transverse | - | graythumb:width=40,height=40 | - | vignette | - | vignette:inner=bf1942,outer=ccc | - | vignette:inner=f00baa,outer=f0f0f0,scale=2.4 | + | transformation | + | blur:radius=2,sigma=4 | + | blur:angle=5,type=radial | + | blur:radius=20,sigma=10,angle=70,type=motion | + | blur:radius=2,sigma=4,type=adaptive | + | border | + | border:width=4,height=5 | + | border:mode=inline,width=4,height=5 | + | canvas | + | canvas:width=700,height=600 | + | contrast:sharpen=1 | + | crop:width=50,height=60,x=1,y=10 | + | crop:width=599,height=417,x=0,y=0 | + | desaturate | + | drawPois | + | flipHorizontally | + | flipVertically | + | histogram | + | histogram:scale=2 | + | histogram:scale=2,ratio=2 | + | level | + | level:channel=r | + | level:channel=cm,amount=-30 | + | maxSize:width=200 | + | maxSize:height=200 | + | maxSize:width=100,height=100 | + | modulate | + | modulate:b=1,s=2 | + | modulate:b=1,s=2,h=3 | + | progressive | + | resize:width=100 | + | resize:height=200 | + | resize:width=100,height=100 | + | rotate:angle=90 | + | sepia | + | sharpen | + | sharpen:radius=2,sigma=1 | + | sharpen:radius=0,sigma=2 | + | sharpen:preset=light | + | sharpen:preset=moderate | + | sharpen:preset=strong | + | sharpen:preset=extreme | + | smartSize:width=300,height=300,poi=0,0 | + | strip | + | thumbnail | + | thumbnail:width=40,height=30 | + | thumbnail:width=40,height=40,fit=inset | + | thumbnail:width=10,height=70,fit=inset | + | transpose | + | transverse | + | graythumb:width=40,height=40 | + | vignette | + | vignette:inner=bf1942,outer=ccc | + | vignette:inner=f00baa,outer=f0f0f0,scale=2.4 | Scenario Outline: Gracefully handle transformation errors - Given I use "publickey" and "privatekey" for public and private keys - And I specify "" as transformation - And I include an access token in the query + Given I specify "" as transformation When I request the image resource for "tests/phpunit/Fixtures/image1.png" as a "png" - Then I should get a response with "" + Then the response status line is "" And the "Content-Type" response header is "application/json" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" @@ -190,8 +183,7 @@ Feature: Imbo enables dynamic transformations of images | smartSize:width=300 | 400 Both width and height needs to be specified | Scenario: Support multiple transformations - Given I use "publickey" and "privatekey" for public and private keys - And I specify the following transformations: + Given I specify the following transformations: """ resize:width=100,height=100 resize:width=123,height=456 @@ -206,24 +198,20 @@ Feature: Imbo enables dynamic transformations of images sepia strip """ - And I include an access token in the query When I request the image resource for "tests/phpunit/Fixtures/image1.png" as a "png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/png" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" And the "X-Imbo-Originalheight" response header is "417" And the "X-Imbo-Originalmimetype" response header is "image/png" And the "X-Imbo-Originalwidth" response header is "599" - And the width of the image is "40" - And the height of the image is "30" + And the image dimension is "40x30" Scenario Outline: Fetch different formats of the image based on the Accept header - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - And the "Accept" request header is "" + Given the "Accept" request header is "" When I request the image resource for "tests/phpunit/Fixtures/image1.png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" @@ -238,10 +226,8 @@ Feature: Imbo enables dynamic transformations of images | image/png | image/png | Scenario Outline: Fetch different formats of the image based on the image extension - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query When I request the image resource for "tests/phpunit/Fixtures/image1.png" as a "" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" diff --git a/tests/behat/features/image-variations.feature b/tests/behat/features/image-variations.feature index a5d95a1c4..59626eed2 100644 --- a/tests/behat/features/image-variations.feature +++ b/tests/behat/features/image-variations.feature @@ -5,93 +5,69 @@ Feature: Imbo provides an event listener that generates variations when adding i Background: Given Imbo uses the "image-variations.php" configuration - And "tests/phpunit/Fixtures/1024x256.png" exists in Imbo + And "tests/phpunit/Fixtures/1024x256.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string Scenario: Request an image with no transformations - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I include an access token in the query When I request the previously added image as a "png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "X-Imbo-ImageVariation" response header does not exist Scenario: Request an image with no scaling transformations - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify "desaturate" as transformation - And I include an access token in the query + Given I specify "desaturate" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "X-Imbo-ImageVariation" response header does not exist Scenario: Request an image with a resize transformation which upscales the original - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify "resize:width=2048" as transformation - And I include an access token in the query + Given I specify "resize:width=2048" as transformation When I request the previously added image as a "png" Then the "X-Imbo-ImageVariation" response header does not exist - And the width of the image is "2048" + And the image width is "2048" Scenario: Request an image with a maxSize transformation which downscales the original - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify "maxSize:width=320" as transformation - And I include an access token in the query + Given I specify "maxSize:width=320" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-Imbo-ImageVariation" response header matches "320x80" - And the width of the image is "320" + Then the response status line is "200 OK" + And the "X-Imbo-ImageVariation" response header is "320x80" + And the image width is "320" Scenario: Request an image with a maxSize transformation which only slightly downscales the original - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify "maxSize:width=1020" as transformation - And I include an access token in the query + Given I specify "maxSize:width=1020" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "X-Imbo-ImageVariation" response header does not exist - And the width of the image is "1020" + And the image width is "1020" Scenario: Request an image with a thumbnail transformation using inset mode and no width - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify "thumbnail:height=128,fit=inset" as transformation - And I include an access token in the query + Given I specify "thumbnail:height=128,fit=inset" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-Imbo-ImageVariation" response header matches "512x128" - And the width of the image is "50" + Then the response status line is "200 OK" + And the "X-Imbo-ImageVariation" response header is "512x128" + And the image width is "50" Scenario: Request an image with a maxSize and crop transformation - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify the following transformations: + Given I specify the following transformations: """ crop:width=256,height=256,x=768,y=0 maxSize:width=100 """ - And I include an access token in the query When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-Imbo-ImageVariation" response header matches "512x128" - And the pixel at coordinate "5, 5" should have a color of "#215d10" - And the width of the image is "100" - And the height of the image is "100" + Then the response status line is "200 OK" + And the "X-Imbo-ImageVariation" response header is "512x128" + And the pixel at coordinate "5,5" has a color of "#215d10" + And the image dimension is "100x100" Scenario: Request an image with crop in the middle of the chain - Given I use "publickey" and "privatekey" for public and private keys - And Imbo uses the "image-variations.php" configuration - And I specify the following transformations: + Given I specify the following transformations: """ rotate:angle=90,bg=000000 crop:width=256,height=256,x=0,y=768 maxSize:width=100 """ - And I include an access token in the query When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-Imbo-ImageVariation" response header matches "512x128" - And the pixel at coordinate "5, 5" should have a color of "#215d10" - And the width of the image is "100" - And the height of the image is "100" + Then the response status line is "200 OK" + And the "X-Imbo-ImageVariation" response header is "512x128" + And the pixel at coordinate "5,5" has a color of "#215d10" + And the image dimension is "100x100" diff --git a/tests/behat/features/image.feature b/tests/behat/features/image.feature index cf2dab08d..74cc996fa 100644 --- a/tests/behat/features/image.feature +++ b/tests/behat/features/image.feature @@ -1,54 +1,68 @@ +@resources + Feature: Imbo provides an image endpoint In order to manipulate images As an HTTP Client I want to make requests against the image endpoint Scenario: Add an image - Given I use "publickey" and "privatekey" for public and private keys + Given the request body contains "tests/phpunit/Fixtures/image1.png" + And I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/image1.png" to the request body When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":"[^"]+","width":599,"height":417,"extension":"png"}/ + { + "imageIdentifier": "@regExp(/^[a-zA-Z0-9_-]{12}$/)", + "width": 599, + "height": 417, + "extension": "png" + } """ Scenario: Add an image that already exists - Given I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And the request body contains "tests/phpunit/Fixtures/image1.png" + And I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/image1.png" to the request body When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":"[^"]+","width":599,"height":417,"extension":"png"}/ + { + "imageIdentifier": "@regExp(/^[a-zA-Z0-9_-]{12}$/)", + "width": 599, + "height": 417, + "extension": "png" + } """ Scenario: Fetch image - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "image/png" When I request the previously added image - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/png" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" And the "X-Imbo-Originalheight" response header is "417" And the "X-Imbo-Originalmimetype" response header is "image/png" And the "X-Imbo-Originalwidth" response header is "599" - And the response body length is "95576" + And the "Content-Length" response header is "95576" + And the response body size is "95576" Scenario: Fetch image information when not accepting images - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string And the "Accept" request header is "application/json" When I request the previously added image - Then I should get a response with "406 Not acceptable" + Then the response status line is "406 Not acceptable" And the "Content-Type" response header is "application/json" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "95576" @@ -57,39 +71,41 @@ Feature: Imbo provides an image endpoint And the "X-Imbo-Originalwidth" response header is "599" Scenario: Delete an image - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys And I sign the request When I request the previously added image using HTTP "DELETE" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":"[^"]+"}/ + { + "imageIdentifier":"@regExp(/^[a-zA-Z0-9_-]{12}$/)" + } """ Scenario: Delete an image that does not exist - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request When I request "/users/user/images/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" using HTTP "DELETE" - Then I should get a response with "404 Image not found" + Then the response status line is "404 Image not found" And the "Content-Type" response header is "application/json" And the Imbo error message is "Image not found" and the error code is "0" Scenario: Add a broken image - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/broken-image.jpg" to the request body + And the request body contains "tests/phpunit/Fixtures/broken-image.jpg" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "415 Invalid image" + Then the response status line is "415 Invalid image" And the "Content-Type" response header is "application/json" And the Imbo error message is "Invalid image" and the error code is "205" Scenario: Add a broken image with identifiable size - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/slightly-broken-image.png" to the request body + And the request body contains "tests/phpunit/Fixtures/slightly-broken-image.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "415 Invalid image" + Then the response status line is "415 Invalid image" And the "Content-Type" response header is "application/json" And the Imbo error message is "Invalid image" and the error code is "205" diff --git a/tests/behat/features/images.feature b/tests/behat/features/images.feature index c0e577ab3..b29fe06f8 100644 --- a/tests/behat/features/images.feature +++ b/tests/behat/features/images.feature @@ -4,149 +4,217 @@ Feature: Imbo provides an images endpoint I want to make requests against the images endpoint Background: - Given Imbo starts with an empty database - And "tests/phpunit/Fixtures/image1.png" exists in Imbo - And "tests/phpunit/Fixtures/image.jpg" exists in Imbo - And "tests/phpunit/Fixtures/image.gif" exists in Imbo - And "tests/phpunit/Fixtures/1024x256.png" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And "tests/phpunit/Fixtures/image.jpg" exists for user "user" + And "tests/phpunit/Fixtures/image.gif" exists for user "user" + And "tests/phpunit/Fixtures/1024x256.png" exists for user "user" - Scenario Outline: Fetch images with no filter - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user/images." - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^{"search":{"hits":4,"page":1,"limit":20,"count":4},"images":\[{.*?},{.*?},{.*?},{.*?}\]}$# | + Scenario: Fetch images with no filter + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user/images" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + { + "search": { + "hits": 4, + "page": 1, + "limit": 20, + "count": 4 + }, + "images": "@arrayLength(4)" + } + """ - Scenario Outline: Fetch images using limit - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user/images.?limit=2" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "" - And the response body matches: - """ - - """ - Examples: - | extension | content-type | response | - | json | application/json | #^{"search":{"hits":4,"page":1,"limit":2,"count":2},"images":\[{.*?},{.*?}\]}$# | + Scenario: Fetch images using limit + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user/images?limit=2" + Then the response status line is "200 OK" + And the "Content-Type" response header is "application/json" + And the response body contains JSON: + """ + { + "search": { + "hits": 4, + "page": 1, + "limit": 2, + "count": 2 + }, + "images": "@arrayLength(2)" + } + """ Scenario: Fetch images with a filter on non-existant image identifier - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user/images.json?ids[]=foobar" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user/images?ids[]=foobar" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"search":{.*?},"images":\[\]}$# - """ + And the response body contains JSON: + """ + { + "search": { + "hits": 0, + "page": 1, + "limit": 20, + "count": 0 + }, + "images": "@arrayLength(0)" + } + """ Scenario: Fetch images with a filter on existing image identifier - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - And I append a query string parameter, "ids[]" with the image identifier of "tests/phpunit/Fixtures/image1.png" - When I request "/users/user/images.json" with the given query string - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + And the query string parameter "ids[]" is set to the image identifier of "tests/phpunit/Fixtures/image1.png" + When I request "/users/user/images" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"search":{.*?},"images":\[{"added":"[^"]+","updated":"[^"]+","checksum":"fc7d2d06993047a0b5056e8fac4462a2","originalChecksum":"fc7d2d06993047a0b5056e8fac4462a2","extension":"png","size":95576,"width":599,"height":417,"mime":"image\\/png","imageIdentifier":".*?","user":"user"}\]}$# - """ + And the response body contains JSON: + """ + { + "search": { + "hits": 1, + "page": 1, + "limit": 20, + "count": 1 + }, + "images": "@arrayLength(1)", + "images[0]": { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "fc7d2d06993047a0b5056e8fac4462a2", + "originalChecksum": "fc7d2d06993047a0b5056e8fac4462a2", + "extension": "png", + "size": 95576, + "width": 599, + "height": 417, + "mime": "image/png", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "user" + } + } + """ Scenario Outline: Fetch images with a filter on checksums - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images.json?checksums[]=" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - - """ + And the response body contains JSON: + """ + + """ Examples: - | filter | response | - | foobar | #^{"search":{.*?},"images":\[\]}$# | - | fc7d2d06993047a0b5056e8fac4462a2 | #^{"search":{.*?},"images":\[{"added":"[^"]+","updated":"[^"]+","checksum":"fc7d2d06993047a0b5056e8fac4462a2","originalChecksum":"fc7d2d06993047a0b5056e8fac4462a2","extension":"png","size":95576,"width":599,"height":417,"mime":"image\\/png","imageIdentifier":".*?","user":"user"}\]}$# | - | f3210f1bb34bfbfa432cc3560be40761 | #^{"search":{.*?},"images":\[{"added":"[^"]+","updated":"[^"]+","checksum":"f3210f1bb34bfbfa432cc3560be40761","originalChecksum":"f3210f1bb34bfbfa432cc3560be40761","extension":"jpg","size":64828,"width":665,"height":463,"mime":"image\\/jpeg","imageIdentifier":".*?","user":"user"}\]}$# | - | b5426b4c008e378c201526d2baaec599 | #^{"search":{.*?},"images":\[{"added":"[^"]+","updated":"[^"]+","checksum":"b5426b4c008e378c201526d2baaec599","originalChecksum":"b5426b4c008e378c201526d2baaec599","extension":"gif","size":66020,"width":665,"height":463,"mime":"image\\/gif","imageIdentifier":".*?","user":"user"}\]}$# | + | filter | response | + | foobar | {"search":{"hits":0,"page":1,"limit":20,"count":0},"images":[]} | + | fc7d2d06993047a0b5056e8fac4462a2 | {"search":{"hits":1,"page":1,"limit":20,"count":1},"images":[{"added":"@isDate()","updated":"@isDate()","checksum":"fc7d2d06993047a0b5056e8fac4462a2","originalChecksum":"fc7d2d06993047a0b5056e8fac4462a2","extension":"png","size":95576,"width":599,"height":417,"mime":"image/png","imageIdentifier":"@regExp(/^[a-zA-Z0-9-_]{12}$/)","user":"user"}]} | + | f3210f1bb34bfbfa432cc3560be40761 | {"search":{"hits":1,"page":1,"limit":20,"count":1},"images":[{"added":"@isDate()","updated":"@isDate()","checksum":"f3210f1bb34bfbfa432cc3560be40761","originalChecksum":"f3210f1bb34bfbfa432cc3560be40761","extension":"jpg","size":64828,"width":665,"height":463,"mime":"image/jpeg","imageIdentifier":"@regExp(/^[a-zA-Z0-9-_]{12}$/)","user":"user"}]} | + | b5426b4c008e378c201526d2baaec599 | {"search":{"hits":1,"page":1,"limit":20,"count":1},"images":[{"added":"@isDate()","updated":"@isDate()","checksum":"b5426b4c008e378c201526d2baaec599","originalChecksum":"b5426b4c008e378c201526d2baaec599","extension":"gif","size":66020,"width":665,"height":463,"mime":"image/gif","imageIdentifier":"@regExp(/^[a-zA-Z0-9-_]{12}$/)","user":"user"}]} | Scenario Outline: Fetch images only displaying certain fields - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user/images.?" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user/images.json?" + Then the response status line is "200 OK" And the response body matches: - """ - - """ + """ + + """ Examples: - | extension | fields | response | - | json | fields[]=size | #^{"search":{.*?},"images":\[{"size":\d+},{"size":\d+},{"size":\d+},{"size":\d+}\]}$# | - | json | fields[]=width&fields[]=height | #^{"search":{.*?},"images":\[{"width":\d+,"height":\d+},{"width":\d+,"height":\d+},{"width":\d+,"height":\d+},{"width":\d+,"height":\d+}\]}$# | + | fields | response | + | fields[]=size | #^{"search":{.*?},"images":\[{"size":\d+},{"size":\d+},{"size":\d+},{"size":\d+}\]}$# | + | fields[]=width&fields[]=height | #^{"search":{.*?},"images":\[{"width":\d+,"height":\d+},{"width":\d+,"height":\d+},{"width":\d+,"height":\d+},{"width":\d+,"height":\d+}\]}$# | Scenario Outline: Fetch images with metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user/images.?metadata=1&fields[]=" - Then I should get a response with "200 OK" + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + When I request "/users/user/images.json?metadata=1&fields[]=" + Then the response status line is "200 OK" And the response body matches: - """ - - """ + """ + + """ Examples: - | extension | fields | response | - | json | size | #^{"search":{.*?},"images":\[{"size":\d+},{"size":\d+},{"size":\d+},{"size":\d+}\]}$# | - | json | metadata | #^{"search":{.*?},"images":\[{"metadata":{}},{"metadata":{}},{"metadata":{}},{"metadata":{}}\]}$# | + | fields | response | + | size | #^{"search":{.*?},"images":\[{"size":\d+},{"size":\d+},{"size":\d+},{"size":\d+}\]}$# | + | metadata | #^{"search":{.*?},"images":\[{"metadata":{}},{"metadata":{}},{"metadata":{}},{"metadata":{}}\]}$# | Scenario Outline: Fetch images and use custom sorting - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images.json?&" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the response body matches: - """ - - """ + """ + + """ Examples: - | fields | sort | response | - | fields[]=size | sort[]=size | #^{"search":{.*?},"images":\[{"size":12505},{"size":64828},{"size":66020},{"size":95576}\]}$# | - | fields[]=size | sort[]=size:desc | #^{"search":{.*?},"images":\[{"size":95576},{"size":66020},{"size":64828},{"size":12505}\]}$# | + | fields | sort | response | + | fields[]=size | sort[]=size | #^{"search":{.*?},"images":\[{"size":12505},{"size":64828},{"size":66020},{"size":95576}\]}$# | + | fields[]=size | sort[]=size:desc | #^{"search":{.*?},"images":\[{"size":95576},{"size":66020},{"size":64828},{"size":12505}\]}$# | | fields[]=size&fields[]=width | sort[]=width&sort[]=size:desc | #^{"search":{.*?},"images":\[{"size":95576,"width":599},{"size":66020,"width":665},{"size":64828,"width":665},{"size":12505,"width":1024}\]}$# | | fields[]=size&fields[]=width | sort[]=width&sort[]=size | #^{"search":{.*?},"images":\[{"size":95576,"width":599},{"size":64828,"width":665},{"size":66020,"width":665},{"size":12505,"width":1024}\]}$# | Scenario: The hits number has the number of hits in the query - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - And I append a query string parameter, "page" with the value "1" - And I append a query string parameter, "limit" with the value "1" - And I append a query string parameter, "ids[]" with the image identifier of "tests/phpunit/Fixtures/image1.png" - And I append a query string parameter, "ids[]" with the image identifier of "tests/phpunit/Fixtures/image.jpg" - When I request "/users/user/images.json" with the given query string - Then I should get a response with "200 OK" - And the response body matches: - """ - #^{"search":{"hits":2,"page":1,"limit":1,"count":1},"images":\[{.*}\]}$# - """ + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + And the query string parameter "page" is set to 1 + And the query string parameter "limit" is set to 1 + And the query string parameter "ids[]" is set to the image identifier of "tests/phpunit/Fixtures/image1.png" + And the query string parameter "ids[]" is set to the image identifier of "tests/phpunit/Fixtures/image.jpg" + When I request "/users/user/images.json" + Then the response status line is "200 OK" + And the response body contains JSON: + """ + { + "search": { + "hits": 2, + "page": 1, + "limit": 1, + "count": 1 + }, + "images": "@arrayLength(1)" + } + """ Scenario: Fetch images with a filter on original checksums - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string When I request "/users/user/images.json?originalChecksums[]=b60df41830245ee8f278e3ddfe5238a3" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"search":{.*?},"images":\[{"added":"[^"]+","updated":"[^"]+","checksum":"b60df41830245ee8f278e3ddfe5238a3","originalChecksum":"b60df41830245ee8f278e3ddfe5238a3","extension":"png","size":12505,"width":1024,"height":256,"mime":"image\\/png","imageIdentifier":"[^"]+","user":"user"}\]}$# - """ + And the response body contains JSON: + """ + { + "search": { + "hits": 1, + "page": 1, + "limit": 20, + "count": 1 + }, + "images": "@arrayLength(1)", + "images[0]": + { + "added": "@isDate()", + "updated": "@isDate()", + "checksum": "b60df41830245ee8f278e3ddfe5238a3", + "originalChecksum": "b60df41830245ee8f278e3ddfe5238a3", + "extension": "png", + "size": 12505, + "width": 1024, + "height": 256, + "mime": "image/png", + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "user": "user" + } + } + """ diff --git a/tests/behat/features/index.feature b/tests/behat/features/index.feature index bb474ea5f..5cbd7a356 100644 --- a/tests/behat/features/index.feature +++ b/tests/behat/features/index.feature @@ -1,30 +1,28 @@ +@resources + Feature: Imbo provides an index endpoint - In order to see the Imbo version + In order to see basic Imbo information As an HTTP Client I want to make requests against the index endpoint - Scenario Outline: Fetch index - Given the "Accept" request header is "" + Scenario: Fetch index When I request "/" - Then I should get a response with "200 Hell Yeah" - And the response body matches: + Then the response code is 200 + And the response reason phrase is "Hell Yeah" + And the response body contains JSON: """ - + {"site":"http://imbo.io"} """ - Examples: - | accept | response | - | application/json | #^{.*}$# | - Scenario Outline: The index endpoint only supports HTTP GET and HEAD When I request "/" using HTTP "" - Then I should get a response with "" + Then the response code is + And the response reason phrase is "" Examples: - | method | status | - | GET | 200 Hell Yeah | - | HEAD | 200 Hell Yeah | - | POST | 405 Method not allowed | - | PUT | 405 Method not allowed | - | DELETE | 405 Method not allowed | - | SEARCH | 405 Method not allowed | + | method | code | reasonPhrase | + | GET | 200 | Hell Yeah | + | HEAD | 200 | Hell Yeah | + | POST | 405 | Method not allowed | + | PUT | 405 | Method not allowed | + | DELETE | 405 | Method not allowed | diff --git a/tests/behat/features/level-transformation.feature b/tests/behat/features/level-transformation.feature index aa003198a..69ba372e8 100644 --- a/tests/behat/features/level-transformation.feature +++ b/tests/behat/features/level-transformation.feature @@ -4,20 +4,18 @@ Feature: Imbo can adjust color levels of images I can use the watermark transformation Background: - Given "tests/phpunit/Fixtures/colors.png" exists in Imbo + Given "tests/phpunit/Fixtures/colors.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string Scenario: Apply a transformation that increases level of red - Given I use "publickey" and "privatekey" for public and private keys - And I specify "level:channel=r,amount=100" as transformation - And I include an access token in the query + Given I specify "level:channel=r,amount=100" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the pixel at coordinate "5, 55" should have a color of "#de3f3f" + Then the response status line is "200 OK" + And the pixel at coordinate "5,55" has a color of "#de3f3f" Scenario: Apply a transformation that increases level for all channels - Given I use "publickey" and "privatekey" for public and private keys - And I specify "level:channel=rgb,amount=100" as transformation - And I include an access token in the query + Given I specify "level:channel=rgb,amount=100" as transformation When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the pixel at coordinate "22, 32" should have a color of "#ffed00" + Then the response status line is "200 OK" + And the pixel at coordinate "22,32" has a color of "#ffed00" diff --git a/tests/behat/features/max-image-size-event-listener.feature b/tests/behat/features/max-image-size-event-listener.feature index 3785c0058..2e47bbce5 100644 --- a/tests/behat/features/max-image-size-event-listener.feature +++ b/tests/behat/features/max-image-size-event-listener.feature @@ -3,41 +3,52 @@ Feature: Imbo provides an event listener for enforcing a max image size As an Imbo admin I must enable the MaxImageSize event listener - Scenario: Add an image that is above the maximum width - Given I use "publickey" and "privatekey" for public and private keys + Background: + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I attach "tests/phpunit/Fixtures/1024x256.png" to the request body And Imbo uses the "enforce-max-image-size.php" configuration + + Scenario: Add an image that is above the maximum width + Given the request body contains "tests/phpunit/Fixtures/1024x256.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":".*?","width":1000,"height":250,"extension":"png"}/ + { + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "width": 1000, + "height": 250, + "extension":"png" + } """ Scenario: Add an image that is above the maximum height - Given I use "publickey" and "privatekey" for public and private keys - And I sign the request - And I attach "tests/phpunit/Fixtures/256x1024.png" to the request body - And Imbo uses the "enforce-max-image-size.php" configuration + Given the request body contains "tests/phpunit/Fixtures/256x1024.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":".*?","width":150,"height":600,"extension":"png"}/ + { + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "width": 150, + "height": 600, + "extension":"png" + } """ Scenario: Add an image that is above the maximum width and height - Given I use "publickey" and "privatekey" for public and private keys - And I sign the request - And I attach "tests/phpunit/Fixtures/1024x1024.png" to the request body - And Imbo uses the "enforce-max-image-size.php" configuration + Given the request body contains "tests/phpunit/Fixtures/1024x1024.png" When I request "/users/user/images" using HTTP "POST" - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - /{"imageIdentifier":".*?","width":600,"height":600,"extension":"png"}/ + { + "imageIdentifier": "@regExp(/^[a-zA-Z0-9-_]{12}$/)", + "width": 600, + "height": 600, + "extension":"png" + } """ diff --git a/tests/behat/features/metadata.feature b/tests/behat/features/metadata.feature index f78baf3c4..5aed30d6c 100644 --- a/tests/behat/features/metadata.feature +++ b/tests/behat/features/metadata.feature @@ -4,191 +4,38 @@ Feature: Imbo provides a metadata endpoint I want to make requests against the metadata endpoint Background: - Given "tests/phpunit/Fixtures/image1.png" is used as the test image for the "metadata" feature - - Scenario Outline: Get metadata when image has no metadata attached - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{}$# | - - Scenario: Attach metadata to an image - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"foo": "bar"} - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "application/json" - And the response body is: - """ - {"foo":"bar"} - """ - - Scenario Outline: Get metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{"foo":"bar"}$# | - - Scenario: Partially update metadata - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"bar":"foo"} - """ - And I sign the request - When I request the metadata of the test image using HTTP "POST" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "application/json" - And the response body is: - """ - {"foo":"bar","bar":"foo"} - """ - - Scenario Outline: Get updated metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{"foo":"bar","bar":"foo"}$# | - - Scenario: Replace metadata - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"key": "value"} - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "application/json" - And the response body is: - """ - {"key":"value"} - """ - - Scenario Outline: Get replaced metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{"key":"value"}$# | - - Scenario: Delete metadata - Given I use "publickey" and "privatekey" for public and private keys - And I sign the request - When I request the metadata of the test image using HTTP "DELETE" - Then I should get a response with "200 OK" - And the "Content-Type" response header is "application/json" - And the response body is: - """ - {} - """ - - Scenario Outline: Get deleted metadata - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{}$# | - - Scenario: Set unparsable metadata - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {foo bar} - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "400 Invalid JSON data" - - Scenario: Set data for invalid metadata key - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"foo.bar": "bar"} - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "400 Invalid metadata. Dot characters ('.') are not allowed in metadata keys" - - Scenario Outline: Set and get metadata with nested info - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"foo": {"bar": "value", "exif:foo": "value2" } } - """ - And I sign the request - And I request the metadata of the test image using HTTP "PUT" - When I include an access token in the query - And I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{"foo":{"bar":"value","exif:foo":"value2"}}$# | - - - Scenario Outline: Set and get metadata with special characters - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"html":"
It's cool
","json":"{\"foo\":\"bar\"}","norwegian":"\u00c5tte karer m\u00f8ter \u00e6rlige Erlend"} - """ - And I sign the request - And I request the metadata of the test image using HTTP "PUT" - When I include an access token in the query - And I request the metadata of the test image as "" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | #^{"html":"
It\'s cool<\\/div>","json":"{\\"foo\\":\\"bar\\"}","norwegian":"\\u00c5tte karer m\\u00f8ter \\u00e6rlige Erlend"}$# | + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + + Scenario: Get metadata when image has no metadata attached + When I request: + | path | method | access token | sign request | request body | + | metadata of previously added image | GET | yes | | | + | metadata of previously added image | PUT | | yes | {"foo": "bar"} | + | metadata of previously added image | GET | yes | | | + | metadata of previously added image | POST | | yes | {"bar": "foo"} | + | metadata of previously added image | GET | yes | | | + | metadata of previously added image | PUT | | yes | {"key": "value"} | + | metadata of previously added image | GET | yes | | | + | metadata of previously added image | DELETE | | yes | | + | metadata of previously added image | GET | yes | | | + | metadata of previously added image | PUT | | yes | {foo bar} | + | metadata of previously added image | PUT | | yes | {"foo.bar": "bar"} | + | metadata of previously added image | PUT | | yes | {"foo": {"bar": "value", "exif:foo": "value2"}} | + | metadata of previously added image | PUT | | yes | {"html":"
It's cool
","json":"{\"foo\":\"bar\"}","norwegian":"\u00c5tte karer m\u00f8ter \u00e6rlige Erlend"} | + + Then the last responses match: + | response | status line | body is | header name | header value | + | 1 | 200 OK | {} | Content-Type | application/json | + | 2 | 200 OK | {"foo":"bar"} | Content-Type | application/json | + | 3 | 200 OK | {"foo":"bar"} | Content-Type | application/json | + | 4 | 200 OK | {"foo":"bar","bar":"foo"} | Content-Type | application/json | + | 5 | 200 OK | {"foo":"bar","bar":"foo"} | Content-Type | application/json | + | 6 | 200 OK | {"key":"value"} | Content-Type | application/json | + | 7 | 200 OK | {"key":"value"} | Content-Type | application/json | + | 8 | 200 OK | {} | Content-Type | application/json | + | 9 | 200 OK | {} | Content-Type | application/json | + | 10 | 400 Invalid JSON data | | Content-Type | application/json | + | 11 | 400 Invalid metadata. Dot characters ('.') are not allowed in metadata keys | | Content-Type | application/json | + | 12 | 200 OK | {"foo":{"bar":"value","exif:foo":"value2"}} | Content-Type | application/json | + | 13 | 200 OK | {"html":"
It's cool<\/div>","json":"{\"foo\":\"bar\"}","norwegian":"\u00c5tte karer m\u00f8ter \u00e6rlige Erlend"} | Content-Type | application/json | diff --git a/tests/behat/features/shorturls.feature b/tests/behat/features/shorturls.feature index 15cfa2e47..d67f4455e 100644 --- a/tests/behat/features/shorturls.feature +++ b/tests/behat/features/shorturls.feature @@ -3,68 +3,77 @@ Feature: Imbo can generate short URLs for images on demand As an HTTP Client I can request the short URLs resource + Background: + Given "tests/phpunit/Fixtures/image.png" exists for user "user" + Scenario: Responds with 404 when the image does not exist - Given I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And the request body contains: + And the request body is: """ {"user": "user", "imageIdentifier": "id", "extension": "gif", "query": null} """ When I request "/users/user/images/id/shorturls" using HTTP "POST" - Then I should get a response with "404 Image does not exist" + Then the response status line is "404 Image does not exist" And the "Content-Type" response header is "application/json" - And the response body matches: - """ - #^{"error":{"code":404,"message":"Image does not exist".*?,"imageIdentifier":"id"}$# - """ + And the response body contains JSON: + """ + { + "error": + { + "code": 404, + "message": "Image does not exist", + "date": "@isDate()", + "imboErrorCode": 0 + }, + "imageIdentifier": "id" + } + """ Scenario: Generate a short URL - Given "tests/phpunit/Fixtures/image.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I generate a short URL with the following parameters: + And I generate a short URL for "tests/phpunit/Fixtures/image.png" with the following parameters: """ {"user": "user", "extension": "gif"} """ - Then I should get a response with "201 Created" + Then the response status line is "201 Created" And the "Content-Type" response header is "application/json" - And the response body matches: + And the response body contains JSON: """ - #^{"id":"[a-zA-Z0-9]{7}"}$# + {"id":"@regExp(/^[a-zA-Z0-9]{7}$/)"} """ Scenario: Generate a short URL without having access to the user - Given "tests/phpunit/Fixtures/image.png" exists for user "other-user" in Imbo + Given "tests/phpunit/Fixtures/image.png" exists for user "other-user" And I use "unpriviledged" and "privatekey" for public and private keys And I sign the request - And I generate a short URL with the following parameters: + And I generate a short URL for "tests/phpunit/Fixtures/image.png" with the following parameters: """ {"user": "other-user", "extension": "gif"} """ - Then I should get a response with "400 Permission denied (public key)" + Then the response status line is "400 Permission denied (public key)" Scenario Outline: Request an image using the short URL - Given "tests/phpunit/Fixtures/image.png" exists in Imbo - And I use "publickey" and "privatekey" for public and private keys + Given I use "publicKey" and "privateKey" for public and private keys And I sign the request - And I generate a short URL with the following parameters: + And I generate a short URL for "tests/phpunit/Fixtures/image.png" with the following parameters: """ """ When I request the image using the generated short URL - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "" And the "X-Imbo-Originalextension" response header is "png" And the "X-Imbo-Originalfilesize" response header is "41423" And the "X-Imbo-Originalwidth" response header is "665" And the "X-Imbo-Originalheight" response header is "463" And the "X-Imbo-Originalmimetype" response header is "image/png" - And the width of the image is "" - And the height of the image is "" + And the image dimension is "x" Examples: - | params | mime | width | height | + | params | mime | width | height | | {"user": "user"} | image/png | 665 | 463 | | {"user": "user", "extension": "gif"} | image/gif | 665 | 463 | | {"user": "user", "query": "t[]=thumbnail"} | image/png | 50 | 50 | diff --git a/tests/behat/features/smartsize-transformation.feature b/tests/behat/features/smartsize-transformation.feature index 74e10a660..a64fb4ce1 100644 --- a/tests/behat/features/smartsize-transformation.feature +++ b/tests/behat/features/smartsize-transformation.feature @@ -4,87 +4,74 @@ Feature: Imbo can crop images using smart size and POIs I can use the smart size transformation Background: - Given "tests/behat/fixtures/trolltunga.jpg" is used as the test image for the "smartsize" feature - And I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"poi": [{"cx": 810, "cy": 568}]} - """ - And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" + Given "tests/behat/fixtures/trolltunga.jpg" exists for user "user" with the following metadata: + """ + {"poi": [{"cx": 810, "cy": 568}]} + """ + And I use "publicKey" and "privateKey" for public and private keys Scenario Outline: Smart size image - Given I include an access token in the query + Given I include an access token in the query string And I specify "" as transformation - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "" - And the height of the image is "" - And the pixel at coordinate "" should have a color of "" + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "x" + And the pixel at coordinate "" has a color of "" And the "X-Imbo-POIs-Used" response header is "1" Examples: | transformation | coord | color | width | height | - | smartsize:width=250,height=250,poi=810,568,crop=close | 0, 0 | #396473 | 250 | 250 | - | smartsize:width=250,height=250,poi=810,568,crop=medium | 0, 0 | #355170 | 250 | 250 | - | smartsize:width=250,height=250,poi=810,568,crop=wide | 0, 0 | #52607c | 250 | 250 | - - | smartsize:width=600,height=250,poi=810,568,crop=close | 0, 0 | #50748c | 600 | 250 | - | smartsize:width=600,height=250,poi=810,568,crop=medium | 0, 0 | #1d2f55 | 600 | 250 | - | smartsize:width=600,height=250,poi=810,568,crop=wide | 0, 0 | #1a2749 | 600 | 250 | - - | smartsize:width=250,height=600,poi=810,568,crop=close | 0, 0 | #5b8089 | 250 | 600 | - | smartsize:width=250,height=600,poi=810,568,crop=medium | 0, 0 | #aaab84 | 250 | 600 | - | smartsize:width=250,height=600,poi=810,568,crop=wide | 0, 0 | #fafff3 | 250 | 600 | + | smartsize:width=250,height=250,poi=810,568,crop=close | 0,0 | #396473 | 250 | 250 | + | smartsize:width=250,height=250,poi=810,568,crop=medium | 0,0 | #355170 | 250 | 250 | + | smartsize:width=250,height=250,poi=810,568,crop=wide | 0,0 | #52607c | 250 | 250 | + | smartsize:width=600,height=250,poi=810,568,crop=close | 0,0 | #50748c | 600 | 250 | + | smartsize:width=600,height=250,poi=810,568,crop=medium | 0,0 | #1d2f55 | 600 | 250 | + | smartsize:width=600,height=250,poi=810,568,crop=wide | 0,0 | #1a2749 | 600 | 250 | + | smartsize:width=250,height=600,poi=810,568,crop=close | 0,0 | #5b8089 | 250 | 600 | + | smartsize:width=250,height=600,poi=810,568,crop=medium | 0,0 | #aaab84 | 250 | 600 | + | smartsize:width=250,height=600,poi=810,568,crop=wide | 0,0 | #fafff3 | 250 | 600 | Scenario: Smart size based on POI stored in metadata Given I specify "smartsize:width=250,height=250" as transformation - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "250" - And the height of the image is "250" - And the pixel at coordinate "0, 0" should have a color of "#355170" + And I include an access token in the query string + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "250x250" + And the pixel at coordinate "0,0" has a color of "#355170" And the "X-Imbo-POIs-Used" response header is "1" Scenario: Smart size based on POI without center coordinates stored in metadata - Given I use "publickey" and "privatekey" for public and private keys - And the request body contains: - """ - {"poi": [{"x": 800, "y": 558, "width": 20, "height": 20}]} - """ + Given the request body is: + """ + {"poi": [{"x": 800, "y": 558, "width": 20, "height": 20}]} + """ And I sign the request - And I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" + And I request the metadata of the previously added image using HTTP "PUT" When I specify "smartsize:width=250,height=250" as transformation - And I include an access token in the query - And I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "250" - And the height of the image is "250" - And the pixel at coordinate "0, 0" should have a color of "#355170" + And I include an access token in the query string + And I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "250x250" + And the pixel at coordinate "0,0" has a color of "#355170" And the "X-Imbo-POIs-Used" response header is "1" Scenario Outline: Smart size falls back to simple crop/resize when no POI data is found - Given the request body contains: - """ - {} - """ + Given the request body is: + """ + {} + """ And I sign the request - When I request the metadata of the test image using HTTP "PUT" - Then I should get a response with "200 OK" - Given I include an access token in the query - And I specify "smartsize:width=,height=" as transformation - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "" - And the height of the image is "" - And the pixel at coordinate "0, 0" should have a color of "" + And I request the metadata of the previously added image using HTTP "PUT" + When I specify "smartsize:width=,height=" as transformation + And I include an access token in the query string + When I request the previously added image as a "png" + Then the response status line is "200 OK" + And the image dimension is "x" + And the pixel at coordinate "0,0" has a color of "" And the "X-Imbo-POIs-Used" response header is "0" Examples: - | coord | color | width | height | - | 0, 0 | #495569 | 250 | 250 | - | 0, 0 | #171c3a | 500 | 150 | - | 0, 0 | #feffef | 150 | 500 | + | color | width | height | + | #495569 | 250 | 250 | + | #171c3a | 500 | 150 | + | #feffef | 150 | 500 | diff --git a/tests/behat/features/stats.feature b/tests/behat/features/stats.feature index 30ba53648..1878b3132 100644 --- a/tests/behat/features/stats.feature +++ b/tests/behat/features/stats.feature @@ -1,88 +1,89 @@ +@resources + Feature: Imbo provides a stats endpoint In order to see Imbo stats As an HTTP Client I want to make requests against the stats endpoint - Background: - Given Imbo starts with an empty database - And "tests/phpunit/Fixtures/image1.png" exists in Imbo - And "tests/phpunit/Fixtures/image.jpg" exists in Imbo - And "tests/phpunit/Fixtures/image.gif" exists in Imbo - - Scenario Outline: Fetch stats - Given Imbo uses the "stats-access-and-custom-stats.php" configuration - When I request "/stats.?statsAllow=*" - Then I should get a response with "200 OK" - And the response body matches: + Scenario: Fetch stats + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And "tests/phpunit/Fixtures/image.jpg" exists for user "user" + And "tests/phpunit/Fixtures/image.gif" exists for user "user" + And Imbo uses the "stats-access-and-custom-stats.php" configuration + And the stats are allowed by "*" + When I request "/stats" + Then the response code is 200 + And the response body contains JSON: """ - + { + "numImages": 3, + "numUsers": 1, + "numBytes": 226424, + "custom": { + "someValue": 123, + "someOtherValue": { + "foo": "bar" + }, + "someList": [ + 1, 2, 3 + ] + } + } """ - Examples: - | extension | response | - | json | {"numImages":3,"numUsers":1,"numBytes":226424,"custom":{.*}} | - Scenario Outline: The stats endpoint only supports HTTP GET and HEAD Given Imbo uses the "stats-access-and-custom-stats.php" configuration - When I request "/stats.json?statsAllow=*" using HTTP "" - Then I should get a response with "" + And the stats are allowed by "*" + When I request "/stats" using HTTP "" + Then the response code is + And the response reason phrase is "" Examples: - | method | status | - | GET | 200 OK | - | HEAD | 200 OK | - | POST | 405 Method not allowed | - | PUT | 405 Method not allowed | - | DELETE | 405 Method not allowed | - | SEARCH | 405 Method not allowed | + | method | code | reasonPhrase | + | GET | 200 | OK | + | HEAD | 200 | OK | + | POST | 405 | Method not allowed | + | PUT | 405 | Method not allowed | + | DELETE | 405 | Method not allowed | Scenario Outline: Stats access event listener decides the access level for the stats endpoint Given Imbo uses the "stats-access-and-custom-stats.php" configuration + And the stats are allowed by "" And the client IP is "" - When I request "/stats.json?statsAllow=" - Then I should get a response with "" + When I request "/stats" + Then the response code is + And the response reason phrase is "" And the "Content-Type" response header is "application/json" And the response body matches: """ - #^{.*}$#ms + /^{.*}$/ms """ Examples: - | client-ip | allow | status | - | 127.0.0.1 | 10.0.0.0 | 403 Access denied | - | 127.0.0.1 | 2001:db8::/48 | 403 Access denied | - | ::1 | 2001:db8::/48 | 403 Access denied | - | ::1 | 127.0.0.1 | 403 Access denied | - | 127.0.0.1 | 127.0.0.1,::1 | 200 OK | - | ::1 | 127.0.0.1,::1 | 200 OK | - | ::1 | * | 200 OK | + | client-ip | allow | code | reasonPhrase | + | 127.0.0.1 | 10.0.0.0 | 403 | Access denied | + | 127.0.0.1 | 2001:db8::/48 | 403 | Access denied | + | ::1 | 2001:db8::/48 | 403 | Access denied | + | ::1 | 127.0.0.1 | 403 | Access denied | + | 127.0.0.1 | 127.0.0.1,::1 | 200 | OK | + | ::1 | 127.0.0.1,::1 | 200 | OK | + | ::1 | * | 200 | OK | Scenario Outline: Stats access event listener authenticates HEAD requests as well Given Imbo uses the "stats-access-and-custom-stats.php" configuration + And the stats are allowed by "" And the client IP is "" - When I request "/stats.json?statsAllow=" using HTTP "HEAD" - Then I should get a response with "" + When I request "/stats" using HTTP "HEAD" + Then the response code is + And the response reason phrase is "" And the "Content-Type" response header is "application/json" Examples: - | client-ip | allow | status | - | 127.0.0.1 | 10.0.0.0 | 403 Access denied | - | 127.0.0.1 | 2001:db8::/48 | 403 Access denied | - | ::1 | 2001:db8::/48 | 403 Access denied | - | ::1 | 127.0.0.1 | 403 Access denied | - | 127.0.0.1 | 127.0.0.1,::1 | 200 OK | - | ::1 | 127.0.0.1,::1 | 200 OK | - | ::1 | * | 200 OK | - - Scenario Outline: Custom statistics can be added through an event listener - Given Imbo uses the "stats-access-and-custom-stats.php" configuration - When I request "/stats.?statsAllow=*" - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ - - Examples: - | extension | response | - | json | {.*?,"custom":{"someValue":123,"someOtherValue":{"foo":"bar"},"someList":\[1,2,3\]}} | + | client-ip | allow | code | reasonPhrase | + | 127.0.0.1 | 10.0.0.0 | 403 | Access denied | + | 127.0.0.1 | 2001:db8::/48 | 403 | Access denied | + | ::1 | 2001:db8::/48 | 403 | Access denied | + | ::1 | 127.0.0.1 | 403 | Access denied | + | 127.0.0.1 | 127.0.0.1,::1 | 200 | OK | + | ::1 | 127.0.0.1,::1 | 200 | OK | + | ::1 | * | 200 | OK | diff --git a/tests/behat/features/status.feature b/tests/behat/features/status.feature index 3819af676..715f6f07d 100644 --- a/tests/behat/features/status.feature +++ b/tests/behat/features/status.feature @@ -1,47 +1,74 @@ +@resources + Feature: Imbo provides a status endpoint In order to see the status of an Imbo installation As an HTTP Client I want to make requests against the status endpoint - Scenario Outline: Fetch status information - When I request "/status." - Then I should get a response with "200 OK" - And the response body matches: + Scenario: Fetch status information + When I request "/status" + Then the response code is 200 + And the response body contains JSON: """ - + { + "date": "@isDate()", + "database": true, + "storage": true + } """ - Examples: - | extension | response | - | json | #^{"date":"[^"]+","database":true,"storage":true}$# | - Scenario Outline: The status endpoint only supports HTTP GET and HEAD - When I request "/status.json" using HTTP "" - Then I should get a response with "" + When I request "/status" using HTTP "" + Then the response code is + And the response reason phrase is "" Examples: - | method | status | - | GET | 200 OK | - | HEAD | 200 OK | - | POST | 405 Method not allowed | - | PUT | 405 Method not allowed | - | DELETE | 405 Method not allowed | - | SEARCH | 405 Method not allowed | + | method | code | reason-phrase | + | GET | 200 | OK | + | HEAD | 200 | OK | + | POST | 405 | Method not allowed | + | PUT | 405 | Method not allowed | + | DELETE | 405 | Method not allowed | Scenario: The status endpoint reports errors when there are issues with the database Given Imbo uses the "status.php" configuration And the database is down When I request "/status" - Then I should get a response with "503 Database error" + Then the response code is 503 + And the response reason phrase is "Database error" + And the response body contains JSON: + """ + { + "database": false, + "storage": true + } + """ Scenario: The status endpoint reports errors when there are issues with the storage Given Imbo uses the "status.php" configuration And the storage is down When I request "/status" - Then I should get a response with "503 Storage error" + Then the response code is 503 + And the response reason phrase is "Storage error" + And the response body contains JSON: + """ + { + "database": true, + "storage": false + } + """ Scenario: The status endpoint reports errors when there are issues with both database and storage Given Imbo uses the "status.php" configuration - And the database and the storage is down + And the storage is down + And the database is down When I request "/status" - Then I should get a response with "503 Database and storage error" + Then the response code is 503 + And the response reason phrase is "Database and storage error" + And the response body contains JSON: + """ + { + "database": false, + "storage": false + } + """ diff --git a/tests/behat/features/strip-exif-transformation.feature b/tests/behat/features/strip-exif-transformation.feature index 37eaf21f9..1f42b812d 100644 --- a/tests/behat/features/strip-exif-transformation.feature +++ b/tests/behat/features/strip-exif-transformation.feature @@ -3,14 +3,12 @@ Feature: Imbo can strip EXIF data from images As an HTTP Client I can use the stripExif transformation - Background: - Given "tests/phpunit/Fixtures/exif-logo.jpg" exists in Imbo - Scenario: Use the stripExif transformation - Given I use "publickey" and "privatekey" for public and private keys + Given "tests/phpunit/Fixtures/exif-logo.jpg" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys And I specify "strip" as transformation - And I include an access token in the query + And I include an access token in the query string When I request the previously added image as a "jpg" - Then I should get a response with "200 OK" + Then the response status line is "200 OK" And the "Content-Type" response header is "image/jpeg" And the image should not have any "exif" properties diff --git a/tests/behat/features/user.feature b/tests/behat/features/user.feature index b6f5d60ca..1ef7a0b37 100644 --- a/tests/behat/features/user.feature +++ b/tests/behat/features/user.feature @@ -3,31 +3,29 @@ Feature: Imbo provides a user endpoint As an HTTP Client I want to make requests against the user endpoint - Scenario Outline: Request user information - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query - When I request "/users/user." - Then I should get a response with "200 OK" - And the response body matches: - """ - - """ + Background: + Given I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string - Examples: - | extension | response | - | json | #^{"user":"user","numImages":0,"lastModified":"[^"]+"}$# | + Scenario: Request user information + When I request "/users/user.json" + Then the response status line is "200 OK" + And the response body contains JSON: + """ + { + "user": "user", + "numImages": 0 + } + """ Scenario: Request user that does not exist - Given I use "foo" and "bar" for public and private keys - When I request "/users/foo.json" - Then I should get a response with "400 Permission denied (public key)" + When I request "/users/foobar.json" + Then the response status line is "400 Permission denied (public key)" And the Imbo error message is "Permission denied (public key)" and the error code is "0" Scenario Outline: The user endpoint only supports HTTP GET and HEAD - Given I use "publickey" and "privatekey" for public and private keys - And I include an access token in the query When I request "/users/user.json" using HTTP "" - Then I should get a response with "" + Then the response status line is "" Examples: | method | status | @@ -36,4 +34,3 @@ Feature: Imbo provides a user endpoint | POST | 405 Method not allowed | | PUT | 405 Method not allowed | | DELETE | 405 Method not allowed | - | SEARCH | 405 Method not allowed | diff --git a/tests/behat/features/varnish-hashtwo-listener.feature b/tests/behat/features/varnish-hashtwo-listener.feature index f051b5be8..6f7374fe6 100644 --- a/tests/behat/features/varnish-hashtwo-listener.feature +++ b/tests/behat/features/varnish-hashtwo-listener.feature @@ -4,16 +4,16 @@ Feature: Imbo provides an event listener for the hashtwo Varnish module I must send correct headers Background: - Given "tests/phpunit/Fixtures/image1.png" exists in Imbo + Given "tests/phpunit/Fixtures/image1.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string + And Imbo uses the "varnish-hashtwo.php" configuration Scenario Outline: The hashtwo header is the same for all image variations - Given I use "publickey" and "privatekey" for public and private keys - And I specify "" as transformation - And I include an access token in the query - And Imbo uses the "varnish-hashtwo.php" configuration - When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-HashTwo" response header matches "imbo;image;user;[A-Za-z0-9_-]{1,255}, imbo;user;user" + Given I specify "" as transformation + When I request the previously added image + Then the response status line is "200 OK" + And the "X-HashTwo" response header matches "/imbo;image;user;[A-Za-z0-9_-]{12}, imbo;user;user/" Examples: | transformation | @@ -25,13 +25,10 @@ Feature: Imbo provides an event listener for the hashtwo Varnish module | transpose | Scenario Outline: A custom hashtwo header can be specified in the configuration - Given I use "publickey" and "privatekey" for public and private keys - And I specify "" as transformation - And I include an access token in the query - And Imbo uses the "varnish-hashtwo.php" configuration - When I request the previously added image as a "png" - Then I should get a response with "200 OK" - And the "X-Imbo-HashTwo" response header matches "imbo;image;user;[A-Za-z0-9_-]{1,255}, imbo;user;user" + Given I specify "" as transformation + When I request the previously added image + Then the response status line is "200 OK" + And the "X-Imbo-HashTwo" response header matches "/imbo;image;user;[A-Za-z0-9_-]{12}, imbo;user;user/" Examples: | transformation | diff --git a/tests/behat/features/watermark-transformation.feature b/tests/behat/features/watermark-transformation.feature index 2f3228dd8..b2763b30e 100644 --- a/tests/behat/features/watermark-transformation.feature +++ b/tests/behat/features/watermark-transformation.feature @@ -4,27 +4,25 @@ Feature: Imbo can apply watermarks to images I can use the watermark transformation Background: - Given "tests/phpunit/Fixtures/image.png" is used as the test image for the "watermark" feature + Given "tests/phpunit/Fixtures/image.png" exists for user "user" + And "tests/phpunit/Fixtures/colors.png" exists for user "user" + And I use "publicKey" and "privateKey" for public and private keys + And I include an access token in the query string Scenario: Apply a non-existing watermark - Given I use "publickey" and "privatekey" for public and private keys - And I specify "watermark:img=foobar" as transformation - And I include an access token in the query - When I request the test image - Then I should get a response with "400 Watermark image not found" + Given I specify "watermark:img=foobar" as transformation + When I request the image resource for "tests/phpunit/Fixtures/image.png" + Then the response status line is "400 Watermark image not found" Scenario Outline: Apply an existing watermark - Given I use "publickey" and "privatekey" for public and private keys - And I use "tests/phpunit/Fixtures/colors.png" as the watermark image with "" as parameters - And I include an access token in the query - When I request the test image as a "png" - Then I should get a response with "200 OK" - And the width of the image is "665" - And the height of the image is "463" - And the pixel at coordinate "" should have a color of "" + Given I use "tests/phpunit/Fixtures/colors.png" as the watermark image with "" as parameters + When I request the image resource for "tests/phpunit/Fixtures/image.png" + Then the response status line is "200 OK" + And the image dimension is "665x463" + And the pixel at coordinate "" has a color of "" Examples: | parameters | coordinates | color | - | | 0, 0 | #000000 | - | position=center | 337, 226 | #00ffff | - | x=10,y=5,position=bottom-right,width=20,height=20 | 659, 453 | #ff0000 | + | | 0,0 | #000000 | + | position=center | 337,226 | #00ffff | + | x=10,y=5,position=bottom-right,width=20,height=20 | 659,453 | #ff0000 | diff --git a/tests/behat/fixtures/access-control-mutable.php b/tests/behat/fixtures/access-control-mutable.php index 1749f3550..7dcb23863 100644 --- a/tests/behat/fixtures/access-control-mutable.php +++ b/tests/behat/fixtures/access-control-mutable.php @@ -21,6 +21,8 @@ Resource::ACCESS_RULES_GET, Resource::ACCESS_RULES_HEAD, Resource::ACCESS_RULES_POST, + + Resource::GROUP_DELETE, ], 'users' => [] ], diff --git a/tests/behat/imbo-configs/access-control.php b/tests/behat/imbo-configs/access-control.php index 87d0e7692..41ad1e3f1 100644 --- a/tests/behat/imbo-configs/access-control.php +++ b/tests/behat/imbo-configs/access-control.php @@ -26,11 +26,7 @@ public static function getSubscribedEvents() { } public function get(EventInterface $event) { - $model = new ListModel(); - $model->setContainer('foo'); - $model->setEntry('bar'); - $model->setList([1, 2, 3]); - $event->getResponse()->setModel($model); + $event->getResponse()->setModel(new ListModel('foo', [1, 2, 3])); } } diff --git a/tests/behat/imbo-configs/config.testing.php b/tests/behat/imbo-configs/config.testing.php index 47e1da792..8048ec39a 100644 --- a/tests/behat/imbo-configs/config.testing.php +++ b/tests/behat/imbo-configs/config.testing.php @@ -8,16 +8,18 @@ * distributed with this source code. */ -use Imbo\Auth\AccessControl\Adapter\ArrayAdapter, - Imbo\Resource; +use Imbo\Auth\AccessControl\Adapter\ArrayAdapter; +use Imbo\Resource; +use Imbo\Database\MongoDB; +use Imbo\Storage\GridFS; // Default config for testing $testConfig = [ 'accessControl' => function() { return new ArrayAdapter([ [ - 'publicKey' => 'publickey', - 'privateKey' => 'privatekey', + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', 'acl' => [[ 'resources' => Resource::getReadWriteResources(), 'users' => ['user', 'other-user'], @@ -25,7 +27,7 @@ ], [ 'publicKey' => 'unpriviledged', - 'privateKey' => 'privatekey', + 'privateKey' => 'privateKey', 'acl' => [[ 'resources' => Resource::getReadWriteResources(), 'users' => ['user'], @@ -43,13 +45,13 @@ }, 'database' => function() { - return new Imbo\Database\MongoDB([ + return new MongoDB([ 'databaseName' => 'imbo_testing', ]); }, 'storage' => function() { - return new Imbo\Storage\GridFS([ + return new GridFS([ 'databaseName' => 'imbo_testing', ]); }, @@ -58,11 +60,11 @@ // Default Imbo config $defaultConfig = require __DIR__ . '/../../../config/config.default.php'; -// Custom test config, if any, specified in the X-Imbo-Test-Config HTTP request header -if (isset($_SERVER['HTTP_X_IMBO_TEST_CONFIG'])) { - $customConfig = require __DIR__ . '/' . basename($_SERVER['HTTP_X_IMBO_TEST_CONFIG']); -} else { - $customConfig = []; +// Custom test config, if any, specified in the X-Imbo-Test-Config-File HTTP request header +$customConfig = []; + +if (isset($_SERVER['HTTP_X_IMBO_TEST_CONFIG_FILE'])) { + $customConfig = require __DIR__ . '/' . basename($_SERVER['HTTP_X_IMBO_TEST_CONFIG_FILE']); } // Return the merged configuration, having the custom config overwrite the default testing config, diff --git a/tests/behat/imbo-configs/stats-access-and-custom-stats.php b/tests/behat/imbo-configs/stats-access-and-custom-stats.php index 53d417459..ff090b56a 100644 --- a/tests/behat/imbo-configs/stats-access-and-custom-stats.php +++ b/tests/behat/imbo-configs/stats-access-and-custom-stats.php @@ -8,35 +8,40 @@ * distributed with this source code. */ +use Imbo\EventListener\StatsAccess; +use Imbo\Http\Request\Request; +use Imbo\Http\Response\Response; +use Imbo\EventManager\EventInterface; + +/** + * Enable the stats access event listener, using a HTTP request header to set the allowed range of + * IPs, and optionally a custom IP for the client which will override $_SERVER['REMOTE_ADDR']. + * + * Also add some custom stats that will be included in the response from the stats endpoint + */ + // Rewrite the client IP when a custom header exists if (isset($_SERVER['HTTP_X_CLIENT_IP'])) { // Overwrite the default client IP $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_CLIENT_IP']; } -/** - * Enable the stats access event listener, using query parameter values to set the allowed range - * of IP's, and optionally a custom IP for the client. - * - * Also add some custom stats that will be included in the response from the stats endpoint - */ return [ 'eventListeners' => [ - 'statsAccess' => function() { + 'statsAccess' => function(Request $request, Response $response) { $statsAllow = []; - if (!empty($_GET['statsAllow'])) { - // Set the range - $statsAllow = explode(',', $_GET['statsAllow']); + if (!empty($_SERVER['HTTP_X_IMBO_STATS_ALLOWED_BY'])) { + $statsAllow = explode(',', $_SERVER['HTTP_X_IMBO_STATS_ALLOWED_BY']); } - return new Imbo\EventListener\StatsAccess([ + return new StatsAccess([ 'allow' => $statsAllow, ]); }, 'customStats' => [ 'events' => ['stats.get'], - 'callback' => function($event) { + 'callback' => function(EventInterface $event) { // Fetch the model from the response $model = $event->getResponse()->getModel(); diff --git a/tests/behat/imbo-configs/status.php b/tests/behat/imbo-configs/status.php index d3fdadc19..736458ad1 100644 --- a/tests/behat/imbo-configs/status.php +++ b/tests/behat/imbo-configs/status.php @@ -8,34 +8,33 @@ * distributed with this source code. */ +use Imbo\Http\Request\Request; +use Imbo\Http\Response\Response; + +use PHPUnit_Framework_MockObject_Generator as Generator; +use PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount as Any; +use PHPUnit_Framework_MockObject_Stub_Return as ReturnValue; + /** * Set a database and storage adapter that has some behaviour determined via request headers */ return [ - 'database' => function() { - $adapter = (new PHPUnit_Framework_MockObject_Generator())->getMock( - 'Imbo\Database\MongoDB', - ['getStatus'], - [['databaseName' => 'imbo_testing']] - ); - - $adapter->expects(new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount()) - ->method('getStatus') - ->will(new PHPUnit_Framework_MockObject_Stub_Return(isset($_GET['databaseDown']) ? false : true)); + 'database' => function(Request $request, Response $response) { + $adapter = (new Generator())->getMock('Imbo\Database\DatabaseInterface'); + $adapter + ->expects(new Any()) + ->method('getStatus') + ->will(new ReturnValue(!isset($_SERVER['HTTP_X_IMBO_STATUS_DATABASE_FAILURE']))); return $adapter; }, - 'storage' => function() { - $adapter = (new PHPUnit_Framework_MockObject_Generator())->getMock( - 'Imbo\Storage\GridFS', - ['getStatus'], - [['databaseName' => 'imbo_testing']] - ); - - $adapter->expects(new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount()) - ->method('getStatus') - ->will(new PHPUnit_Framework_MockObject_Stub_Return(isset($_GET['storageDown']) ? false : true)); + 'storage' => function(Request $request, Response $response) { + $adapter = (new Generator())->getMock('Imbo\Storage\StorageInterface'); + $adapter + ->expects(new Any()) + ->method('getStatus') + ->will(new ReturnValue(!isset($_SERVER['HTTP_X_IMBO_STATUS_STORAGE_FAILURE']))); return $adapter; }, diff --git a/tests/behat/router.php b/tests/behat/router.php index 3c55758ac..d97705e61 100644 --- a/tests/behat/router.php +++ b/tests/behat/router.php @@ -9,10 +9,10 @@ */ /** - * Router for the built in httpd in php-5.4. Route everything through index.php. When ran from the - * base project directory, the command looks like this: + * Router for the built in httpd in PHP. Route everything through index.php. When ran from the base + * project directory, the command looks like this: * - * php -S localhost:8888 -t public tests/router.php + * php -S localhost:8080 -t public tests/behat/router.php */ // Hack to bypass limited support for non-standard HTTP verbs in the built-in PHP HTTP server if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { @@ -23,74 +23,6 @@ unset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); } -if (isset($_SERVER['HTTP_X_COLLECT_COVERAGE']) && isset($_SERVER['HTTP_X_TEST_SESSION_ID'])) { - // Output code coverage stored in the .cov files - $coverageDir = sys_get_temp_dir() . '/behat-coverage'; - - if (!is_dir($coverageDir)) { - // Create tmp dir - mkdir($coverageDir); - } - - $files = new FilesystemIterator( - $coverageDir, - FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS - ); - $data = []; - $suffix = $_SERVER['HTTP_X_TEST_SESSION_ID'] . '.cov'; - - foreach ($files as $filename) { - if (!preg_match('/' . preg_quote($suffix, '/') . '$/', $filename)) { - continue; - } - - $content = unserialize(file_get_contents($filename)); - unlink($filename); - - foreach ($content as $file => $lines) { - if (is_file($file)) { - if (!isset($data[$file])) { - $data[$file] = $lines; - } else { - foreach ($lines as $line => $flag) { - if (!isset($data[$file][$line]) || $flag > $data[$file][$line]) { - $data[$file][$line] = $flag; - } - } - } - } - } - } - - echo serialize($data); - exit; -} - -if (isset($_SERVER['HTTP_X_ENABLE_COVERAGE']) && isset($_SERVER['HTTP_X_TEST_SESSION_ID']) && extension_loaded('xdebug')) { - // Register a shutdown function that stops code coverage and stores the coverage of the current - // request - register_shutdown_function(function() { - $data = xdebug_get_code_coverage(); - xdebug_stop_code_coverage(); - - $coverageDir = sys_get_temp_dir() . '/behat-coverage'; - - if (is_dir($coverageDir) || mkdir($coverageDir, 0775, true)) { - $filename = sprintf( - '%s/%s.%s.cov', - $coverageDir, - md5(uniqid('', true)), - $_SERVER['HTTP_X_TEST_SESSION_ID'] - ); - - file_put_contents($filename, serialize($data)); - } - }); - - // Start code coverage - xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); -} - // Define a custom configuration file define('IMBO_CONFIG_PATH', __DIR__ . '/imbo-configs/config.testing.php'); diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php index e15174ff0..70985563d 100644 --- a/tests/phpunit/bootstrap.php +++ b/tests/phpunit/bootstrap.php @@ -8,4 +8,8 @@ * distributed with this source code. */ +// Set the Fixtures path define('FIXTURES_DIR', realpath(__DIR__ . '/Fixtures')); + +// Require the FeatureContext file as it's not part of the regular autolading functionality +require __DIR__ . '/../behat/features/bootstrap/FeatureContext.php'; diff --git a/tests/phpunit/unit/FeatureContextTest.php b/tests/phpunit/unit/FeatureContextTest.php new file mode 100644 index 000000000..8c988de34 --- /dev/null +++ b/tests/phpunit/unit/FeatureContextTest.php @@ -0,0 +1,2760 @@ + + * + * For the full copyright and license information, please view the LICENSE file that was + * distributed with this source code. + */ + +namespace ImboUnitTest; + +use FeatureContext; +use Micheh\Cache\CacheUtil; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use PHPUnit_Framework_TestCase; +use Behat\Gherkin\Node\PyStringNode; +use Behat\Gherkin\Node\TableNode; + +/** + * @coversDefaultClass FeatureContext + * @group unit + * @group behat + */ +class FeatureContextTest extends PHPUnit_Framework_TestCase { + /** + * @var FeatureContext + */ + private $context; + + /** + * @var CacheUtil + */ + private $cacheUtil; + + /** + * @var Client + */ + private $client; + + /** + * @var array + */ + private $history; + + /** + * @var MockHandler + */ + private $mockHandler; + + /** + * @var HandlerStack + */ + private $handlerStack; + + /** + * @var string + */ + private $baseUri = 'http://localhost:8080'; + + /** + * @var string + */ + private $publicKey = 'publicKey'; + + /** + * @var string + */ + private $privateKey = 'privateKey'; + + /** + * Set up the feature context + */ + public function setUp() { + $this->history = []; + + $this->mockHandler = new MockHandler(); + $this->handlerStack = HandlerStack::create($this->mockHandler); + $this->handlerStack->push(Middleware::history($this->history)); + $this->client = new Client([ + 'handler' => $this->handlerStack, + 'base_uri' => $this->baseUri, + ]); + $this->cacheUtil = $this->createMock('Micheh\Cache\CacheUtil'); + + $this->context = new FeatureContext($this->cacheUtil); + $this->context->setClient($this->client); + } + + /** + * Convenience method to make a single request and return the request instance + * + * @param string $path + * @return Request + */ + private function makeRequest($path = '/somepath') { + $this->mockHandler->append(new Response(200)); + $this->context->requestPath($path); + + return $this->history[count($this->history) - 1]['request']; + } + + /** + * @covers ::setClient + */ + public function testCanSetAnApiClient() { + $handlerStack = $this->createMock('GuzzleHttp\HandlerStack'); + $handlerStack + ->expects($this->once()) + ->method('push') + ->with($this->isInstanceOf('Closure'), $this->isType('string')); + + $client = $this->createMock('GuzzleHttp\ClientInterface'); + $client + ->expects($this->at(0)) + ->method('getConfig') + ->with('handler') + ->willReturn($handlerStack); + $client + ->expects($this->at(1)) + ->method('getConfig') + ->with('base_uri') + ->willReturn('http://localhost:8080'); + + $context = new FeatureContext(); + $this->assertSame($context, $context->setClient($client)); + } + + /** + * @covers ::setArrayContainsComparator + */ + public function testAttachesComparatorFunctions() { + $comparator = $this->createMock('Imbo\BehatApiExtension\ArrayContainsComparator'); + $comparator + ->expects($this->once()) + ->method('addFunction') + ->with($this->isType('string'), $this->isType('array')); + $this->assertSame($this->context, $this->context->setArrayContainsComparator($comparator)); + } + + /** + * @covers ::setRequestHeader + */ + public function testCanSetRequestHeader() { + $this->assertSame($this->context, $this->context->setRequestHeader('X-Foo', 'current-timestamp')); + $this->assertSame($this->context, $this->context->setRequestHeader('X-Bar', 'current')); + + $request = $this->makeRequest(); + + $this->assertTrue( + (boolean) preg_match( + '/^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$/', + $request->getHeaderLine('X-Foo') + ), + 'setRequestHeader does not support the magic "current-timestamp" value.' + ); + $this->assertSame('current', $request->getHeaderLine('X-Bar')); + } + + /** + * Data provider + * + * @return array[] + */ + public function getValidDates() { + return [ + ['date' => 'Wed, 15 Mar 2017 21:28:14 GMT'], + ]; + } + + /** + * @dataProvider getValidDates + * @covers ::isDate + * @param string $date Date to validate + */ + public function testIsDateFunctionValidatesDates($date) { + $this->assertNull($this->context->isDate($date)); + } + + /** + * @covers ::isDate + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Date is not properly formatted: "invalid date". + */ + public function testIsDateFunctionCanFail() { + $this->context->isDate('invalid date'); + } + + /** + * Data provider + * + * @return array[] + */ + public function getImboConfigFiles() { + return array_map(function($file) { + return [basename($file)]; + }, glob(__DIR__ . '/../../behat/imbo-configs/*.php')); + } + + /** + * @dataProvider getImboConfigFiles + * @covers ::setImboConfigHeader + * @param string $path + */ + public function testCanSetAConfigHeader($path) { + $this->assertSame($this->context, $this->context->setImboConfigHeader($path)); + $this->assertSame($path, $this->makeRequest()->getHeaderLine('X-Imbo-Test-Config-File')); + } + + /** + * @covers ::setImboConfigHeader + * @expectedException InvalidArgumentException + * @expectedExceptionMessageRegExp |Configuration file "foobar" does not exist in the ".*?/imbo-configs" directory\.| + */ + public function testSettingConfigHeaderFailsWithNonExistingFile() { + $this->context->setImboConfigHeader('foobar'); + } + + /** + * @covers ::statsAllowedBy + */ + public function testCanSetStatsAllowedByHeader() { + $this->assertSame($this->context, $this->context->statsAllowedBy('*')); + $this->assertSame('*', $this->makeRequest()->getHeaderLine('X-Imbo-Stats-Allowed-By')); + } + + /** + * Data provider + * + * @return array[] + */ + public function getAdaptersForFailure() { + return [ + ['adapter' => 'database', 'header' => 'X-Imbo-Status-Database-Failure'], + ['adapter' => 'storage', 'header' => 'X-Imbo-Status-Storage-Failure'], + ]; + } + + /** + * @dataProvider getAdaptersForFailure + * @covers ::forceAdapterFailure + * @param string $adapter + * @param string $header + */ + public function testCanForceAdapterFailureBySettingAHeader($adapter, $header) { + $this->assertSame($this->context, $this->context->forceAdapterFailure($adapter)); + $this->assertSame('1', $this->makeRequest()->getHeaderLine($header)); + } + + /** + * @covers ::forceAdapterFailure + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid adapter: "foobar". + */ + public function testThrowsExecptionWhenSpecifyingInvalidAdapterForFailure() { + $this->context->forceAdapterFailure('foobar'); + } + + /** + * @covers ::setPublicAndPrivateKey + * @covers ::signRequest + */ + public function testCanSignRequest() { + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey($this->publicKey, $this->privateKey) + ->signRequest() + ); + $path = '/path'; + $request = $this->makeRequest($path); + + // Generate the URI and make sure the request URI is the same + $uri = parse_url($request->getUri()); + $query = []; + parse_str($uri['query'], $query); + + $data = sprintf('%s|%s|%s|%s', + $request->getMethod(), + sprintf('%s%s?publicKey=%s', $this->baseUri, $path, $this->publicKey), + $this->publicKey, + $query['timestamp'] + ); + $signature = hash_hmac('sha256', $data, $this->privateKey); + + $this->assertSame($this->publicKey, $query['publicKey']); + $this->assertSame($signature, $query['signature'], 'Signature mismatch.'); + } + + /** + * @covers ::setPublicAndPrivateKey + * @covers ::signRequestUsingHttpHeaders + * @covers ::signRequest + */ + public function testCanSignRequestUsingHttpHeaders() { + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey($this->publicKey, $this->privateKey) + ->signRequestUsingHttpHeaders() + ); + $path = '/path'; + $request = $this->makeRequest($path); + + $this->assertTrue($request->hasHeader('X-Imbo-PublicKey')); + $this->assertTrue($request->hasHeader('X-Imbo-Authenticate-Signature')); + $this->assertTrue($request->hasHeader('X-Imbo-Authenticate-Timestamp')); + + $data = sprintf('%s|%s|%s|%s', + $request->getMethod(), + sprintf('%s%s', $this->baseUri, $path), + $this->publicKey, + $request->getHeaderLine('X-Imbo-Authenticate-Timestamp') + ); + $signature = hash_hmac('sha256', $data, $this->privateKey); + + $this->assertSame($this->publicKey, $request->getHeaderLine('X-Imbo-PublicKey')); + $this->assertSame($signature, $request->getHeaderLine('X-Imbo-Authenticate-Signature'), 'Signature mismatch.'); + } + + /** + * @covers ::signRequest + * @expectedException RuntimeException + * @expectedExceptionMessage The authentication handler is currently added to the stack. It can not be added more than once. + */ + public function testCanNotAttachSignatureHandlerMoreThanOnce() { + $this->context + ->signRequest() + ->signRequest(); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForAccessTokens() { + return [ + 'path with no query params' => [ + 'path' => '/path', + 'expectedUrl' => 'http://localhost:8080/path?publicKey=publicKey&accessToken=582386896ffacd2c34a39476f0fa71ac9e6b22f079482ea7ee687e15826b08ef', + ], + 'path with query params' => [ + 'path' => '/path?foo=bar', + 'expectedUrl' => 'http://localhost:8080/path?foo=bar&publicKey=publicKey&accessToken=67bd5be81cd63180d9dba642e22fc6c9940c4313913dee5db692b0eb86aabb6b', + ], + 'path with problematic query params' => [ + 'path' => '/path?bar=foo&publicKey=foobar&accessToken=sometoken', + 'expectedUrl' => 'http://localhost:8080/path?bar=foo&publicKey=publicKey&accessToken=f43f2db7f8c34c521456c4bb6f926812b39c3081a7a3d295ca14ccdc38926f2c', + ], + ]; + } + + /** + * @dataProvider getDataForAccessTokens + * @covers ::setPublicAndPrivateKey + * @covers ::appendAccessToken + * @param string $path + * @param string $expectedUrl + */ + public function testCanAppendAccessToken($path, $expectedUrl) { + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey($this->publicKey, $this->privateKey) + ->appendAccessToken() + ); + $request = $this->makeRequest($path); + + // Generate the URI and make sure the request URI is the same + $this->assertSame($expectedUrl, (string) $request->getUri()); + } + + /** + * @covers ::appendAccessToken + * @expectedException RuntimeException + * @expectedExceptionMessage The access token handler is currently added to the stack. It can not be added more than once. + */ + public function testCanNotAttachAccessTokenHandlerMoreThanOnce() { + $this->context + ->appendAccessToken() + ->appendAccessToken(); + } + + /** + * @covers ::signRequest + * @expectedException RuntimeException + * @expectedExceptionMessage The access token handler is currently added to the stack. These handlers should not be added to the same request. + */ + public function testCanNotAddBothAccessTokenAndSignatureHandlers() { + $this->context + ->appendAccessToken() + ->signRequest(); + } + + /** + * @covers ::appendAccessToken + * @expectedException RuntimeException + * @expectedExceptionMessage The authentication handler is currently added to the stack. These handlers should not be added to the same request. + */ + public function testCanNotAddBothSignatureAndAccessTokenHandlers() { + $this->context + ->signRequest() + ->appendAccessToken(); + } + + /** + * @covers ::addUserImageToImbo + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No keys exist for user "some user". + */ + public function testThrowsExceptionWhenAddingUserImageWithUnknownUser() { + $this->context->addUserImageToImbo(__FILE__, 'some user'); + } + + /** + * @covers ::addUserImageToImbo + * @expectedException InvalidArgumentException + * @expectedExceptionMessage File does not exist: "/some/path". + */ + public function testThrowsExceptionWhenAddingUserImageWithInvalidFilename() { + $this->context->addUserImageToImbo('/some/path', 'user'); + } + + /** + * @covers ::addUserImageToImbo + * @expectedException RuntimeException + * @expectedExceptionMessage Image was not successfully added. Response body: + */ + public function testAddingUserImageToImboFailsWhenImboDoesNotIncludeImageIdentifierInResponse() { + $this->mockHandler->append(new Response(400, ['Content-Type' => 'application/json'], '{"error": {"message": "some id"}}')); + $this->context->addUserImageToImbo(FIXTURES_DIR . '/image1.png', 'user'); + } + + /** + * @covers ::addUserImageToImbo + */ + public function testCanAddUserImageToImbo() { + $this->mockHandler->append(new Response(200, ['Content-Type' => 'application/json'], '{"imageIdentifier": "some id"}')); + + $this->assertSame( + $this->context, + $this->context->addUserImageToImbo(FIXTURES_DIR . '/image1.png', 'user') + ); + + $this->assertSame( + 1, + $num = count($this->history), + sprintf('There should be exactly 1 transction in the history, found %d.', $num) + ); + + $request = $this->history[0]['request']; + + $this->assertStringStartsWith( + 'http://localhost:8080/users/user/images?publicKey=publicKey&signature=', + (string) $request->getUri() + ); + $this->assertSame('POST', $request->getMethod()); + } + + /** + * @covers ::addUserImageToImbo + */ + public function testCanAddUserImageWithMetadataToImbo() { + $this->mockHandler->append( + new Response(200, [], '{"imageIdentifier": "imageId"}'), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context->addUserImageToImbo( + FIXTURES_DIR . '/image1.png', + 'user', + new PyStringNode(['{"foo": "bar"}'], 1) + ) + ); + + $this->assertSame( + 2, + $num = count($this->history), + sprintf('There should be exactly 2 transctions in the history, found %d.', $num) + ); + + $imageRequest = $this->history[0]['request']; + $metadataRequest = $this->history[1]['request']; + + $this->assertStringStartsWith( + 'http://localhost:8080/users/user/images?publicKey=publicKey&signature=', + (string) $imageRequest->getUri() + ); + $this->assertSame('POST', $imageRequest->getMethod()); + + $this->assertStringStartsWith( + 'http://localhost:8080/users/user/images/imageId/metadata?publicKey=publicKey&signature=', + (string) $metadataRequest->getUri() + ); + $this->assertSame('POST', $metadataRequest->getMethod()); + } + + /** + * @covers ::setClientIp + */ + public function testCanSetClientIpHeader() { + $ip = '1.2.3.4'; + $this->assertSame( + $this->context, + $this->context + ->setClientIp($ip) + ); + $request = $this->makeRequest('/path'); + + $this->assertTrue($request->hasHeader('X-Client-Ip')); + $this->assertSame($ip, $request->getHeaderLine('X-Client-Ip')); + } + + /** + * @covers ::applyTransformation + * @covers ::setRequestQueryParameter + */ + public function testCanApplyImageTransformation() { + $this->assertSame( + $this->context, + $this->context->applyTransformation('t1') + ); + + $request = $this->makeRequest('/path'); + + $this->assertSame( + 'http://localhost:8080/path?t%5B0%5D=t1', + (string) $request->getUri() + ); + } + + /** + * @covers ::applyTransformations + * @covers ::applyTransformation + * @covers ::setRequestQueryParameter + */ + public function testCanApplyImageTransformations() { + $this->assertSame( + $this->context, + $this->context->applyTransformations(new PyStringNode(['t1', 't2', 't3'], 1)) + ); + + $request = $this->makeRequest('/path'); + + $this->assertSame( + 'http://localhost:8080/path?t%5B0%5D=t1&t%5B1%5D=t2&t%5B2%5D=t3', + (string) $request->getUri() + ); + } + + /** + * @covers ::primeDatabase + * @expectedException InvalidArgumentException + * @expectedExceptionMessageRegExp |Fixture file "foobar.php" does not exist in ".*?/tests/behat/fixtures"\.| + */ + public function testThrowsExceptionWhenPrimingDatabaseWithScriptThatDoesNotExist() { + $this->context->primeDatabase('foobar.php'); + } + + /** + * @covers ::authenticateRequest + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid authentication method: "auth". + */ + public function testThrowsExceptionWhenSpecifyingInvalidAuthenticationType() { + $this->context->authenticateRequest('auth'); + } + + /** + * Data provider + * + * @return array[] + */ + public function getAuthDetails() { + return [ + 'access-token' => [ + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', + 'authMethod' => 'access-token', + 'uriRegExp' => '|^http://localhost:8080/path\?publicKey=publicKey&accessToken=582386896ffacd2c34a39476f0fa71ac9e6b22f079482ea7ee687e15826b08ef$|', + 'headers' => [], + ], + 'access-token #2' => [ + 'publicKey' => 'key', + 'privateKey' => 'secret', + 'authMethod' => 'access-token', + 'uriRegExp' => '|^http://localhost:8080/path\?publicKey=key&accessToken=dd4217a681cf8abdcecdc68cf49630df1e57dc733735e902b8a69859e50797a8$|', + 'headers' => [], + ], + 'signature' => [ + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', + 'authMethod' => 'signature', + 'uriRegExp' => '|^http://localhost:8080/path\?publicKey=publicKey&signature=[a-z0-9]{64}×tamp=[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$|', + 'headers' => [], + ], + 'signature #2' => [ + 'publicKey' => 'key', + 'privateKey' => 'secret', + 'authMethod' => 'signature', + 'uriRegExp' => '|^http://localhost:8080/path\?publicKey=key&signature=[a-z0-9]{64}×tamp=[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$|', + 'headers' => [], + ], + 'signature (headers)' => [ + 'publicKey' => 'publicKey', + 'privateKey' => 'privateKey', + 'authMethod' => 'signature (headers)', + 'uriRegExp' => '|^http://localhost:8080/path$|', + 'headers' => [ + 'X-Imbo-PublicKey' => '/^publicKey$/', + 'X-Imbo-Authenticate-Signature' => '/^[a-z0-9]{64}$/', + 'X-Imbo-Authenticate-Timestamp' => '/^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$/', + ] + ], + 'signature (headers) #2' => [ + 'publicKey' => 'key', + 'privateKey' => 'secret', + 'authMethod' => 'signature (headers)', + 'uriRegExp' => '|^http://localhost:8080/path$|', + 'headers' => [ + 'X-Imbo-PublicKey' => '/^key$/', + 'X-Imbo-Authenticate-Signature' => '/^[a-z0-9]{64}$/', + 'X-Imbo-Authenticate-Timestamp' => '/^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$/', + ] + ], + ]; + } + + /** + * @dataProvider getAuthDetails + * @covers ::setPublicAndPrivateKey + * @covers ::authenticateRequest + * @param string $publicKey + * @param string $privateKey + * @param string $authMethod + * @param string $uriRegExp + * @param array $headers + */ + public function testCanUseDifferentAuthenticationMethods($publicKey, $privateKey, $authMethod, $uriRegExp, array $headers = []) { + $this->assertSame( + $this->context, + $this->context->setPublicAndPrivateKey($publicKey, $privateKey) + ); + $this->assertSame( + $this->context, + $this->context->authenticateRequest($authMethod) + ); + + $request = $this->makeRequest('/path'); + $this->assertRegExp($uriRegExp, (string) $request->getUri()); + + foreach ($headers as $name => $regExp) { + $this->assertTrue($request->hasHeader($name)); + $this->assertRegExp($regExp, $request->getHeaderLine($name)); + } + } + + /** + * Data provider + * + * @return array[] + */ + public function getRequestQueryParams() { + return [ + 'single key / value' => [ + 'params' => [ + ['name' => 'key', 'value' => 'value'], + ], + 'uri' => 'http://localhost:8080/path?key=value', + ], + 'multiple key / value' => [ + 'params' => [ + ['name' => 'foo', 'value' => 'bar'], + ['name' => 'bar', 'value' => 'foo'], + ['name' => 'foobar', 'value' => 'barfoo'], + ], + 'uri' => 'http://localhost:8080/path?foo=bar&bar=foo&foobar=barfoo', + ], + 'array values' => [ + 'params' => [ + ['name' => 't[]', 'value' => 'border'], + ['name' => 't[]', 'value' => 'thumb'], + ], + 'uri' => 'http://localhost:8080/path?t%5B0%5D=border&t%5B1%5D=thumb', + ], + 'mixed values' => [ + 'params' => [ + ['name' => 'foo', 'value' => 'bar'], + ['name' => 't[]', 'value' => 'border'], + ['name' => 'bar', 'value' => 'foo'], + ['name' => 't[]', 'value' => 'thumb'], + ], + 'uri' => 'http://localhost:8080/path?foo=bar&t%5B0%5D=border&t%5B1%5D=thumb&bar=foo', + ], + ]; + } + + /** + * @dataProvider getRequestQueryParams + * @covers ::setRequestQueryParameter + * @param array $params + * @param string $uri + */ + public function testCanSetRequestQueryParameters(array $params, $uri) { + foreach ($params as $param) { + $this->assertSame( + $this->context, + $this->context->setRequestQueryParameter($param['name'], $param['value']) + ); + } + + $this->assertSame($uri, (string) $this->makeRequest('/path')->getUri()); + } + + /** + * @covers ::setRequestQueryParameter + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The "t" query parameter already exists and it's not an array, so can't append more values to it. + */ + public function testThrowsExceptionWhenAppendingArrayParamToRegularParam() { + $this->context + ->setRequestQueryParameter('t', 'border') + ->setRequestQueryParameter('t[]', 'thumb'); + } + + /** + * @covers ::setRequestParameterToImageIdentifier + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No image identifier exists for image: "/path". + */ + public function testThrowsExceptionWhenSettingARequestParameterToAnNonExistingImageIdentifier() { + $this->context->setRequestParameterToImageIdentifier('foo', '/path'); + } + + /** + * @covers ::setRequestParameterToImageIdentifier + */ + public function testCanSetQueryParameterToImageIdentifier() { + $this->mockHandler->append( + new Response(200, [], '{"imageIdentifier": "1"}'), + new Response(200, [], '{"imageIdentifier": "2"}'), + new Response(200, [], '{"imageIdentifier": "3"}') + ); + + $this->assertSame( + $this->context, + $this->context + ->addUserImageToImbo(FIXTURES_DIR . '/image1.png', 'user') + ->addUserImageToImbo(FIXTURES_DIR . '/image2.png', 'user') + ->addUserImageToImbo(FIXTURES_DIR . '/image3.png', 'user') + ); + + $this->assertSame( + $this->context, + $this->context + ->setRequestParameterToImageIdentifier('id1', FIXTURES_DIR . '/image1.png') + ->setRequestParameterToImageIdentifier('id2', FIXTURES_DIR . '/image2.png') + ->setRequestParameterToImageIdentifier('id3', FIXTURES_DIR . '/image3.png') + ); + + $this->assertSame( + 'http://localhost:8080/path?id1=1&id2=2&id3=3', + (string) $this->makeRequest('/path')->getUri() + ); + } + + /** + * @covers ::generateShortImageUrl + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No image identifier exists for path: "/path". + */ + public function testThrowsExceptionWheyGeneratingShortImageUrlForNonExistingImage() { + $this->context->generateShortImageUrl('/path', new PyStringNode([], 1)); + } + + /** + * Data provider + * + * @return array[] + */ + public function getShortUrlParams() { + return [ + [ + 'image' => FIXTURES_DIR . '/image1.png', + 'user' => 'user', + 'imageIdentifier' => 'fc7d2d06993047a0b5056e8fac4462a2', + 'params' => [ + 'user' => 'user', + ], + ], + [ + 'image' => FIXTURES_DIR . '/image2.png', + 'user' => 'user', + 'imageIdentifier' => 'b914b28f4d5faa516e2049b9a6a2577c', + 'params' => [ + 'user' => 'user', + 'extension' => 'gif', + ], + ], + [ + 'image' => FIXTURES_DIR . '/image3.png', + 'user' => 'user', + 'imageIdentifier' => '1d5b88aec8a3e1c4c57071307b2dae3a', + 'params' => [ + 'user' => 'user', + 'query' => 't[]=thumbnail:width=45,height=55&t[]=desaturate', + ], + ], + [ + 'image' => FIXTURES_DIR . '/image4.png', + 'user' => 'user', + 'imageIdentifier' => 'a501051db16e3cbf88ea50bfb0138a47', + 'params' => [ + 'user' => 'user', + 'extension' => 'jpg', + 'query' => 't[]=thumbnail:width=45,height=55&t[]=desaturate', + ], + ], + ]; + } + + /** + * @dataProvider getShortUrlParams + * @covers ::generateShortImageUrl + * @param string $image + * @param string $user + * @param string $imageIdentifier + * @param array $params + */ + public function testCanGenerateShortUrls($image, $user, $imageIdentifier, array $params) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context->addUserImageToImbo($image, $user) + ); + + $this->assertSame( + $this->context, + $this->context->generateShortImageUrl( + $image, + new PyStringNode([json_encode($params)], 1) + ) + ); + + $this->assertCount( + 2, + $this->history, 'There should exist exactly 2 requests in the history, found %d.', + count($this->history) + ); + + $request = $this->history[1]['request']; + + $this->assertSame( + sprintf('http://localhost:8080/users/user/images/%s/shorturls', $imageIdentifier), + (string) $request->getUri() + ); + + $this->assertSame('POST', $request->getMethod()); + + $this->assertSame( + array_merge($params, ['imageIdentifier' => $imageIdentifier]), + json_decode((string) $request->getBody(), true)) + ; + } + + /** + * @covers ::specifyAsTheWatermarkImage + * @expectedException InvalidArgumentException + * @expectedExceptionMessage No image exists for path: "/path". + */ + public function testThrowsExceptionWhenSpecifyingWatermarkImageThatDoesNotExist() { + $this->context->specifyAsTheWatermarkImage('/path'); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForWatermarkImages() { + return [ + 'no params' => [ + 'image' => FIXTURES_DIR . '/image1.png', + 'imageIdentifier' => 'someId', + 'params' => null, + 'uri' => 'http://localhost:8080/path?t%5B0%5D=watermark%3Aimg%3DsomeId', + ], + 'with params' => [ + 'image' => FIXTURES_DIR . '/image1.png', + 'imageIdentifier' => 'someId', + 'params' => 'x=10,y=5,position=bottom-right,width=20,height=20', + 'uri' => 'http://localhost:8080/path?t%5B0%5D=watermark%3Aimg%3DsomeId%2Cx%3D10%2Cy%3D5%2Cposition%3Dbottom-right%2Cwidth%3D20%2Cheight%3D20', + ] + ]; + } + + /** + * @dataProvider getDataForWatermarkImages + * @covers ::specifyAsTheWatermarkImage + * @param string $image + * @param string $imageIdentifier + * @param string $params + * @param string $uri + */ + public function testCanSpecifyWatermarkImage($image, $imageIdentifier, $params = null, $uri) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context->addUserImageToImbo($image, 'user') + ); + + $this->assertSame( + $this->context, + $this->context->specifyAsTheWatermarkImage($image, $params) + ); + + $request = $this->makeRequest('/path'); + + $this->assertSame($uri, (string) $request->getUri()); + } + + /** + * @covers ::requestPreviouslyAddedImage + * @covers ::getUserAndImageIdentifierOfPreviouslyAddedImage + * @expectedException RuntimeException + * @expectedExceptionMessage Could not find any response in the history with an image identifier. + */ + public function testThrowsExceptionWhenTryingToRequestPreviouslyAddedImageWhenNoImageHasBeenAdded() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->requestPreviouslyAddedImage(); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForRequestingPreviouslyAddedImage() { + return [ + 'HTTP GET' => [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'GET', + ], + 'HTTP DELETE' => [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'DELETE', + ], + ]; + } + + /** + * @dataProvider getDataForRequestingPreviouslyAddedImage + * @covers ::requestPreviouslyAddedImage + * @param string $imageIdentifier + * @param string $image + * @param string $method + */ + public function testCanRequestPreviouslyAddedImage($imageIdentifier, $image, $method) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context + ->addUserImageToImbo($image, 'user') + ->requestPreviouslyAddedImage($method) + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 requests, got: %d.', count($this->history)) + ); + + $request = $this->history[1]['request']; + + $this->assertSame($method, $request->getMethod()); + + $this->assertSame( + sprintf( + 'http://localhost:8080/users/user/images/%s', + $imageIdentifier + ), + (string) $request->getUri() + ); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForRequestingPreviouslyAddedImageWithExtension() { + return [ + [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'HEAD', + 'extension' => 'png', + ], + [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'GET', + 'extension' => 'jpg', + ], + ]; + } + + /** + * @dataProvider getDataForRequestingPreviouslyAddedImageWithExtension + * @covers ::requestPreviouslyAddedImageAsType + * @covers ::getUserAndImageIdentifierOfPreviouslyAddedImage + */ + public function testCanRequestPreviouslyAddedImageUsingAlternativeMethod($imageIdentifier, $image, $method, $extension) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context + ->addUserImageToImbo($image, 'user') + ->requestPreviouslyAddedImageAsType($extension, $method) + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 requests, got: %d.', count($this->history)) + ); + + $request = $this->history[1]['request']; + + $this->assertSame($method, $request->getMethod()); + + $this->assertSame( + sprintf( + 'http://localhost:8080/users/user/images/%s%s', + $imageIdentifier, + $extension ? '.' . $extension : '' + ), + (string) $request->getUri() + ); + } + + /** + * @covers ::requestPreviouslyAddedImageAsType + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid extension: "jpeg". + */ + public function testThrowsExceptionWhenTryingToFetchPreviouslyAddedImageWithInvalidExtension() { + $this->context->requestPreviouslyAddedImageAsType('jpeg'); + } + + /** + * @covers ::makeSameRequest + * @expectedException RuntimeException + * @expectedExceptionMessage No request has been made yet. + */ + public function testThrowsExceptionWhenTryingToReplayRequestWhenNoRequestHasBeenMade() { + $this->context->makeSameRequest(); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForReplayingRequests() { + return [ + 'use original method' => [ + 'orignalMethod' => 'GET', + 'method' => null, + 'expectedUrl' => 'http://localhost:8080/path', + 'publicKey' => null, + 'privateKey' => null, + ], + 'specify custom method' => [ + 'orignalMethod' => 'GET', + 'method' => 'HEAD', + 'expectedUrl' => 'http://localhost:8080/path', + 'publicKey' => null, + 'privateKey' => null, + ], + 'specify custom method that the same as the original' => [ + 'orignalMethod' => 'DELETE', + 'method' => 'DELETE', + 'expectedUrl' => 'http://localhost:8080/path', + 'publicKey' => null, + 'privateKey' => null, + ], + 'specify custom method and append access token' => [ + 'orignalMethod' => 'DELETE', + 'method' => 'DELETE', + 'expectedUrl' => 'http://localhost:8080/path?publicKey=key&accessToken=dd4217a681cf8abdcecdc68cf49630df1e57dc733735e902b8a69859e50797a8', + 'publicKey' => 'key', + 'privateKey' => 'secret', + ], + ]; + } + + /** + * @dataProvider getDataForReplayingRequests + * @covers ::makeSameRequest + * @param string $originalMethod + * @param string $method + * @param string $expectedUrl + * @param string $publicKey + * @param string $privateKey + */ + public function testCanReplayTheLastRequest($originalMethod, $method, $expectedUrl, $publicKey, $privateKey) { + $this->mockHandler->append(new Response(200), new Response(200)); + + $this->context->setPublicAndPrivateKey($publicKey, $privateKey); + + if ($publicKey && $privateKey) { + $this->context->appendAccessToken(); + } + + $this->context->requestPath('/path', $originalMethod); + + $this->assertSame( + $this->context, + $this->context->makeSameRequest($method) + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 requests, got: %d.', count($this->history)) + ); + + $this->assertSame($originalMethod, $this->history[0]['request']->getMethod()); + $this->assertSame($method ?: $originalMethod, $this->history[1]['request']->getMethod()); + $this->assertSame($expectedUrl, (string) $this->history[0]['request']->getUri()); + $this->assertSame( + (string) $this->history[0]['request']->getUri(), + (string) $this->history[1]['request']->getUri() + ); + } + + /** + * @covers ::requestMetadataOfPreviouslyAddedImage + * @expectedException RuntimeException + * @expectedExceptionMessage Could not find any response in the history with an image identifier. + */ + public function testThrowsExceptionWhenRequestingMetadataOfPreviouslyAddedImageWhenNoImageHasBeenAdded() { + $this->makeRequest('/path'); + $this->makeRequest('/anotherPath'); + $this->context->requestMetadataOfPreviouslyAddedImage(); + } + + /** + * @covers ::requestMetadataOfPreviouslyAddedImage + * @expectedException RuntimeException + * @expectedExceptionMessage Could not find any response in the history with an image identifier. + */ + public function testThrowsExceptionWhenRequestingMetadataOfPreviouslyAddedImageWhenNoRequestHasBeenMade() { + $this->context->requestMetadataOfPreviouslyAddedImage(); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForRequestingMetadataOfPreviouslyAddedImage() { + return [ + 'no metadata' => [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'GET', + 'metadata' => [], + ], + 'with metadata and custom method' => [ + 'imageIdentifier' => 'imageId', + 'image' => FIXTURES_DIR . '/image1.png', + 'method' => 'HEAD', + 'metadata' => ['key' => 'value'], + ], + ]; + } + + /** + * @dataProvider getDataForRequestingMetadataOfPreviouslyAddedImage + * @covers ::requestMetadataOfPreviouslyAddedImage + * @param string $imageIdentifier + * @param string $image + * @param string $method + * @param array $metadata + */ + public function testCanRequestMetadataOfPreviouslyAddedImage($imageIdentifier, $image, $method, array $metadata) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200, [], json_encode($metadata)) + ); + + $this->assertSame( + $this->context, + $this->context + ->addUserImageToImbo($image, 'user') + ->requestMetadataOfPreviouslyAddedImage($method) + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 requests, got: %d.', count($this->history)) + ); + + $request = $this->history[1]['request']; + + $this->assertSame($method, $request->getMethod()); + + $this->assertSame( + sprintf( + 'http://localhost:8080/users/user/images/%s/metadata', + $imageIdentifier + ), + (string) $request->getUri() + ); + $this->assertSame($metadata, json_decode((string) $this->history[1]['response']->getBody(), true)); + } + + /** + * @covers ::requestImageResourceForLocalImage + * @expectedException InvalidArgumentException + * @expectedExceptionMessageRegExp |Image URL for image with path ".*?/tests/phpunit/Fixtures/image1\.png" can not be found\.| + */ + public function testThrowsExceptionWhenTryingToRequestImageUsingLocalPathAndImageDoesNotExistInImbo() { + $this->context->requestImageResourceForLocalImage(FIXTURES_DIR . '/image1.png'); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForRequestingImageWithLocalPath() { + return [ + 'default values' => [ + 'image' => FIXTURES_DIR . '/image1.png', + 'imageIdentifier' => 'imageId', + 'extension' => null, + 'method' => 'GET', + 'expectedPath' => '/users/user/images/imageId', + ], + 'custom extension and method' => [ + 'image' => FIXTURES_DIR . '/image1.png', + 'imageIdentifier' => 'imageId', + 'extension' => 'gif', + 'method' => 'HEAD', + 'expectedPath' => '/users/user/images/imageId.gif', + ], + ]; + } + + /** + * @dataProvider getDataForRequestingImageWithLocalPath + * @covers ::requestImageResourceForLocalImage + * @param string $image + * @param string $imageIdentifier + * @param string $extension + * @param string $method + * @param string $expectedPath + */ + public function testCanRequestImageUsingLocalFilePath($image, $imageIdentifier, $extension, $method, $expectedPath) { + $this->mockHandler->append( + new Response(200, [], json_encode(['imageIdentifier' => $imageIdentifier])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context + ->addUserImageToImbo($image, 'user') + ->requestImageResourceForLocalImage($image, $extension, $method) + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 transactions in the history, got %d.', count($this->history)) + ); + + $request = $this->history[1]['request']; + + $this->assertSame($expectedPath, $request->getUri()->getPath()); + $this->assertSame($method, $request->getMethod()); + } + + /** + * @covers ::requestPaths + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Missing or empty "path" key. + */ + public function testThrowsExceptionWhenBulkRequestingWithMissingPath() { + $this->context->requestPaths(new TableNode([ + ['method'], + ['GET'], + ])); + } + + /** + * @covers ::requestPaths + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Both "sign request" and "access token" can not be set to "yes". + */ + public function testThrowsExceptionWhenBulkRequestingAndUsingBothAccessTokenAndSignature() { + $this->context->requestPaths(new TableNode([ + ['path', 'access token', 'sign request'], + ['/path', 'yes', 'yes' ] + ])); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForBulkRequests() { + return [ + 'single request with no options' => [ + 'table' => new TableNode([ + ['path'], + ['/path'] + ]), + 'requests' => [ + [ + 'path' => '/path', + 'method' => 'GET', + 'query' => '', + 'requestBody' => '', + ], + ], + ], + 'append access token' => [ + 'table' => new TableNode([ + ['path', 'access token'], + ['/path', 'yes'] + ]), + 'requests' => [ + [ + 'path' => '/path', + 'method' => 'GET', + 'query' => 'publicKey=publicKey&accessToken=582386896ffacd2c34a39476f0fa71ac9e6b22f079482ea7ee687e15826b08ef', + 'requestBody' => '', + ], + ], + ], + 'add transformation' => [ + 'table' => new TableNode([ + ['path', 'transformation'], + ['/path', 'border'] + ]), + 'requests' => [ + [ + 'path' => '/path', + 'method' => 'GET', + 'query' => 't%5B0%5D=border', + 'requestBody' => '', + ], + ], + ], + 'set request body' => [ + 'table' => new TableNode([ + ['path', 'request body'], + ['/path', 'some data'] + ]), + 'requests' => [ + [ + 'path' => '/path', + 'method' => 'GET', + 'query' => '', + 'requestBody' => 'some data', + ], + ], + ], + ]; + } + + /** + * @dataProvider getDataForBulkRequests + * @covers ::requestPaths + * @param TableNode $table + * @param array $requests + */ + public function testCanBulkRequest(TableNode $table, array $requests) { + for ($i = 0; $i < count($table->getRows()) - 1; $i++) { + $this->mockHandler->append(new Response(200)); + } + + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey('publicKey', 'privateKey') + ->requestPaths($table) + ); + + foreach ($requests as $i => $data) { + $this->assertSame($data['path'], $this->history[$i]['request']->getUri()->getPath()); + $this->assertSame($data['method'], $this->history[$i]['request']->getMethod()); + $this->assertSame($data['query'], $this->history[$i]['request']->getUri()->getQuery()); + $this->assertSame($data['requestBody'], (string) $this->history[$i]['request']->getBody()); + } + } + + /** + * @covers ::requestPaths + */ + public function testCanBulkRequestWithPreviouslyAddedImage() { + $this->mockHandler->append( + // Response from adding image + new Response(200, [], json_encode(['imageIdentifier' => 'imageId'])), + + // Bulk responses + new Response(200), + new Response(200), + new Response(200) + ); + + $requests = new TableNode([ + ['path', 'method', 'transformation', 'extension', 'access token'], + ['previously added image', '', 'border', '', 'yes' ], + ['previously added image', 'HEAD', 'thumbnail', '', 'yes' ], + ['previously added image', '', 'strip', 'gif', 'yes' ], + ]); + + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey('publicKey', 'privateKey') + ->addUserImageToImbo(FIXTURES_DIR . '/image1.png', 'user') + ->requestPaths($requests) + ); + + $this->assertCount( + 4, + $this->history, + sprintf('Expected exactly 3 requests, got %d.', count($this->history)) + ); + + $this->assertSame('GET', $this->history[1]['request']->getMethod()); + $this->assertSame('/users/user/images/imageId', $this->history[1]['request']->getUri()->getPath()); + $this->assertSame('t%5B0%5D=border&publicKey=publicKey&accessToken=ec92fc446856c31b43facd62617f23c84d44e013ab3fff66db050291242f73e5', $this->history[1]['request']->getUri()->getQuery()); + + $this->assertSame('HEAD', $this->history[2]['request']->getMethod()); + $this->assertSame('/users/user/images/imageId', $this->history[2]['request']->getUri()->getPath()); + $this->assertSame('t%5B0%5D=thumbnail&publicKey=publicKey&accessToken=2b2eb39e7b8542fe0ca68f9c7c005759b0c4e05eb036fb46358c7e00dc9df141', $this->history[2]['request']->getUri()->getQuery()); + + $this->assertSame('GET', $this->history[3]['request']->getMethod()); + $this->assertSame('/users/user/images/imageId.gif', $this->history[3]['request']->getUri()->getPath()); + $this->assertSame('t%5B0%5D=strip&publicKey=publicKey&accessToken=5064e745998642e65c559c5ce5566f33926bcd18c041f10d93f602320c0d3c50', $this->history[3]['request']->getUri()->getQuery()); + } + + /** + * @covers ::requestPaths + */ + public function testCanBulkRequestWithSignedRequests() { + $requests = new TableNode([ + ['path', 'method', 'sign request'], + ['/path1', '', 'yes' ], + ['/path2', 'GET', '' ], + ['/path3', 'HEAD', 'yes' ], + ]); + $publicKey = 'publicKey'; + $privateKey = 'privateKey'; + + $this->mockHandler->append( + new Response(200), + new Response(200), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey($publicKey, $privateKey) + ->requestPaths($requests) + ); + + $this->assertCount( + 3, + $this->history, + sprintf('Expected exactly 1 request, got %d.', count($this->history)) + ); + + $this->assertSame('GET', $this->history[0]['request']->getMethod()); + $this->assertSame('/path1', $this->history[0]['request']->getUri()->getPath()); + $this->assertRegExp( + '/^publicKey=publicKey&signature=[a-z0-9]{64}×tamp=[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$/', + $this->history[0]['request']->getUri()->getQuery() + ); + + $this->assertSame('GET', $this->history[1]['request']->getMethod()); + $this->assertSame('/path2', $this->history[1]['request']->getUri()->getPath()); + $this->assertEmpty($this->history[1]['request']->getUri()->getQuery()); + + $this->assertSame('HEAD', $this->history[2]['request']->getMethod()); + $this->assertSame('/path3', $this->history[2]['request']->getUri()->getPath()); + $this->assertRegExp( + '/^publicKey=publicKey&signature=[a-z0-9]{64}×tamp=[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}Z$/', + $this->history[2]['request']->getUri()->getQuery() + ); + } + + /** + * @covers ::requestPaths + */ + public function testCanBulkRequestWithMetadataOfPreviouslyAddedImage() { + $this->mockHandler->append( + // Response of adding image + new Response(200, [], json_encode(['imageIdentifier' => 'imageId'])), + + // Response of adding metadata + new Response(200, [], json_encode(['foo' => 'bar'])), + + // Responses to bulk requests + new Response(200), + new Response(200) + ); + + $requests = new TableNode([ + ['path', 'method', 'access token'], + ['metadata of previously added image', '', 'yes' ], + ['metadata of previously added image', 'HEAD', 'yes' ], + ]); + + $this->assertSame( + $this->context, + $this->context + ->setPublicAndPrivateKey('publicKey', 'privateKey') + ->addUserImageToImbo( + FIXTURES_DIR . '/image1.png', + 'user', + new PyStringNode(['{"foo": "bar"}'], 1) + ) + ->requestPaths($requests) + ); + + $this->assertCount( + 4, + $this->history, + sprintf('Expected exactly 3 requests, got %d.', count($this->history)) + ); + + $this->assertSame('GET', $this->history[2]['request']->getMethod()); + $this->assertSame('/users/user/images/imageId/metadata', $this->history[2]['request']->getUri()->getPath()); + $this->assertSame('publicKey=publicKey&accessToken=78ed8225148fb3cc09d61ccd133831ef36e4bbd8ee757d6ff1378c65067d7775', $this->history[2]['request']->getUri()->getQuery()); + + $this->assertSame('HEAD', $this->history[3]['request']->getMethod()); + $this->assertSame('/users/user/images/imageId/metadata', $this->history[3]['request']->getUri()->getPath()); + $this->assertSame('publicKey=publicKey&accessToken=78ed8225148fb3cc09d61ccd133831ef36e4bbd8ee757d6ff1378c65067d7775', $this->history[3]['request']->getUri()->getQuery()); + } + + /** + * @covers ::requestImageUsingShortUrl + * @expectedException RuntimeException + * @expectedExceptionMessage Invalid response body in the current response instance + */ + public function testThrowsExceptionWhenTryingToRequestImageWithShortUrlWhenResponseHasInvalidBody() { + $this->mockHandler->append( + new Response(200), + new Response(200) + ); + + $this->context + ->requestPath('/path') + ->requestImageUsingShortUrl(); + } + + /** + * @covers ::requestImageUsingShortUrl + * @expectedException RuntimeException + * @expectedExceptionMessage Missing "id" from body: "{"foo":"bar"}". + */ + public function testThrowsExceptionWhenTryingToRequestImageWithShortUrlWhenResponseBodyIsMissingId() { + $this->mockHandler->append( + new Response(200, [], json_encode(['foo' => 'bar'])), + new Response(200) + ); + + $this->context + ->requestPath('/path') + ->requestImageUsingShortUrl(); + } + + /** + * @covers ::requestImageUsingShortUrl + */ + public function testCanRequestImageUsingShortUrlCreatedInPreviousRequest() { + $this->mockHandler->append( + new Response(200, [], json_encode(['id' => 'someId'])), + new Response(200) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->requestImageUsingShortUrl() + ); + + $this->assertCount( + 2, + $this->history, + sprintf('Expected exactly 2 requests, got %d.', count($this->history)) + ); + + $request = $this->history[1]['request']; + + $this->assertSame('GET', $request->getMethod()); + $this->assertSame('/s/someId', $request->getUri()->getPath()); + } + + /** + * @covers ::assertImboError + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The status code of the last response is lower than 400, so it is not considered an error. + */ + public function testThrowsExceptionWhenAssertingImboErrorWhenResponseIsNotAnError() { + $this->makeRequest('/path'); + $this->context->assertImboError('some message'); + } + + /** + * @covers ::assertImboError + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Expected error message "foobar", got "error message". + */ + public function testAssertingImboErrorMessageCanFailWhenMessageIsWrong() { + $this->mockHandler->append( + new Response(500, [], json_encode(['error' => [ + 'message' => 'error message', + 'imboErrorCode' => 1, + ]])) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImboError('foobar') + ); + } + + /** + * @covers ::assertImboError + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Expected imbo error code "2", got "1". + */ + public function testAssertingImboErrorMessageCanFailWhenCodeIsWrong() { + $this->mockHandler->append( + new Response(500, [], json_encode(['error' => [ + 'message' => 'error message', + 'imboErrorCode' => 1, + ]])) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImboError('error message', 2) + ); + } + + /** + * @covers ::assertImboError + */ + public function testCanAssertImboErrorMessage() { + $this->mockHandler->append( + new Response(500, [], json_encode(['error' => [ + 'message' => 'error message', + 'imboErrorCode' => 1, + ]])) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImboError('error message', 1) + ); + } + + /** + * @covers ::assertImageWidth + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Incorrect image width, expected 123, got 599. + */ + public function testAssertingImageWidthCanFail() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageWidth(123) + ); + } + + /** + * @covers ::assertImageWidth + */ + public function testCanAssertImageWidth() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageWidth(599) + ); + } + + /** + * @covers ::assertImageHeight + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Incorrect image height, expected 123, got 417. + */ + public function testAssertingImageHeightCanFail() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageHeight(123) + ); + } + + /** + * @covers ::assertImageHeight + */ + public function testCanAssertImageHeight() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageHeight(417) + ); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForImageDimensionAssertion() { + $image1 = file_get_contents(FIXTURES_DIR . '/image1.png'); + $image2 = file_get_contents(FIXTURES_DIR . '/image2.png'); + + return [ + [ + 'image' => $image1, + 'dimension' => '123x456', + 'exceptionMessage' => 'Incorrect image width, expected 123, got 599.', + ], + [ + 'image' => $image1, + 'dimension' => '599x456', + 'exceptionMessage' => 'Incorrect image height, expected 456, got 417.', + ], + [ + 'image' => $image2, + 'dimension' => '123x456', + 'exceptionMessage' => 'Incorrect image width, expected 123, got 539.', + ], + [ + 'image' => $image2, + 'dimension' => '539x123', + 'exceptionMessage' => 'Incorrect image height, expected 123, got 375.', + ], + ]; + } + + /** + * @dataProvider getDataForImageDimensionAssertion + * @covers ::assertImageDimension + * @expectedException Assert\InvalidArgumentException + * @param string $imageData + * @param string $dimension + * @param string $exceptionMessage + */ + public function testAssertingImageDimensionCanFail($imageData, $dimension, $exceptionMessage) { + $this->expectExceptionMessage($exceptionMessage); + $this->mockHandler->append( + new Response(200, [], $imageData) + ); + + $this->context + ->requestPath('/path') + ->assertImageDimension($dimension); + } + + /** + * @covers ::assertImageDimension + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid dimension value: "123 x 456". Specify "x". + */ + public function testThrowsExceptionWhenAssertingImageDimensionWhenInvalidDimensionString() { + $this->mockHandler->append( + new Response(200) + ); + $this->context + ->requestPath('/path') + ->assertImageDimension('123 x 456'); + } + + /** + * @covers ::assertImageDimension + */ + public function testCanAssertImageDimension() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageDimension('599x417') + ); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForAssertingImagePixelInfoFailures() { + $image = file_get_contents(FIXTURES_DIR . '/image1.png'); + + return [ + [ + 'imageData' => $image, + 'coordinate' => '1,1', + 'color' => '000000', + 'exceptionMessage' => 'Incorrect color at coordinate "1,1", expected "000000", got "ffffff".', + ], + [ + 'imageData' => $image, + 'coordinate' => '247,32', + 'color' => '000000', + 'exceptionMessage' => 'Incorrect color at coordinate "247,32", expected "000000", got "e8e7e6".', + ], + [ + 'imageData' => $image, + 'coordinate' => '275,150', + 'color' => 'ffffff', + 'exceptionMessage' => 'Incorrect color at coordinate "275,150", expected "ffffff", got "000000".', + ], + ]; + } + + /** + * @dataProvider getDataForAssertingImagePixelInfoFailures + * @covers ::assertImagePixelColor + * @covers ::getImagePixelInfo + * @expectedException Assert\InvalidArgumentException + * @param string $imageData + * @param string $coordinate + * @param string $color + * @param string $exceptionMessage + */ + public function testAssertingImagePixelColorCanFail($imageData, $coordinate, $color, $exceptionMessage) { + $this->mockHandler->append( + new Response(200, [], $imageData) + ); + + $this->expectExceptionMessage($exceptionMessage); + $this->context + ->requestPath('/path') + ->assertImagePixelColor($coordinate, $color); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForAssertingImagePixelInfo() { + $image = file_get_contents(FIXTURES_DIR . '/image1.png'); + + return [ + [ + 'imageData' => $image, + 'coordinate' => '1,1', + 'color' => 'ffffff', + ], + [ + 'imageData' => $image, + 'coordinate' => '247,32', + 'color' => 'e8e7e6', + ], + [ + 'imageData' => $image, + 'coordinate' => '275,150', + 'color' => '000000', + ], + ]; + } + + /** + * @dataProvider getDataForAssertingImagePixelInfo + * @covers ::assertImagePixelColor + * @covers ::getImagePixelInfo + * @param string $imageData + * @param string $coordinate + * @param string $color + */ + public function testCanAssertImagePixelColor($imageData, $coordinate, $color) { + $this->mockHandler->append( + new Response(200, [], $imageData) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImagePixelColor($coordinate, $color) + ); + } + + /** + * @covers ::assertImagePixelColor + * @covers ::getImagePixelInfo + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid coordinates: "1, 1". Format is "x", no spaces allowed. + */ + public function testThrowsExceptionWhenAssertingImagePixelColorWithInvalidCoordinate() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/image1.png')) + ); + + $this->context + ->requestPath('/path') + ->assertImagePixelColor('1, 1', 'ffffff'); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForAssertingImagePixelAlphaFailures() { + $image = file_get_contents(FIXTURES_DIR . '/transparency.png'); + + return [ + [ + 'imageData' => $image, + 'coordinate' => '448,192', + 'alpha' => '0', + 'exceptionMessage' => sprintf('Incorrect alpha value at coordinate "448,192", expected "%f", got "%f".', 0, 1), + ], + [ + 'imageData' => $image, + 'coordinate' => '192,64', + 'alpha' => '1', + 'exceptionMessage' => sprintf('Incorrect alpha value at coordinate "192,64", expected "%f", got "%f".', 1, 0), + ], + ]; + } + + /** + * @dataProvider getDataForAssertingImagePixelAlphaFailures + * @covers ::assertImagePixelAlpha + * @covers ::getImagePixelInfo + * @expectedException Assert\InvalidArgumentException + * @param string $imageData + * @param string $coordinate + * @param string $alpha + * @param string $exceptionMessage + */ + public function testAssertingImagePixelAlphaCanFail($imageData, $coordinate, $alpha, $exceptionMessage) { + $this->mockHandler->append( + new Response(200, [], $imageData) + ); + + $this->expectExceptionMessage($exceptionMessage); + $this->context + ->requestPath('/path') + ->assertImagePixelAlpha($coordinate, $alpha); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForAssertingImagePixelAlpha() { + $image = file_get_contents(FIXTURES_DIR . '/transparency.png'); + + return [ + [ + 'imageData' => $image, + 'coordinate' => '448,192', + 'alpha' => '1', + ], + [ + 'imageData' => $image, + 'coordinate' => '192,64', + 'alpha' => '0', + ], + ]; + } + + /** + * @dataProvider getDataForAssertingImagePixelAlpha + * @covers ::assertImagePixelAlpha + * @covers ::getImagePixelInfo + * @param string $imageData + * @param string $coordinate + * @param string $alpha + */ + public function testCanAssertImagePixelAlpha($imageData, $coordinate, $alpha) { + $this->mockHandler->append( + new Response(200, [], $imageData) + ); + + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImagePixelAlpha($coordinate, $alpha) + ); + } + + /** + * @covers ::assertImagePixelAlpha + * @covers ::getImagePixelInfo + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid coordinates: "1, 1". Format is "x", no spaces allowed. + */ + public function testThrowsExceptionWhenAssertingImagePixelAlphaWithInvalidCoordinate() { + $this->mockHandler->append( + new Response(200, [], file_get_contents(FIXTURES_DIR . '/transparency.png')) + ); + + $this->context + ->requestPath('/path') + ->assertImagePixelAlpha('1, 1', '1'); + } + + /** + * @covers ::assertAclRuleWithIdDoesNotExist + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage ACL rule "someId" with public key "publicKey" still exists. Expected "404 Access rule not found", got "200 OK". + */ + public function testThrowsExceptionWhenAssertingThatAclRuleWithIdDoesNotExistWhenItDoesExist() { + $this->mockHandler->append(new Response(200, [], '', '1.1', 'OK')); + $this->context->assertAclRuleWithIdDoesNotExist('publicKey', 'someId'); + } + + /** + * @covers ::assertAclRuleWithIdDoesNotExist + */ + public function testCanAssertThatAclRuleWithIdDoesNotExist() { + $this->mockHandler->append(new Response(404, [], '', '1.1', 'Access rule not found')); + $this->assertSame( + $this->context, + $this->context->assertAclRuleWithIdDoesNotExist('publicKey', 'someId') + ); + + $this->assertCount( + 1, + $this->history, + sprintf('Expected exactly 1 request, got %d.', count($this->history)) + ); + + $this->assertSame( + '/keys/publicKey/access/someId', + $this->history[0]['request']->getUri()->getPath() + ); + } + + /** + * @covers ::assertPublicKeyDoesNotExist + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Public key "publicKey" still exists. Expected "404 Public key not found", got "200 OK". + */ + public function testThrowsExceptionWhenAssertingThatPublicKeyDoesNotExistWhenItDoes() { + $this->mockHandler->append(new Response(200, [], '', '1.1', 'OK')); + $this->context->assertPublicKeyDoesNotExist('publicKey'); + } + + /** + * @covers ::assertPublicKeyDoesNotExist + */ + public function testCanAssertThatPublicKeyDoesNotExist() { + $this->mockHandler->append(new Response(404, [], '', '1.1', 'Public key not found')); + $this->assertSame( + $this->context, + $this->context->assertPublicKeyDoesNotExist('publicKey', 'someId') + ); + + $this->assertCount( + 1, + $this->history, + sprintf('Expected exactly 1 request, got %d.', count($this->history)) + ); + + $this->assertSame('/keys/publicKey', $this->history[0]['request']->getUri()->getPath()); + } + + /** + * Data provider + * + * @return array[] + */ + public function getCacheabilityData() { + return [ + 'cacheable, expect cacheable' => [ + 'cacheable' => true, + 'expected' => true, + ], + 'not cacheable, expect not cacheable' => [ + 'cacheable' => false, + 'expected' => false, + ], + 'cacheable, expect not cacheable' => [ + 'cacheable' => true, + 'expected' => false, + 'exceptionMessage' => 'Response was not supposed to be cacheable, but it is.', + ], + 'not cacheable, expect cacheable' => [ + 'cacheable' => false, + 'expected' => true, + 'exceptionMessage' => 'Response was supposed to be cacheble, but it\'s not.', + ], + ]; + } + + /** + * @dataProvider getCacheabilityData + * @covers ::__construct + * @covers ::assertCacheability + * @param boolean $actual + * @param boolean $expected + * @param string $exceptionMessage + */ + public function testCanAssertResponseCacheability($actual, $expected, $exceptionMessage = null) { + $this->cacheUtil + ->expects($this->once()) + ->method('isCacheable') + ->with($this->isInstanceOf('GuzzleHttp\Psr7\Response')) + ->will($this->returnValue($actual)); + + $this->mockHandler->append(new Response(200)); + $this->context->requestPath('/path'); + + if ($exceptionMessage) { + $this->expectException('Assert\InvalidArgumentException'); + $this->expectExceptionMessage($exceptionMessage); + $this->context->assertCacheability($expected); + } else { + $this->assertSame( + $this->context, + $this->context->assertCacheability($expected) + ); + } + } + + /** + * @covers ::assertMaxAge + * @expectedException RuntimeException + * @expectedExceptionMessage Response does not have a cache-control header. + */ + public function testThrowsExceptionWhenAssertingMaxAgeAndResponseDoesNotHaveCacheControlHeader() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->assertMaxAge(123); + } + + /** + * @covers ::assertMaxAge + * @expectedException RuntimeException + * @expectedExceptionMessage Response cache-control header does not include a max-age directive: "private". + */ + public function testThrowsExceptionWhenAssertingMaxAgeAndResponseCacheControlHeaderDoesNotHaveMaxAgeDirective() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private'])); + $this->context + ->requestPath('/path') + ->assertMaxAge(123); + } + + /** + * @covers ::assertMaxAge + */ + public function testCanAssertResponseMaxAge() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=600'])); + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertMaxAge(600) + ); + } + + /** + * @covers ::assertMaxAge + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage The max-age directive in the cache-control header is not correct. Expected 123, got 456. Complete cache-control header: "private, max-age=456". + */ + public function testAssertingResponseMaxAgeCanFail() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=456'])); + $this->context + ->requestPath('/path') + ->assertMaxAge(123); + } + + /** + * @covers ::assertResponseHasCacheControlDirective + */ + public function testCanAssertThatASpecificCacheControlDirectiveExists() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=600, must-revalidate'])); + $this->context->requestPath('/path'); + foreach (['private', 'max-age', 'must-revalidate'] as $directive) { + $this->assertSame( + $this->context, + $this->context + ->assertResponseHasCacheControlDirective($directive) + ); + } + } + + /** + * @covers ::assertResponseHasCacheControlDirective + * @expectedException RuntimeException + * @expectedExceptionMessage Response does not have a cache-control header. + */ + public function testThrowsExceptionWhenAssertingCacheControlHeaderDirectiveWhenResponseDoesNotHaveACacheControlHeader() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->assertResponseHasCacheControlDirective('must-revalidate'); + } + + /** + * @covers ::assertResponseHasCacheControlDirective + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage The cache-control header does not contain the "must-revalidate" directive. Complete cache-control header: "private, max-age=600". + */ + public function testThrowsExceptionWhenAssertingThatACacheControlDirectiveExistsWhenItDoesNot() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=600'])); + $this->context + ->requestPath('/path') + ->assertResponseHasCacheControlDirective('must-revalidate'); + } + + /** + * @covers ::assertResponseDoesNotHaveCacheControlDirective + */ + public function testCanAssertThatASpecificCacheControlDirectiveDoesNotExists() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=600, must-revalidate'])); + $this->context->requestPath('/path'); + foreach (['public', 'no-cache', 'no-store'] as $directive) { + $this->assertSame( + $this->context, + $this->context + ->assertResponseDoesNotHaveCacheControlDirective($directive) + ); + } + } + + /** + * @covers ::assertResponseDoesNotHaveCacheControlDirective + * @expectedException RuntimeException + * @expectedExceptionMessage Response does not have a cache-control header. + */ + public function testThrowsExceptionWhenAssertingResponseDoesNotHaveCacheControlHeaderDirectiveWhenResponseDoesNotHaveACacheControlHeader() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->assertResponseDoesNotHaveCacheControlDirective('must-revalidate'); + } + + /** + * @covers ::assertResponseDoesNotHaveCacheControlDirective + * @expectedException RuntimeException + * @expectedExceptionMessage The cache-control header contains the "max-age" directive when it should not. Complete cache-control header: "private, max-age=600, must-revalidate". + */ + public function testThrowsExceptionWhenAssertingThatACacheControlDirectiveDoesNotExistWhenItDoes() { + $this->mockHandler->append(new Response(200, ['cache-control' => 'private, max-age=600, must-revalidate'])); + $this->context + ->requestPath('/path') + ->assertResponseDoesNotHaveCacheControlDirective('max-age'); + } + + /** + * @covers ::assertLastResponseHeaders + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Need to compare at least 2 responses. + */ + public function testThrowsExceptionWhenAssertingTheLastResponseHeadersAndOnlyOneResponseExist() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->assertLastResponseHeaders(1, 'content-length'); + } + + /** + * @covers ::assertLastResponseHeaders + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Not enough responses in the history. Need at least 4, there are currently 2. + */ + public function testThrowsExceptionWhenAssertingTheLastResponseHeadersAndThereIsNotEnoughResponses() { + $this->mockHandler->append(new Response(200), new Response(200)); + $this->context + ->requestPath('/path') + ->requestPath('/anotherPath') + ->assertLastResponseHeaders(4, 'content-length'); + } + + /** + * @covers ::assertLastResponseHeaders + * @expectedException RuntimeException + * @expectedExceptionMessage The "content-length" header is not present in all of the last 3 response headers. + */ + public function testThrowsExceptionWhenAssertingLastResponseHeadersAndHeaderIsNotPresentInAllResponses() { + $this->mockHandler->append( + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 123]), + new Response(200) + ); + $this->context + ->requestPath('/path1') + ->requestPath('/path2') + ->requestPath('/path3') + ->assertLastResponseHeaders(3, 'content-length'); + } + + /** + * @covers ::assertLastResponseHeaders + */ + public function testCanAssertLastResponesHeadersForUniqueness() { + $this->mockHandler->append( + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 456]), + new Response(200, ['content-length' => 789]) + ); + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path1') + ->requestPath('/path2') + ->requestPath('/path3') + ->assertLastResponseHeaders(3, 'content-length', true) + ); + } + + /** + * @covers ::assertLastResponseHeaders + */ + public function testCanAssertLastResponesHeadersForNonUniqueness() { + $this->mockHandler->append( + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 123]) + ); + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path1') + ->requestPath('/path2') + ->requestPath('/path3') + ->assertLastResponseHeaders(3, 'content-length') + ); + } + + /** + * @covers ::assertLastResponseHeaders + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Expected 3 unique values, got 2. Values compared: + */ + public function testCanAssertingLastResponesHeadersForUniquenessCanFail() { + $this->mockHandler->append( + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 456]), + new Response(200, ['content-length' => 456]) + ); + $this->context + ->requestPath('/path1') + ->requestPath('/path2') + ->requestPath('/path3') + ->assertLastResponseHeaders(3, 'content-length', true); + } + + /** + * @covers ::assertLastResponseHeaders + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Expected all values to be the same. Values compared: + */ + public function testCanAssertingLastResponesHeadersForNonUniquenessCanFail() { + $this->mockHandler->append( + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 123]), + new Response(200, ['content-length' => 456]) + ); + $this->context + ->requestPath('/path1') + ->requestPath('/path2') + ->requestPath('/path3') + ->assertLastResponseHeaders(3, 'content-length'); + } + + /** + * @covers ::assertResponseBodySize + */ + public function testCanAssertResponseBodySize() { + $this->mockHandler->append(new Response(200, [], 'some string')); + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertResponseBodySize(11) + ); + } + + /** + * @covers ::assertResponseBodySize + * @expectedException Assert\InvalidArgumentException + * @expectedExceptionMessage Expected response body size: 123, actual: 11. + */ + public function testAssertingResponseBodySizeCanFail() { + $this->mockHandler->append(new Response(200, [], 'some string')); + $this->context + ->requestPath('/path') + ->assertResponseBodySize(123); + } + + /** + * @covers ::assertLastResponsesMatch + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Missing response column + */ + public function testThrowsExceptionWhenMatchingResponsesWithNoResponseKeyInTable() { + $this->context->assertLastResponsesMatch(new TableNode([['num'], ['3']])); + } + + /** + * @covers ::assertLastResponsesMatch + * @expectedException RuntimeException + * @expectedExceptionMessage Not enough transactions in the history. Needs at least 3, actual: 2. + */ + public function testThrowsExceptionWhenMatchingMoreResponsesThanWhatIsPresentInTheHistory() { + $this->mockHandler->append(new Response(200), new Response(200)); + $this->context + ->requestPath('/path') + ->requestPath('/path') + ->assertLastResponsesMatch(new TableNode([ + ['response'], + ['3'], + ])); + } + + /** + * @covers ::assertLastResponsesMatch + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Each row must refer to a response by using the "response" column. + */ + public function testThrowsExceptionWhenMatchingResponsesAndARowIsMissingResponseNumber() { + $this->mockHandler->append(new Response(200), new Response(200)); + $this->context + ->requestPath('/path') + ->requestPath('/path') + ->assertLastResponsesMatch(new TableNode([ + ['response'], + ['1'], + [''], + ])); + } + + /** + * @covers ::assertLastResponsesMatch + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Invalid column name: "foobar". + */ + public function testThrowsExceptionWhenMatchingResponsesAndAnInvalidColumnIsUsed() { + $this->mockHandler->append(new Response(200)); + $this->context + ->requestPath('/path') + ->assertLastResponsesMatch(new TableNode([ + ['response', 'foobar'], + ['1', 'baz' ], + ])); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForMatchingSeveralResponses() { + return [ + 'status line' => [ + 'responses' => [ + new Response(200), + new Response(204), + new Response(404), + new Response(500), + ], + 'match' => new TableNode([ + ['response', 'status line' ], + ['1', '200 OK' ], + ['2', '204 No Content' ], + ['3', '404 Not Found' ], + ['4', '500 Internal Server Error'], + ]), + ], + 'headers' => [ + 'responses' => [ + new Response(200, [ + 'content-type' => 'application/json', + 'content-length' => 13, + ], '{"foo":"bar"}'), + new Response(200, [ + 'x-imbo-foo' => 'bar', + ], '{"foo":"bar"}'), + ], + 'match' => new TableNode([ + ['response', 'header name', 'header value' ], + ['1', 'content-type', 'application/json'], + ['1', 'content-length', '13' ], + ['2', 'x-imbo-foo', 'bar' ], + ]), + ], + 'checksum' => [ + 'responses' => [ + new Response(200, [], '{"foo":"bar"}'), + new Response(200, [], '{"bar":"foo"}'), + ], + 'match' => new TableNode([ + ['response', 'checksum' ], + ['1', '9bb58f26192e4ba00f01e2e7b136bbd8'], + ['2', 'e561e07998cff8eca9f3acc8a2fdb12f'], + ]), + ], + 'image width / height' => [ + 'responses' => [ + new Response(200, [], file_get_contents(FIXTURES_DIR . '/1024x256.png')), + new Response(200, [], file_get_contents(FIXTURES_DIR . '/256x1024.png')) + ], + 'match' => new TableNode([ + ['response', 'image width', 'image height'], + ['1', 1024, 256 ], + ['2', 256, 1024 ], + ]), + ], + 'body is' => [ + 'responses' => [ + new Response(200, [], '{"foo":"bar"}'), + new Response(200, [], '{"bar":"foo"}'), + ], + 'match' => new TableNode([ + ['response', 'body is' ], + ['1', '{"foo":"bar"}'], + ['2', '{"bar":"foo"}'], + ]), + ], + ]; + } + + /** + * @dataProvider getDataForMatchingSeveralResponses + * @covers ::assertLastResponsesMatch + * @param Response[] $responses + * @param TableNode $table + */ + public function testCanMatchResponses(array $responses, TableNode $table) { + $this->mockHandler->append(...$responses); + + for ($i = 0; $i < count($responses); $i++) { + $this->context->requestPath('/path'); + } + + $this->assertSame( + $this->context, + $this->context->assertLastResponsesMatch($table) + ); + } + + /** + * Data provider + * + * @return array[] + */ + public function getDataForMatchingSeveralResponsesWhenFailing() { + return [ + 'status line' => [ + 'responses' => [ + new Response(200), + new Response(201), + ], + 'match' => new TableNode([ + ['response', 'status line' ], + ['1', '200 OK' ], + ['2', '204 No Content'], + ]), + 'exceptionMessage' => 'Incorrect status line in response 2, expected "204 No Content", got: "201 Created".', + ], + 'headers' => [ + 'responses' => [ + new Response(200, [ + 'content-type' => 'application/json', + ], '{"foo":"bar"}'), + new Response(200, [ + 'x-imbo-foo' => 'bar', + ], '{"foo":"bar"}'), + ], + 'match' => new TableNode([ + ['response', 'header name', 'header value' ], + ['1', 'content-type', 'application/json'], + ['2', 'x-imbo-foo', 'foobar' ], + ]), + 'exceptionMessage' => 'Incorrect "x-imbo-foo" header value in response 2, expected "foobar", got: "bar".', + ], + 'checksum' => [ + 'responses' => [ + new Response(200, [], '{"foo":"bar"}'), + new Response(200, [], '{"bar":"foo"}'), + ], + 'match' => new TableNode([ + ['response', 'checksum' ], + ['1', '9bb58f26192e4ba00f01e2e7b136bbd8'], + ['2', '9bb58f26192e4ba00f01e2e7b136bbd8'], + ]), + 'exceptionMessage' => 'Incorrect checksum in response 2, expected "9bb58f26192e4ba00f01e2e7b136bbd8", got: "e561e07998cff8eca9f3acc8a2fdb12f".', + ], + 'image width / height (failure on width)' => [ + 'responses' => [ + new Response(200, [], file_get_contents(FIXTURES_DIR . '/1024x256.png')), + new Response(200, [], file_get_contents(FIXTURES_DIR . '/256x1024.png')) + ], + 'match' => new TableNode([ + ['response', 'image width', 'image height'], + ['1', 1024, 256 ], + ['2', 255, 1024 ], + ]), + 'exceptionMessage' => 'Expected image in response 2 to be 255 pixel(s) wide, actual: 256.', + ], + 'image width / height (failure on height)' => [ + 'responses' => [ + new Response(200, [], file_get_contents(FIXTURES_DIR . '/1024x256.png')), + new Response(200, [], file_get_contents(FIXTURES_DIR . '/256x1024.png')) + ], + 'match' => new TableNode([ + ['response', 'image width', 'image height'], + ['1', 1024, 256 ], + ['2', 256, 1023 ], + ]), + 'exceptionMessage' => 'Expected image in response 2 to be 1023 pixel(s) high, actual: 1024.', + ], + 'body is' => [ + 'responses' => [ + new Response(200, [], '{"foo":"bar"}'), + new Response(200, [], '{"bar":"foo"}'), + ], + 'match' => new TableNode([ + ['response', 'body is' ], + ['1', '{"foo":"bar"}' ], + ['2', '{"bar":"foobar"}'], + ]), + 'exceptionMessage' => 'Incorrect response body for request 2, expected "{"bar":"foobar"}", got: "{"bar":"foo"}".', + ], + ]; + } + + /** + * @dataProvider getDataForMatchingSeveralResponsesWhenFailing + * @covers ::assertLastResponsesMatch + * @expectedException Assert\InvalidArgumentException + * @param Response[] $responses + * @param TableNode $table + * @param string $exceptionMessage + */ + public function testAssertLastResponsesMatchCanFail(array $responses, TableNode $table, $exceptionMessage) { + $this->mockHandler->append(...$responses); + + for ($i = 0; $i < count($responses); $i++) { + $this->context->requestPath('/path'); + } + + $this->expectExceptionMessage($exceptionMessage); + $this->context->assertLastResponsesMatch($table); + } + + /** + * @covers ::assertImageProperties + * @expectedException RuntimeException + * @expectedExceptionMessage Imagick could not read response body: "no decode delegate for this image format + */ + public function testThrowsExceptionWhenAssertingImagePropertiesAndResponseDoesNotContainAValidImage() { + $this->mockHandler->append(new Response(200, [], 'foobar')); + $this->context + ->requestPath('/path') + ->assertImageProperties('prefix'); + } + + /** + * @covers ::assertImageProperties + */ + public function testCanAssertThatImageDoesNotHaveAnyPropertiesWithASpecificPrefix() { + $this->mockHandler->append(new Response(200, [], file_get_contents(FIXTURES_DIR . '/image.png'))); + $this->assertSame( + $this->context, + $this->context + ->requestPath('/path') + ->assertImageProperties('foobar') + ); + } + + /** + * @covers ::assertImageProperties + * @expectedException Imbo\BehatApiExtension\Exception\AssertionFailedException + * @expectedExceptionMessage Image properties have not been properly stripped. Did not expect properties that starts with "png", found: "png: + */ + public function testAssertingThatImageDoesNotHaveAnyPropertiesWithASpecificPrefixCanFail() { + $this->mockHandler->append(new Response(200, [], file_get_contents(FIXTURES_DIR . '/image.png'))); + $this->context + ->requestPath('/path') + ->assertImageProperties('png'); + } +}