- A Linux server running Nginx or Apache 2.4 (tested on Ubuntu 16.04 and 18.04)
- A Linux server running Nginx or Apache 2.4
- PHP 7.0.7 or later with the following extensions:
- PHP 7.1.0 or later with the following extensions:
- [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [dom](http://php.net/manual/en/book.dom.php)
- [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [dom](http://php.net/manual/en/book.dom.php)
- [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php)
- [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php)
- One of:
- One of:
- [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases
- [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases
- [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases
- [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 10 or later databases
- [mysqli](http://php.net/manual/en/book.mysqli.php) or [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases
- [mysqli](http://php.net/manual/en/book.mysqli.php) or [pdo_mysql](http://php.net/manual/en/ref.pdo-mysql.php) for MySQL/Percona 8.0.11 or later databases
other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}}
other {Authenticated user is not authorized to perform the action "{action}" on behalf of {user}}
}',
}',
'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist',
'Exception.JKingWeb/Arsse/User/ExceptionSession.invalid' => 'Session with ID {0} does not exist',
'Exception.JKingWeb/Arsse/Feed/Exception.internalError' => 'Could not download feed "{url}" because of an internal error which is probably a bug',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidCertificate' => 'Could not download feed "{url}" because its server is serving an invalid SSL certificate',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid',
'Exception.JKingWeb/Arsse/Feed/Exception.invalidUrl' => 'Feed URL "{url}" is invalid',
'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections',
'Exception.JKingWeb/Arsse/Feed/Exception.maxRedirect' => 'Could not download feed "{url}" because its server reached its maximum number of HTTP redirections',
@ -151,6 +152,8 @@ return [
'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out',
'Exception.JKingWeb/Arsse/Feed/Exception.timeout' => 'Could not download feed "{url}" because its server timed out',
'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it',
'Exception.JKingWeb/Arsse/Feed/Exception.forbidden' => 'Could not download feed "{url}" because you do not have permission to access it',
'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials',
'Exception.JKingWeb/Arsse/Feed/Exception.unauthorized' => 'Could not download feed "{url}" because you provided insufficient or invalid credentials',
'Exception.JKingWeb/Arsse/Feed/Exception.transmissionError' => 'Could not download feed "{url}" because of a network error',
'Exception.JKingWeb/Arsse/Feed/Exception.connectionFailed' => 'Could not download feed "{url}" because its server could not be reached',
'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed',
'Exception.JKingWeb/Arsse/Feed/Exception.malformedXml' => 'Could not parse feed "{url}" because it is malformed',
'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack',
'Exception.JKingWeb/Arsse/Feed/Exception.xmlEntity' => 'Refused to parse feed "{url}" because it contains an XXE attack',
'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"',
'Exception.JKingWeb/Arsse/Feed/Exception.subscriptionNotFound' => 'Unable to find a feed at location "{url}"',
\Phake::when(Arsse::$db)->feedUpdate(2, true)->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/", new \PicoFeed\Client\InvalidUrlException));
$this->assertResult([['id' => 1]], Arsse::$db->feedMatchIds(1, ['e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda'])); // this ID appears in both feed 1 and feed 2; only one result should be returned
$this->assertResult([['id' => 1]], Arsse::$db->feedMatchIds(1, ['e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda'])); // this ID appears in both feed 1 and feed 2; only one result should be returned
}
}
public function testUpdateAFeed() {
public function testUpdateAFeed(): void {
// update a valid feed with both new and changed items
// update a valid feed with both new and changed items
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union all select 1970 as year, 20 as century union all select 2112 as year, 22 as century"));
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union all select 1970 as year, 20 as century union all select 2112 as year, 22 as century"));
@ -110,7 +110,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame(null, $test->getValue());
$this->assertSame(null, $test->getValue());
}
}
public function testGetRows() {
public function testGetRows(): void {
$exp = [
$exp = [
['album' => '2112', 'track' => '2112'],
['album' => '2112', 'track' => '2112'],
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
@ -121,7 +121,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame(null, $test->getRow());
$this->assertSame(null, $test->getRow());
}
}
public function testGetAllRows() {
public function testGetAllRows(): void {
$exp = [
$exp = [
['album' => '2112', 'track' => '2112'],
['album' => '2112', 'track' => '2112'],
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
@ -46,12 +46,12 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
self::clearData();
self::clearData();
}
}
public function testConstructStatement() {
public function testConstructStatement(): void {
$this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value")));
$this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value")));
}
}
/** @dataProvider provideBindings */
/** @dataProvider provideBindings */
public function testBindATypedValue($value, string $type, string $exp) {
public function testBindATypedValue($value, string $type, string $exp): void {
if ($exp === "null") {
if ($exp === "null") {
$query = "SELECT (? is null) as pass";
$query = "SELECT (? is null) as pass";
} else {
} else {
@ -65,7 +65,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
}
}
/** @dataProvider provideBinaryBindings */
/** @dataProvider provideBinaryBindings */
public function testHandleBinaryData($value, string $type, string $exp) {
public function testHandleBinaryData($value, string $type, string $exp): void {
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
$this->markTestIncomplete("Correct handling of binary data with PostgreSQL is not currently implemented");
$this->markTestIncomplete("Correct handling of binary data with PostgreSQL is not currently implemented");
}
}
@ -81,13 +81,13 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertTrue((bool) $act);
$this->assertTrue((bool) $act);
}
}
public function testBindMissingValue() {
public function testBindMissingValue(): void {
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"]));
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"]));
$val = $s->runArray()->getRow()['value'];
$val = $s->runArray()->getRow()['value'];
$this->assertSame(null, $val);
$this->assertSame(null, $val);
}
}
public function testBindMultipleValues() {
public function testBindMultipleValues(): void {
$exp = [
$exp = [
'one' => "A",
'one' => "A",
'two' => "B",
'two' => "B",
@ -97,7 +97,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame($exp, $val);
$this->assertSame($exp, $val);
}
}
public function testBindRecursively() {
public function testBindRecursively(): void {
$exp = [
$exp = [
'one' => "A",
'one' => "A",
'two' => "B",
'two' => "B",
@ -109,20 +109,20 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame($exp, $val);
$this->assertSame($exp, $val);
}
}
public function testBindWithoutType() {
public function testBindWithoutType(): void {
$this->assertException("paramTypeMissing", "Db");
$this->assertException("paramTypeMissing", "Db");
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", []));
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", []));
$s->runArray([1]);
$s->runArray([1]);
}
}
public function testViolateConstraint() {
public function testViolateConstraint(): void {
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(\"key\" varchar(255) primary key not null, value text)")))->run();
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(\"key\" varchar(255) primary key not null, value text)")))->run();
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(\"key\") values(?)", ["str"]));
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(\"key\") values(?)", ["str"]));
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run();
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run();
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"]));
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"]));
@ -222,7 +222,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
$this->compareExpectations($this->drv, $exp);
$this->compareExpectations($this->drv, $exp);
}
}
public function testImportAFeed() {
public function testImportAFeed(): void {
$in = [[
$in = [[
['url' => "http://localhost:8000/Import/some-feed", 'title' => "Some Feed", 'folder' => 0, 'tags' => ["frequent", "cryptic"]], //one existing tag and one new one
['url' => "http://localhost:8000/Import/some-feed", 'title' => "Some Feed", 'folder' => 0, 'tags' => ["frequent", "cryptic"]], //one existing tag and one new one
], []];
], []];
@ -237,7 +237,7 @@ class TestImportExport extends \JKingWeb\Arsse\Test\AbstractTest {
$this->compareExpectations($this->drv, $exp);
$this->compareExpectations($this->drv, $exp);
}
}
public function testImportAFeedWithAnInvalidTag() {
public function testImportAFeedWithAnInvalidTag(): void {
@ -11,14 +11,14 @@ use JKingWeb\Arsse\Misc\ValueInfo;
/** @covers \JKingWeb\Arsse\Misc\Query */
/** @covers \JKingWeb\Arsse\Misc\Query */
class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest {
class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest {
public function testBasicQuery() {
public function testBasicQuery(): void {
$q = new Query("select * from table where a = ?", "int", 3);
$q = new Query("select * from table where a = ?", "int", 3);
$this->assertSame("select * from table where a = ?", $q->getQuery());
$this->assertSame("select * from table where a = ?", $q->getQuery());
$this->assertSame(["int"], $q->getTypes());
$this->assertSame(["int"], $q->getTypes());
$this->assertSame([3], $q->getValues());
$this->assertSame([3], $q->getValues());
}
}
public function testWhereQuery() {
public function testWhereQuery(): void {
// simple where clause
// simple where clause
$q = (new Query("select * from table"))->setWhere("a = ?", "int", 3);
$q = (new Query("select * from table"))->setWhere("a = ?", "int", 3);
$this->assertSame("select * from table WHERE a = ?", $q->getQuery());
$this->assertSame("select * from table WHERE a = ?", $q->getQuery());
@ -46,21 +46,21 @@ class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame([2, 4, 1, 3], $q->getValues());
$this->assertSame([2, 4, 1, 3], $q->getValues());
}
}
public function testGroupedQuery() {
public function testGroupedQuery(): void {
$q = (new Query("select col1, col2, count(*) as count from table"))->setGroup("col1", "col2");
$q = (new Query("select col1, col2, count(*) as count from table"))->setGroup("col1", "col2");
$this->assertSame("select col1, col2, count(*) as count from table GROUP BY col1, col2", $q->getQuery());
$this->assertSame("select col1, col2, count(*) as count from table GROUP BY col1, col2", $q->getQuery());
$this->assertSame([], $q->getTypes());
$this->assertSame([], $q->getTypes());
$this->assertSame([], $q->getValues());
$this->assertSame([], $q->getValues());
}
}
public function testOrderedQuery() {
public function testOrderedQuery(): void {
$q = (new Query("select col1, col2, col3 from table"))->setOrder("col1 desc", "col2")->setOrder("col3 asc");
$q = (new Query("select col1, col2, col3 from table"))->setOrder("col1 desc", "col2")->setOrder("col3 asc");
$this->assertSame("select col1, col2, col3 from table ORDER BY col1 desc, col2, col3 asc", $q->getQuery());
$this->assertSame("select col1, col2, col3 from table ORDER BY col1 desc, col2, col3 asc", $q->getQuery());
$this->assertSame([], $q->getTypes());
$this->assertSame([], $q->getTypes());
$this->assertSame([], $q->getValues());
$this->assertSame([], $q->getValues());
}
}
public function testLimitedQuery() {
public function testLimitedQuery(): void {
// no offset
// no offset
$q = (new Query("select * from table"))->setLimit(5);
$q = (new Query("select * from table"))->setLimit(5);
$this->assertSame("select * from table LIMIT 5", $q->getQuery());
$this->assertSame("select * from table LIMIT 5", $q->getQuery());
@ -78,7 +78,7 @@ class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame([], $q->getValues());
$this->assertSame([], $q->getValues());
}
}
public function testQueryWithCommonTableExpression() {
public function testQueryWithCommonTableExpression(): void {
$q = (new Query("select * from table where a in (select * from cte where a = ?)", "int", 1))->setCTE("cte", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3]);
$q = (new Query("select * from table where a in (select * from cte where a = ?)", "int", 1))->setCTE("cte", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3]);
$this->assertSame("WITH RECURSIVE cte as (select * from other_table where a = ? and b = ?) select * from table where a in (select * from cte where a = ?)", $q->getQuery());
$this->assertSame("WITH RECURSIVE cte as (select * from other_table where a = ? and b = ?) select * from table where a in (select * from cte where a = ?)", $q->getQuery());
['code' => 5, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", new \PicoFeed\Client\UnauthorizedException()))->getMessage()],
['code' => 5, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", $this->mockGuzzleException(ClientException::class, "", 401)))->getMessage()],
['code' => 1, 'feed_id' => 0],
['code' => 1, 'feed_id' => 0],
['code' => 0, 'feed_id' => 3],
['code' => 0, 'feed_id' => 3],
['code' => 0, 'feed_id' => 1],
['code' => 0, 'feed_id' => 1],
['code' => 3, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://localhost:8000/Feed/Discovery/Invalid", new \PicoFeed\Reader\SubscriptionNotFoundException()))->getMessage()],
['code' => 3, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://localhost:8000/Feed/Discovery/Invalid", new \PicoFeed\Reader\SubscriptionNotFoundException()))->getMessage()],
['code' => 2, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", new \PicoFeed\Client\InvalidUrlException()))->getMessage()],
['code' => 2, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", $this->mockGuzzleException(ClientException::class, "", 404)))->getMessage()],
['code' => 6, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()))->getMessage()],
['code' => 6, 'message' => (new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()))->getMessage()],
\Phake::when(Arsse::$db)->subscriptionAdd(...$db[1])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/1", new \PicoFeed\Client\UnauthorizedException()));
\Phake::when(Arsse::$db)->subscriptionAdd(...$db[6])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/6", new \PicoFeed\Client\InvalidUrlException()));
\Phake::when(Arsse::$db)->subscriptionAdd(...$db[7])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()));
\Phake::when(Arsse::$db)->subscriptionAdd(...$db[7])->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/7", new \PicoFeed\Parser\MalformedXmlException()));
@ -33,7 +33,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
* @dataProvider provideAuthentication
* @dataProvider provideAuthentication
* @group slow
* @group slow
*/
*/
public function testAuthenticateAUser(bool $authorized, string $user, $password, bool $exp) {
public function testAuthenticateAUser(bool $authorized, string $user, $password, bool $exp): void {
if ($authorized) {
if ($authorized) {
\Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret"
\Phake::when(Arsse::$db)->userPasswordGet("john.doe@example.com")->thenReturn('$2y$10$1zbqRJhxM8uUjeSBPp4IhO90xrqK0XjEh9Z16iIYEFRV4U.zeAFom'); // hash of "secret"
\Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman"
\Phake::when(Arsse::$db)->userPasswordGet("jane.doe@example.com")->thenReturn('$2y$10$bK1ljXfTSyc2D.NYvT.Eq..OpehLRXVbglW.23ihVuyhgwJCd.7Im'); // hash of "superman"
@ -74,12 +74,12 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
public function assertTime($exp, $test, string $msg = '') {
public function assertTime($exp, $test, string $msg = ''): void {
$test = $this->approximateTime($exp, $test);
$test = $this->approximateTime($exp, $test);
$exp = Date::transform($exp, "iso8601");
$exp = Date::transform($exp, "iso8601");
$test = Date::transform($test, "iso8601");
$test = Date::transform($test, "iso8601");
@ -299,7 +301,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
return $out;
return $out;
}
}
public function assertResult(array $expected, Result $data) {
public function assertResult(array $expected, Result $data): void {
$data = $data->getAll();
$data = $data->getAll();
$this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")");
$this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")");
if (sizeof($expected)) {
if (sizeof($expected)) {
@ -325,4 +327,16 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
$this->assertArraySubset($expected, [], false, "Expectations not in result set.");
$this->assertArraySubset($expected, [], false, "Expectations not in result set.");
}
}
}
}
/** Guzzle's exception classes require some fairly complicated construction; this abstracts it all away so that only message and code need be supplied */