diff --git a/lib/Database.php b/lib/Database.php index 660fbc4..bd0d25b 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -248,7 +248,7 @@ class Database { /** Adds a user to the database * * @param string $user The user to add - * @param string|null $passwordThe user's password in cleartext. It will be stored hashed + * @param string|null $passwordThe user's password in cleartext. It will be stored hashed. If null is provided the user will not be able to log in */ public function userAdd(string $user, ?string $password): bool { if ($this->userExists($user)) { diff --git a/lib/User.php b/lib/User.php index 2fec130..1ef8317 100644 --- a/lib/User.php +++ b/lib/User.php @@ -108,10 +108,6 @@ class User { Arsse::$db->userPasswordSet($user, null); // also invalidate any current sessions for the user Arsse::$db->sessionDestroy($user); - } else { - // if the user does not exist - Arsse::$db->userAdd($user, ""); - Arsse::$db->userPasswordSet($user, null); } return $out; } @@ -119,24 +115,29 @@ class User { public function generatePassword(): string { return (new PassGen)->length(Arsse::$conf->userTempPasswordLength)->get(); } - + public function propertiesGet(string $user): array { $extra = $this->u->userPropertiesGet($user); // synchronize the internal database if (!Arsse::$db->userExists($user)) { - Arsse::$db->userAdd($user, $this->generatePassword()); + Arsse::$db->userAdd($user, null); + Arsse::$db->userPropertiesSet($user, $extra); } - // unconditionally retrieve from the database to get at least the user number, and anything else the driver does not provide + // retrieve from the database to get at least the user number, and anything else the driver does not provide $out = Arsse::$db->userPropertiesGet($user); // layer on the driver's data - foreach (["lang", "tz", "admin", "sort_asc"] as $k) { + foreach (["tz", "admin", "sort_asc"] as $k) { if (array_key_exists($k, $extra)) { $out[$k] = $extra[$k] ?? $out[$k]; } } + // treat language specially since it may legitimately be null + if (array_key_exists("lang", $extra)) { + $out['lang'] = $extra['lang']; + } return $out; } - + public function propertiesSet(string $user, array $data): array { $in = []; if (array_key_exists("tz", $data)) { diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 9e34a2e..313a054 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -243,4 +243,105 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { \Phake::verify(Arsse::$db)->sessionDestroy($user); \Phake::verify(Arsse::$db)->userExists($user); } + + public function testSetAPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "secret"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($pass, $u->passwordSet($user, $pass)); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testSetARandomPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userPasswordSet->thenReturn(null)->thenReturn($pass); + \Phake::when(Arsse::$db)->userPasswordSet->thenReturn($pass); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertSame($pass, $u->passwordSet($user, null)); + \Phake::verify($this->drv)->userPasswordSet($user, null, null); + \Phake::verify($this->drv)->userPasswordSet($user, $pass, null); + \Phake::verify(Arsse::$db)->userAdd($user, $pass); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testSetARandomPasswordForAMissingUser(): void { + $user = "john.doe@example.com"; + $pass = "random password"; + $u = \Phake::partialMock(User::class, $this->drv); + \Phake::when($u)->generatePassword->thenReturn($pass); + \Phake::when($this->drv)->userPasswordSet->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->passwordSet($user, null); + } finally { + \Phake::verify($this->drv)->userPasswordSet($user, null, null); + } + } + + public function testUnsetAPassword(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertTrue($u->passwordUnset($user)); + \Phake::verify($this->drv)->userPasswordUnset($user, null); + \Phake::verify(Arsse::$db)->userPasswordSet($user, null); + \Phake::verify(Arsse::$db)->sessionDestroy($user); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testUnsetAPasswordForAUserWeDoNotKnow(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userPasswordUnset->thenReturn(true); + \Phake::when(Arsse::$db)->userExists->thenReturn(false); + $this->assertTrue($u->passwordUnset($user)); + \Phake::verify($this->drv)->userPasswordUnset($user, null); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function testUnsetAPasswordForAMissingUser(): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPasswordUnset->thenThrow(new ExceptionConflict("doesNotExist")); + $this->assertException("doesNotExist", "User", "ExceptionConflict"); + try { + $u->passwordUnset($user); + } finally { + \Phake::verify($this->drv)->userPasswordUnset($user, null); + } + } + + /** @dataProvider provideProperties */ + public function testGetThePropertiesOfAUser(array $exp, array $base, array $extra): void { + $user = "john.doe@example.com"; + $u = new User($this->drv); + \Phake::when($this->drv)->userPropertiesGet->thenReturn($extra); + \Phake::when(Arsse::$db)->userPropertiesGet->thenReturn($base); + \Phake::when(Arsse::$db)->userExists->thenReturn(true); + $this->assertSame($exp, $u->propertiesGet($user)); + \Phake::verify($this->drv)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userPropertiesGet($user); + \Phake::verify(Arsse::$db)->userExists($user); + } + + public function provideProperties(): iterable { + $defaults = ['num' => 1, 'admin' => false, 'lang' => null, 'tz' => "Etc/UTC", 'sort_asc' => false]; + return [ + [$defaults, $defaults, []], + [$defaults, $defaults, ['num' => 2112, 'blah' => "bloo"]], + [['num' => 1, 'admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true], $defaults, ['admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true]], + [['num' => 1, 'admin' => true, 'lang' => null, 'tz' => "America/Toronto", 'sort_asc' => true], ['num' => 1, 'admin' => true, 'lang' => "fr", 'tz' => "America/Toronto", 'sort_asc' => true], ['lang' => null]], + ]; + } }