diff --git a/lib/Database.php b/lib/Database.php
index e680a801..a16979f2 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 ae97f071..8a50ed1d 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 00000000..be10b889
--- /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 b85c39b5..9d60c8b6 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 00000000..95d4b1ba
--- /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 2a2b4434..bf4e2e4b 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