Skip to content

Add test infrastructure: PHPUnit harness, CI, and scoped PHPCS - #37

Open
coraislovely-code wants to merge 2 commits into
Frumph:masterfrom
coraislovely-code:security/0-test-harness
Open

Add test infrastructure: PHPUnit harness, CI, and scoped PHPCS#37
coraislovely-code wants to merge 2 commits into
Frumph:masterfrom
coraislovely-code:security/0-test-harness

Conversation

@coraislovely-code

Copy link
Copy Markdown
Contributor

This theme has had no automated checks of any kind. This adds them without changing a single line of theme code — every file in this pull request is new.

It is the first of three; the two that follow contain actual changes and are stacked on this one.

A PHPUnit harness that does not boot WordPress

The theme's functions depend on their arguments plus a small set of WordPress helpers, so tests/stubs.php supplies those helpers and the tests require theme files directly. No database, no WordPress checkout, no fixtures — the suite runs in well under a second.

A few stubs are deliberately faithful rather than convenient, because tests hang on the difference:

  • esc_html() / esc_attr() do not re-encode existing entities; esc_textarea() does.
  • esc_url() refuses a scheme that esc_attr() passes straight through.
  • wp_filter_nohtml_kses() — the options screen's only sanitiser — strips tags but leaves quotes alone.
  • current_user_can() defaults to false, so a missing capability check surfaces as a failing test rather than a silent pass.

tests/HarnessTest.php asserts each of those. If the harness ever drifts, that file fails first rather than the rest of the suite quietly reporting nonsense.

Characterization tests

The second commit pins current behaviour — including several things that are currently wrong — so that the changes in the follow-up pull requests visibly flip an assertion instead of being taken on trust. Each docblock states what the assertion becomes afterwards.

CI

  • php -l on 7.4 through 8.4. The cheapest useful signal here: recent work on this theme has been PHP 8 compatibility, and until now nothing verified it on more than one interpreter.
  • PHPUnit on 8.2 / 8.3 / 8.4.
  • PHPCS, scoped to the lines a pull request adds, and advisory rather than blocking.

That last choice is deliberate. The theme predates the WordPress coding standards by about a decade and carries several hundred pre-existing findings, so a per-file run would go red on any pull request touching legacy code whether or not it made anything worse — and a job that is red by default is one everyone learns to scroll past. .github/scripts/phpcs-added-lines.py maps the report onto the diff and reports only added lines, printing a count of what it suppressed so the backlog stays visible rather than forgotten.

It is advisory because some of what it reports cannot reasonably be fixed: the sniffs cannot see through apply_filters() to an escaper inside it, cannot know an interpolated ORDER BY direction was whitelisted, and object to the $before_title / $after_title that every WordPress widget emits. Pass --strict to make it blocking if that is ever wanted.

Verification

Ten jobs, all green on their first run — six php -l, three PHPUnit, one PHPCS. composer test reports OK (48 tests, 178 assertions) on this branch.

coraislovely-code and others added 2 commits July 26, 2026 16:12
The theme has never had a test of any kind. The obstacle has always been that testing a
WordPress theme appears to require WordPress, and therefore a database.

It does not, for the part that matters. The theme's logic -- escaping decisions, SQL
construction, nonce and capability checks on the options screen -- depends only on its
arguments plus a small set of WordPress helpers. Stubbing those gives a suite that runs in
milliseconds with no database, no Docker and no WordPress checkout.

Several of the stubs have to be faithful or the tests they support quietly stop meaning
anything, and this is documented at the top of tests/stubs.php:

- esc_html()/esc_attr() call _wp_specialchars() with $double_encode = false, so they leave
  existing entities alone, while esc_textarea() double-encodes. Much of this theme's
  escaping behaviour turns on that difference, so a naive htmlspecialchars() stub would
  give the wrong answer.
- esc_url() drops a disallowed scheme and esc_attr() does not. An href escaped with
  esc_attr() is well-formed HTML and a working javascript: URL, and several places in the
  theme do exactly that, so the two stubs must not be interchangeable.
- wp_filter_nohtml_kses() is the only sanitiser the options screen uses. WordPress defines
  it as addslashes( wp_kses( stripslashes( $data ), array() ) ): it removes every tag and
  leaves quotes ALONE. A stub that also escaped quotes would make unescaped attribute
  output look safe.
- The nonce functions are a test-driven seam, not a reimplementation. A test can make
  verification pass, make it fail, and -- the part that matters -- see whether it was
  attempted at all, because a handler with no nonce check passes every happy-path test.
- current_user_can() defaults to false, so a missing capability check fails a test instead
  of sailing through one.

Also included is a $wpdb spy that records the SQL it is handed, which is what allows query
construction to be tested without a database. It answers $wpdb->escape() as well, removed
from WordPress in 5.3 but still called by widgets/calendar.php.

Loading theme code needs one wrinkle. functions.php cannot be required from a test: it
calls easel_themeinfo() before defining it, glob-autoloads all of functions/ and widgets/
through get_template_part(), and pulls in options.php under is_admin(). So tests require
one file at a time, and easel_themeinfo()/easel_load_options() are stubbed in the
bootstrap. Those two are worth testing themselves, so the stubs are dispatchers:
Easel_TestCase::loadRealThemeInfo() lifts the real bodies out of functions.php verbatim
and switches both over together. No theme file is modified -- the harness has to be able
to test the theme as shipped.

CI runs php -l across 7.4 through 8.4, PHPUnit on 8.2 through 8.4, and an advisory PHPCS
job scoped to the lines a pull request adds rather than the files it touches. The theme
carries roughly six hundred pre-existing findings; a per-file run would be red by default,
and a job that is always red is a job nobody reads.

HarnessTest.php asserts these properties of the harness itself. If it fails, nothing else
in the suite should be trusted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These pin the theme's current behaviour rather than the behaviour it
ought to have, so that when the escaping, the request handling and the
option loading are changed the assertions have to be inverted rather
than merely kept passing. Every test says in its docblock what it is
recording and what the change will turn it into.

Covered: how author contact details and the menubar social links are
concatenated into markup, the order in which the options screen reads
the request against its form token, the sanitize_callback names the
customizer registers and the type of its three width settings, the
default array built on a site that has never pressed Save, and two
$_SERVER reads that assume the key is present.

The harness grows three things the new tests needed: a recorder that
stands in for $wp_customize, a helper that lifts a single function out
of functions.php, and a helper that buffers output and collects the
diagnostics a call raises so a test can assert on them instead of the
run failing on them. No theme file is touched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant