<?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\atto\MailSite;
use seekquarry\yioop\library\mail\MailSiteFactory;
use seekquarry\yioop\library\mail\YioopUserAuthenticator;
use seekquarry\yioop\library\UnitTest;

/**
 * Tests how the reserved bot account is authenticated when the site
 * sends its own mail. Just before logging in as the bot the site's
 * mail sender writes a random secret to a shared file, and the
 * authenticator checks the supplied password against that file. These
 * cases cover that a correct secret is accepted and then consumed (the
 * file is emptied so it cannot be replayed), that a wrong secret is
 * rejected and leaves the pending secret untouched, that an empty or
 * missing file rejects any password, and that the bot is recognized by
 * its local part with or without a domain.
 *
 * @author Chris Pollett
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
class BotSecretAuthTest extends UnitTest
{
    /**
     * Authenticator under test.
     * @var YioopUserAuthenticator
     */
    public $authenticator;
    /**
     * Path to the shared bot secret file.
     * @var string
     */
    public $path;
    /**
     * Any secret file content present before a case, restored after
     * each case so a real file in the work directory is not disturbed.
     * @var string|null
     */
    public $saved;
    /**
     * Saves any existing secret file, makes sure its directory is
     * present, and builds a fresh authenticator for each case.
     */
    public function setUp()
    {
        /* The class under test extends Authenticator, which is
           defined inside MailSite.php rather than in its own file,
           so make sure that file is loaded before constructing the
           authenticator or the class would fail to load. */
        class_exists(MailSite::class);
        $this->path = MailSiteFactory::botSecretPath();
        $this->saved = is_file($this->path) ?
            file_get_contents($this->path) : null;
        $directory = dirname($this->path);
        if (!is_dir($directory)) {
            mkdir($directory, 0700, true);
        }
        $this->authenticator = new YioopUserAuthenticator();
    }
    /**
     * Restores the secret file to whatever was there before the case.
     */
    public function tearDown()
    {
        if ($this->saved !== null) {
            file_put_contents($this->path, $this->saved);
        } else if (is_file($this->path)) {
            unlink($this->path);
        }
    }
    /**
     * Writes a secret to the shared file the way the mail sender does.
     * @param string $secret the secret to place in the file
     */
    public function writeSecret($secret)
    {
        file_put_contents($this->path, $secret);
    }
    /**
     * A password equal to the file's secret authenticates the bot.
     */
    public function correctSecretAuthenticatesTestCase()
    {
        $secret = bin2hex(random_bytes(32));
        $this->writeSecret($secret);
        $this->assertTrue(
            $this->authenticator->verifyPassword("bot", $secret),
            "correct bot secret authenticates");
    }
    /**
     * A successful check empties the secret file so the secret can be
     * used only once.
     */
    public function matchEmptiesSecretFileTestCase()
    {
        $secret = bin2hex(random_bytes(32));
        $this->writeSecret($secret);
        $this->authenticator->verifyPassword("bot", $secret);
        $this->assertEqual(file_get_contents($this->path), "",
            "secret file emptied after a successful check");
    }
    /**
     * Once a secret has been used it cannot be presented again.
     */
    public function usedSecretCannotBeReplayedTestCase()
    {
        $secret = bin2hex(random_bytes(32));
        $this->writeSecret($secret);
        $this->authenticator->verifyPassword("bot", $secret);
        $this->assertFalse(
            $this->authenticator->verifyPassword("bot", $secret),
            "a consumed secret cannot be replayed");
    }
    /**
     * A password that does not equal the secret is rejected.
     */
    public function wrongSecretIsRejectedTestCase()
    {
        $secret = bin2hex(random_bytes(32));
        $this->writeSecret($secret);
        $this->assertFalse(
            $this->authenticator->verifyPassword("bot", "wrong" . $secret),
            "wrong bot secret rejected");
    }
    /**
     * A wrong password leaves the pending secret in place so a bad
     * guess cannot wipe out a secret a legitimate send is about to use.
     */
    public function wrongSecretLeavesFileIntactTestCase()
    {
        $secret = bin2hex(random_bytes(32));
        $this->writeSecret($secret);
        $this->authenticator->verifyPassword("bot", "nope");
        $this->assertEqual(trim(file_get_contents($this->path)), $secret,
            "a wrong guess leaves the secret in place");
    }
    /**
     * An empty secret file rejects any password.
     */
    public function emptySecretFileIsRejectedTestCase()
    {
        $this->writeSecret("");
        $this->assertFalse(
            $this->authenticator->verifyPassword("bot", "anything"),
            "empty secret file rejects");
    }
    /**
     * A missing secret file rejects any password.
     */
    public function missingSecretFileIsRejectedTestCase()
    {
        if (is_file($this->path)) {
            unlink($this->path);
        }
        $this->assertFalse(
            $this->authenticator->verifyPassword("bot", "anything"),
            "missing secret file rejects");
    }
    /**
     * The bot is recognized by local part, with or without a domain.
     */
    public function botLocalPartIsRecognizedTestCase()
    {
        $this->assertTrue(MailSiteFactory::isBotUsername("bot"),
            "bare bot name recognized");
        $this->assertTrue(
            MailSiteFactory::isBotUsername("bot@example.com"),
            "bot with domain recognized");
    }
    /**
     * An ordinary username is not treated as the bot.
     */
    public function ordinaryUsernameIsNotBotTestCase()
    {
        $this->assertFalse(MailSiteFactory::isBotUsername("alice"),
            "ordinary name is not the bot");
        $this->assertFalse(
            MailSiteFactory::isBotUsername("alice@example.com"),
            "ordinary name with domain is not the bot");
    }
}
