From 56d21dc72631371cc4b48d73e79a476393242f90 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 29 Dec 2016 15:45:09 -0500 Subject: [PATCH] Add functional API tests --- .travis.yml | 18 ++- ChangeLog | 1 + Makefile | 4 + app/handlers/feed.php | 2 +- app/models/favicon.php | 2 +- jsonrpc.php | 60 +++++++- tests/ci/apache_vhost.conf | 19 +++ tests/ci/config.postgres.php | 7 + tests/ci/install.sh | 18 +++ tests/functional/ApiTest.php | 207 ++++++++++++++++++++++++++ tests/functional/BaseApiTest.php | 19 +++ tests/phpunit.functional.postgres.xml | 10 ++ tests/phpunit.functional.sqlite.xml | 11 ++ tests/unit/FaviconModelTest.php | 4 +- 14 files changed, 368 insertions(+), 14 deletions(-) create mode 100644 tests/ci/apache_vhost.conf create mode 100644 tests/ci/config.postgres.php create mode 100755 tests/ci/install.sh create mode 100644 tests/functional/ApiTest.php create mode 100644 tests/functional/BaseApiTest.php create mode 100644 tests/phpunit.functional.postgres.xml create mode 100644 tests/phpunit.functional.sqlite.xml diff --git a/.travis.yml b/.travis.yml index 36b4c38..c3bcccd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ git: depth: 3 language: php -sudo: false +sudo: required php: - 7.0 @@ -10,11 +10,23 @@ php: - 5.5 - 5.4 - 5.3 - - hhvm + +before_install: + - sudo apt-get update -qq + - sudo apt-get install -y apache2 libapache2-mod-fastcgi + +install: + - composer install before_script: - - composer install + - ./tests/ci/install.sh script: - ./vendor/bin/phpunit -c tests/phpunit.unit.sqlite.xml - ./vendor/bin/phpunit -c tests/phpunit.unit.postgres.xml + - ./vendor/bin/phpunit -c tests/phpunit.functional.sqlite.xml + - cp ./tests/ci/config.postgres.php $TRAVIS_BUILD_DIR/config.php && ./vendor/bin/phpunit -c tests/phpunit.functional.postgres.xml + +after_failure: + - cat apache_error.log + - cat apache_access.log diff --git a/ChangeLog b/ChangeLog index 6603156..25c1a55 100644 --- a/ChangeLog +++ b/ChangeLog @@ -16,6 +16,7 @@ Version 1.2.0 (unreleased) * Add support for Expires and Cache-Control headers (HTTP cache) * Update Docker image to Ubuntu 16.04 and PHP 7.0 * Add Docker compose file +* Add functional tests (Json-RPC API) * Add unit tests Migration procedure from 1.1.x to 1.2.0: diff --git a/Makefile b/Makefile index 2ae3bad..50ead11 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,10 @@ $(JS_FILE): assets/js/app.js \ archive: @ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip +functional-test-sqlite: + @ rm -f data/db.sqlite + @ ./vendor/bin/phpunit -c tests/phpunit.functional.sqlite.xml + unit-test-sqlite: @ ./vendor/bin/phpunit -c tests/phpunit.unit.sqlite.xml diff --git a/app/handlers/feed.php b/app/handlers/feed.php index ccfee3e..20c1226 100644 --- a/app/handlers/feed.php +++ b/app/handlers/feed.php @@ -84,7 +84,7 @@ function create_feed($user_id, $url, $download_content = false, $rtl = false, $c } else { fetch_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon()); - if (! empty($feed_group_ids)) { + if (! empty($feed_group_ids) || ! empty($group_name)) { Model\Group\update_feed_groups($user_id, $feed_id, $feed_group_ids, $group_name); } } diff --git a/app/models/favicon.php b/app/models/favicon.php index 235c4d7..1d3fabb 100644 --- a/app/models/favicon.php +++ b/app/models/favicon.php @@ -141,7 +141,7 @@ function get_favicons_with_data_url($user_id) ->findAll(); foreach ($favicons as &$favicon) { - $favicon['url'] = get_favicon_data_url($favicon['hash'], $favicon['type']); + $favicon['data_url'] = get_favicon_data_url($favicon['hash'], $favicon['type']); } return $favicons; diff --git a/jsonrpc.php b/jsonrpc.php index 2db7010..9ea679d 100644 --- a/jsonrpc.php +++ b/jsonrpc.php @@ -2,12 +2,14 @@ require __DIR__.'/app/common.php'; +use JsonRPC\Exception\AccessDeniedException; use JsonRPC\Exception\AuthenticationFailureException; use JsonRPC\MiddlewareInterface; use JsonRPC\Server; use Miniflux\Handler; use Miniflux\Model; use Miniflux\Session\SessionStorage; +use Miniflux\Validator; class AuthMiddleware implements MiddlewareInterface { @@ -28,7 +30,37 @@ $procedureHandler = $server->getProcedureHandler(); // Get version $procedureHandler->withCallback('getVersion', function () { - return array('version' => APP_VERSION); + return APP_VERSION; +}); + +// Create user +$procedureHandler->withCallback('createUser', function ($username, $password, $is_admin = false) { + if (! SessionStorage::getInstance()->isAdmin()) { + throw new AccessDeniedException('Reserved to administrators'); + } + + $values = array( + 'username' => $username, + 'password' => $password, + 'confirmation' => $password, + ); + + list($valid) = Validator\User\validate_creation($values); + + if ($valid) { + return Model\User\create_user($username, $password, $is_admin); + } + + return false; +}); + +// Get user +$procedureHandler->withCallback('getUserByUsername', function ($username) { + if (! SessionStorage::getInstance()->isAdmin()) { + throw new AccessDeniedException('Reserved to administrators'); + } + + return Model\User\get_user_by_username($username); }); // Get all feeds @@ -46,13 +78,27 @@ $procedureHandler->withCallback('getFeeds', function () { // Get one feed $procedureHandler->withCallback('getFeed', function ($feed_id) { $user_id = SessionStorage::getInstance()->getUserId(); - return Model\Feed\get_feed($user_id, $feed_id); + $feed = Model\Feed\get_feed($user_id, $feed_id); + + if (! empty($feed)) { + $feed['groups'] = Model\Group\get_feed_groups($feed['id']); + } + + return $feed; }); // Add a new feed -$procedureHandler->withCallback('createFeed', function ($url) { +$procedureHandler->withCallback('createFeed', function ($url, $download_content = false, $rtl = false, $group_name = null) { $user_id = SessionStorage::getInstance()->getUserId(); - list($feed_id,) = Handler\Feed\create_feed($user_id, $url); + list($feed_id,) = Handler\Feed\create_feed( + $user_id, + $url, + $download_content, + $rtl, + false, + array(), + $group_name + ); if ($feed_id > 0) { return $feed_id; @@ -62,7 +108,7 @@ $procedureHandler->withCallback('createFeed', function ($url) { }); // Delete a feed -$procedureHandler->withCallback('deleteFeed', function ($feed_id) { +$procedureHandler->withCallback('removeFeed', function ($feed_id) { $user_id = SessionStorage::getInstance()->getUserId(); return Model\Feed\remove_feed($user_id, $feed_id); }); @@ -74,9 +120,9 @@ $procedureHandler->withCallback('refreshFeed', function ($feed_id) { }); // Get all items -$procedureHandler->withCallback('getItems', function ($since_id = null, array $item_ids = array(), $offset = 50) { +$procedureHandler->withCallback('getItems', function ($since_id = null, array $item_ids = array(), $limit = 50) { $user_id = SessionStorage::getInstance()->getUserId(); - return Model\Item\get_items($user_id, $since_id, $item_ids, $offset); + return Model\Item\get_items($user_id, $since_id, $item_ids, $limit); }); // Get one item diff --git a/tests/ci/apache_vhost.conf b/tests/ci/apache_vhost.conf new file mode 100644 index 0000000..42d480d --- /dev/null +++ b/tests/ci/apache_vhost.conf @@ -0,0 +1,19 @@ + + DocumentRoot %TRAVIS_BUILD_DIR% + ErrorLog "%TRAVIS_BUILD_DIR%/apache_error.log" + CustomLog "%TRAVIS_BUILD_DIR%/apache_access.log" combined + + + Options FollowSymLinks MultiViews ExecCGI + AllowOverride All + Order deny,allow + Allow from all + + + + AddHandler php5-fcgi .php + Action php5-fcgi /php5-fcgi + Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi + FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization + + \ No newline at end of file diff --git a/tests/ci/config.postgres.php b/tests/ci/config.postgres.php new file mode 100644 index 0000000..1242b45 --- /dev/null +++ b/tests/ci/config.postgres.php @@ -0,0 +1,7 @@ +> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +echo "always_populate_raw_post_data = -1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + +~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm + +sudo a2enmod rewrite actions fastcgi alias ssl + +sudo cp -f tests/ci/apache_vhost.conf /etc/apache2/sites-available/default +sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/default +sudo service apache2 restart diff --git a/tests/functional/ApiTest.php b/tests/functional/ApiTest.php new file mode 100644 index 0000000..ab1393a --- /dev/null +++ b/tests/functional/ApiTest.php @@ -0,0 +1,207 @@ +assertEquals('master', $this->getApiClient()->getVersion()); + } + + public function testCreateUser() + { + $this->assertFalse($this->getApiClient()->createUser('admin', 'test123')); + $this->assertNotFalse($this->getApiClient()->createUser(array( + 'username' => 'api_test', + 'password' => 'test123', + ))); + } + + public function testGetUser() + { + $this->assertNull($this->getApiClient()->getUserByUsername('notfound')); + + $user = $this->getApiClient()->getUserByUsername('api_test'); + $this->assertEquals('api_test', $user['username']); + $this->assertFalse((bool) $user['is_admin']); + $this->assertArrayHasKey('password', $user); + $this->assertArrayHasKey('api_token', $user); + } + + public function testCreateUserAsNonAdmin() + { + $user = $this->getApiClient()->getUserByUsername('api_test'); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->getApiClient($user)->createUser('someone', 'secret'); + } + + public function testGetUserAsNonAdmin() + { + $user = $this->getApiClient()->getUserByUsername('api_test'); + + $this->setExpectedException('JsonRPC\Exception\AccessDeniedException'); + $this->getApiClient($user)->getUserByUsername('admin'); + } + + public function testCreateFeed() + { + $this->assertNotFalse($this->getApiClient()->createFeed(array( + 'url' => 'https://miniflux.net/feed', + 'group_name' => 'open source software', + ))); + } + + public function testGetAllFeeds() + { + $feeds = $this->getApiClient()->getFeeds(); + $this->assertCount(1, $feeds); + $this->assertEquals(1, $feeds[0]['id']); + $this->assertEquals('https://miniflux.net/feed', $feeds[0]['feed_url']); + $this->assertTrue((bool) $feeds[0]['enabled']); + $this->assertEquals('open source software', $feeds[0]['groups'][0]['title']); + } + + public function testGetFeed() + { + $this->assertNull($this->getApiClient()->getFeed(999)); + + $feed = $this->getApiClient()->getFeed(1); + $this->assertEquals('https://miniflux.net/feed', $feed['feed_url']); + $this->assertTrue((bool) $feed['enabled']); + $this->assertEquals('open source software', $feed['groups'][0]['title']); + } + + public function testRefreshFeed() + { + $this->assertTrue($this->getApiClient()->refreshFeed(1)); + } + + public function testGetItems() + { + $items = $this->getApiClient()->getItems(); + $this->assertNotEmpty($items); + $this->assertEquals(1, $items[0]['id']); + $this->assertEquals(1, $items[0]['feed_id']); + $this->assertNotEmpty($items[0]['title']); + $this->assertNotEmpty($items[0]['author']); + $this->assertNotEmpty($items[0]['content']); + $this->assertNotEmpty($items[0]['url']); + } + + public function testGetItemsSinceId() + { + $items = $this->getApiClient()->getItems(array('since_id' => 2)); + $this->assertNotEmpty($items); + $this->assertEquals(3, $items[0]['id']); + } + + public function testGetSpecificItems() + { + $items = $this->getApiClient()->getItems(array('item_ids' => array(2, 3))); + $this->assertNotEmpty($items); + $this->assertEquals(2, $items[0]['id']); + $this->assertEquals(3, $items[1]['id']); + } + + public function testGetItem() + { + $this->assertNull($this->getApiClient()->getItem(999)); + + $item = $this->getApiClient()->getItem(1); + $this->assertNotEmpty($item); + $this->assertEquals(1, $item['id']); + $this->assertEquals(1, $item['feed_id']); + $this->assertEquals('unread', $item['status']); + $this->assertNotEmpty($item['title']); + $this->assertNotEmpty($item['author']); + $this->assertNotEmpty($item['content']); + $this->assertNotEmpty($item['url']); + } + + public function testChangeItemsStatus() + { + $this->assertTrue($this->getApiClient()->changeItemsStatus(array(1), 'read')); + + $item = $this->getApiClient()->getItem(1); + $this->assertEquals('read', $item['status']); + + $item = $this->getApiClient()->getItem(2); + $this->assertEquals('unread', $item['status']); + } + + public function testAddBookmark() + { + $this->assertTrue($this->getApiClient()->addBookmark(1)); + + $item = $this->getApiClient()->getItem(1); + $this->assertTrue((bool) $item['bookmark']); + } + + public function testRemoveBookmark() + { + $this->assertTrue($this->getApiClient()->removeBookmark(1)); + + $item = $this->getApiClient()->getItem(1); + $this->assertFalse((bool) $item['bookmark']); + } + + public function testGetGroups() + { + $groups = $this->getApiClient()->getGroups(); + $this->assertCount(1, $groups); + + $this->assertEquals(1, $groups[0]['id']); + $this->assertEquals(1, $groups[0]['user_id']); + $this->assertEquals('open source software', $groups[0]['title']); + } + + public function testCreateGroup() + { + $this->assertEquals(2, $this->getApiClient()->createGroup('foobar')); + $this->assertEquals(2, $this->getApiClient()->createGroup('foobar')); + + $groups = $this->getApiClient()->getGroups(); + $this->assertCount(2, $groups); + } + + public function testSetFeedGroups() + { + $this->assertTrue($this->getApiClient()->setFeedGroups(1, array(2))); + + $feed = $this->getApiClient()->getFeed(1); + $this->assertCount(1, $feed['groups']); + $this->assertEquals('foobar', $feed['groups'][0]['title']); + } + + public function testGetFavicons() + { + $favicons = $this->getApiClient()->getFavicons(); + + $this->assertCount(1, $favicons); + $this->assertEquals(1, $favicons[0]['feed_id']); + $this->assertNotEmpty($favicons[0]['hash']); + $this->assertNotEmpty($favicons[0]['type']); + $this->assertNotEmpty($favicons[0]['data_url']); + } + + public function testDeleteFeed() + { + $this->assertTrue($this->getApiClient()->removeFeed(1)); + } + + protected function getApiClient(array $user = array()) + { + if (empty($user)) { + $user = $this->adminUser; + } + + $apiUserClient = new Client(API_URL); + $apiUserClient->authentication($user['username'], $user['api_token']); + + return $apiUserClient; + } +} diff --git a/tests/functional/BaseApiTest.php b/tests/functional/BaseApiTest.php new file mode 100644 index 0000000..c4f31bc --- /dev/null +++ b/tests/functional/BaseApiTest.php @@ -0,0 +1,19 @@ +exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME); + } + + $db = Miniflux\Database\get_connection(); + $this->adminUser = $db->table(Miniflux\Model\User\TABLE)->eq('username', 'admin')->findOne(); + } +} diff --git a/tests/phpunit.functional.postgres.xml b/tests/phpunit.functional.postgres.xml new file mode 100644 index 0000000..873d284 --- /dev/null +++ b/tests/phpunit.functional.postgres.xml @@ -0,0 +1,10 @@ + + + + functional + + + + + + diff --git a/tests/phpunit.functional.sqlite.xml b/tests/phpunit.functional.sqlite.xml new file mode 100644 index 0000000..061657a --- /dev/null +++ b/tests/phpunit.functional.sqlite.xml @@ -0,0 +1,11 @@ + + + + functional + + + + + + + diff --git a/tests/unit/FaviconModelTest.php b/tests/unit/FaviconModelTest.php index 50d1137..a468cf4 100644 --- a/tests/unit/FaviconModelTest.php +++ b/tests/unit/FaviconModelTest.php @@ -131,12 +131,12 @@ class FaviconModelTest extends BaseTest $this->assertEquals(1, $favicons[0]['feed_id']); $this->assertEquals('57978a20204f7af6967571041c79d907a8a8072c', $favicons[0]['hash']); $this->assertEquals('image/png', $favicons[0]['type']); - $this->assertEquals('data:image/png;base64,YmluYXJ5IGRhdGE=', $favicons[0]['url']); + $this->assertEquals('data:image/png;base64,YmluYXJ5IGRhdGE=', $favicons[0]['data_url']); $this->assertEquals(2, $favicons[1]['feed_id']); $this->assertEquals('36242b50974c41478569d66616346ee5f2ad7b6e', $favicons[1]['hash']); $this->assertEquals('image/gif', $favicons[1]['type']); - $this->assertEquals('data:image/gif;base64,c29tZSBiaW5hcnkgZGF0YQ==', $favicons[1]['url']); + $this->assertEquals('data:image/gif;base64,c29tZSBiaW5hcnkgZGF0YQ==', $favicons[1]['data_url']); } public function testGetItemsFavicons()