/ tests / TurnServerTest.php
<?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
 * <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>
 *
 * 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;

/**
 * Says the file may be read for the class it holds, without starting a
 * relay or taking a port. Defined before the class is named below.
 */
if (!defined(
    "seekquarry\\yioop\\executables\\TURN_SERVER_TEST_LOAD")) {
    define("seekquarry\\yioop\\executables\\TURN_SERVER_TEST_LOAD",
        true);
}

use seekquarry\yioop\executables\TurnServer;
use seekquarry\yioop\library\UnitTest;
use seekquarry\atto\TurnSite;

/**
 * RecordingTurnSite is a relay whose one added method reaches the
 * protected logLine helper, so a test can call it the way the relay's
 * own request handlers do and see what onLog was given. It starts no
 * listener and takes no port; it exists only to exercise the log path.
 */
class RecordingTurnSite extends TurnSite
{
    /**
     * sayLine hands one line to the relay's log path, standing in for
     * the request handlers that call the protected helper when a
     * datagram arrives or an allocation is made.
     *
     * @param string $line the sentence to record
     * @return void
     */
    public function sayLine($line)
    {
        $this->logLine($line);
    }
    /**
     * bindForTest reaches the protected bindRelaySocket so a case can
     * see which host the relay would hand a browser for an allocation,
     * the loopback or the advertised public address, without standing up
     * a whole allocation. It closes the socket the bind took so the case
     * leaves no port held.
     *
     * @param string $client_host address a client is treated as coming
     *      from, which picks the address family
     * @return string the host the relay would put in the relayed
     *      address, or the empty string where no port was free
     */
    public function bindForTest($client_host)
    {
        list($sock, $host, $port) =
            $this->bindRelaySocket($client_host);
        if ($sock === false) {
            return "";
        }
        fclose($sock);
        return $host;
    }
}

/**
 * Tests for TurnServer::deriveRelayConfig, which turns the site's
 * settings into what the relay is told to do. The cases walk the range
 * of ports the relay may hand out, since a range written the wrong way
 * round, or with one port, or with none, is the part a site owner is
 * most likely to get wrong, and a relay handed such a range would
 * either refuse every call or take a port it was never meant to.
 *
 * @author Chris Pollett
 */
