Browse Source

Add backend functionality to rename users

rpm
J. King 3 years ago
parent
commit
5ec04d33c6
  1. 14
      lib/Database.php
  2. 27
      lib/User.php
  3. 9
      lib/User/Driver.php
  4. 23
      lib/User/Internal/Driver.php
  5. 25
      tests/cases/Database/SeriesUser.php
  6. 24
      tests/cases/User/TestInternal.php
  7. 50
      tests/cases/User/TestUser.php

14
lib/Database.php

@ -273,6 +273,20 @@ class Database {
return true; return true;
} }
public function userRename(string $user, string $name): bool {
if ($user === $name) {
return false;
}
try {
if (!$this->db->prepare("UPDATE arsse_users set id = ? where id = ?", "str", "str")->run($name, $user)->changes()) {
throw new User\ExceptionConflict("doesNotExist", ["action" => __FUNCTION__, "user" => $user]);
}
} catch (Db\ExceptionInput $e) {
throw new User\ExceptionConflict("alreadyExists", ["action" => __FUNCTION__, "user" => $name], $e);
}
return true;
}
/** Removes a user from the database */ /** Removes a user from the database */
public function userRemove(string $user): bool { public function userRemove(string $user): bool {
if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) { if ($this->db->prepare("DELETE from arsse_users where id = ?", "str")->run($user)->changes() < 1) {

27
lib/User.php

@ -42,6 +42,21 @@ class User {
return (string) $this->id; return (string) $this->id;
} }
public function begin(): Db\Transaction {
/* TODO: A proper implementation of this would return a meta-transaction
object which would contain both a user-manager transaction (when
applicable) and a database transaction, and commit or roll back both
as the situation calls.
In theory, an external user driver would probably have to implement its
own approximation of atomic transactions and rollback. In practice the
only driver is the internal one, which is always backed by an ACID
database; the added complexity is thus being deferred until such time
as it is actually needed for a concrete implementation.
*/
return Arsse::$db->begin();
}
public function auth(string $user, string $password): bool { public function auth(string $user, string $password): bool {
$prevUser = $this->id; $prevUser = $this->id;
$this->id = $user; $this->id = $user;
@ -89,6 +104,18 @@ class User {
return $out; return $out;
} }
public function rename(string $user, string $newName): bool {
if ($this->u->userRename($user, $newName)) {
if (!Arsse::$db->userExists($user)) {
Arsse::$db->userAdd($newName, null);
return true;
} else {
return Arsse::$db->userRename($user, $newName);
}
}
return false;
}
public function remove(string $user): bool { public function remove(string $user): bool {
try { try {
$out = $this->u->userRemove($user); $out = $this->u->userRemove($user);

9
lib/User/Driver.php

@ -27,6 +27,13 @@ interface Driver {
*/ */
public function userAdd(string $user, string $password = null): ?string; public function userAdd(string $user, string $password = null): ?string;
/** Renames a user
*
* The implementation must retain all user metadata as well as the
* user's password
*/
public function userRename(string $user, string $newName): bool;
/** Removes a user */ /** Removes a user */
public function userRemove(string $user): bool; public function userRemove(string $user): bool;
@ -44,7 +51,7 @@ interface Driver {
* @param string|null $password The cleartext password to assign to the user, or null to generate a random password * @param string|null $password The cleartext password to assign to the user, or null to generate a random password
* @param string|null $oldPassword The user's previous password, if known * @param string|null $oldPassword The user's previous password, if known
*/ */
public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null); public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string;
/** Removes a user's password; this makes authentication fail unconditionally /** Removes a user's password; this makes authentication fail unconditionally
* *

23
lib/User/Internal/Driver.php

@ -40,6 +40,16 @@ class Driver implements \JKingWeb\Arsse\User\Driver {
return $password; return $password;
} }
public function userRename(string $user, string $newName): bool {
// do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception)
// throw an exception if the user does not exist
if (!$this->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]);
} else {
return !($user === $newName);
}
}
public function userRemove(string $user): bool { public function userRemove(string $user): bool {
return Arsse::$db->userRemove($user); return Arsse::$db->userRemove($user);
} }
@ -50,14 +60,19 @@ class Driver implements \JKingWeb\Arsse\User\Driver {
public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string { public function userPasswordSet(string $user, ?string $newPassword, string $oldPassword = null): ?string {
// do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception)
return $newPassword; // throw an exception if the user does not exist
if (!$this->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]);
} else {
return $newPassword;
}
} }
public function userPasswordUnset(string $user, string $oldPassword = null): bool { public function userPasswordUnset(string $user, string $oldPassword = null): bool {
// do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception) // do nothing: the internal database is updated regardless of what the driver does (assuming it does not throw an exception)
// throw an exception if the user does not exist // throw an exception if the user does not exist
if (!$this->userExists($user)) { if (!$this->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]);
} else { } else {
return true; return true;
} }
@ -74,7 +89,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver {
public function userPropertiesGet(string $user, bool $includeLarge = true): array { public function userPropertiesGet(string $user, bool $includeLarge = true): array {
// do nothing: the internal database will retrieve everything for us // do nothing: the internal database will retrieve everything for us
if (!$this->userExists($user)) { if (!$this->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]);
} else { } else {
return []; return [];
} }
@ -83,7 +98,7 @@ class Driver implements \JKingWeb\Arsse\User\Driver {
public function userPropertiesSet(string $user, array $data): array { public function userPropertiesSet(string $user, array $data): array {
// do nothing: the internal database will set everything for us // do nothing: the internal database will set everything for us
if (!$this->userExists($user)) { if (!$this->userExists($user)) {
throw new ExceptionConflict("doesNotExist", ['action' => "userPasswordUnset", 'user' => $user]); throw new ExceptionConflict("doesNotExist", ['action' => __FUNCTION__, 'user' => $user]);
} else { } else {
return $data; return $data;
} }

25
tests/cases/Database/SeriesUser.php

@ -180,4 +180,29 @@ trait SeriesUser {
$this->assertException("doesNotExist", "User", "ExceptionConflict"); $this->assertException("doesNotExist", "User", "ExceptionConflict");
Arsse::$db->userLookup(2112); Arsse::$db->userLookup(2112);
} }
public function testRenameAUser(): void {
$this->assertTrue(Arsse::$db->userRename("john.doe@example.com", "juan.doe@example.com"));
$state = $this->primeExpectations($this->data, [
'arsse_users' => ['id', 'num'],
'arsse_user_meta' => ["owner", "key", "value"]
]);
$state['arsse_users']['rows'][2][0] = "juan.doe@example.com";
$state['arsse_user_meta']['rows'][6][0] = "juan.doe@example.com";
$this->compareExpectations(static::$drv, $state);
}
public function testRenameAUserToTheSameName(): void {
$this->assertFalse(Arsse::$db->userRename("john.doe@example.com", "john.doe@example.com"));
}
public function testRenameAMissingUser(): void {
$this->assertException("doesNotExist", "User", "ExceptionConflict");
Arsse::$db->userRename("juan.doe@example.com", "john.doe@example.com");
}
public function testRenameAUserToADuplicateName(): void {
$this->assertException("alreadyExists", "User", "ExceptionConflict");
Arsse::$db->userRename("john.doe@example.com", "jane.doe@example.com");
}
} }

24
tests/cases/User/TestInternal.php

@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\User;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\User\Driver as DriverInterface; use JKingWeb\Arsse\User\Driver as DriverInterface;
use JKingWeb\Arsse\User\ExceptionConflict;
use JKingWeb\Arsse\User\Internal\Driver; use JKingWeb\Arsse\User\Internal\Driver;
/** @covers \JKingWeb\Arsse\User\Internal\Driver */ /** @covers \JKingWeb\Arsse\User\Internal\Driver */
@ -88,6 +89,21 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
\Phake::verify(Arsse::$db)->userAdd; \Phake::verify(Arsse::$db)->userAdd;
} }
public function testRenameAUser(): void {
$john = "john.doe@example.com";
\Phake::when(Arsse::$db)->userExists->thenReturn(true);
$this->assertTrue((new Driver)->userRename($john, "jane.doe@example.com"));
$this->assertFalse((new Driver)->userRename($john, $john));
\Phake::verify(Arsse::$db, \Phake::times(2))->userExists($john);
}
public function testRenameAMissingUser(): void {
$john = "john.doe@example.com";
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
$this->assertException("doesNotExist", "User", "ExceptionConflict");
(new Driver)->userRename($john, "jane.doe@example.com");
}
public function testRemoveAUser(): void { public function testRemoveAUser(): void {
$john = "john.doe@example.com"; $john = "john.doe@example.com";
\Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist")); \Phake::when(Arsse::$db)->userRemove->thenReturn(true)->thenThrow(new \JKingWeb\Arsse\User\ExceptionConflict("doesNotExist"));
@ -104,12 +120,18 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest {
public function testSetAPassword(): void { public function testSetAPassword(): void {
$john = "john.doe@example.com"; $john = "john.doe@example.com";
\Phake::verifyNoFurtherInteraction(Arsse::$db); \Phake::when(Arsse::$db)->userExists->thenReturn(true);
$this->assertSame("superman", (new Driver)->userPasswordSet($john, "superman")); $this->assertSame("superman", (new Driver)->userPasswordSet($john, "superman"));
$this->assertSame(null, (new Driver)->userPasswordSet($john, null)); $this->assertSame(null, (new Driver)->userPasswordSet($john, null));
\Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordSet; \Phake::verify(Arsse::$db, \Phake::times(0))->userPasswordSet;
} }
public function testSetAPasswordForAMssingUser(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
$this->assertException("doesNotExist", "User", "ExceptionConflict");
(new Driver)->userPasswordSet("john.doe@example.com", "secret");
}
public function testUnsetAPassword(): void { public function testUnsetAPassword(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(true); \Phake::when(Arsse::$db)->userExists->thenReturn(true);
$this->assertTrue((new Driver)->userPasswordUnset("john.doe@example.com")); $this->assertTrue((new Driver)->userPasswordUnset("john.doe@example.com"));

50
tests/cases/User/TestUser.php

@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\User;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Database;
use JKingWeb\Arsse\User; use JKingWeb\Arsse\User;
use JKingWeb\Arsse\Db\Transaction;
use JKingWeb\Arsse\User\ExceptionConflict; use JKingWeb\Arsse\User\ExceptionConflict;
use JKingWeb\Arsse\User\ExceptionInput; use JKingWeb\Arsse\User\ExceptionInput;
use JKingWeb\Arsse\User\Driver; use JKingWeb\Arsse\User\Driver;
@ -43,6 +44,13 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
$this->assertSame("", (string) $u); $this->assertSame("", (string) $u);
} }
public function testStartATransaction(): void {
\Phake::when(Arsse::$db)->begin->thenReturn(\Phake::mock(Transaction::class));
$u = new User($this->drv);
$this->assertInstanceOf(Transaction::class, $u->begin());
\Phake::verify(Arsse::$db)->begin();
}
public function testGeneratePasswords(): void { public function testGeneratePasswords(): void {
$u = new User($this->drv); $u = new User($this->drv);
$pass1 = $u->generatePassword(); $pass1 = $u->generatePassword();
@ -174,9 +182,48 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
\Phake::verify(Arsse::$db)->userExists($user); \Phake::verify(Arsse::$db)->userExists($user);
} }
public function testRenameAUser(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(true);
\Phake::when(Arsse::$db)->userAdd->thenReturn(true);
\Phake::when(Arsse::$db)->userRename->thenReturn(true);
\Phake::when($this->drv)->userRename->thenReturn(true);
$u = new User($this->drv);
$old = "john.doe@example.com";
$new = "jane.doe@example.com";
$this->assertTrue($u->rename($old, $new));
\Phake::verify($this->drv)->userRename($old, $new);
\Phake::verify(Arsse::$db)->userExists($old);
\Phake::verify(Arsse::$db)->userRename($old, $new);
}
public function testRenameAUserWeDoNotKnow(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
\Phake::when(Arsse::$db)->userAdd->thenReturn(true);
\Phake::when(Arsse::$db)->userRename->thenReturn(true);
\Phake::when($this->drv)->userRename->thenReturn(true);
$u = new User($this->drv);
$old = "john.doe@example.com";
$new = "jane.doe@example.com";
$this->assertTrue($u->rename($old, $new));
\Phake::verify($this->drv)->userRename($old, $new);
\Phake::verify(Arsse::$db)->userExists($old);
\Phake::verify(Arsse::$db)->userAdd($new, null);
}
public function testRenameAUserWithoutEffect(): void {
\Phake::when(Arsse::$db)->userExists->thenReturn(false);
\Phake::when(Arsse::$db)->userAdd->thenReturn(true);
\Phake::when(Arsse::$db)->userRename->thenReturn(true);
\Phake::when($this->drv)->userRename->thenReturn(false);
$u = new User($this->drv);
$old = "john.doe@example.com";
$new = "jane.doe@example.com";
$this->assertFalse($u->rename($old, $old));
\Phake::verify($this->drv)->userRename($old, $old);
}
public function testRemoveAUser(): void { public function testRemoveAUser(): void {
$user = "john.doe@example.com"; $user = "john.doe@example.com";
$pass = "secret";
$u = new User($this->drv); $u = new User($this->drv);
\Phake::when($this->drv)->userRemove->thenReturn(true); \Phake::when($this->drv)->userRemove->thenReturn(true);
\Phake::when(Arsse::$db)->userExists->thenReturn(true); \Phake::when(Arsse::$db)->userExists->thenReturn(true);
@ -188,7 +235,6 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
public function testRemoveAUserWeDoNotKnow(): void { public function testRemoveAUserWeDoNotKnow(): void {
$user = "john.doe@example.com"; $user = "john.doe@example.com";
$pass = "secret";
$u = new User($this->drv); $u = new User($this->drv);
\Phake::when($this->drv)->userRemove->thenReturn(true); \Phake::when($this->drv)->userRemove->thenReturn(true);
\Phake::when(Arsse::$db)->userExists->thenReturn(false); \Phake::when(Arsse::$db)->userExists->thenReturn(false);

Loading…
Cancel
Save