logger, $this->database->conn, $this->mailer); } public function setUp(): void { $this->mailer = $this->createMock(MailManager::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); } }