Files
comic-easel/tests/ArchiveQueryTest.php
T
coraislovely-code 2fe70a3d87 Add characterization tests for the code about to be changed
Security fixes to output escaping carry a specific risk: the fix silently changes what
users see. These tests pin the behaviour that must NOT change, so that any such drift shows
up as a failing assertion rather than as a bug report.

Chosen deliberately to be stable across the fixes that follow, so they stay green
throughout rather than needing to be rewritten by each one. The security assertions -- that
a shortcode attribute cannot reach the SQL string, that a javascript: URI cannot be stored
as a URL -- ship with the commits that make them true, so each fix arrives with the test
that would have caught its absence.

The two most valuable assertions here encode properties rather than values:

- Rendering a transcript must be stable across saves: a value stored the way the save
  handler stores it, through esc_textarea(), must render back as exactly those bytes. The
  case that discriminates is an author who typed a literal entity -- esc_textarea() stores
  "<b>" as "<b>", and an escape that declines to re-encode existing
  entities hands back one level short, so the author's literal text decays into a live tag a
  save at a time.
- The same round-trip property for widget link labels, fed through update() twice.

ArchiveQueryTest uses the $wpdb spy rather than a database, and asserts that table names
come from $wpdb rather than a hardcoded prefix -- a query with the prefix baked in silently
returns nothing on any site that did not accept the installer default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 08:36:46 -04:00

82 lines
3.0 KiB
PHP

<?php
/**
* The archive listing queries — functions/shortcodes.php
*
* These functions build SQL by hand, and their $order and $chapter arguments arrive from
* [comic-archive] shortcode attributes. A spy $wpdb records the SQL, which lets us assert
* on the query text with no database at all — the plugin's queries use MySQL-only date
* functions, so a SQLite test database would misreport them anyway.
*
* Characterization only: these assert the shape of the query, which does not change when
* the arguments start being sanitised. The injection assertions ship with that fix.
*/
class ArchiveQueryTest extends CE_TestCase {
/** @var CE_WPDB_Spy */
private $wpdb;
protected function setUp(): void {
parent::setUp();
self::loadPluginFile( 'functions/shortcodes.php' );
$this->wpdb = $this->useWpdbSpy();
// Pin the year so the functions do not go looking for the newest comic.
$_GET['archive_year'] = '2020';
}
protected function tearDown(): void {
unset( $_GET['archive_year'] );
parent::tearDown();
}
public function testByYearWithoutChapterQueriesPublishedComics() {
ceo_archive_list_by_year( false, 'ASC', 0 );
$sql = $this->wpdb->last();
$this->assertStringContainsString( 'DISTINCT YEAR(post_date)', $sql );
$this->assertStringContainsString( "post_type='comic'", $sql );
$this->assertStringContainsString( "post_status = 'publish'", $sql );
}
public function testByYearWithChapterJoinsTheChaptersTaxonomy() {
ceo_archive_list_by_year( false, 'ASC', 5 );
$sql = $this->wpdb->last();
$this->assertStringContainsString( 'term_relationships', $sql );
$this->assertStringContainsString( 'term_taxonomy', $sql );
$this->assertStringContainsString( "taxonomy = 'chapters'", $sql );
}
public function testByAllYearsWithChapterJoinsTheChaptersTaxonomy() {
ceo_archive_list_by_all_years( false, 'ASC', 5 );
$this->assertStringContainsString( "taxonomy = 'chapters'", $this->wpdb->last() );
}
/**
* Table names must come from $wpdb rather than being hardcoded, or the query silently
* returns nothing on any site whose prefix is not the default.
*/
public function testArchiveQueriesUseTheConfiguredTablePrefix() {
$this->wpdb->posts = 'xyz_posts';
ceo_archive_list_by_year( false, 'ASC', 0 );
$sql = $this->wpdb->last();
$this->assertStringContainsString( 'xyz_posts', $sql );
$this->assertStringNotContainsString( 'wp_posts', $sql );
}
public function testByYearRendersTheSelectedYearHeading() {
$out = ceo_archive_list_by_year( false, 'ASC', 0 );
$this->assertStringContainsString( '2020', $out );
$this->assertStringContainsString( 'archive-yearlist', $out );
}
/**
* archive_year comes from the query string and is cast to int before use, so a
* non-numeric value must not reach the output.
*/
public function testArchiveYearFromQueryStringIsCastToInteger() {
$_GET['archive_year'] = '2020<script>alert(1)</script>';
$out = ceo_archive_list_by_year( false, 'ASC', 0 );
$this->assertStringNotContainsString( '<script>', $out );
$this->assertStringContainsString( '2020', $out );
}
}