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()