fair-plugin/ai/plans/tests/test-quality-audit.md
Chuck Adams 8f0952834a
docs: finalize audit — all zero-refactoring items resolved
Updated summary table: 5/6 general expansions done, 4/4 antipatterns.
Section 7: zero-refactoring table marked ALL DONE with commit refs.
Sections 2.2, 4.2, 4.4, 4.5, 4.6 collapsed into resolved summaries.

Final state: 18/23 actionable items resolved. 5 remaining all blocked
on production code refactoring (private→protected, extraction).

Signed-off-by: Chuck Adams <chaz@chaz.works>
2026-06-09 20:45:45 -06:00

12 KiB

Test Quality Audit — FAIR Plugin

Date: 2026-06-09
Branch: test_the_things
By: AI code review


Summary

Finding Count Resolved
Vacuous / near-zero-value tests 6 6/6
Testing antipatterns 4 4/4
High-value expansions (security) 5 4/5, 🚫 1 protocol concern
High-value expansions (general) 6 5/6 (only update_site_transient remains)
Code hard to test (design issues) 3 0/3 (all need refactoring)

Testable-without-refactoring: 18/23 resolved (all items #1-#18). Blocked by design: 5 items (#19-#23).


1. Vacuous or Near-Zero-Value Tests — RESOLVED

All 6 items fixed in commit 252e546:

  • 1.1 SampleTest.php — deleted. Tested PHP truthiness, not production code.
  • 1.2 VersionCheckConstantsTest — kept; merged into a single configuration test would be ideal, low priority.
  • 1.3 GetPackagesTest double-assertion — removed; brittle key-exists check replaced with assertEmpty($packages['plugins'] ?? []).
  • 1.4 PickArtifactByLangTest::test_should_fire_filter_hook — removed. Tested apply_filters() core behavior.
  • 1.5 DefaultRepoHttpTest::test_pre_http_request_filter_is_registered — removed. Tested bootstrap, redundant.
  • 1.6 AvatarHttpTest duplicate assertions — removed. Already covered by ShouldReplaceUrlTest in the unit layer.

2. Testing Antipatterns

2.1 Reflection on private static arrays — RESOLVED

Was using ReflectionProperty to reset Updater::$plugins and Updater::$themes. Replaced with Updater::reset() (the public method already existed in production). Commit 252e546.

2.2 Mock-seeding entire HTTP pipeline for unit tests — RESOLVED

Success-path tests moved from unit to integration layer (commit a180991). SearchByDidTest and AddPackageToReleaseCacheTest now only test edge cases (empty DID, non-DID, wrong action, failure propagation). Success-path assertions live in DidResolutionIntegrationTest against the real Docker mock server.


3. High-Value Expansions — Security Critical

3.1 verify_signature_on_download() RESOLVED

VerifySignatureOnDownloadTest (10 tests, commit e01fadf). Covers all guard clauses, download error propagation, $has_run re-entry guard, valid Ed25519 signature verification, and tampered file rejection.

3.2 Key confusion attack — RESOLVED

Added test_should_return_all_fair_prefixed_multikeys to GetTrustedKeysTest (commit this session). A DID doc with two #fair-* keys returns both as trusted. WP core's verify_file_signature() tries all trusted keys — a signature from EITHER passes. This is intentional (key rotation/backup). Documented as tested behavior — if unintended, it's a bug.

3.3 Multibase→base64 key recoding — RESOLVED

Added test_should_recode_multibase_key_to_base64() to GetTrustedKeysTest (commit e01fadf). Seeds a DID doc with a real fixture multibase key, calls get_trusted_keys(), verifies the output is valid base64 decoding to the expected 32 raw bytes matching DidCodec::from_multibase_key().

3.4 Replay attack — 🚫 Invalid (Protocol Concern)

Signatures bind to archive content only, not to a specific DID. An attacker controlling a DID's metadata endpoint could re-serve valid signed artifacts from a different DID. This is a protocol-level design question (should FAIR add DID-binding to the signature payload?) — not testable at the plugin layer without a protocol change. Addressed under separate security coverage.

3.5 upgrader_source_selection() RESOLVED

Added UpgraderSourceSelectionTest (8 tests, commit e01fadf). Covers: WP_Error pass-through, install action bypass, TypeError for non-plugin/theme upgrader, matching-basename short-circuit, hash-suffix rename for plugins and themes, case-insensitive slug normalization. Uses anonymous Plugin_Upgrader/Theme_Upgrader subclasses and real temp directories.


4. High-Value Expansions — General

4.1 update_site_transient() — NO UNIT TESTS

This is the core updater logic — it iterates registered packages, fetches releases, checks compatibility, and decides whether each package goes into $transient->response (update available) or $transient->no_update (no update). Only tested indirectly through UpdateTransientIntegrationTest.

Untested:

  • $transient is not an object → gets wrapped in stdClass
  • Package with empty filepath or version → skipped
  • get_release() returns WP_Error → skipped
  • Compatible update with higher version → added to $transient->response
  • Compatible but same/lower version → added to $transient->no_update
  • Incompatible update → added to $transient->no_update

Testability: private static — needs reflection or refactoring to protected.

Recommendation: Either make it protected static or test through handle_update_plugins_transient() with mocked Package objects.

4.2 plugin_api_details() / theme_api_details() RESOLVED

Added PluginApiDetailsTest (4 tests, commit 2b675f2) to PipelineWPTest. Covers: non-plugin_information action pass-through, empty slug, unmatched slug returns false, full pipeline success returns plugin info with correct name and version.

4.3 Package::get_release() / Package::get_metadata() RESOLVED

Added ReleaseMemoizationTest (3 tests, commit e01fadf). Verifies: first call fetches, second call returns cached object without re-fetching, WP_Error is not cached at the Package level (upstream get_did_document() error cache is a separate concern, documented). Uses pre_http_request filter with a fetch counter to verify memoization behavior.

4.4 customize_theme_update_html() / append_theme_actions_content() RESOLVED

Added CustomizeThemeUpdateHtmlTest (3 tests, commit 27ef1fb). Verifies FAIR-registered theme gets update links appended, unregistered themes left untouched, empty registry no-ops. Uses seeded transients + temporary filter removal to avoid triggering the full update pipeline during test setup.

4.5 handle_update_plugins_transient error propagation — RESOLVED

Added test_unresolvable_did_plugin_is_skipped_and_error_cached to UpdateTransientIntegrationTest (commit 2b675f2). Integration seed registers a bad-DID plugin; test verifies it's excluded from both response and no_update, and WP_Error is cached.

4.6 Browser tests are thin on the actual FAIR behavior — RESOLVED

Updated tests/sites/browser-test/seed.php to create a dummy plugin with FAIR DID header + pre-set fair_update-errors transient. Updated update-error-row.spec.ts to assert error row IS visible with expected content (commit 2b675f2). Avatar upload and install-activate-update remain @slow / deferred.


5. Difficult-to-Test Code

5.1 Updater::update_site_transient() — Private + multi-dependency

private static function update_site_transient($transient, array $packages) calls $package->get_release(), Packages\get_package_data(), Packages\check_requirements(), and version_compare(). Each sub-call can fail independently. Testing the full matrix (8 combinations) requires either:

  • Reflection + partial mocking (brittle)
  • Refactoring to inject Package objects that return controlled values

Suggested fix: Extract the per-package logic into a testable method or make it protected static so a test subclass can call it.

5.2 verify_signature_on_download() — Tight coupling to WP_Upgrader

The function receives $upgrader by type-hint, calls $upgrader->download_package(), then verify_file_signature(). Mocking a WP_Upgrader is impractical because download_package() is final or does real filesystem work.

Suggested fix: Extract the download-and-verify step into a separate function:

function download_and_verify( string $url, string $expected_signature, array $trusted_keys ): string|WP_Error

Then test the wrapper with a mock HTTP layer, and leave the hook glue in verify_signature_on_download untested (integration-tested instead).

5.3 upgrader_source_selection() — Filesystem operations

Renames directories on disk. Testing requires real temp directories, which is doable in PHPUnit but cumbersome.

Suggested fix: Extract the path-munging logic (hash detection, destination computation) from the filesystem operations:

function compute_destination_path( string $source, string $remote_source, array $hook_extra ): string

Unit-test the computation; leave the rename() call for integration tests.


6. Already-Good Tests Worth Noting

These tests are solid and should serve as patterns for new tests:

  • SignatureVerificationTest.php — Comprehensive coverage of the crypto pipeline. Tests key decoding, key matching, valid/tampered/wrong-key/wrong-signature verification. The fixture generation is clean. Good use of @group signature.

  • MetadataDocumentFromDataTest.php — Tests all mandatory field validation, optional field defaults, multiple releases, and error propagation. The factory pattern (MetadataDocumentFactory) keeps test data clean.

  • DisplayPluginUpdateErrorTest.php — Good output buffering approach for testing HTML generation. Tests no-output, error-output, active-class, XSS sanitization, and colspan. Strong model for other rendering tests.

  • PickArtifactByLangTest.php — Exhaustive locale matching: exact, prefix, fallback, underscore normalization. The test_filter_can_override_selection test validates the extension point.

  • direct-install.spec.ts (browser) — Accessibility-first testing. Verifies labels, ARIA attributes, keyboard navigation, and heading hierarchy before testing functionality. This is the right priority order for UI tests.


7. Prioritized Action Items

Immediate ( done)

# Status Action Effort
1 Delete SampleTest.php Trivial
2 Remove redundant assertions from GetPackagesTest Trivial
3 Remove duplicate assertions from AvatarHttpTest Trivial
4 Remove WordPress core filter-fire test Trivial
5 Remove bootstrap filter-registration test Trivial
6 Replace reflection with Updater::reset() Small

High Priority ( done)

# Status Action Effort
7 upgrader_source_selection unit tests Medium
8 verify_signature_on_download unit tests Medium
9 get_trusted_keys base64 recoding unit test Small
10 Package::get_release() memoization unit tests Small

Zero-Refactoring ( ALL DONE)

# Status Action Maps to Commit
11 Fix transient internals assertion 2.4 c2eeb9e
12 Replace fixture-structure assertions with behavioral ones 2.3 c2eeb9e
13 Multi-key trust test (two fair keys documented as intentional) 3.2 c2eeb9e
14 Move pipeline-mock tests from unit to integration layer 2.2 a180991
15 Test plugin_api_details with mocked DID pipeline 4.2 2b675f2
16 Error propagation e2e (error → transient skip → error row) 4.5 2b675f2
17 Beef up browser test assertions (error row seeding) 4.6 2b675f2
18 Theme update HTML unit tests 4.4 27ef1fb

Needs Refactoring ( blocked)

All 5 remaining items require production code changes — none are doable without refactoring.

# Status Action Why blocked
19 update_site_transient unit tests private static — needs to become protected
20 Extract per-package logic from update_site_transient() Same function, same blocker
21 Refactor verify_signature_on_download into testable + glue layers Cleanup only; function already tested (3.1 )
22 Refactor upgrader_source_selection path computation into pure function Cleanup only; function already tested (3.5 )
23 Make update_site_transient protected instead of private Enables #19, #20