Write tests for UserManager

Also fixes a bug with checking token validity dates, reorders some methods for clarity, and ensures that the login menu is not shown in the password reset screen.
This commit is contained in:
Florine W. Dekker 2022-11-28 17:41:16 +01:00
parent 6fb88c5454
commit 1c62c73055
Signed by: FWDekker
GPG Key ID: D3DCFAA8A4560BE0
10 changed files with 735 additions and 128 deletions

View File

@ -1,6 +1,6 @@
{
"name": "death-notifier",
"version": "0.14.10", "_comment_version": "Also update version in `composer.json`!",
"version": "0.14.11", "_comment_version": "Also update version in `composer.json`!",
"description": "Get notified when a famous person dies.",
"author": "Florine W. Dekker",
"browser": "dist/bundle.js",

View File

@ -56,7 +56,7 @@
<a href="./">Click here to return to the main page</a>
</p>
<div id="welcome-part" class="grid">
<div id="welcome-part" class="grid hidden">
<article>
<header>
<hgroup>

View File

@ -117,8 +117,7 @@ class UserManager
*/
private function minutes_until_interval_elapsed(string $timestamp, int $interval): int
{
$minutes_since_timestamp = (new DateTime("@$timestamp"))->diff(new DateTime(), absolute: true)->i;
return $interval - $minutes_since_timestamp;
return $interval - ((time() - intval($timestamp)) / 60);
}
@ -148,20 +147,6 @@ class UserManager
});
}
/**
* Deletes the user with the given UUID.
*
* @param string $uuid the UUID of the user to delete
* @return Response a satisfied `Response` with payload `null`
*/
public function delete_user(string $uuid): Response
{
$stmt = $this->conn->prepare("DELETE FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
return Response::satisfied();
}
/**
* Validates a login attempt with the given email address and password.
*
@ -175,15 +160,16 @@ class UserManager
$stmt = $this->conn->prepare("SELECT uuid, password FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$user = $stmt->fetch();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// TODO: Expose whether account exists, it's exposed in forgot password anyway
return $user === false || !password_verify($password, $user["password"])
? [Response::unsatisfied("Incorrect combination of email and password.", "password"), null]
: [Response::satisfied(), $user["uuid"]];
}
/**
* Returns `true` if and only if a user with the given UUID exists.
* Returns `true` if and only if a user with the given UUID exists.
*
* @param string $uuid the UUID of the user to check
* @return bool `true` if and only if a user with the given UUID exists
@ -218,6 +204,20 @@ class UserManager
: Response::satisfied($user);
}
/**
* Deletes the user with the given UUID.
*
* @param string $uuid the UUID of the user to delete
* @return Response a satisfied `Response` with payload `null`
*/
public function delete_user(string $uuid): Response
{
$stmt = $this->conn->prepare("DELETE FROM users WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
return Response::satisfied();
}
/**
* Updates the indicated user's email address.
@ -229,6 +229,7 @@ class UserManager
*/
public function set_email(string $uuid, string $email): Response
{
// TODO: Also send message to old email address
return Database::transaction($this->conn, function () use ($uuid, $email) {
$stmt = $this->conn->prepare("SELECT EXISTS(SELECT 1 FROM users WHERE uuid=:uuid AND email=:email);");
$stmt->bindValue(":uuid", $uuid);
@ -255,46 +256,6 @@ class UserManager
});
}
/**
* Resends the email verification email.
*
* @param string $uuid the UUID of the user to resend the email for
* @return Response a satisfied `Response` with payload `null` if the email was sent, or an unsatisfied `Response`
* otherwise
*/
public function resend_verify_email(string $uuid): Response
{
return Database::transaction($this->conn, function () use ($uuid) {
$stmt = $this->conn->prepare("SELECT email, email_verification_token, email_verification_token_timestamp
FROM users
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user["email_verification_token"] === null)
return Response::unsatisfied("Your email address is already verified.");
$minutes_left = $this->minutes_until_interval_elapsed(
$user["email_verification_token_timestamp"],
self::MINUTES_BETWEEN_VERIFICATION_EMAILS
);
if ($minutes_left > 0) {
return Response::unsatisfied(
"A verification email was sent recently. " .
"Please wait $minutes_left more minute(s) before requesting a new email."
);
}
$stmt = $this->conn->prepare("UPDATE users
SET email_verification_token_timestamp=unixepoch()
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
return $this->mailer->queue_email(new VerifyEmailEmail($user["email"], $user["email_verification_token"]));
});
}
/**
* Verifies an email address with a token.
*
@ -312,19 +273,21 @@ class UserManager
$stmt->bindValue(":email", $email);
$stmt->bindValue(":token", $token);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (sizeof($results) === 0)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false)
return Response::unsatisfied(
"Failed to verify email address. " .
"Maybe you already verified your email address?"
);
$minutes_remaining = $this->minutes_until_interval_elapsed(
$results[0]["email_verification_token_timestamp"],
$user["email_verification_token_timestamp"],
self::MINUTES_VALID_VERIFICATION
);
if ($minutes_remaining < 0)
return Response::unsatisfied("This email verification link has expired.");
return Response::unsatisfied(
"This email verification link has expired. Log in and request a new verification email."
);
$stmt = $this->conn->prepare("UPDATE users SET email_verification_token=null WHERE email=:email;");
$stmt->bindValue(":email", $email);
@ -334,6 +297,49 @@ class UserManager
});
}
/**
* Resends the email verification email.
*
* @param string $uuid the UUID of the user to resend the email for
* @return Response a satisfied `Response` with payload `null` if the email was sent, or an unsatisfied `Response`
* otherwise
*/
public function resend_verify_email(string $uuid): Response
{
return Database::transaction($this->conn, function () use ($uuid) {
$stmt = $this->conn->prepare("SELECT email, email_verification_token, email_verification_token_timestamp
FROM users
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false || $user["email_verification_token"] === null)
return Response::unsatisfied("Your email address is already verified.");
$minutes_left = $this->minutes_until_interval_elapsed(
$user["email_verification_token_timestamp"],
self::MINUTES_BETWEEN_VERIFICATION_EMAILS
);
if ($minutes_left > 0) {
return Response::unsatisfied(
"A verification email was sent recently. " .
"Please wait $minutes_left more minute(s) before requesting a new email."
);
}
$stmt = $this->conn->prepare("UPDATE users
SET email_verification_token=lower(hex(randomblob(16))),
email_verification_token_timestamp=unixepoch()
WHERE uuid=:uuid
RETURNING email_verification_token;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$verification_token = $stmt->fetchAll(PDO::FETCH_ASSOC)[0]["email_verification_token"];
return $this->mailer->queue_email(new VerifyEmailEmail($user["email"], $verification_token));
});
}
/**
* Toggles whether the user receives death notifications.
*
@ -349,8 +355,8 @@ class UserManager
WHERE uuid=:uuid;");
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$user = $stmt->fetchAll(PDO::FETCH_ASSOC)[0];
if ($user["email_verification_token"] !== null)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false || $user["email_verification_token"] !== null)
return Response::unsatisfied("Please verify your email address before enabling notifications.");
$stmt = $this->conn->prepare("UPDATE users SET email_notifications_enabled=:enabled WHERE uuid=:uuid;");
@ -379,7 +385,7 @@ class UserManager
$stmt->bindValue(":uuid", $uuid);
$stmt->execute();
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!password_verify($password_old, $user["password"]))
if ($user === false || !password_verify($password_old, $user["password"]))
return Response::unsatisfied("Incorrect old password.", "password_old");
$stmt = $this->conn->prepare("UPDATE users
@ -407,12 +413,12 @@ class UserManager
$stmt = $this->conn->prepare("SELECT password_reset_token_timestamp FROM users WHERE email=:email;");
$stmt->bindValue(":email", $email);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (sizeof($results) === 0)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false)
return Response::unsatisfied("No account with that email address has been registered.");
$minutes_left = $this->minutes_until_interval_elapsed(
$results[0]["password_reset_token_timestamp"],
$user["password_reset_token_timestamp"],
self::MINUTES_BETWEEN_PASSWORD_RESETS
);
if ($minutes_left > 0) {
@ -452,14 +458,14 @@ class UserManager
$stmt->bindValue(":email", $email);
$stmt->bindValue(":token", $token);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (sizeof($results) === 0)
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user === false)
return Response::unsatisfied(
"This password reset link is invalid. Maybe you already reset your password?"
);
$minutes_remaining = $this->minutes_until_interval_elapsed(
$results[0]["password_reset_token_timestamp"],
$user["password_reset_token_timestamp"],
self::MINUTES_VALID_PASSWORD_RESET
);
if ($minutes_remaining < 0)

View File

@ -35,7 +35,7 @@ abstract class DatabaseTestCase extends TestCase
/**
* @return Mailer the `Mailer` to install the database schema of
*/
function getMailer(): Mailer
function get_mailer(): Mailer
{
return $this->createStub(Mailer::class);
}
@ -43,7 +43,7 @@ abstract class DatabaseTestCase extends TestCase
/**
* @return TrackingManager the `TrackingManager` to install the database schema of
*/
function getTrackingManager(): TrackingManager
function get_tracking_manager(): TrackingManager
{
return $this->createStub(TrackingManager::class);
}
@ -51,7 +51,7 @@ abstract class DatabaseTestCase extends TestCase
/**
* @return UserManager the `UserManager` to install the database schema of
*/
function getUserManager(): UserManager
function get_user_manager(): UserManager
{
return $this->createStub(UserManager::class);
}
@ -93,6 +93,6 @@ abstract class DatabaseTestCase extends TestCase
$this->logger = new Logger("DatabaseTestCase");
$this->database = new Database($this->logger, $db_tmp_file);
$this->database->auto_install($this->getMailer(), $this->getUserManager(), $this->getTrackingManager());
$this->database->auto_install($this->get_mailer(), $this->get_user_manager(), $this->get_tracking_manager());
}
}

View File

@ -2,8 +2,13 @@
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;
@ -16,39 +21,119 @@ class UserManagerTest extends DatabaseTestCase
private Mailer&MockObject $mailer;
public function getUserManager(): UserManager
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->getUserManager();
$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->mailer->method("queue_email")->willReturn(Response::satisfied());
$this->user_manager->register_user("test@test.test", "password");
$this->user_manager->register_user("john@example.com", "password");
$response = $this->user_manager->register_user("test@test.test", "password");
$response = $this->user_manager->register_user("john@example.com", "password");
self::assertFalse($response->satisfied);
}
public function test_register_user_returns_a_satisfied_response_if_the_email_is_unused(): void
public function test_register_user_registers_the_user_and_returns_a_satisfied_response_if_the_email_is_unused(): void
{
$this->mailer->method("queue_email")->willReturn(Response::satisfied());
$response = $this->user_manager->register_user("test@test.test", "password");
$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
@ -56,11 +141,527 @@ class UserManagerTest extends DatabaseTestCase
$this->mailer
->expects($this->once())
->method("queue_email")
->with($this->callback(fn($email) => $email instanceof RegisterEmail && $email->recipient === "test@test.test"))
->willReturn(Response::satisfied());
->with($this->callback(
fn($email) => $email instanceof RegisterEmail && $email->recipient === "john@example.com")
);
$response = $this->user_manager->register_user("test@test.test", "password");
$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);
}
}

