Files
xxx/tests/php/Unit/SaveDataTest.php

218 lines
11 KiB
PHP

<?php
namespace Xxx\Tests\Unit;
use PHPUnit\Framework\Attributes\DataProvider;
use Xxx\Tests\Support\FakeDb;
use Xxx\Tests\Support\Mail;
use Xxx\Tests\Support\SiteData;
use Xxx\Tests\Support\TestCase;
// save_data() in publish/post.lib.php
class SaveDataTest extends TestCase {
private const TABLES = array(
'xxx' => array(array('id' => 1, 'title' => 'Live', 'date' => '2020-01-01 10:00:00', 'story' => '<p>Old.</p>')),
'queue' => array(array('id' => 5, 'title' => 'Waiting', 'date' => '2020-03-01 10:00:00', 'story' => '',
'img' => 'QUEUED IMAGE', 'mime' => 'image/gif')),
);
protected function setUp(): void {
parent::setUp();
SiteData::install(self::TABLES);
open_db('open');
}
// Save as an editor, with $action and the POST fields given
private function save(string $action, array $post, array $get = array()): string {
$this->asEditor();
$GLOBALS['action'] = $action;
$this->request($get, $post + array('title' => 'Title', 'author' => 'Ann', 'story' => '<p>Text.</p>'));
return (string)save_data();
}
private function pendingImage(): string {
$name = bin2hex(random_bytes(16));
file_put_contents(upload_dir() . "/$name", self::png());
$_SESSION['pending_upload'] = $name;
return upload_dir() . "/$name";
}
public function testEditorSavesANewStoryToTheQueue(): void {
$id = $this->save('save', array('email' => 'ann@example.org', 'comments' => 'Note', 'editor' => 'ed',
'hidden' => '1', 'date' => '2021-05-06 07:08:09'));
$this->assertSame('6', $id);
$row = SiteData::find('queue', 6);
$this->assertSame(array('Title', 'Ann', 'ann@example.org', '<p>Text.</p>', 'Note', 'ed', 1, '2021-05-06 07:08:09'),
array($row['title'], $row['author'], $row['email'], $row['story'], $row['comments'], $row['editor'], $row['hidden'], $row['date']));
$this->assertCount(1, SiteData::$tables['xxx']);
}
public function testANewStoryGetsTheDateNow(): void {
$this->save('save', array());
$date = SiteData::find('queue', 6)['date'];
$this->assertEqualsWithDelta(time(), strtotime($date . ' UTC'), 5);
}
public function testEditorUpdatesAQueuedStoryInPlace(): void {
$id = $this->save('save', array('id' => '5', 'title' => 'Changed', 'date' => '2030-01-01 00:00:00'));
$this->assertSame('5', $id);
$this->assertSame('Changed', SiteData::find('queue', 5)['title']);
// An update keeps the date
$this->assertSame('2020-03-01 10:00:00', SiteData::find('queue', 5)['date']);
$update = FakeDb::matching('/^UPDATE queue/')[0];
$this->assertStringEndsWith('WHERE `id` = ? LIMIT 1', $update['sql']);
$this->assertSame(5, end($update['params']));
}
public function testEditorUpdatesALiveStory(): void {
$this->assertSame('1', $this->save('post', array('id' => '1', 'title' => 'Edited')));
$this->assertSame('Edited', SiteData::find('xxx', 1)['title']);
$this->assertCount(1, SiteData::$tables['xxx']);
}
public function testPostingFromTheQueueMovesTheStory(): void {
$id = $this->save('post', array('id' => '5', 'title' => 'Now live', 'date' => '2021-01-01 00:00:00'), array('queue' => '1'));
$this->assertSame('6', $id);
$row = SiteData::find('xxx', 6);
$this->assertSame(array('Now live', '2021-01-01 00:00:00', '<p>Text.</p>', NULL), array($row['title'], $row['date'], $row['story'], $row['img']));
$this->assertNull(SiteData::find('queue', 5));
$this->assertFalse($GLOBALS['queue']);
}
public function testPostingFromTheQueueCanKeepTheQueuedImage(): void {
$this->save('post', array('id' => '5', 'uploaded' => '5'), array('queue' => '1'));
$row = SiteData::find('xxx', 6);
$this->assertSame(array('QUEUED IMAGE', 'image/gif', ''), array($row['img'], $row['mime'], $row['story']));
}
public function testAPendingUploadBecomesTheImage(): void {
$file = $this->pendingImage();
$this->save('save', array('uploaded' => 'pending'));
$row = SiteData::find('queue', 6);
$this->assertSame(array(self::png(), 'image/png', ''), array($row['img'], $row['mime'], $row['story']));
$this->assertFileDoesNotExist($file);
$this->assertArrayNotHasKey('pending_upload', $_SESSION);
}
public function testRemovingTheImageKeepsTheText(): void {
$this->save('save', array('id' => '5', 'remove_img' => '1', 'story' => '<p>Words.</p>'));
$row = SiteData::find('queue', 5);
$this->assertSame(array(NULL, NULL, '<p>Words.</p>'), array($row['img'], $row['mime'], $row['story']));
}
public function testTheStoryIsSanitised(): void {
$this->save('save', array('story' => '<p onclick="x()">a -- b<script>alert(1)</script></p>'));
$this->assertSame('<p>a — b</p>', SiteData::find('queue', 6)['story']);
}
public function testValuesAreBoundAsParameters(): void {
$this->save('save', array('title' => "O'Brien\"; DROP TABLE xxx; --"));
$insert = FakeDb::matching('/^INSERT INTO queue/')[0];
$this->assertStringNotContainsString('DROP', $insert['sql']);
$this->assertContains("O'Brien\"; DROP TABLE xxx; --", $insert['params']);
}
public static function failedQueries(): array {
return array(
'visitor insert' => array(FALSE, 'post', array(), array(), '/^INSERT INTO queue/'),
'editor insert' => array(TRUE, 'submit', array(), array(), '/^INSERT INTO queue/'),
'queue update' => array(TRUE, 'submit', array('id' => '5'), array(), '/^UPDATE queue/'),
'live update' => array(TRUE, 'post', array('id' => '1'), array(), '/^UPDATE xxx/'),
'move from queue' => array(TRUE, 'post', array('id' => '5'), array('queue' => '1'), '/^INSERT INTO xxx/'),
'queue image lookup' => array(TRUE, 'post', array('id' => '5', 'uploaded' => '5'), array('queue' => '1'), '/^SELECT img, mime FROM queue/'),
);
}
#[DataProvider('failedQueries')]
public function testAFailedQueryKeepsTheStoryAndUploadWithoutSendingMail(bool $admin, string $action, array $post, array $get, string $pattern): void {
$pending = $this->pendingImage();
$session = $_SESSION + array('captcha_ok' => time());
$state = $this->tmp . '/failed-save';
file_put_contents($state, serialize(array(
'tables' => self::TABLES, 'admin' => $admin, 'action' => $action, 'pattern' => $pattern,
'post' => $post + array('title' => 'Title', 'author' => 'Ann', 'story' => '<p>Text.</p>', 'uploaded' => 'pending'),
'get' => $get, 'session' => $session,
)));
$file = var_export($state, TRUE);
$code = <<<PHP
\$state = unserialize(file_get_contents($file));
Xxx\Tests\Support\SiteData::install(\$state['tables']);
Xxx\Tests\Support\FakeDb::on(\$state['pattern'], FALSE);
open_db('open');
\$is_admin = \$state['admin'];
\$action = \$state['action'];
\$_POST = \$state['post'];
\$_GET = \$state['get'];
\$_REQUEST = array_merge(\$_GET, \$_POST);
\$_SESSION = \$state['session'];
\$before = Xxx\Tests\Support\SiteData::\$tables;
\$returned = FALSE;
// db_fail() exits, so save the result at shutdown.
register_shutdown_function(function () use (\$before, &\$returned) {
file_put_contents($file, serialize(array(
'before' => \$before, 'tables' => Xxx\Tests\Support\SiteData::\$tables,
'session' => \$_SESSION, 'queries' => Xxx\Tests\Support\FakeDb::queries(),
'returned' => \$returned, 'status' => http_response_code(),
)));
});
save_data();
\$returned = TRUE;
PHP;
$result = Mail::run($code, array('UPLOAD_DIR' => $this->tmp . '/uploads'));
$this->assertSame(0, $result['status'], $result['stderr']);
$this->assertSame('', $result['stderr']);
$this->assertSame('<p>Sorry, something went wrong. Please try again later.</p>', $result['stdout']);
$this->assertStringContainsString('xxx: database error at post.lib.php:', $result['log']);
$this->assertStringNotContainsString('Warning', $result['log']);
$this->assertStringNotContainsString('Notice', $result['log']);
$this->assertSame(array(), $result['mails']);
$saved = unserialize(file_get_contents($state));
$this->assertFalse($saved['returned']);
$this->assertSame(500, $saved['status']);
$this->assertSame($saved['before'], $saved['tables']);
$this->assertSame($session, $saved['session']);
$this->assertFileExists($pending);
$this->assertSame(self::png(), file_get_contents($pending));
$this->assertCount(1, preg_grep($pattern, $saved['queries']));
$this->assertMatchesRegularExpression($pattern, end($saved['queries']), 'no queries run after the failure');
}
// Visitors always add to the queue, and each story sends mail
public function testVisitorsCanOnlyAddToTheQueue(): void {
$state = $this->tmp . '/state';
file_put_contents($state, serialize(array(
'tables' => self::TABLES,
'post' => array('id' => '1', 'title' => 'Mine', 'author' => 'Vi', 'story' => '<p>Hi.</p>', 'comments' => 'sneaky',
'editor' => 'ed', 'hidden' => '1', 'date' => '1999-01-01 00:00:00'),
'session' => array('captcha_ok' => time()),
)));
$file = var_export($state, TRUE);
$code = <<<PHP
\$state = unserialize(file_get_contents($file));
Xxx\Tests\Support\SiteData::install(\$state['tables']);
open_db('open');
\$action = 'post';
\$_POST = \$_REQUEST = \$state['post'];
\$_SESSION = \$state['session'];
\$id = save_data();
file_put_contents($file, serialize(array('id' => \$id, 'tables' => Xxx\Tests\Support\SiteData::\$tables, 'session' => \$_SESSION, 'action' => \$action)));
PHP;
$result = Mail::run($code, array('UPLOAD_DIR' => $this->tmp . '/uploads'));
$this->assertSame(0, $result['status'], $result['stderr']);
$this->assertSame('', $result['log']);
$saved = unserialize(file_get_contents($state));
$this->assertSame('6', (string)$saved['id']);
$this->assertSame('submit', $saved['action']);
$this->assertSame(self::TABLES['xxx'][0]['title'], $saved['tables']['xxx'][0]['title']);
$row = end($saved['tables']['queue']);
$this->assertSame(array('6', 'Mine', 'original author', 0, '', '<p>Hi.</p>'),
array($row['id'], $row['title'], $row['editor'], $row['hidden'], $row['comments'], $row['story']));
$this->assertNotSame('1999-01-01 00:00:00', $row['date']);
$this->assertSame(array('allow_queue_image' => '6'), $saved['session']);
$this->assertCount(1, $result['mails']);
$this->assertSame('New submission: Mine', $result['mails'][0]['subject']);
$this->assertStringContainsString('Vi has sent the following new submission', $result['mails'][0]['body']);
}
}