class TurnServerTest extends UnitTest
{
    /**
     * The cases stand on their own; nothing is set up for them.
     */
    public function setUp()
    {
    }
    /**
     * Nothing is left behind by these cases.
     */
    public function tearDown()
    {
    }
    /**
     * Checks that the address, port, name, secret and realm a site
     * sets are handed to the relay as they stand, with the port read
     * as a number so a setting typed as text still binds.
     */
    public function whatTheSiteSetsIsWhatTheRelayIsToldTestCase()
    {
        $told = TurnServer::deriveRelayConfig("127.0.0.1", "3478",
            "yioop", "a secret", "yioop", "60000-60100");
        $this->assertEqual("127.0.0.1", $told["BIND"],
            "the relay is told the address the site set");
        $this->assertEqual(3478, $told["TURN_PORT"],
            "and the port as a number, so a setting typed as text binds");
        $this->assertEqual(["yioop" => "a secret"], $told["users"],
            "the name and secret a browser gives are handed over");
        $this->assertEqual("yioop", $told["realm"],
            "as is the realm the relay names itself by");
    }
    /**
     * Checks that a range of ports written lowest first is handed over
     * as two numbers.
     */
    public function rangeOfPortsIsReadAsTwoNumbersTestCase()
    {
        $told = TurnServer::deriveRelayConfig("127.0.0.1", 3478,
            "yioop", "", "yioop", "60000-60100");
        $this->assertEqual(60000, $told["low"] ?? 0,
            "the lowest port the relay may hand out");
        $this->assertEqual(60100, $told["high"] ?? 0,
            "and the highest");
    }
    /**
     * Checks that a range the relay cannot use leaves it with none: one
     * written the wrong way round, one with a single port, and one left
     * empty. A relay given such a range would hand out a port outside
     * whatever the site opened in its router.
     */
    public function rangeTheRelayCannotUseLeavesItNoneTestCase()
    {
        foreach (["60100-60000", "60000", "", "-", "0-60100"] as $said) {
            $told = TurnServer::deriveRelayConfig("127.0.0.1", 3478,
                "yioop", "", "yioop", $said);
            $this->assertTrue(!isset($told["low"]),
                "a range of '$said' leaves the relay choosing for itself");
        }
    }
    /**
     * relaySendsALineToWhereverOnLogNamedTestCase checks that a line
     * the relay would write about a datagram reaches the function handed
     * to onLog, so the daemon can put each such line in its log and a
     * browser that reached the relay leaves a trace.
     */
    public function relaySendsALineToWhereverOnLogNamedTestCase()
    {
        $relay = new RecordingTurnSite();
        $seen = [];
        $relay->onLog(function ($line) use (&$seen) {
            $seen[] = $line;
        });
        $relay->sayLine("STUN/TURN datagram from 10.0.0.1:5555");
        $this->assertEqual(1, count($seen),
            "the line reaches the function onLog was given");
        $this->assertEqual("STUN/TURN datagram from 10.0.0.1:5555",
            $seen[0], "and reaches it unchanged");
    }
    /**
     * relayStaysSilentWithNoLogSetTestCase checks that a relay given
     * no place to log makes no line and does not fail, so logging is a
     * thing a caller turns on rather than something the relay forces.
     */
    public function relayStaysSilentWithNoLogSetTestCase()
    {
        $relay = new RecordingTurnSite();
        $relay->sayLine("this line has nowhere to go");
        $this->assertTrue(true,
            "a relay with no log callback records nothing and does " .
            "not fail");
    }
    /**
     * relayHandsOutThePublicAddressWhenSetTestCase checks that a
     * relay told a public address puts that address in an allocation
     * rather than the loopback, so the other browser is given an address
     * it can reach. A loopback address there is why a call failed even
     * once both browsers reached the relay.
     */
    public function relayHandsOutThePublicAddressWhenSetTestCase()
    {
        $relay = new RecordingTurnSite();
        $relay->relayPortRange(60000, 60100);
        $relay->relayAddress("203.0.113.7");
        $host = $relay->bindForTest("198.51.100.9");
        $this->assertEqual("203.0.113.7", $host,
            "the relay hands out the public address it was told");
    }
    /**
     * relayKeepsToLoopbackWithNoAddressSetTestCase checks that a
     * relay told no public address keeps to the loopback, so a site that
     * has set nothing is no worse off than before and a call between two
     * browsers on one machine still works.
     */
    public function relayKeepsToLoopbackWithNoAddressSetTestCase()
    {
        $relay = new RecordingTurnSite();
        $relay->relayPortRange(60000, 60100);
        $host = $relay->bindForTest("198.51.100.9");
        $this->assertEqual("127.0.0.1", $host,
            "the relay keeps to the loopback when told no address");
    }
    /**
     * betweenWorkRunsAtItsPaceNotPerPacketTestCase checks that
     * betweenWorkDue says yes once per interval rather than every time
     * it is asked, since the loop asks once per datagram when traffic
     * flows and the work behind it writes files each run: run per
     * packet it slowed the relay path until a call's connection checks
     * crawled.
     */
    public function betweenWorkRunsAtItsPaceNotPerPacketTestCase()
    {
        $relay = new RecordingTurnSite();
        $relay->betweenRounds(function () {
        }, 5.0);
        $moment = 1000000.0;
        $this->assertTrue($relay->betweenWorkDue($moment),
            "the first ask after the interval is due");
        $this->assertTrue(!$relay->betweenWorkDue($moment + 0.001),
            "an ask a packet later is not due");
        $this->assertTrue(!$relay->betweenWorkDue($moment + 4.9),
            "an ask just inside the interval is not due");
        $this->assertTrue($relay->betweenWorkDue($moment + 5.0),
            "an ask at the interval is due again");
    }
}
X