View File

@ -30,7 +30,7 @@ class IsEmailRuleTest extends RuleTest
$is_valid = $rule->check(["email" => "example@example.com"], "email");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_response_message_if_email_is_invalid(): void
@ -39,7 +39,7 @@ class IsEmailRuleTest extends RuleTest
$is_valid = $rule->check(["email" => "example.com"], "email");
$this->assertNotNull($is_valid);
$this->assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
self::assertNotNull($is_valid);
self::assertEquals("Enter a valid email address.", $is_valid->payload["message"]);
}
}

View File

@ -30,7 +30,7 @@ class IsNotBlankRuleTest extends RuleTest
$is_valid = $rule->check(["input" => "not-blank"], "input");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_response_message_if_input_is_the_empty_string(): void
@ -39,8 +39,8 @@ class IsNotBlankRuleTest extends RuleTest
$is_valid = $rule->check(["input" => ""], "input");
$this->assertNotNull($is_valid);
$this->assertEquals("Use at least one character.", $is_valid->payload["message"]);
self::assertNotNull($is_valid);
self::assertEquals("Use at least one character.", $is_valid->payload["message"]);
}
public function test_returns_response_message_if_input_contains_whitespace_only(): void
@ -49,7 +49,7 @@ class IsNotBlankRuleTest extends RuleTest
$is_valid = $rule->check(["input" => " "], "input");
$this->assertNotNull($is_valid);
$this->assertEquals("Use at least one character.", $is_valid->payload["message"]);
self::assertNotNull($is_valid);
self::assertEquals("Use at least one character.", $is_valid->payload["message"]);
}
}

