<?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\WebSite;
use seekquarry\yioop\library\UnitTest;
/**
* Unit tests for the status line given to a response whose code set
* headers but never said what the status was. What decides it is whether
* a Location or a Refresh header was sent; what a header happens to be
* carrying is not a header, and a picture whose name merely mentions a
* location has not moved anywhere.
*
* @author Chris Pollett
*/
class WebSiteStatusLineTest extends UnitTest
{
/**
* The server whose choice of status line is under test.
* @var WebSite
*/
public $site;
/**
* The method that chooses the status line, reached through
* reflection because a server keeps it to itself.
* @var \ReflectionMethod
*/
public $status_line;
/**
* The headers gathered so far, which the choice is made from,
* reached through reflection because a server keeps them to itself.
* @var \ReflectionProperty
*/
public $header_data;
/**
* Builds the server without its constructor, which would want to
* listen on a port that the status line has nothing to do with.
*/
public function setUp()
{
$reflection = new \ReflectionClass(WebSite::class);
$this->site = $reflection->newInstanceWithoutConstructor();
/* what a class keeps to itself has been reachable through
reflection without asking since PHP 8.1, and asking is an error
as of 8.5, so nothing is asked for here. */
$this->status_line = new \ReflectionMethod(WebSite::class,
"statusLineForHeaders");
$this->header_data = new \ReflectionProperty(WebSite::class,
"header_data");
$_SERVER['SERVER_PROTOCOL'] = "HTTP/1.1";
}
/**
* Nothing to take down between test cases.
*/
public function tearDown()
{
}
/**
* Asks what status line a given block of headers is given.
*
* @param string $headers the headers gathered so far
* @return string the status line they are given
*/
private function statusFor($headers)
{
$this->header_data->setValue($this->site, $headers);
return $this->status_line->invoke($this->site);
}
/**
* Sending a Location header says the thing asked for is somewhere
* else, and that is answered as having moved.
*/
public function locationHeaderMovedPermanentlyTestCase()
{
$this->assertEqual($this->statusFor(
"Location: /elsewhere\x0D\x0A"),
"HTTP/1.1 301 Moved Permanently\x0D\x0A",
"a Location header says the thing has moved");
$this->assertEqual($this->statusFor(
"Content-Type: text/html\x0D\x0ALocation: /elsewhere\x0D\x0A"),
"HTTP/1.1 301 Moved Permanently\x0D\x0A",
"a Location header counts wherever in the block it stands");
$this->assertEqual($this->statusFor(
"location: /elsewhere\x0D\x0A"),
"HTTP/1.1 301 Moved Permanently\x0D\x0A",
"a header name is the same name however it is capitalised");
}
/**
* Sending a Refresh header says the reader is to be sent on shortly.
*/
public function refreshHeaderFoundTestCase()
{
$this->assertEqual($this->statusFor("Refresh: 5; url=/x\x0D\x0A"),
"HTTP/1.1 302 Found\x0D\x0A",
"a Refresh header says the reader is sent on shortly");
}
/**
* A reply to what was asked for is answered as such.
*/
public function ordinaryReplyIsOkayTestCase()
{
$this->assertEqual($this->statusFor("Content-Type: image/webp\x0D\x0A"),
"HTTP/1.1 200 OK\x0D\x0A",
"an ordinary reply is answered as one");
$this->assertEqual($this->statusFor(""),
"HTTP/1.1 200 OK\x0D\x0A",
"headers saying nothing in particular are an ordinary reply");
}
/**
* What a header carries is not itself a header. A picture whose name
* mentions a location has not moved, and one whose name mentions a
* refresh is not sending the reader on; both were served with a
* redirect's status stamped over a picture, and browsers showed
* nothing at all.
*/
public function whatAHeaderCarriesIsNotAHeaderTestCase()
{
$this->assertEqual($this->statusFor("Content-Disposition: " .
"filename=\"link_game_location1.webp\"\x0D\x0A"),
"HTTP/1.1 200 OK\x0D\x0A",
"a picture named for a location has not moved anywhere");
$this->assertEqual($this->statusFor("Content-Disposition: " .
"filename=\"page_refresh_button.png\"\x0D\x0A"),
"HTTP/1.1 200 OK\x0D\x0A",
"a picture named for a refresh sends nobody anywhere");
$this->assertEqual($this->statusFor(
"X-Note: the location of the refresh is here\x0D\x0A"),
"HTTP/1.1 200 OK\x0D\x0A",
"the words in a header's value decide nothing");
$this->assertEqual($this->statusFor(
"Content-Disposition: filename=\"a.png\"\x0D\x0A" .
"Location: /elsewhere\x0D\x0A"),
"HTTP/1.1 301 Moved Permanently\x0D\x0A",
"a real Location header still counts beside other headers");
}
/**
* The page sent with a request timeout is a complete, self-contained
* HTML document that names the timeout and holds no site content,
* since the timeout is exactly the real page not being finishable.
*/
public function requestTimeoutPageIsSelfContainedTestCase()
{
$page_method = new \ReflectionMethod(WebSite::class,
"requestTimeoutPage");
$page = $page_method->invoke($this->site);
$this->assertTrue(strpos($page, "<!DOCTYPE html>") === 0,
"the timeout page is a whole HTML document");
$this->assertTrue(strpos($page, "408") !== false,
"the timeout page names the 408 status");
$this->assertTrue(stripos($page, "too long") !== false,
"the timeout page says the request took too long");
}
}