diff --git a/lib/Database.php b/lib/Database.php
index d683fdad..8371a2a1 100644
--- a/lib/Database.php
+++ b/lib/Database.php
@@ -18,12 +18,17 @@ class Database {
return (string) preg_filter("[^0-9a-zA-Z_\.]", "", $name);
}
- public function __construct() {
- $this->driver = $driver = Data::$conf->dbDriver;
- $this->db = new $driver(INSTALL);
- $ver = $this->db->schemaVersion();
- if(!INSTALL && $ver < self::SCHEMA_VERSION) {
- $this->db->schemaUpdate(self::SCHEMA_VERSION);
+ public function __construct(Db\Driver $db = null) {
+ // if we're fed a pre-prepared driver, use it'
+ if($db) {
+ $this->db = $db;
+ } else {
+ $this->driver = $driver = Data::$conf->dbDriver;
+ $this->db = new $driver(INSTALL);
+ $ver = $this->db->schemaVersion();
+ if(!INSTALL && $ver < self::SCHEMA_VERSION) {
+ $this->db->schemaUpdate(self::SCHEMA_VERSION);
+ }
}
}
@@ -186,15 +191,21 @@ class Database {
}
public function userList(string $domain = null): array {
+ $out = [];
if($domain !== null) {
if(!Data::$user->authorize("@".$domain, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $domain]);
$domain = str_replace(["\\","%","_"],["\\\\", "\\%", "\\_"], $domain);
$domain = "%@".$domain;
- return $this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain)->getAll();
+ foreach($this->db->prepare("SELECT id from arsse_users where id like ?", "str")->run($domain) as $user) {
+ $out[] = $user['id'];
+ }
} else {
if(!Data::$user->authorize("", __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => "global"]);
- return $this->db->prepare("SELECT id from arsse_users")->run()->getAll();
+ foreach($this->db->prepare("SELECT id from arsse_users")->run() as $user) {
+ $out[] = $user['id'];
+ }
}
+ return $out;
}
public function userPasswordGet(string $user): string {
@@ -208,7 +219,7 @@ class Database {
if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
if($password===null) $password = (new PassGen)->length(Data::$conf->userTempPasswordLength)->get();
$hash = "";
- if(strlen($password > 0)) $hash = password_hash($password, \PASSWORD_DEFAULT);
+ if(strlen($password) > 0) $hash = password_hash($password, \PASSWORD_DEFAULT);
$this->db->prepare("UPDATE arsse_users set password = ? where id is ?", "str", "str")->run($hash, $user);
return $password;
}
@@ -216,16 +227,16 @@ class Database {
public function userPropertiesGet(string $user): array {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$prop = $this->db->prepare("SELECT name,rights from arsse_users where id is ?", "str")->run($user)->getRow();
- if(!$prop) return [];
+ if(!$prop) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
return $prop;
}
- public function userPropertiesSet(string $user, array &$properties): array {
+ public function userPropertiesSet(string $user, array $properties): array {
if(!Data::$user->authorize($user, __FUNCTION__)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
$valid = [ // FIXME: add future properties
"name" => "str",
];
- if(!$this->userExists($user)) return [];
+ if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
$this->db->begin();
foreach($valid as $prop => $type) {
if(!array_key_exists($prop, $properties)) continue;
@@ -242,7 +253,7 @@ class Database {
public function userRightsSet(string $user, int $rights): bool {
if(!Data::$user->authorize($user, __FUNCTION__, $rights)) throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
- if(!$this->userExists($user)) return false;
+ if(!$this->userExists($user)) throw new User\Exception("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
$this->db->prepare("UPDATE arsse_users set rights = ? where id is ?", "int", "str")->run($rights, $user);
return true;
}
diff --git a/tests/Db/SQLite3/Database/TestDatabaseUser.php b/tests/Db/SQLite3/Database/TestDatabaseUser.php
new file mode 100644
index 00000000..3abaa9b9
--- /dev/null
+++ b/tests/Db/SQLite3/Database/TestDatabaseUser.php
@@ -0,0 +1,289 @@
+ [
+ 'columns' => [
+ 'id' => 'str',
+ 'password' => 'str',
+ 'name' => 'str',
+ 'rights' => 'int',
+ ],
+ 'rows' => [
+ ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", User\Driver::RIGHTS_GLOBAL_ADMIN], // password is hash of "secret"
+ ["jane.doe@example.com", "", "Jane Doe", User\Driver::RIGHTS_NONE],
+ ["john.doe@example.com", "", "John Doe", User\Driver::RIGHTS_NONE],
+ ],
+ ],
+ ];
+
+ function setUp() {
+ // establish a clean baseline
+ $this->clearData();
+ // create a default configuration
+ Data::$conf = new Conf();
+ // configure and create the relevant database driver
+ Data::$conf->dbSQLite3File = ":memory:";
+ $this->drv = new Db\SQLite3\Driver(true);
+ // create the database interface with the suitable driver
+ Data::$db = new Database($this->drv);
+ Data::$db->schemaUpdate();
+ // create a mock user manager
+ Data::$user = Phake::mock(User::class);
+ Phake::when(Data::$user)->authorize->thenReturn(true);
+ // call the additional setup method if it exists
+ if(method_exists($this, "setUpSeries")) $this->setUpSeries();
+ }
+
+ function tearDown() {
+ // call the additional teardiwn method if it exists
+ if(method_exists($this, "tearDownSeries")) $this->tearDownSeries();
+ // clean up
+ $this->drv = null;
+ $this->clearData();
+ }
+
+ function setUpSeries() {
+ $this->primeDatabase($this->data);
+ }
+
+ function testCheckThatAUserExists() {
+ $this->assertTrue(Data::$db->userExists("jane.doe@example.com"));
+ $this->assertFalse(Data::$db->userExists("jane.doe@example.org"));
+ Phake::verify(Data::$user)->authorize("jane.doe@example.com", "userExists");
+ Phake::verify(Data::$user)->authorize("jane.doe@example.org", "userExists");
+ $this->compareExpectations($this->data);
+ }
+
+ function testCheckThatAUserExistsWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userExists("jane.doe@example.com");
+ }
+
+ function testGetAPassword() {
+ $hash = Data::$db->userPasswordGet("admin@example.net");
+ $this->assertSame('$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', $hash);
+ Phake::verify(Data::$user)->authorize("admin@example.net", "userPasswordGet");
+ $this->assertTrue(password_verify("secret", $hash));
+ }
+
+ function testGetAPasswordWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userPasswordGet("admin@example.net");
+ }
+
+ function testAddANewUser() {
+ $this->assertSame("", Data::$db->userAdd("john.doe@example.org", ""));
+ Phake::verify(Data::$user)->authorize("john.doe@example.org", "userAdd");
+ $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','name','rights']]);
+ $state['arsse_users']['rows'][] = ["john.doe@example.org", null, User\Driver::RIGHTS_NONE];
+ $this->compareExpectations($state);
+ }
+
+ /**
+ * @depends testGetAPassword
+ * @depends testAddANewUser
+ */
+ function testAddANewUserWithARandomPassword() {
+ $user1 = "john.doe@example.org";
+ $user2 = "john.doe@example.net";
+ $pass1 = Data::$db->userAdd($user1);
+ $pass2 = Data::$db->userAdd($user2);
+ $this->assertSame(Data::$conf->userTempPasswordLength, strlen($pass1));
+ $this->assertSame(Data::$conf->userTempPasswordLength, strlen($pass2));
+ $this->assertNotEquals($pass1, $pass2);
+ $hash1 = Data::$db->userPasswordGet($user1);
+ $hash2 = Data::$db->userPasswordGet($user2);
+ Phake::verify(Data::$user)->authorize($user1, "userAdd");
+ Phake::verify(Data::$user)->authorize($user2, "userAdd");
+ Phake::verify(Data::$user)->authorize($user1, "userPasswordGet");
+ Phake::verify(Data::$user)->authorize($user2, "userPasswordGet");
+ $this->assertTrue(password_verify($pass1, $hash1), "Failed verifying password of $user1 '$pass1' against hash '$hash1'.");
+ $this->assertTrue(password_verify($pass2, $hash2), "Failed verifying password of $user2 '$pass2' against hash '$hash2'.");
+ }
+
+ function testAddAnExistingUser() {
+ $this->assertException("alreadyExists", "User");
+ Data::$db->userAdd("john.doe@example.com", "");
+ }
+
+ function testAddANewUserWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userAdd("john.doe@example.org", "");
+ }
+
+ function testRemoveAUser() {
+ $this->assertTrue(Data::$db->userRemove("admin@example.net"));
+ Phake::verify(Data::$user)->authorize("admin@example.net", "userRemove");
+ $state = $this->primeExpectations($this->data, ['arsse_users' => ['id']]);
+ array_shift($state['arsse_users']['rows']);
+ $this->compareExpectations($state);
+ }
+
+ function testRemoveAMissingUser() {
+ $this->assertException("doesNotExist", "User");
+ Data::$db->userRemove("john.doe@example.org");
+ }
+
+ function testRemoveAUserWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userRemove("admin@example.net");
+ }
+
+ function testListAllUsers() {
+ $users = ["admin@example.net", "jane.doe@example.com", "john.doe@example.com"];
+ $this->assertSame($users, Data::$db->userList());
+ Phake::verify(Data::$user)->authorize("", "userList");
+ }
+
+ function testListUsersOnADomain() {
+ $users = ["jane.doe@example.com", "john.doe@example.com"];
+ $this->assertSame($users, Data::$db->userList("example.com"));
+ Phake::verify(Data::$user)->authorize("@example.com", "userList");
+ }
+
+ function testListAllUsersWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userList();
+ }
+
+ function testListUsersOnADomainWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userList("example.com");
+ }
+
+ /**
+ * @depends testGetAPassword
+ */
+ function testSetAPassword() {
+ $user = "john.doe@example.com";
+ $this->assertEquals("", Data::$db->userPasswordGet($user));
+ $pass = Data::$db->userPasswordSet($user, "secret");
+ $hash = Data::$db->userPasswordGet($user);
+ $this->assertNotEquals("", $hash);
+ Phake::verify(Data::$user)->authorize($user, "userPasswordSet");
+ $this->assertTrue(password_verify($pass, $hash), "Failed verifying password of $user '$pass' against hash '$hash'.");
+ }
+
+ function testSetThePasswordOfAMissingUser() {
+ $this->assertException("doesNotExist", "User");
+ Data::$db->userPasswordSet("john.doe@example.org", "secret");
+ }
+
+ function testSetAPasswordWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userPasswordSet("john.doe@example.com", "secret");
+ }
+
+ function testGetUserProperties() {
+ $exp = [
+ 'name' => 'Hard Lip Herbert',
+ 'rights' => User\Driver::RIGHTS_GLOBAL_ADMIN,
+ ];
+ $props = Data::$db->userPropertiesGet("admin@example.net");
+ Phake::verify(Data::$user)->authorize("admin@example.net", "userPropertiesGet");
+ $this->assertArraySubset($exp, $props);
+ $this->assertArrayNotHasKey("password", $props);
+ }
+
+ function testGetThePropertiesOfAMissingUser() {
+ $this->assertException("doesNotExist", "User");
+ Data::$db->userPropertiesGet("john.doe@example.org");
+ }
+
+ function testGetUserPropertiesWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userPropertiesGet("john.doe@example.com");
+ }
+
+ function testSetUserProperties() {
+ $try = [
+ 'name' => 'James Kirk', // only this should actually change
+ 'password' => '000destruct0',
+ 'rights' => User\Driver::RIGHTS_NONE,
+ 'lifeform' => 'tribble',
+ ];
+ $exp = [
+ 'name' => 'James Kirk',
+ 'rights' => User\Driver::RIGHTS_GLOBAL_ADMIN,
+ ];
+ $props = Data::$db->userPropertiesSet("admin@example.net", $try);
+ Phake::verify(Data::$user)->authorize("admin@example.net", "userPropertiesSet");
+ $this->assertArraySubset($exp, $props);
+ $this->assertArrayNotHasKey("password", $props);
+ $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','password','name','rights']]);
+ $state['arsse_users']['rows'][0][2] = "James Kirk";
+ $this->compareExpectations($state);
+ }
+
+ function testSetThePropertiesOfAMissingUser() {
+ $try = ['name' => 'John Doe'];
+ $this->assertException("doesNotExist", "User");
+ Data::$db->userPropertiesSet("john.doe@example.org", $try);
+ }
+
+ function testSetUserPropertiesWithoutAuthority() {
+ $try = ['name' => 'John Doe'];
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userPropertiesSet("john.doe@example.com", $try);
+ }
+
+ function testGetUserRights() {
+ $user1 = "john.doe@example.com";
+ $user2 = "admin@example.net";
+ $this->assertSame(User\Driver::RIGHTS_NONE, Data::$db->userRightsGet($user1));
+ $this->assertSame(User\Driver::RIGHTS_GLOBAL_ADMIN, Data::$db->userRightsGet($user2));
+ Phake::verify(Data::$user)->authorize($user1, "userRightsGet");
+ Phake::verify(Data::$user)->authorize($user2, "userRightsGet");
+ }
+
+ function testGetTheRightsOfAMissingUser() {
+ $this->assertSame(User\Driver::RIGHTS_NONE, Data::$db->userRightsGet("john.doe@example.org"));
+ Phake::verify(Data::$user)->authorize("john.doe@example.org", "userRightsGet");
+ }
+
+ function testGetUserRightsWithoutAuthority() {
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userRightsGet("john.doe@example.com");
+ }
+
+ function testSetUserRights() {
+ $user = "john.doe@example.com";
+ $rights = User\Driver::RIGHTS_GLOBAL_ADMIN;
+ $this->assertTrue(Data::$db->userRightsSet($user, $rights));
+ Phake::verify(Data::$user)->authorize($user, "userRightsSet", $rights);
+ $state = $this->primeExpectations($this->data, ['arsse_users' => ['id','rights']]);
+ $state['arsse_users']['rows'][2][1] = $rights;
+ $this->compareExpectations($state);
+ }
+
+ function testSetTheRightsOfAMissingUser() {
+ $rights = User\Driver::RIGHTS_GLOBAL_ADMIN;
+ $this->assertException("doesNotExist", "User");
+ Data::$db->userRightsSet("john.doe@example.org", $rights);
+ }
+
+ function testSetUserRightsWithoutAuthority() {
+ $rights = User\Driver::RIGHTS_GLOBAL_ADMIN;
+ Phake::when(Data::$user)->authorize->thenReturn(false);
+ $this->assertException("notAuthorized", "User", "ExceptionAuthz");
+ Data::$db->userRightsSet("john.doe@example.com", $rights);
+ }
+}
\ No newline at end of file
diff --git a/tests/Db/TestDatabase.php b/tests/Db/TestDatabase.php
deleted file mode 100644
index 078630da..00000000
--- a/tests/Db/TestDatabase.php
+++ /dev/null
@@ -1,10 +0,0 @@
-begin();
+ function primeDatabase(array $data): bool {
+ $this->drv->begin();
foreach($data as $table => $info) {
$cols = implode(",", array_keys($info['columns']));
$bindings = array_values($info['columns']);
@@ -17,21 +17,50 @@ trait Tools {
$this->assertEquals(1, $s->runArray($row)->changes());
}
}
- $drv->commit();
+ $this->drv->commit();
return true;
}
- function compare(array $expected): bool {
+ function compareExpectations(array $expected): bool {
foreach($expected as $table => $info) {
$cols = implode(",", array_keys($info['columns']));
foreach($this->drv->prepare("SELECT $cols from $table")->run() as $num => $row) {
$row = array_values($row);
- $assertSame($expected[$table]['rows'][$num], $row, "Row $num of table $table does not match expectation.");
+ $this->assertSame($expected[$table]['rows'][$num], $row, "Row ".($num+1)." of table $table does not match expectations at array index $num.");
}
}
+ return true;
}
- function setUp() {
-
+ function primeExpectations(array $source, array $tableSpecs = null): array {
+ $out = [];
+ foreach($tableSpecs as $table => $columns) {
+ if(!isset($source[$table])) {
+ $this->assertTrue(false, "Source for expectations does not contain requested table $table.");
+ return [];
+ }
+ $out[$table] = [
+ 'columns' => [],
+ 'rows' => [],
+ ];
+ $transformations = [];
+ foreach($columns as $target => $col) {
+ if(!isset($source[$table]['columns'][$col])) {
+ $this->assertTrue(false, "Source for expectations does not contain requested column $col of table $table.");
+ return [];
+ }
+ $found = array_search($col, array_keys($source[$table]['columns']));
+ $transformations[$found] = $target;
+ $out[$table]['columns'][$col] = $source[$table]['columns'][$col];
+ }
+ foreach($source[$table]['rows'] as $sourceRow) {
+ $newRow = [];
+ foreach($transformations as $from => $to) {
+ $newRow[$to] = $sourceRow[$from];
+ }
+ $out[$table]['rows'][] = $newRow;
+ }
+ }
+ return $out;
}
}
\ No newline at end of file
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 2331c18f..103fbd0e 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -38,8 +38,8 @@
Db/SQLite3/TestDbUpdateSQLite3.php
-
- Db/TestDatabase.php
+
+ Db/SQLite3/Database/TestDatabaseUser.php