<?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\controllers\AdminController;
use seekquarry\yioop\controllers\components\AccountaccessComponent;
/**
* AccountaccessComponentTest checks the parts of the accounts component
* that stand on their own. What is here is the stand-in the two password
* boxes are drawn holding, which has to be the same string in the
* request that draws the form and the one that sends it back, or a
* reader who changed nothing would have their password written over.
*
* @author Chris Pollett
*/
class AccountaccessComponentTest extends UnitTest
{
/**
* $component is the accounts component the cases call, built without
* running its constructor since the stand-in needs nothing from a
* controller.
* @var object
*/
public $component;
/**
* $stand_in reaches the private method the cases are about, since
* the value it gives is drawn into a form rather than handed to
* another part of the code.
* @var object
*/
public $stand_in;
/**
* setUp builds the component and opens the way to the method the
* cases read.
*/
public function setUp()
{
$reflection = new \ReflectionClass(AccountaccessComponent::class);
$this->component = $reflection->newInstanceWithoutConstructor();
$this->stand_in = $reflection->getMethod("passwordStandIn");
}
/**
* tearDown has nothing to do, since these cases leave nothing
* behind.
*/
public function tearDown()
{
}
/**
* standInIsTheSameEveryTimeTestCase checks the one thing the
* stand-in must do. The form is drawn in one request and sent back
* in another, so a value that differed between the two would read as
* a password the reader had typed, and their real one would be
* written over with it.
*/
public function standInIsTheSameEveryTimeTestCase()
{
$once = $this->stand_in->invoke($this->component);
$twice = $this->stand_in->invoke($this->component);
$this->assertEqual($once, $twice,
"the stand-in is the same string each time it is asked for");
$this->assertTrue(strpos($once, "\$2y\$12\$") === 0,
"and it is made the way a sign-in is checked, bcrypt at 12");
}
/**
* addingARoleLeavesTheListOpenTestCase checks that adding a role to
* an account leaves the list of that account's roles open. The box a
* role is typed into is only drawn while that list is open, so a
* reader who adds a role was looking at the list and should still be
* looking at it afterwards. What settles that is the field the
* redirect carries.
*/
public function addingARoleLeavesTheListOpenTestCase()
{
$reach = new \ReflectionClass(AccountaccessComponent::class);
$arm = $reach->getMethod("addUserRole");
foreach (["true", "false", null] as $sent) {
$_REQUEST = ['arg' => "edituser", 'user_name' => "nobody"];
if ($sent !== null) {
$_REQUEST['visible_roles'] = $sent;
}
try {
$arm->invoke($this->component, 0, "Admin", []);
} catch (\Throwable $caught) {
/* The account is not there, so this ends in a redirect
the case does not follow. What it leaves behind is
what is being read. */
}
$this->assertEqual('true', $_REQUEST['visible_roles'] ?? "gone",
"the list stays open whatever the form sent");
}
}
/**
* addingAGroupLeavesTheListOpenTestCase checks the same for the list
* of an account's groups, since the box a group is typed into is
* drawn under the same rule.
*/
public function addingAGroupLeavesTheListOpenTestCase()
{
$reach = new \ReflectionClass(AccountaccessComponent::class);
$arm = $reach->getMethod("addUserGroup");
$data = [];
foreach (["true", "false", null] as $sent) {
$_REQUEST = ['arg' => "edituser", 'user_name' => "nobody"];
if ($sent !== null) {
$_REQUEST['visible_groups'] = $sent;
}
try {
$arm->invokeArgs($this->component, [&$data, 0, "Public", []]);
} catch (\Throwable $caught) {
/* As above. */
}
$this->assertEqual('true', $_REQUEST['visible_groups'] ?? "gone",
"the list stays open whatever the form sent");
}
}
/**
* nameNoGroupAnswersToIsRefusedTestCase checks that adding a group
* by a name no group answers to writes nothing down. The lookup
* gives -1 for a name it does not know, and -1 read as a yes or no
* is a yes, so the arm used to write a membership row pointing at no
* group: the count of the account's groups rose while the list
* itself did not, and the page said the group had been added.
*/
public function nameNoGroupAnswersToIsRefusedTestCase()
{
$controller = new AdminController();
$component = new AccountaccessComponent($controller);
$group_model = $controller->model("group");
$user_id = $controller->model("signin")->getUserId("root");
/* A run against code that let this through leaves the row
behind, so it goes before the case runs. */
$group_model->deleteUserGroup($user_id, -1);
$reach = new \ReflectionClass(AccountaccessComponent::class);
$arm = $reach->getMethod("addUserGroup");
$data = [];
$_REQUEST = ['arg' => "edituser", 'user_name' => "root"];
try {
$arm->invokeArgs($component,
[&$data, $user_id, "no group answers to this", []]);
} catch (\Throwable $caught) {
/* The refusal ends in a redirect this case does not follow.
What it is about is what was written, not where the reader
is sent. */
}
$this->assertTrue(!$group_model->checkUserGroup($user_id, -1),
"no membership row is written pointing at no group at all");
}
/**
* nameNoRoleAnswersToIsRefusedTestCase checks the same for a role
* name, since the role lookup gives -1 in the same way.
*/
public function nameNoRoleAnswersToIsRefusedTestCase()
{
$controller = new AdminController();
$component = new AccountaccessComponent($controller);
$role_model = $controller->model("role");
$user_id = $controller->model("signin")->getUserId("root");
/* As above: a row left by an earlier run goes first. */
$role_model->deleteUserRole($user_id, -1);
$reach = new \ReflectionClass(AccountaccessComponent::class);
$arm = $reach->getMethod("addUserRole");
$_REQUEST = ['arg' => "edituser", 'user_name' => "root"];
try {
$arm->invoke($component, $user_id, "no role answers to this", []);
} catch (\Throwable $caught) {
/* As above. */
}
$this->assertTrue(!$role_model->checkUserRole($user_id, -1),
"no role row is written pointing at no role at all");
}
/**
* standInFitsThePasswordBoxTestCase checks that the value the
* form is drawn with survives being sent back. A box refuses more
* characters than it allows, so a stand-in longer than that would
* come back cut short and read as something the reader typed.
*/
public function standInFitsThePasswordBoxTestCase()
{
$held = $this->stand_in->invoke($this->component);
$this->assertTrue(strlen($held) <= C\LONG_NAME_LEN,
"the stand-in fits the length a password box allows");
}
}