death-notifier/src/test/php/com/fwdekker/deathnotifier/UserManagerTest.php

668 lines
27 KiB
PHP

<?php
namespace com\fwdekker\deathnotifier;
use com\fwdekker\deathnotifier\mailer\ChangedEmailEmail;
use com\fwdekker\deathnotifier\mailer\ChangedPasswordEmail;
use com\fwdekker\deathnotifier\mailer\Mailer;
use com\fwdekker\deathnotifier\mailer\RegisterEmail;
use com\fwdekker\deathnotifier\mailer\ResetPasswordEmail;
use com\fwdekker\deathnotifier\mailer\VerifyEmailEmail;
use PDO;
use PHPUnit\Framework\MockObject\MockObject;
/**
* Unit tests for `UserManager`.
*/
class UserManagerTest extends DatabaseTestCase
{
private UserManager $user_manager;
private Mailer&MockObject $mailer;
public function get_user_manager(): UserManager
{
return new UserManager($this->logger, $this->database->conn, $this->mailer);
}
public function setUp(): void
{
$this->mailer = $this->createMock(Mailer::class);
$this->mailer->method("queue_email")->willReturn(Response::satisfied());
parent::setUp();
$this->user_manager = $this->get_user_manager();
}
/**
* Returns the UUID of the given email address.
*
* @param string $email the email address to return the email verification token of
* @return string|null the email verification token of the given email address
*/
private function get_uuid(string $email): ?string
{
$stmt = $this->database->conn->prepare("SELECT uuid FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($results) ? null : $results[0]["uuid"];
}
/**
* Returns the email verification token of the given email address.
*
* @param string $email the email address to return the email verification token of
* @return string|null the email verification token of the given email address, or `null` if there is no user with
* the given email address
*/
private function get_email_token(string $email): ?string
{
$stmt = $this->database->conn->prepare("SELECT email_verification_token FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($results) ? null : $results[0]["email_verification_token"];
}
/**
* Changes the timestamp of the email verification token of the given email address, if a user with that email
* address exists.
*
* @param string $email the email address to set the timestamp of
* @param int $timestamp the timestamp to set
*/
private function set_email_token_timestamp(string $email, int $timestamp): void
{
$stmt = $this->database->conn->prepare("UPDATE users
SET email_verification_token_timestamp=:timestamp
WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":timestamp", $timestamp);
$stmt->execute();
}
/**
* Returns the password verification token of the given email address.
*
* @param string $email the email address to return the password verification token of
* @return string|null the password verification token of the given email address, or `null` if there is no user
* with the given email address
*/
private function get_password_token(string $email): ?string
{
$stmt = $this->database->conn->prepare("SELECT password_reset_token FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
return empty($results) ? null : $results[0]["password_reset_token"];
}
/**
* Changes the timestamp of the password verification token of the given email address, if a user with that email
* address exists.
*
* @param string $email the email address to set the timestamp of
* @param int $timestamp the timestamp to set
*/
private function set_password_token_timestamp(string $email, int $timestamp): void
{
$stmt = $this->database->conn->prepare("UPDATE users
SET password_reset_token_timestamp=:timestamp
WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->bindValue(":timestamp", $timestamp);
$stmt->execute();
}
public function test_register_user_returns_an_unsatisfied_response_if_the_email_is_used(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->register_user("john@example.com", "password");
self::assertFalse($response->satisfied);
}
public function test_register_user_registers_the_user_and_returns_a_satisfied_response_if_the_email_is_unused(): void
{
$response = $this->user_manager->register_user("john@example.com", "password");
self::assertTrue($response->satisfied);
self::assertNotNull($this->get_uuid("john@example.com"));
}
public function test_register_user_sends_an_email_with_the_verification_token(): void
{
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(
fn($email) => $email instanceof RegisterEmail && $email->recipient === "john@example.com")
);
$response = $this->user_manager->register_user("john@example.com", "password");
self::assertTrue($response->satisfied);
}
public function test_check_login_returns_an_unsatisfied_response_if_the_email_does_not_exist(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->check_login("lydia@other.example.com", "password");
self::assertFalse($response[0]->satisfied);
self::assertNull($response[1]);
}
public function test_check_login_returns_an_unsatisfied_response_if_the_email_is_incorrect(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->check_login("lydia@other.example.com", "password");
self::assertFalse($response[0]->satisfied);
self::assertNull($response[1]);
}
public function test_check_login_returns_a_satisfied_response_if_the_email_and_password_are_correct(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->check_login("john@example.com", "password");
self::assertTrue($response[0]->satisfied);
self::assertNotNull($response[1]);
}
public function test_user_exists_returns_false_if_the_user_does_not_exist(): void
{
self::assertFalse($this->user_manager->user_exists("invalid-uuid"));
}
public function test_user_exists_returns_true_if_the_user_exists(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
self::assertTrue($this->user_manager->user_exists($uuid));
}
public function test_get_user_returns_a_satisfied_response_with_the_user_data_if_the_uuid_exists(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->get_user($uuid);
self::assertTrue($response->satisfied);
self::assertArrayHasKey("email", $response->payload);
}
public function test_get_user_returns_an_unsatisfied_response_if_the_uuid_does_not_exist(): void
{
$response = $this->user_manager->get_user("invalid-uuid");
self::assertFalse($response->satisfied);
}
public function test_delete_user_deletes_the_user_and_returns_a_satisfied_response_if_the_uuid_is_used(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->delete_user($uuid);
self::assertTrue($response->satisfied);
self::assertFalse($this->user_manager->user_exists($uuid));
}
public function test_delete_user_returns_a_satisfied_response_even_if_the_user_was_already_deleted(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$this->user_manager->delete_user($uuid);
$response = $this->user_manager->delete_user($uuid);
self::assertTrue($response->satisfied);
}
public function test_delete_user_returns_a_satisfied_response_even_if_the_uuid_does_not_exist(): void
{
$response = $this->user_manager->delete_user("invalid-uuid");
self::assertTrue($response->satisfied);
}
public function test_set_email_returns_an_unsatisfied_response_if_the_email_is_not_different(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->set_email($uuid, "john@example.com");
self::assertFalse($response->satisfied);
}
public function test_set_email_returns_an_unsatisfied_response_if_the_email_is_already_used(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->user_manager->register_user("lydia@other.example.com", "another-password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->set_email($uuid, "lydia@other.example.com");
self::assertFalse($response->satisfied);
}
public function test_set_email_returns_a_satisfied_response_and_sends_an_email_if_the_email_is_unused(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(
fn($email) => $email instanceof ChangedEmailEmail && $email->recipient === "lydia@other.example.com")
);
$response = $this->user_manager->set_email($uuid, "lydia@other.example.com");
self::assertTrue($response->satisfied);
self::assertNull($this->get_email_token("john@example.com"));
self::assertNotNull($this->get_email_token("lydia@other.example.com"));
}
public function test_set_email_retains_the_uuid_of_the_user(): void
{
$this->user_manager->register_user("john@example.com", "password");
$old_uuid = $this->get_uuid("john@example.com");
$this->user_manager->set_email($old_uuid, "lydia@other.example.com");
$new_uuid = $this->get_uuid("lydia@other.example.com");
self::assertEquals($old_uuid, $new_uuid);
}
public function test_set_email_disallows_logging_in_with_the_old_email_address(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$this->user_manager->set_email($uuid, "lydia@other.example.com");
self::assertFalse($this->user_manager->check_login("john@example.com", "password")[0]->satisfied);
}
public function test_verify_email_returns_an_unsatisfied_response_if_the_email_does_not_exist(): void
{
$response = $this->user_manager->verify_email("does-not-exist@example.com", "token");
self::assertFalse($response->satisfied);
}
public function test_verify_email_returns_an_unsatisfied_response_if_the_wrong_token_is_given(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->verify_email("john@example.com", "incorrect-token");
self::assertFalse($response->satisfied);
}
public function test_verify_email_returns_an_unsatisfied_response_if_an_email_is_already_verified(): void
{
$this->user_manager->register_user("john@example.com", "password");
$token = $this->get_email_token("john@example.com");
$this->user_manager->verify_email("john@example.com", $token);
$response = $this->user_manager->verify_email("john@example.com", $token);
self::assertFalse($response->satisfied);
}
public function test_verify_email_returns_an_unsatisfied_response_if_the_token_is_expired(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_email_token_timestamp("john@example.com", "1640995200");
$token = $this->get_email_token("john@example.com");
$response = $this->user_manager->verify_email("john@example.com", $token);
self::assertFalse($response->satisfied);
}
public function test_verify_email_verifies_the_email_and_returns_a_satisfied_response_if_the_verification_token_is_valid(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$token = $this->get_email_token("john@example.com");
$response = $this->user_manager->verify_email("john@example.com", $token);
self::assertTrue($response->satisfied);
self::assertNull($this->get_email_token("john@example.com"));
self::assertTrue($this->user_manager->get_user($uuid)->payload["email_verified"] === 1);
}
public function test_resend_verify_email_returns_an_unsatisfied_response_if_the_uuid_does_not_exist(): void
{
$response = $this->user_manager->resend_verify_email("invalid-uuid");
self::assertFalse($response->satisfied);
}
public function test_resend_verify_email_does_not_send_an_email_and_returns_an_unsatisfied_response_if_the_email_is_already_verified(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$token = $this->get_email_token("john@example.com");
$this->user_manager->verify_email("john@example.com", $token);
$this->mailer->expects($this->never())->method("queue_email");
$response = $this->user_manager->resend_verify_email($uuid);
self::assertFalse($response->satisfied);
}
public function test_resend_verify_email_does_not_send_an_email_and_returns_an_unsatisfied_response_if_the_user_recently_registered(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$this->mailer->expects($this->never())->method("queue_email");
$response = $this->user_manager->resend_verify_email($uuid);
self::assertFalse($response->satisfied);
}
public function test_resend_verify_email_does_not_send_an_email_and_returns_an_unsatisfied_response_if_an_email_was_recently_sent(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_email_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_VERIFICATION_EMAILS + 1)
);
$uuid = $this->get_uuid("john@example.com");
$this->user_manager->resend_verify_email($uuid);
$this->mailer->expects($this->never())->method("queue_email");
$response = $this->user_manager->resend_verify_email($uuid);
self::assertFalse($response->satisfied);
}
public function test_resend_verify_email_sends_an_email_and_returns_a_satisfied_response_if_no_email_was_recently_sent(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_email_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_VERIFICATION_EMAILS + 1)
);
$uuid = $this->get_uuid("john@example.com");
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(
fn($email) => $email instanceof VerifyEmailEmail && $email->recipient === "john@example.com")
);
$response = $this->user_manager->resend_verify_email($uuid);
self::assertTrue($response->satisfied);
}
public function test_resend_verify_email_disallows_verifying_the_email_with_the_old_token(): void
{
$this->user_manager->register_user("john@example.com", "password");
$old_token = $this->get_email_token("john@example.com");
$this->set_email_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_VERIFICATION_EMAILS + 1)
);
$uuid = $this->get_uuid("john@example.com");
$this->user_manager->resend_verify_email($uuid);
$response = $this->user_manager->verify_email("john@example.com", $old_token);
self::assertFalse($response->satisfied);
}
public function test_toggle_notifications_returns_an_unsatisfied_response_if_the_uuid_does_not_exist(): void
{
$response = $this->user_manager->toggle_notifications("invalid-uuid");
self::assertFalse($response->satisfied);
}
public function test_toggle_notifications_does_not_toggle_notifications_and_returns_an_unsatisfied_response_if_the_email_is_not_verified(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->toggle_notifications($uuid);
self::assertFalse($response->satisfied);
self::assertTrue($this->user_manager->get_user($uuid)->payload["email_notifications_enabled"] === 1);
}
public function test_toggle_notifications_enables_notifications_and_returns_a_satisfied_response_if_they_are_disabled(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$token = $this->get_email_token("john@example.com");
$this->user_manager->verify_email("john@example.com", $token);
$this->user_manager->toggle_notifications($uuid);
$response = $this->user_manager->toggle_notifications($uuid);
self::assertTrue($response->satisfied);
self::assertTrue($this->user_manager->get_user($uuid)->payload["email_notifications_enabled"] === 1);
}
public function test_toggle_notifications_disables_notifications_and_returns_a_satisfied_response_if_they_are_enabled(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$token = $this->get_email_token("john@example.com");
$this->user_manager->verify_email("john@example.com", $token);
$response = $this->user_manager->toggle_notifications($uuid);
self::assertTrue($response->satisfied);
self::assertFalse($this->user_manager->get_user($uuid)->payload["email_notifications_enabled"] === 1);
}
public function test_set_password_returns_an_unsatisfied_response_if_the_uuid_does_not_exist(): void
{
$response = $this->user_manager->set_password("invalid_uuid", "old-password", "new-password");
self::assertFalse($response->satisfied);
}
public function test_set_password_returns_an_unsatisfied_response_if_the_old_password_is_incorrect(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$response = $this->user_manager->set_password($uuid, "wrong-password", "new-password");
self::assertFalse($response->satisfied);
self::assertFalse($this->user_manager->check_login("john@example.com", "new-password")[0]->satisfied);
}
public function test_set_password_sends_an_email_and_returns_a_satisfied_response_if_the_old_password_is_correct(): void
{
$this->user_manager->register_user("john@example.com", "password");
$uuid = $this->get_uuid("john@example.com");
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(
fn($email) => $email instanceof ChangedPasswordEmail && $email->recipient === "john@example.com")
);
$response = $this->user_manager->set_password($uuid, "password", "new-password");
self::assertTrue($response->satisfied);
self::assertTrue($this->user_manager->check_login("john@example.com", "new-password")[0]->satisfied);
}
public function test_set_password_disallows_resetting_the_password_with_an_old_token(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$uuid = $this->get_uuid("john@example.com");
$token = $this->get_password_token("john@example.com");
$this->user_manager->set_password($uuid, "password", "new-password");
$response = $this->user_manager->reset_password("john@example.com", $token, "newer-password");
self::assertFalse($response->satisfied);
self::assertFalse($this->user_manager->check_login("john@example.com", "newer-password")[0]->satisfied);
}
public function test_send_password_reset_returns_an_unsatisfied_response_if_the_email_does_not_exist(): void
{
$response = $this->user_manager->send_password_reset("invalid@example.com");
self::assertFalse($response->satisfied);
}
public function test_send_password_reset_does_not_send_an_email_and_returns_an_unsatisfied_response_if_the_user_recently_registered(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->mailer->expects($this->never())->method("queue_email");
$response = $this->user_manager->send_password_reset("john@example.com");
self::assertFalse($response->satisfied);
}
public function test_send_password_reset_does_not_send_an_email_and_returns_an_unsatisfied_response_if_an_email_was_recently_sent(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$this->mailer->expects($this->never())->method("queue_email");
$response = $this->user_manager->send_password_reset("john@example.com");
self::assertFalse($response->satisfied);
}
public function test_send_password_reset_sends_an_email_and_returns_a_satisfied_response_if_no_email_was_recently_sent(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(
fn($email) => $email instanceof ResetPasswordEmail && $email->recipient === "john@example.com")
);
$response = $this->user_manager->send_password_reset("john@example.com");
self::assertTrue($response->satisfied);
}
public function test_send_password_reset_disallows_resetting_the_password_with_the_old_token(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$old_token = $this->get_password_token("john@example.com");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$response = $this->user_manager->reset_password("john@example.com", $old_token, "password-new");
self::assertFalse($response->satisfied);
}
public function test_reset_password_returns_an_unsatisfied_response_if_the_email_does_not_exist(): void
{
$response = $this->user_manager->reset_password("does-not-exist@example.com", "token", "new-password");
self::assertFalse($response->satisfied);
}
public function test_reset_password_does_not_change_the_password_and_returns_an_unsatisfied_response_if_the_wrong_token_is_given(): void
{
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->reset_password("john@example.com", "incorrect-token", "new-password");
self::assertFalse($response->satisfied);
self::assertFalse($this->user_manager->check_login("john@example.com", "new-password")[0]->satisfied);
}
public function test_reset_password_does_not_change_the_password_and_returns_an_unsatisfied_response_if_the_token_is_expired(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$this->set_password_token_timestamp("john@example.com", "1640995200");
$token = $this->get_password_token("john@example.com");
$response = $this->user_manager->reset_password("john@example.com", $token, "new-password");
self::assertFalse($response->satisfied);
self::assertFalse($this->user_manager->check_login("john@example.com", "new-password")[0]->satisfied);
}
public function test_reset_password_changes_the_password_if_the_verification_token_is_valid(): void
{
$this->user_manager->register_user("john@example.com", "password");
$this->set_password_token_timestamp(
"john@example.com",
time() - 60 * (UserManager::MINUTES_BETWEEN_PASSWORD_RESETS + 1)
);
$this->user_manager->send_password_reset("john@example.com");
$token = $this->get_password_token("john@example.com");
$response = $this->user_manager->reset_password("john@example.com", $token, "new-password");
self::assertTrue($response->satisfied);
self::assertNull($this->get_password_token("john@example.com"));
self::assertTrue($this->user_manager->check_login("john@example.com", "new-password")[0]->satisfied);
}
}