<?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\controllers\RegisterController;
use seekquarry\yioop\library\UnitTest;
/**
* Checks the proof-of-work captcha that now guards account creation and
* recovery. The image captcha has been retired, so every sign-up and
* recovery form relies on the browser solving a small hash puzzle; this
* test pins down that the server accepts a correct solution and rejects
* a wrong one.
*
* @author Chris Pollett
*/
class RegisterControllerTest extends UnitTest
{
/**
* The controller whose proof-of-work check is under test.
* @var RegisterController
*/
public $controller;
/**
* Builds a controller for each test case without running its
* constructor, since the proof-of-work check only reads the session
* and request and needs none of the controller's normal set-up.
*/
public function setUp()
{
$reflection = new \ReflectionClass(RegisterController::class);
$this->controller = $reflection->newInstanceWithoutConstructor();
}
/**
* Nothing to take down between test cases.
*/
public function tearDown()
{
}
/**
* Sets up a proof-of-work challenge in the session at difficulty two
* (the answer's hash must start with two zeros) and returns the text
* prefix the browser hashes its guesses against.
*
* @return string the "random:request_time:" prefix a guess is added to
*/
private function startChallenge()
{
$_SESSION = [];
$_SESSION["random_string"] = "test_random_string";
$_SESSION["request_time"] = time();
$_SESSION["level"] = 2;
return $_SESSION["random_string"] . ':' .
$_SESSION["request_time"] . ':';
}
/**
* A guess whose hash meets the difficulty is accepted as a human.
*/
public function acceptsValidProofOfWorkTestCase()
{
$prefix = $this->startChallenge();
$nonce = 0;
while (substr(sha1($prefix . $nonce), 0, 2) !== "00") {
$nonce++;
}
$_REQUEST['nonce_for_string'] = (string)$nonce;
$this->assertTrue($this->controller->validateHashCode(),
"a guess meeting the difficulty passes the proof-of-work");
}
/**
* A guess whose hash misses the difficulty is rejected.
*/
public function rejectsBadProofOfWorkTestCase()
{
$prefix = $this->startChallenge();
$bad = 0;
while (substr(sha1($prefix . $bad), 0, 2) === "00") {
$bad++;
}
$_REQUEST['nonce_for_string'] = (string)$bad;
$this->assertFalse($this->controller->validateHashCode(),
"a guess missing the difficulty fails the proof-of-work");
}
/**
* The shared proof-of-work check passes a hash with enough zeroes and
* fails one without, given the same inputs directly.
*/
public function meetsProofOfWorkChecksLeadingZeroesTestCase()
{
$random_string = "abc";
$request_time = "123";
$good = 0;
while (substr(sha1($random_string . ':' . $request_time . ':' .
$good), 0, 2) !== "00") {
$good++;
}
$this->assertTrue($this->controller->meetsProofOfWork($random_string,
$request_time, (string)$good, 2),
"a nonce producing two leading zeroes passes");
$bad = 0;
while (substr(sha1($random_string . ':' . $request_time . ':' .
$bad), 0, 2) === "00") {
$bad++;
}
$this->assertFalse($this->controller->meetsProofOfWork($random_string,
$request_time, (string)$bad, 2),
"a nonce missing two leading zeroes fails");
}
}