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