diff --git a/lib/Database.php b/lib/Database.php index e680a80..a16979f 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -9,7 +9,7 @@ use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Date; class Database { - const SCHEMA_VERSION = 1; + const SCHEMA_VERSION = 2; /** @var Db\Driver */ public $db; @@ -267,7 +267,7 @@ class Database { return $this->db->prepare("DELETE FROM arsse_sessions where expires < CURRENT_TIMESTAMP or created < ?", "datetime")->run($maxAge)->changes(); } - protected function sessionExpiringSoon(DateTimeInterface $expiry): bool { + protected function sessionExpiringSoon(\DateTimeInterface $expiry): bool { // calculate half the session timeout as a number of seconds $now = time(); $max = Date::add(Arsse::$conf->userSessionTimeout, $now)->getTimestamp(); diff --git a/sql/SQLite3/1.sql b/sql/SQLite3/1.sql index ae97f07..8a50ed1 100644 --- a/sql/SQLite3/1.sql +++ b/sql/SQLite3/1.sql @@ -3,7 +3,7 @@ create table arsse_sessions ( id text primary key, -- UUID of session created datetime not null default CURRENT_TIMESTAMP, -- Session start timestamp expires datetime not null, -- Time at which session is no longer valid - user text not null references arsse_users(id) on delete cascade on update cascade, -- user associated with the session + user text not null references arsse_users(id) on delete cascade on update cascade -- user associated with the session ) without rowid; -- User-defined article labels for Tiny Tiny RSS @@ -26,4 +26,4 @@ create table arsse_label_members ( -- set version marker pragma user_version = 2; -insert into arsse_meta(key,value) values('schema_version','2'); \ No newline at end of file +update arsse_meta set value = '2' where key is 'schema_version'; \ No newline at end of file diff --git a/tests/Db/SQLite3/Database/TestDatabaseSessionSQLite3.php b/tests/Db/SQLite3/Database/TestDatabaseSessionSQLite3.php new file mode 100644 index 0000000..be10b88 --- /dev/null +++ b/tests/Db/SQLite3/Database/TestDatabaseSessionSQLite3.php @@ -0,0 +1,10 @@ + */ +class TestDatabaseSessionSQLite3 extends Test\AbstractTest { + use Test\Database\Setup; + use Test\Database\DriverSQLite3; + use Test\Database\SeriesSession; +} diff --git a/tests/lib/Database/SeriesCleanup.php b/tests/lib/Database/SeriesCleanup.php index b85c39b..9d60c8b 100644 --- a/tests/lib/Database/SeriesCleanup.php +++ b/tests/lib/Database/SeriesCleanup.php @@ -13,6 +13,8 @@ trait SeriesCleanup { $daybefore = gmdate("Y-m-d H:i:s", strtotime("now - 2 days")); $daysago = gmdate("Y-m-d H:i:s", strtotime("now - 7 days")); $weeksago = gmdate("Y-m-d H:i:s", strtotime("now - 21 days")); + $soon = gmdate("Y-m-d H:i:s", strtotime("now + 1 minute")); + $faroff = gmdate("Y-m-d H:i:s", strtotime("now + 1 hour")); $this->data = [ 'arsse_users' => [ 'columns' => [ @@ -25,6 +27,21 @@ trait SeriesCleanup { ["john.doe@example.com", "", "John Doe"], ], ], + 'arsse_sessions' => [ + 'columns' => [ + 'id' => "str", + 'created' => "datetime", + 'expires' => "datetime", + 'user' => "str", + ], + 'rows' => [ + ["a", $nowish, $faroff, "jane.doe@example.com"], // not expired and recently created, thus kept + ["b", $nowish, $soon, "jane.doe@example.com"], // not expired and recently created, thus kept + ["c", $daysago, $soon, "jane.doe@example.com"], // created more than a day ago, thus deleted + ["d", $nowish, $nowish, "jane.doe@example.com"], // recently created but expired, thus deleted + ["e", $daysago, $nowish, "jane.doe@example.com"], // created more than a day ago and expired, thus deleted + ], + ], 'arsse_feeds' => [ 'columns' => [ 'id' => "int", @@ -165,4 +182,16 @@ trait SeriesCleanup { ]); $this->compareExpectations($state); } + + public function testCleanUpExpiredSessions() { + Arsse::$db->sessionCleanup(); + $state = $this->primeExpectations($this->data, [ + 'arsse_sessions' => ["id"] + ]); + foreach ([3,4,5] as $id) { + unset($state['arsse_sessions']['rows'][$id - 1]); + } + $this->compareExpectations($state); + + } } diff --git a/tests/lib/Database/SeriesSession.php b/tests/lib/Database/SeriesSession.php new file mode 100644 index 0000000..95d4b1b --- /dev/null +++ b/tests/lib/Database/SeriesSession.php @@ -0,0 +1,119 @@ +data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ], + ], + 'arsse_sessions' => [ + 'columns' => [ + 'id' => "str", + 'user' => "str", + 'created' => "datetime", + 'expires' => "datetime", + ], + 'rows' => [ + ["80fa94c1a11f11e78667001e673b2560", "jane.doe@example.com", $past, $faroff], + ["27c6de8da13311e78667001e673b2560", "jane.doe@example.com", $past, $past], // expired + ["ab3b3eb8a13311e78667001e673b2560", "jane.doe@example.com", $old, $future], // too old + ["da772f8fa13c11e78667001e673b2560", "john.doe@example.com", $past, $future], + ], + ], + ]; + } + + public function testResumeAValidSession() { + $exp1 = [ + 'id' => "80fa94c1a11f11e78667001e673b2560", + 'user' => "jane.doe@example.com" + ]; + $exp2 = [ + 'id' => "da772f8fa13c11e78667001e673b2560", + 'user' => "john.doe@example.com" + ]; + $this->assertArraySubset($exp1, Arsse::$db->sessionResume("80fa94c1a11f11e78667001e673b2560")); + $this->assertArraySubset($exp2, Arsse::$db->sessionResume("da772f8fa13c11e78667001e673b2560")); + $now = time(); + // sessions near timeout should be refreshed automatically + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + $state['arsse_sessions']['rows'][3][2] = Date::transform(Date::add(Arsse::$conf->userSessionTimeout, $now), "sql"); + $this->compareExpectations($state); + // session resumption should not check authorization + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertArraySubset($exp1, Arsse::$db->sessionResume("80fa94c1a11f11e78667001e673b2560")); + } + + public function testResumeAMissingSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("thisSessionDoesNotExist"); + } + + public function testResumeAnExpiredSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("27c6de8da13311e78667001e673b2560"); + } + + public function testResumeAStaleSession() { + $this->assertException("invalid", "User", "ExceptionSession"); + Arsse::$db->sessionResume("ab3b3eb8a13311e78667001e673b2560"); + } + + public function testCreateASession() { + $user = "jane.doe@example.com"; + $id = Arsse::$db->sessionCreate($user); + $now = time(); + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + $state['arsse_sessions']['rows'][] = [$id, Date::transform($now, "sql"), Date::transform(Date::add(Arsse::$conf->userSessionTimeout, $now), "sql"), $user]; + $this->compareExpectations($state); + } + + public function testCreateASessionWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->sessionCreate("jane.doe@example.com"); + } + + public function testDestroyASession() { + $user = "jane.doe@example.com"; + $id = "80fa94c1a11f11e78667001e673b2560"; + $this->assertTrue(Arsse::$db->sessionDestroy($user, $id)); + $state = $this->primeExpectations($this->data, ['arsse_sessions' => ["id", "created", "expires", "user"]]); + unset($state['arsse_sessions']['rows'][0]); + $this->compareExpectations($state); + // destroying a session which does not exist is not an error + $this->assertFalse(Arsse::$db->sessionDestroy($user, $id)); + } + + public function testDestroyASessionForTheWrongUser() { + $user = "john.doe@example.com"; + $id = "80fa94c1a11f11e78667001e673b2560"; + $this->assertFalse(Arsse::$db->sessionDestroy($user, $id)); + } + + public function testDestroyASessionWithoutAuthority() { + Phake::when(Arsse::$user)->authorize->thenReturn(false); + $this->assertException("notAuthorized", "User", "ExceptionAuthz"); + Arsse::$db->sessionDestroy("jane.doe@example.com", "80fa94c1a11f11e78667001e673b2560"); + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 2a2b443..bf4e2e4 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -56,6 +56,7 @@ Db/SQLite3/Database/TestDatabaseMiscellanySQLite3.php Db/SQLite3/Database/TestDatabaseMetaSQLite3.php Db/SQLite3/Database/TestDatabaseUserSQLite3.php + Db/SQLite3/Database/TestDatabaseSessionSQLite3.php Db/SQLite3/Database/TestDatabaseFolderSQLite3.php Db/SQLite3/Database/TestDatabaseFeedSQLite3.php Db/SQLite3/Database/TestDatabaseSubscriptionSQLite3.php