/ tests / UserModelTest.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\UnitTest;
use seekquarry\yioop\models\UserModel;
use seekquarry\yioop\models\ProfileModel;
use seekquarry\yioop\models\datasources\Sqlite3Manager;

/**
 * Tests the look-up of an account by its email address, used to stop the
 * site's bot address from being pointed at an address some member
 * already uses. Each test runs against a fresh throwaway sqlite file
 * holding a small USERS table.
 *
 * @author Chris Pollett
 */
class UserModelTest extends UnitTest
{
    /**
     * Filesystem path for the throwaway test DB
     * @var string
     */
    public $db_path;
    /**
     * Model under test
     * @var UserModel
     */
    public $model;
    /**
     * Sets up a small USERS table holding one account and hands it to a
     * fresh model instance.
     */
    public function setUp()
    {
        $tag = getmypid() . "_" . random_int(1000, 9999);
        $this->db_path = C\WORK_DIRECTORY . "/temp/" .
            "user_model_email_test_$tag.db";
        $db = new Sqlite3Manager();
        $db->connect("", "", "", $this->db_path);
        /* Build the table from ProfileModel's definition, the same one the
           live database uses, so this test tracks any schema change rather
           than a hand-copied CREATE TABLE that could drift. */
        $dbinfo = ["DBMS" => "Sqlite3", "DB_HOST" => ""];
        $profile = new ProfileModel(C\DB_NAME, false);
        $profile->initializeSql($db, $dbinfo);
        $db->execute($profile->create_statements['USERS']);
        $db->execute(
            "INSERT INTO USERS (USER_ID, USER_NAME, EMAIL) VALUES (?, ?, ?)",
            [11, "alice", "Alice@Example.com"]);
        $this->model = new UserModel(C\DB_NAME, false);
        $this->model->db = $db;
    }
    /**
     * Disconnects and removes the throwaway DB.
     */
    public function tearDown()
    {
        if ($this->model && $this->model->db) {
            $this->model->db->disconnect();
            unset(Sqlite3Manager::$active_connections[
                $this->model->db->connect_string]);
        }
        if ($this->db_path && file_exists($this->db_path)) {
            unlink($this->db_path);
        }
    }
    /**
     * An account is found by its email address even when the lookup uses
     * different letter case from how the address was stored.
     */
    public function foundCaseInsensitiveTestCase()
    {
        $row = $this->model->getUserByEmail("alice@example.com");
        $this->assertTrue(is_array($row) && isset($row['USER_ID']),
            "getUserByEmail finds the account regardless of case");
        $this->assertEqual(11, intval($row['USER_ID']),
            "the matching account is returned");
    }
    /**
     * An address no account uses returns false.
     */
    public function notFoundTestCase()
    {
        $row = $this->model->getUserByEmail("nobody@example.com");
        $this->assertFalse(!empty($row),
            "an unused address matches no account");
    }
    /**
     * A member's recovery question is stored as plain text while their
     * answer is kept only as a hash; the correct answer then verifies and
     * a wrong one does not.
     */
    public function recoveryQuestionRoundTripTestCase()
    {
        $this->model->setRecoveryQuestion(11, "First pet's name?", "Rex");
        $result = $this->model->db->execute(
            "SELECT * FROM USERS WHERE USER_ID = ?", [11]);
        $user = $this->model->db->fetchArray($result);
        $this->assertEqual("First pet's name?",
            $user['RECOVERY_QUESTION'],
            "the question is stored back as plain text");
        $this->assertNotEqual("Rex", $user['RECOVERY_ANSWER'],
            "the answer is not stored in the clear");
        $this->assertTrue(
            $this->model->checkRecoveryAnswer($user, "Rex"),
            "the correct answer verifies");
        $this->assertFalse(
            $this->model->checkRecoveryAnswer($user, "rex"),
            "a wrong answer does not verify");
    }
    /**
     * An account that has set no recovery answer rejects every answer.
     */
    public function emptyRecoveryAnswerTestCase()
    {
        $result = $this->model->db->execute(
            "SELECT * FROM USERS WHERE USER_ID = ?", [11]);
        $user = $this->model->db->fetchArray($result);
        $this->assertFalse(
            $this->model->checkRecoveryAnswer($user, "anything"),
            "no stored answer means no answer verifies");
    }
    /**
     * Rewording the question with a null answer leaves the previously
     * stored answer in place so it still verifies.
     */
    public function keepAnswerOnQuestionEditTestCase()
    {
        $this->model->setRecoveryQuestion(11, "First pet's name?", "Rex");
        $this->model->setRecoveryQuestion(11, "Name of first pet?", null);
        $result = $this->model->db->execute(
            "SELECT * FROM USERS WHERE USER_ID = ?", [11]);
        $user = $this->model->db->fetchArray($result);
        $this->assertEqual("Name of first pet?",
            $user['RECOVERY_QUESTION'],
            "the question is updated");
        $this->assertTrue(
            $this->model->checkRecoveryAnswer($user, "Rex"),
            "the earlier answer still verifies after a question-only edit");
    }
}
X