<?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\SearchController;
use seekquarry\yioop\library\UnitTest;

/**
 * Unit tests for the check that decides whether a search came from a
 * person or from a program crawling the site. A search the check turns
 * away is never run, so a browser wrongly caught by it cannot use the
 * site at all; the user agents below are the real ones this has to get
 * right.
 *
 * @author Chris Pollett
 */
class SearchControllerTest extends UnitTest
{
    /**
     * The controller whose check for bots is under test.
     * @var SearchController
     */
    public $controller;
    /**
     * Builds the controller without its constructor, which would want a
     * web site and a database that this check has nothing to do with.
     */
    public function setUp()
    {
        $reflection = new \ReflectionClass(SearchController::class);
        $this->controller = $reflection->newInstanceWithoutConstructor();
    }
    /**
     * Nothing to take down between test cases.
     */
    public function tearDown()
    {
    }
    /**
     * The user agents real browsers send are let through. Each of these
     * was sent by a browser people search with, and every one of them
     * names Apple, because the engine Chrome and Edge are built on
     * carries that name wherever it runs.
     */
    public function browsersAreNotBotsTestCase()
    {
        $browsers = [
            "Chrome on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64;" .
                " x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126" .
                ".0.0.0 Safari/537.36",
            "Chrome on macOS" => "Mozilla/5.0 (Macintosh; Intel Mac OS X" .
                " 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome" .
                "/126.0.0.0 Safari/537.36",
            "Chrome on Linux" => "Mozilla/5.0 (X11; Linux x86_64) Apple" .
                "WebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 " .
                "Safari/537.36",
            "Chrome on Android" => "Mozilla/5.0 (Linux; Android 10; K) " .
                "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0" .
                " Mobile Safari/537.36",
            "Edge on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64; " .
                "x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126." .
                "0.0.0 Safari/537.36 Edge/126.0.0.0",
            "Safari on macOS" => "Mozilla/5.0 (Macintosh; Intel Mac OS X" .
                " 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) " .
                "Version/17.0 Safari/605.1.15",
            "Safari on iPhone" => "Mozilla/5.0 (iPhone; CPU iPhone OS " .
                "17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like " .
                "Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
            "Firefox on Windows" => "Mozilla/5.0 (Windows NT 10.0; Win64" .
                "; x64; rv:126.0) Gecko/20100101 Firefox/126.0",
            "Firefox on Linux" => "Mozilla/5.0 (X11; Linux x86_64; rv:" .
                "126.0) Gecko/20100101 Firefox/126.0",
            "Lynx" => "Lynx/2.9.0dev.10 libwww-FM/2.14 SSL-MM/1.4.1 " .
                "OpenSSL/3.0.11"
        ];
        foreach ($browsers as $who => $user_agent) {
            $this->assertTrue(
                !$this->controller->isBotUserAgent($user_agent),
                "$who is a person searching, not a bot");
        }
    }
    /**
     * A program that names itself a crawler is taken at its word, wherever
     * in its user agent it says so.
     */
    public function namedBotsAreBotsTestCase()
    {
        $bots = [
            "Googlebot" => "Mozilla/5.0 (compatible; Googlebot/2.1; +" .
                "http://www.google.com/bot.html)",
            "Bingbot" => "Mozilla/5.0 (compatible; bingbot/2.0; +http://" .
                "www.bing.com/bingbot.htm)",
            "Baidu" => "Mozilla/5.0 (compatible; Baiduspider/2.0; +http:" .
                "//www.baidu.com/search/spider.html)",
            "Yandex" => "Mozilla/5.0 (compatible; YandexBot/3.0; +http://" .
                "yandex.com/bots)",
            "Ahrefs" => "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://" .
                "ahrefs.com/robot/)",
            "an unnamed crawler" => "Some Program (crawler)",
            "an unnamed spider" => "Some Program (spider)"
        ];
        foreach ($bots as $who => $user_agent) {
            $this->assertTrue($this->controller->isBotUserAgent($user_agent),
                "$who says it crawls and is treated as a bot");
        }
    }
    /**
     * A user agent claiming to be every browser at once, or to be running
     * on every system at once, is not any of them.
     */
    public function claimingEverythingIsABotTestCase()
    {
        $this->assertTrue($this->controller->isBotUserAgent(
            "Mozilla/5.0 Chrome/1.0 Safari/1.0 Edge/1.0 Firefox/1.0"),
            "claiming four browsers at once is a program listing them");
        $this->assertTrue($this->controller->isBotUserAgent(
            "Mozilla/5.0 (Windows NT 10.0; Linux x86_64; Mac OS X) " .
            "AppleWebKit/537.36"),
            "claiming three systems at once is not one machine");
        $this->assertTrue(!$this->controller->isBotUserAgent(
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" .
            " (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edge/" .
            "126.0.0.0"),
            "Edge claims three browsers and two systems and is a browser");
    }
    /**
     * What a request asks for says nothing about who is asking. The site's
     * own pages ask for further results in a form of their own, and
     * reading that as evidence of a bot had the site turning away its own
     * readers mid-page.
     */
    public function whatIsAskedForIsNotWhoIsAskingTestCase()
    {
        $chrome = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Apple" .
            "WebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/" .
            "537.36";
        $_REQUEST["f"] = "api";
        $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.1";
        $this->assertTrue(!$this->controller->isBotUserAgent($chrome),
            "a browser reading further results is still a browser");
        $_SERVER["SERVER_PROTOCOL"] = "HTTP/1.0";
        $this->assertTrue(!$this->controller->isBotUserAgent($chrome),
            "the protocol a request arrives on says nothing about it");
        unset($_REQUEST["f"]);
        unset($_SERVER["SERVER_PROTOCOL"]);
    }
    /**
     * A request that says nothing about itself is let through. An empty
     * user agent is rude rather than damning, and turning it away would
     * catch anyone whose browser was told to send nothing.
     */
    public function sayingNothingIsNotABotTestCase()
    {
        $this->assertTrue(!$this->controller->isBotUserAgent(""),
            "a request saying nothing about itself is let through");
    }
    /**
     * A limit a person could page to is used as it was sent, so clamping
     * the large ones costs an ordinary search nothing. Someone reading
     * the seventy-eighth page of results is still a reader.
     */
    public function anOrdinaryLimitIsLeftAloneTestCase()
    {
        $this->assertEqual(0, $this->controller->limitWithinReach(0),
            "a search with no limit asks for none");
        $this->assertEqual(770, $this->controller->limitWithinReach(770),
            "a limit a reader could page to is kept as it is");
        $this->assertEqual(C\MAX_QUERY_LIMIT,
            $this->controller->limitWithinReach(C\MAX_QUERY_LIMIT),
            "and so is one exactly at the largest allowed");
    }
    /**
     * A limit far past anything a person pages to is clamped. The number
     * of results the index is asked for grows with this offset, as does
     * the number of summaries loaded afterwards, so a crawler sending a
     * huge offset can exhaust the server's memory on one request.
     */
    public function limitPastWhatAReaderWouldAskForIsHeldTestCase()
    {
        $this->assertEqual(C\MAX_QUERY_LIMIT,
            $this->controller->limitWithinReach(10000),
            "a limit of ten thousand is clamped to the largest allowed");
        $this->assertEqual(C\MAX_QUERY_LIMIT,
            $this->controller->limitWithinReach(1000000),
            "and so is one of a million");
        $group = C\p("MIN_RESULTS_TO_GROUP");
        $held = $this->controller->limitWithinReach(1000000);
        $gathered = ceil(($held + C\NUM_RESULTS_PER_PAGE) / $group) *
            $group;
        $this->assertTrue($gathered <= C\MAX_QUERY_LIMIT + $group,
            "the results gathered stay within the largest allowed plus " .
            "one group, and came to $gathered");
    }
    /**
     * A negative limit is read as none rather than reaching backwards
     * through the result list.
     */
    public function limitBelowZeroAsksForNoneTestCase()
    {
        $this->assertEqual(0, $this->controller->limitWithinReach(-5),
            "a limit below zero asks for none");
        $this->assertEqual(0,
            $this->controller->limitWithinReach(-1000000),
            "however far below zero it is");
    }
}