View File

@ -30,7 +30,7 @@ class LengthRuleTest extends RuleTest
$is_valid = $rule->check(["input" => "a"], "input");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_null_if_input_is_exactly_maximum_length(): void
@ -39,7 +39,7 @@ class LengthRuleTest extends RuleTest
$is_valid = $rule->check(["input" => "123"], "input");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_null_if_input_is_strictly_inside_range(): void
@ -48,7 +48,7 @@ class LengthRuleTest extends RuleTest
$is_valid = $rule->check(["input" => "12"], "input");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_not_null_if_input_is_strictly_below_minimum(): void
@ -57,7 +57,7 @@ class LengthRuleTest extends RuleTest
$is_valid = $rule->check(["input" => ""], "input");
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
public function test_returns_not_null_if_input_is_strictly_above_maximum(): void
@ -66,6 +66,6 @@ class LengthRuleTest extends RuleTest
$is_valid = $rule->check(["input" => "1234"], "input");
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
}

View File

@ -37,7 +37,7 @@ abstract class RuleTest extends TestCase
$is_valid = $rule->check(["input" => $this->get_valid_input()], "input");
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
public function test_returns_not_null_if_input_is_invalid(): void
@ -46,7 +46,7 @@ abstract class RuleTest extends TestCase
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
public function test_returns_not_null_if_input_is_not_set(): void
@ -55,7 +55,7 @@ abstract class RuleTest extends TestCase
$is_valid = $rule->check(["input" => $this->get_valid_input()], "does_not_exist");
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
public function test_returns_unsatisfied_payload_if_input_is_invalid(): void
@ -64,8 +64,8 @@ abstract class RuleTest extends TestCase
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
$this->assertNotNull($is_valid);
$this->assertFalse($is_valid->satisfied);
self::assertNotNull($is_valid);
self::assertFalse($is_valid->satisfied);
}
public function test_returns_payload_with_overridden_message_if_input_is_invalid(): void
@ -75,8 +75,8 @@ abstract class RuleTest extends TestCase
$is_valid = $rule->check(["input" => $this->get_invalid_input()], "input");
$this->assertNotNull($is_valid);
$this->assertEquals($override, $is_valid->payload["message"]);
self::assertNotNull($is_valid);
self::assertEquals($override, $is_valid->payload["message"]);
}
public function test_returns_payload_with_key_of_invalid_input_if_input_is_invalid(): void
@ -92,7 +92,7 @@ abstract class RuleTest extends TestCase
"invalid"
);
$this->assertNotNull($is_valid);
$this->assertEquals("invalid", $is_valid->payload["target"]);
self::assertNotNull($is_valid);
self::assertEquals("invalid", $is_valid->payload["target"]);
}
}

