<?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");
}
}