Files
xxx/tests/php/Pages/FrontPagesTest.php
T
tim 71f768bff2 Log out with a POST form.
The logout link put the CSRF token in the URL, which goes into logs and the browser history.
2026-09-26 15:27:51 +02:00

134 lines
6.2 KiB
PHP

<?php
namespace Xxx\Tests\Pages;
use Xxx\Tests\Support\PageTestCase;
// The public pages: the front page, the archive, a story and about
class FrontPagesTest extends PageTestCase {
protected function setUp(): void {
parent::setUp();
$this->setTables(array('xxx' => array(
array('id' => 1, 'title' => 'Newest', 'author' => 'Ann', 'date' => '2021-01-03 10:00:00', 'story' => '<p>First <em>story</em>.</p>'),
array('id' => 2, 'title' => 'Middle', 'author' => 'Bo', 'date' => '2021-01-02 10:00:00', 'story' => '<p onclick="x()">Second.</p>'),
array('id' => 3, 'title' => 'Secret', 'author' => 'Cy', 'date' => '2021-01-01 10:00:00', 'story' => '<p>Hidden.</p>', 'hidden' => '1'),
)));
}
public function testFrontPageListsTheVisibleStories(): void {
$page = $this->get('/');
$this->assertSame(200, $page['status']);
$this->assertStringContainsString('<a href="/article?id=1">Newest</a>', $page['body']);
$this->assertStringContainsString('<a href="/article?id=2">Middle</a>', $page['body']);
$this->assertStringNotContainsString('Secret', $page['body']);
$this->assertStringNotContainsString('onclick', $page['body']);
$this->assertStringEndsWith("</html>\n", $page['body']);
}
public function testPagesAreNotCached(): void {
$page = $this->get('/');
$this->assertStringContainsString('no-store', implode(', ', $page['headers']['cache-control']));
}
public function testTheSessionCookieIsSecure(): void {
$cookie = $this->header($this->get('/'), 'Set-Cookie');
$this->assertMatchesRegularExpression('/^PHPSESSID=[0-9a-z,-]+;/', $cookie);
foreach (array('path=/', 'domain=.site.test', 'secure', 'HttpOnly', 'SameSite=Lax') as $flag)
$this->assertStringContainsString("; $flag", $cookie);
}
// Another site cannot show a page in a hidden frame to trick an editor into a click
public function testNoPageCanBeFramed(): void {
$paths = array('/', '/article', '/about', '/publish', '/publish/login');
$this->loginAs();
$paths = array_merge($paths, array('/publish/edit', '/publish/addeditor'));
foreach ($paths as $path) {
$page = $this->get($path);
$this->assertSame('DENY', $this->header($page, 'X-Frame-Options'), $path);
$this->assertSame("frame-ancestors 'none'", $this->header($page, 'Content-Security-Policy'), $path);
}
$this->assertSame('DENY', $this->header($this->get('/', array('X-Test-Plain' => '1')), 'X-Frame-Options'));
}
// Browsers then use HTTPS for a year, so nobody can catch the first plain request
public function testHttpsPagesSendHsts(): void {
foreach (array('/', '/publish', '/publish/login') as $path)
$this->assertSame('max-age=31536000', $this->header($this->get($path), 'Strict-Transport-Security'), $path);
// Browsers ignore the header over plain HTTP
$this->assertNull($this->header($this->get('/', array('X-Test-Plain' => '1')), 'Strict-Transport-Security'));
}
// PHP makes a new id for a session id that it did not make, so nobody can plant one
public function testAnUnknownSessionIdIsReplaced(): void {
$planted = str_repeat('ab', 16);
foreach (array('/', '/display?id=1') as $path) {
$this->cookies = array('PHPSESSID' => $planted);
$this->get($path);
$this->assertNotSame($planted, $this->cookies['PHPSESSID'] ?? NULL, $path);
$this->assertFileDoesNotExist(self::$state . "/sessions/sess_$planted", $path);
}
}
public function testPlainHttpStartsNoSession(): void {
$page = $this->get('/', array('X-Test-Plain' => '1'));
$this->assertSame(200, $page['status']);
$this->assertArrayNotHasKey('set-cookie', $page['headers']);
}
public function testNoAnalyticsOrEditorLinksForVisitors(): void {
$body = $this->get('/')['body'];
$this->assertStringNotContainsString('google-analytics', $body);
$this->assertStringNotContainsString('/publish/logout', $body);
$this->assertStringNotContainsString('/publish/addeditor', $body);
}
public function testEditorsSeeHiddenStoriesAndTheirLinks(): void {
$token = $this->loginAs();
$body = $this->get('/')['body'];
$this->assertStringContainsString('Secret', $body);
$this->assertStringContainsString('<form action="/publish/logout" method="post"><input type="hidden" name="csrf_token" value="' . $token . '" />'
. '<button type="submit">Logout</button></form>', $body);
$this->assertStringNotContainsString('?token=', $body);
$this->assertStringContainsString('<a href="/publish/addeditor">Add editor</a>', $body);
}
public function testTheArticlePageShowsOneStory(): void {
$body = $this->get('/article?id=1')['body'];
$this->assertStringContainsString('<p>First <em>story</em>.</p>', $body);
$this->assertStringNotContainsString('Middle', $body);
}
public function testAHiddenStoryIsNotShown(): void {
$body = $this->get('/article?id=3')['body'];
$this->assertStringNotContainsString('Hidden.', $body);
}
public function testSearchFindsStories(): void {
$body = $this->get('/article?search=second')['body'];
$this->assertStringContainsString('Middle', $body);
$this->assertStringNotContainsString('Newest', $body);
$this->assertSame(array('%second%'), array_slice($this->queries('/LIKE/')[0]['params'], 0, 1));
}
public function testTheSearchTermIsEscaped(): void {
$body = $this->get('/article?search=' . rawurlencode('"><script>x</script>'))['body'];
$this->assertStringNotContainsString('<script>x</script>', $body);
}
public function testThePageRemembersWhereTheVisitorWas(): void {
$this->get('/article?id=2');
$this->assertSame('/article?id=2', $this->session()['referrer']);
}
public function testTheAboutPageLoads(): void {
$page = $this->get('/about');
$this->assertSame(200, $page['status']);
$this->assertStringContainsString('</html>', $page['body']);
// The webmaster's address, at the host of SITE_URL, hidden from harvesters
$this->assertStringContainsString('<a href="' . hidden_email('mailto:webmaster@site.test') . '">email us</a>', $page['body']);
}
public function testTheTestsAreNotServed(): void {
$this->assertSame(404, $this->get('/tests/php/fixtures/test.env')['status']);
}
}