View File

@ -17,7 +17,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
function test_validate_inputs_returns_null_if_all_inputs_are_valid(): void
@ -30,7 +30,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
function test_validate_inputs_considers_empty_rule_set_to_always_be_valid(): void
@ -40,7 +40,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
function test_validate_inputs_returns_not_null_if_at_least_one_rule_is_violated(): void
@ -53,7 +53,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
function test_validate_inputs_returns_first_violation_if_multiple_inputs_are_invalid(): void
@ -66,8 +66,8 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNotNull($is_valid);
$this->assertEquals("among", $is_valid->payload["target"]);
self::assertNotNull($is_valid);
self::assertEquals("among", $is_valid->payload["target"]);
}
function test_validate_inputs_returns_first_violation_if_one_input_is_invalid_in_multiple_ways(): void
@ -80,9 +80,9 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_inputs($inputs, $rule_sets);
$this->assertNotNull($is_valid);
$this->assertEquals("among", $is_valid->payload["target"]);
$this->assertEquals("Field 'among' required.", $is_valid->payload["message"]);
self::assertNotNull($is_valid);
self::assertEquals("among", $is_valid->payload["target"]);
self::assertEquals("Field 'among' required.", $is_valid->payload["message"]);
}
@ -92,7 +92,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_logged_in($session);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
function test_validate_logged_in_returns_not_null_if_uuid_is_not_set(): void
@ -101,7 +101,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_logged_in($session);
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
@ -111,7 +111,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_logged_out($session);
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
function test_validate_logged_out_returns_null_if_uuid_is_not_set(): void
@ -120,7 +120,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_logged_out($session);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
@ -131,7 +131,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_token($array, $token);
$this->assertNull($is_valid);
self::assertNull($is_valid);
}
function test_validate_token_returns_not_null_if_token_is_not_set(): void
@ -141,7 +141,7 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_token($array, $token);
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
function test_validate_token_returns_not_null_if_token_does_not_equal_input(): void
@ -151,6 +151,6 @@ class ValidatorTest extends TestCase
$is_valid = Validator::validate_token($array, "woof");
$this->assertNotNull($is_valid);
self::assertNotNull($is_valid);
}
}