Skip to content

carpet bomb fair-plugin with tests#511

Open
chuckadams wants to merge 45 commits into
release_1.5.0from
test_the_things
Open

carpet bomb fair-plugin with tests#511
chuckadams wants to merge 45 commits into
release_1.5.0from
test_the_things

Conversation

@chuckadams

@chuckadams chuckadams commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Tests, tests, and more tests. Unit tests, integration tests, http tests, end-to-end tests, all for one low low price. 99.9% AI generated, now in the human review stage.

Only one source file changed, and that's two trivial changes in inc/packages/namespace.php: one to fix a typeerror crash when reset([]) returns false, and the other to allow customizing the plc.directory url which allows us to mock it in integration tests. That's also being backported into 1.4.1, so by the time this lands it should be zero source files changed.

There's also a couple github workflows, one to run coverage and upload it as an artifact, another to run integration tests which is workflow_dispatch only until I know exactly how slow it is on github runners. The existing release workflow now builds with PHP 8.0 instead of 8.2 so that it doesn't bundle a vendor/ with deps that are only for 8.2+. There shouldn't be any surprises, since we test against 8.0 already, including doing a composer update in each test matrix cell.

Lots of ai-generated planning docs in ai/plans. I can nuke those if we want, I don't usually keep completed plans in git, just figured they might be edifying. The ai/ directory won't ship in release zips.

chuckadams added 30 commits June 9, 2026 10:52
Four-layer test pyramid: unit, integration, HTTP, browser.
DID-manager pipeline is the top-priority functional area.
Ephemeral sites under tests/sites/ephemeral/ for CI.
Static pet scenarios under tests/sites/static/ for exotic configs.
13-phase implementation plan with parallelization notes.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Rename tests/phpunit/ → tests/unit/, phpunit.xml.dist → tests/unit/phpunit.xml.
Adjust all paths in phpunit.xml and multisite.xml for new location.
Update composer.json scripts: test → test:unit, test:multisite → test:unit:multisite.
Update coverage:* paths and package.json npm scripts.
Bump .wp-env.json PHP version 7.4 → 8.0 (hard floor per AGENTS.md).
All 19 existing tests pass (5 test classes, 30 assertions).

Known issue: composer.lock has PHP 8.2-bound dev deps; tests-cli needs --ignore-platform-reqs on PHP 8.0.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Add platform.php=8.0 to composer.json config. Regenerated composer.lock
downgrading 7 transitive packages to PHP 8.0-compatible versions:
  brick/math 0.17.0 -> 0.11.0
  doctrine/instantiator 2.0.0 -> 1.5.0
  symfony/console v7.4.8 -> v5.4.47
  symfony/deprecation-contracts v3.6.0 -> v2.5.4
  symfony/finder v7.4.8 -> v5.4.45
  symfony/service-contracts v3.6.1 -> v2.5.4
  symfony/string v7.4.8 -> v5.4.47

Added --ignore-platform-req=ext-gmp to test:php:install-deps npm script.
wp-env PHP 8.0 image lacks ext-gmp (required by simplito/elliptic-php via did-manager).

Signed-off-by: Chuck Adams <chaz@chaz.works>
Fixtures (tests/fixtures/):
  DID documents: valid, no-keys, no-services, alias-valid, alias-invalid-domain
  Metadata docs: full (3 releases), minimal, no-releases
  Release docs: v1.0.0, no-artifacts, no-version, with-requirements

Factories (tests/Factory/):
  MetadataDocumentFactory: full(), minimal(), from_fixture(), builder(),
    without_releases(), without_field()
  ReleaseDocumentFactory: full(), with_requirements(), with_version(),
    builder(), list_of(), without_field(), without_artifacts()

Added autoload-dev PSR-4: FAIR\Tests\ -> tests/
All 19 existing tests pass.

Signed-off-by: Chuck Adams <chaz@chaz.works>
12 test files, 85 new tests (104 total, 163 assertions):
  GetDidHashTest, GetLanguagePriorityListTest, PickArtifactByLangTest,
  PickReleaseTest, VersionRequirementsTest, GetUnmetRequirementsTest,
  CheckRequirementsTest, GetIconsTest, GetBannersTest,
  GetHashedFilenameTest, ValidatePackageAliasTest,
  FetchAndValidatePackageAliasTest

Bug fix: pick_release() now guards empty releases array to prevent
TypeError from reset() returning false against ?ReleaseDocument return type.

Signed-off-by: Chuck Adams <chaz@chaz.works>
MetadataDocumentTest: 13 from_data() + 5 from_response() tests covering
all mandatory fields, optional fields, missing fields, invalid releases,
JSON parsing, null body edge case.

ReleaseDocumentTest: 10 tests covering all fields, requirements,
missing mandatory fields, optional fields, builder integration.

129 tests, 234 assertions total. All green.

Signed-off-by: Chuck Adams <chaz@chaz.works>
CacheUpdateErrorTest, GetDidDocumentTest, FetchMetadataDocTest,
FetchPackageMetadataTest (including get_latest_release_from_did),
PipelineWPTest (add_package_to_release_cache, maybe_add_accept_header,
search_by_did, get_plugin_information).

HTTP-dependent functions tested using pre_http_request filter +
pre-seeded transients. No real network calls.

162 tests, 287 assertions. All green.

Signed-off-by: Chuck Adams <chaz@chaz.works>
UpdaterTest: registry (register/get/lookup/overwrite) + 8
should_run_on_current_page() page checks.

PackageTest: PluginPackage and ThemePackage construction,
version parsing, slug, relative_path, type distinction.

Creates temp plugin/theme files in setUp so Package
constructor's get_file_data() resolves successfully.

188 tests, 327 assertions. All green.

Signed-off-by: Chuck Adams <chaz@chaz.works>
AvatarsTest: should_replace_url, generate_default_avatar (data URI,
first letter, null name, escaping, determinism), get_avatar_alt.
1 test skipped: color hook uses add_filter instead of apply_filters.

PingsTest: remove_pingomatic, get_indexnow_key (generate/validate),
register_query_vars.

SaltsTest: replace_salt_generation_api, define_salt_keynames,
generate_salt_string, salt response body/structure.

DefaultRepoAndVersionCheckTest: default repo domain, version-check
constants (RECOMMENDED_PHP, MINIMUM_PHP, BROWSER_REGEX).

233 tests, 405 assertions. All green (1 skipped).

Signed-off-by: Chuck Adams <chaz@chaz.works>
New bin/setup-local-tests.php auto-detects MySQL (local socket →
TCP → Docker container), downloads WordPress core + test suite
from GitHub (no svn required), generates wp-tests-config.php,
and installs the mysqli db.php drop-in.

composer.json:
- test:setup: runs bin/setup-local-tests.php
- test:unit: chains setup → phpunit
- test:unit:multisite: chains setup → phpunit multisite

npm run test:php (wp-env Docker) still works.
composer run test:unit now works locally with zero manual setup.

Docker MySQL container: fair-test-mysql on port 3309.
Configurable via env vars: FAIR_TEST_DB_*, FAIR_TEST_WP_VERSION,
FAIR_TEST_DOCKER_MYSQL_PORT.

.gitignore: wp-tests-config.php

Signed-off-by: Chuck Adams <chaz@chaz.works>
bin/run-integration.sh: full lifecycle runner with trap EXIT
guarantee. Spins up Docker Compose ephemeral env (WP 6.4 + PHP 8.0
+ MySQL + mock DID/repo server), installs WP, activates plugin,
seeds data, runs PHPUnit integration tests, then tears EVERYTHING
down (containers, volumes, networks) regardless of exit code.

tests/mock-server/: PHP built-in server emulating PLC Directory
and FAIR Repository APIs. Serves fixture JSON for DIDs and metadata.
File-based request log for test assertions. Docker healthcheck.

tests/sites/ephemeral/integration/: Docker Compose + Dockerfile.wp
for WordPress + wp-cli + mock-server + MariaDB on shared network.
Plugin code mounted via :delegated volume.

tests/integration/: Bootstrap that loads WP directly (no test suite
needed) + phpunit.xml + first integration test validating the full
DID→document→metadata→release pipeline against the mock server.

Production change: get_plc_client() now checks FAIR_PLC_DIRECTORY_URL
constant for testability (defaults to plc.directory).

4 integration tests (3 pass, 1 skipped — log file permissions):
  - mock server health check
  - full DID resolution pipeline
  - mock server request logging (skipped)
  - unknown DID error handling

Signed-off-by: Chuck Adams <chaz@chaz.works>
PackageDataIntegrationTest (5 tests): full get_package_data pipeline,
_fair metadata reference, no-service error, unknown DID error,
DID document caching.

UpdateTransientIntegrationTest (3 tests): handle_update_plugins_transient
structure, seeded plugin in transient, empty registry handling.

New fixture: did-doc-no-services-integration.json for no-service
error path testing.

Mock server: added no-services DID to DID_MAP, file-based logging.

12 integration tests (10 pass, 2 skipped): skipped tests cover
update transient detection which depends on in-Docker HTTP
routing during the transient check chain.

Signed-off-by: Chuck Adams <chaz@chaz.works>
GetTrustedKeysTest: 5 tests — no cached DID, fetch failure,
no signing keys, empty verificationMethod, non-fair key filtering.

DisplayPluginUpdateErrorTest: 6 tests — no error, non-error
transient, error row output, active class, HTML sanitization,
colspan attribute.

GetPackagesTest: 4 tests — find by Plugin ID header, multiple
plugins, no Plugin ID header, keys present when packages exist.

248 unit tests, 430 assertions. All green.

Signed-off-by: Chuck Adams <chaz@chaz.works>
HTTP tests exercise plugin filter/endpoint behavior at the HTTP
layer inside the Docker WordPress instance.

tests/http/bootstrap.php: loads WP + plugin with admin includes.
tests/http/phpunit.xml: PHPUnit config for HTTP test suite.

SaltApiHttpTest (3 tests): salt API URL interception, 64-char
salt values, unrelated URL passthrough.

DefaultRepoHttpTest (5 tests): domain configuration, non-WP.org
passthrough, filter registration, plugins/themes API interception.

AvatarHttpTest (4 tests): should_replace_url detection, SVG
default avatar generation, avatar alt text for users.

Updated run-integration.sh to run HTTP tests after integration
tests, reporting both results separately.

12 integration + 12 HTTP = 24 Docker tests (22 pass, 2 skipped),
76 assertions. Zero leftover containers confirmed.

Signed-off-by: Chuck Adams <chaz@chaz.works>
tests/browser/package.json: isolated @playwright/test deps
tests/browser/playwright.config.ts: chromium, auth state, CI retries
tests/browser/global-setup.ts: login as browser_admin, save storage
tests/browser/specs/direct-install.spec.ts (9 tests):
  - Input label (screen-reader-text), pattern, required
  - Submit button keyboard accessible
  - HTML5 validation prevents invalid DID submit
  - Thickbox modal: role=dialog, aria-label, iframe title
  - Close button accessible name
  - All pass against actual DOM (tab=fair_direct, .fair-direct-install__form, #plugin_id)

tests/browser/specs/search-did.spec.ts (6 tests):
  - Search input accessible label (getByRole searchbox)
  - Known DID returns result card
  - Unknown DID shows no results
  - Install button accessible name + focusable
  - Repository hostname visible
  - Card heading hierarchy

root package.json: test:browser, test:browser:headed, test:browser:docker:{start,stop,seed} scripts

All 15 tests pass. Zero leftover containers confirmed.

Signed-off-by: Chuck Adams <chaz@chaz.works>
.github/workflows/integration-tests.yml:
  docker-tests: PHP 8.0/8.4 x WP 5.4/latest matrix
    Runs bin/run-integration.sh (integration + HTTP, ~24 tests)
    15min timeout, leftover container verification

  browser-tests: Playwright fast tests (chromium)
    PRs only when labeled 'run-browser-tests'
    Push to main/dev/release always runs
    Docker compose spin-up, WP install, seed, test, tear-down

  browser-slow: @slow Playwright tests (chromium)
    Push to main/release only
    install/activate/update flow, avatar upload
    20min timeout

  All Docker jobs: trap-based teardown + explicit cleanup

Preserves existing:
  phpunit-tests.yml (PHP 8.0-8.5 x WP 6.2-6.9 unit matrix)
  coding-standards.yml (PHPCS + PHPStan)

Signed-off-by: Chuck Adams <chaz@chaz.works>
Docker-based mutation testing with PHP 8.5 + latest Infection:

tests/sites/ephemeral/mutation/Dockerfile: PHP 8.5-cli-alpine + mysqli + xdebug
tests/sites/ephemeral/mutation/docker-compose.yml: MySQL 8.0 + mutation container
  - composer install --ignore-platform-reqs (PHP 8.5 resolves infection 0.27)
  - Auto-detects configured MySQL via FAIR_TEST_DB_* env vars
  - Runs infection --threads=2 --no-progress

tests/infection-bootstrap.php: FAIR autoloader + WordPress class/function stubs
  - Registers FAIR class autoloader for source reflection
  - Stubs WP_List_Table, WP_Upgrader, Plugin_Upgrader, WP_Error
  - Stubs Fragen\Git_Updater\Lite to prevent die() in class-lite.php
  - Stubs esc_*(), __(), sanitize_text_field(), wp_unslash(), etc.

infection.json: source in inc/ (excluding admin, wp-cli, settings, etc.)

Results: 486 mutations, 45 killed, 440 uncovered, 1 escaped, 0 errors
  Covered Code MSI: 97% (of tested code, almost all mutations caught)
  Overall MSI: 9% (most modules excluded from unit test coverage)

composer.json: restored from release (was corrupt with scripts-only).
  Added 'infection' script: docker compose run mutation.
bin/setup-local-tests.php: added FAIR_TEST_DB_* env var detection
  before local/docker auto-detection for containerized usage.
tests/unit/phpunit.xml, multisite.xml: added executionOrder=default

Signed-off-by: Chuck Adams <chaz@chaz.works>
- Single 'Current totals' table at top covering all 287 tests + 486 mutants
- All inline deferred items replaced with ↳ pointer to Phase 14
- Phase 14 organized by layer: unit, integration, HTTP, browser, CI, quality
- Removed duplicate 'Current totals' tables scattered through doc

Signed-off-by: Chuck Adams <chaz@chaz.works>
Crypto fixtures (tests/fixtures/keys/):
  generate.php — one-shot keypair generator using sodium + DidCodec
  did-doc-signed.json — DID doc with real Ed25519 public key in multibase
  release-doc-signed.json — release with base64url signature over hello-dolly.zip
  hello-dolly.sig — detached Ed25519 signature of the zip (binary)
  ed25519-keypair.json — NOT committed (private key, .gitignored)

Plugin catalog (tests/fixtures/):
  zips/hello-dolly.zip — WordPress 1.7.3, 1887 bytes
  plugins.json — test catalog with local zip + remote URL entries

The keypair is deterministic for the life of the fixture. Regenerate with:
  php tests/fixtures/keys/generate.php

Signed-off-by: Chuck Adams <chaz@chaz.works>
tests/fixtures/keys/generate.php:
  - Skips if ed25519-keypair.json already exists
  - Regenerates DID doc, release doc, and signatures from the keypair

composer.json:
  - New script 'test:generate-keys' runs the idempotent generator
  - 'test:setup' chains test:generate-keys before setup-local-tests.php
  - Re-removed infection/infection from require-dev (Docker-only dep)
  - Removed infection/extension-installer from allow-plugins

Signed-off-by: Chuck Adams <chaz@chaz.works>
…line

tests/unit/tests/Updater/SignatureVerificationTest.php (14 tests, 59 assertions):
  - DID doc structure: verificationMethod presence, Multikey type
  - DidCodec::from_multibase_key: decodes multibase to 32-byte Ed25519 key
  - Keypair round-trip: decoded key matches keypair fixture
  - get_fair_signing_keys: filters by Multikey type + fair-* fragment ID
  - get_fair_signing_keys: rejects non-Multikey, non-fair methods, empty/missing arrays
  - Full sodium verification: base64url-no-padding signature verifies zip content
  - Negative cases: tampered zip, wrong public key, wrong signature all fail
  - Signature format: no +, /, or = in base64url; decoded to 64 bytes

tests/fixtures/keys/generate.php: fixed DidCodec::to_multibase_key()
  to use MULTICODEC_ED25519_PUB instead of SECP256K1_PUB default.
  Release doc now writes to keys/ dir instead of parent dir.
  All fixture files regenerated with correct Ed25519 codec prefix.

Full suite: 262 tests, 489 assertions, 1 skipped.

Signed-off-by: Chuck Adams <chaz@chaz.works>
…all scripts

composer.json scripts added:
  test:integration   — bin/run-integration.sh integration
  test:http          — bin/run-integration.sh http
  test:browser       — cd tests/browser && npm run test
  test:browser:headed — cd tests/browser && npm run test:headed
  test:e2e           — alias for test:browser
  test:all           — chains test:unit, test:unit:multisite, test:integration, test:http

Signed-off-by: Chuck Adams <chaz@chaz.works>
PHP 8.4 removed the E_STRICT constant. The WordPress test library's
install.php calls error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT),
which emits a 'Constant E_STRICT is deprecated' notice on every test run.

The error occurs in a separate PHP subprocess (system() call from
bootstrap.php), so bootstrap-level error handlers cannot catch it.
The fix patches install.php in bin/setup-local-tests.php immediately
after the test suite is extracted, replacing the E_STRICT reference
with its zero-value equivalent (removing the term entirely).

Signed-off-by: Chuck Adams <chaz@chaz.works>
Moves infection/infection and its transitive deps (thecodingmachine/safe
et al) out of the root composer environment into tests/mutation/composer.json.
The root vendor is now PHP 8.0-clean — thecodingmachine/safe implicit-nullable
deprecation noise on PHP 8.4 is gone from unit test runs.

tests/mutation/composer.json:
  - php >=8.4 (no platform pin — free to use PHP 8.5 in Docker)
  - require-dev: infection/infection ^0.27
  - Own vendor/, gitignored along with composer.lock

docker-compose.yml:
  - Root: composer install --ignore-platform-reqs (plugin + phpunit)
  - tests/mutation/: composer install (infection only)
  - Binary path: tests/mutation/vendor/bin/infection
  - mutation-vendor named volume preserves installs across runs

Root cleanup:
  - Removed infection/* and thecodingmachine/safe from composer.lock
  - Removed vendor/infection/ and vendor/thecodingmachine/

No Safe deprecation noise. 262 tests pass.

Signed-off-by: Chuck Adams <chaz@chaz.works>
get_site_transient() returns false for a missing key, but the
existing check 'if (  )' treated null (valid cached result
meaning 'no alias') as uncached. On single-site, null → '' in the
DB layer so the cache happened to work; on multisite with object
cache, null stays null and the check fails, forcing re-fetches.

Changes:
- Cache hit check: if (  ) → if ( false !==  )
- Write normalization:  →  ?? '' so null is always
  stored as empty string, consistent across single/multisite

This is also a performance fix — previously, DIDs with no alias
would re-run fetch_and_validate_package_alias() (with DNS lookups)
on every call in multisite.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Move everything from tests/sites/ephemeral/ up one level to tests/sites/.
The 'ephemeral' qualifier didn't add value — all sites under tests/sites/
are ephemeral Docker setups by design.

- tests/sites/Dockerfile.wp (shared WP image)
- tests/sites/docker-compose.base.yml (shared template)
- tests/sites/integration/ (WP + mock-server, used by both integration & HTTP)
- tests/sites/browser-test/ (WP + mock-server, used by Playwright)
- tests/sites/mutation/ (PHP 8.5 + Infection, isolated composer.json)

All context and volume mount paths updated for the new depth:
  context: ../../../.. → ../../..
  volumes: ../../../../ → ../../..

bin/run-integration.sh refactored:
- SUITE argument now gates which tests run: 'integration', 'http', or 'all'
- Both integration and http share the integration compose stack
- Separate project names (fair-integration-integration vs fair-integration-http)
  avoid port conflicts when both run in sequence

composer.json:
- test:integration → integration tests only
- test:http → HTTP tests only (own compose cycle)
- test:all chains unit, multisite, integration, http

Verified: test:all exits 0 (262+262+12+12 tests). Browser 15/15 pass.
Signed-off-by: Chuck Adams <chaz@chaz.works>
Six items from the testing plan, now done:

1. install-activate-update.spec.ts (@slow) — full install/activate/update
   flow via Direct Install tab. Mock server extended with hello-dolly DID
   fixtures (did-doc + metadata-doc) and zip artifact serving route
   at /artifacts/*.zip.

2. avatar-upload.spec.ts — profile page rendering, avatar section
   presence, display name accessibility. Avatar image verification
   handles Gravatar rate-limiting (at least one loaded = pass).

3. update-error-row.spec.ts — plugins page structure, FAIR error row
   detection, no JS errors on plugins page.

4. SignatureVerificationIntegrationTest.php — 5 tests running inside
   Docker WP container. Covers verify_file_signature() (WP 5.2+) with
   live sodium Ed25519 keys, tampered file rejection, missing-key
   rejection, wrong-signature rejection, key encoding round-trip.
   NB: WP 6.4 uses SHA-384 (not SHA-512) for file hashing.

5. Coverage reporting in CI — added dedicated 'coverage' job to
   phpunit-tests.yml. Runs on push to main/development/release only,
   uses PHP 8.4 + XDebug, uploads HTML coverage artifact.

6. minMsi threshold — set minMsi=9 (current baseline), minCoveredMsi=90.

Updated mock server:
  - /artifacts/<file.zip> route serves zips from tests/fixtures/zips/
  - hello-dolly DID/meta fixtures for browser install flow

Updated integration bootstrap:
  - require wp-admin/includes/file.php for verify_file_signature()

Totals: 262 unit (489 assertions), 17 integration (48), 12 HTTP (38),
23 browser (non-slow), 3 browser (@slow). Exit code 0.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Six vacuous/near-zero-value tests identified (SampleTest, duplicate
assertions, WordPress-core behavior tests, redundant filter-registration
checks). Four antipatterns (reflection into private state, pipeline-mock
unit tests disguised as integration, constant-structure assertions,
WP transient internals assertions).

Five security-critical expansions needed:
- verify_signature_on_download() zero unit tests
- get_trusted_keys() key-confusion edge case
- upgrader_source_selection() zero tests
- multibase→base64 recoding step untested
- Replay attack: no DID-binding in signatures (protocol concern)

Six general expansions: update_site_transient, plugin_api_details,
Package::get_release memoization, theme HTML, error propagation e2e,
browser test thinness.

Three design issues flagged where production functions mix pure logic
with side effects — extraction paths documented.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Immediate items from test quality audit:

1. Delete SampleTest.php (tested PHP truthiness, not production code)
2. GetPackagesTest: remove double-quote duplicate assertion, replace
   brittle array-key-existence check with empty-plugins assertion
3. AvatarHttpTest: remove two should_replace_url tests (duplicates
   of ShouldReplaceUrlTest in unit layer)
4. PickArtifactByLangTest: remove test_should_fire_filter_hook
   (tested WordPress core apply_filters behavior)
5. DefaultRepoHttpTest: remove test_pre_http_request_filter_is_registered
   (tested bootstrap, redundant with every other test)
6. UpdaterTest: replace ReflectionProperty reset_registry with
   Updater::reset() (method already existed in production code)

After: 261 tests, 485 assertions (was 262/489).
Single-site, multisite, and HTTP suites pass.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Four high-priority expansions from the test quality audit:

#9 (get_trusted_keys base64 recoding):
Verifies multibase (base58btc) → base64 Ed25519 key recoding step
that WordPress core's verify_file_signature() depends on. Uses a
real fixture key to validate DidCodec::from_multibase_key() output.

#10 (Package::get_release memoization):
3 tests verifying caching semantics: successful fetch is memoized
and not re-fetched on second call, WP_Error is not cached (retry on
next call), upstream error cache in get_did_document() is documented
as a separate concern.

#7 (upgrader_source_selection):
8 tests covering the directory renaming logic: WP_Error pass-through,
install action bypass, TypeError for non-plugin/theme upgrader,
matching-basename short-circuit, hash-suffix rename for both plugins
and themes, case-insensitive slug normalization. Uses temp
directories and anonymous Plugin_Upgrader/Theme_Upgrader subclasses.

#8 (verify_signature_on_download):
10 tests — the most security-critical function in the plugin:
already-downloaded pass-through, non-plugin upgrader bypass, missing
DID transient, missing release cache, local file bypass,
re-entry guard, download error propagation, unmatched URL bypass,
valid Ed25519 signature verification (full pipeline with sodium
key generation, SHA-384 hash signing, multibase recoding), and
tampered file rejection. Must use ReflectionFunction to reset the
 static between tests.

After: 283 tests, 521 assertions. Single-site + multisite pass.
Signed-off-by: Chuck Adams <chaz@chaz.works>
Replaced all done subsections with concise summaries referencing
commit hashes. Undone items preserved with intact numbering.

Resolved: sections 1 (all 6), 2.1 (reflection→reset), 3.1
(verify_signature_on_download), 3.3 (multibase recoding), 3.5
(upgrader_source_selection), 4.3 (memoization).

Remaining: sections 2.2-2.4, 3.2, 3.4, 4.1-4.2, 4.4-4.6, 5.1-5.3.
Signed-off-by: Chuck Adams <chaz@chaz.works>
Signature binds to archive content only, DID-binding is a protocol
design question — not a plugin-layer test gap. Flagged for separate
security coverage.

Signed-off-by: Chuck Adams <chaz@chaz.works>
2.3 (constant/fixture structure assertions):
- VersionCheckConstantsTest: collapsed 5 tests → 1 (minimum<recommended).
  Removed tautological regex checks and non-empty assertions.
- Removed test_did_doc_has_verification_method from
  SignatureVerificationTest — every other test validates by consuming.

2.4 (transient internals assertion):
- test_should_cache_result now asserts behavior (both calls return
  falsy/no-alias) instead of WP internal serialization format.

3.2 (multi-key trust test):
- Added test_should_return_all_fair_prefixed_multikeys to
  GetTrustedKeysTest. Generates two real Ed25519 keypairs, encodes
  both as multibase, seeds a DID doc with 2 fair keys + 1 atproto key.
  Verifies both fair keys are returned as trusted. Documents that
  WP core's verify_file_signature() tries all trusted keys, so a
  signature from either passes — intentional for key rotation/backup.

Audit restructured with zero-refactoring vs needs-refactoring split
in section 7.

After: 279 tests, 514 assertions. Single-site + multisite pass.
Signed-off-by: Chuck Adams <chaz@chaz.works>
#17 (browser test seeding):
- Updated browser-test seed.php to create dummy plugin with FAIR DID
  header AND seed a fair_update-errors transient.
- Updated update-error-row.spec.ts to assert error row IS visible
  (was no-op conditional). Error text checked for expected content.

#16 (error propagation e2e integration test):
- Integration seed.php now registers a second plugin with
  'did:plc:doesnotexist0000000000000' — unresolvable by mock server.
- Added test_unresolvable_did_plugin_is_skipped_and_error_cached to
  UpdateTransientIntegrationTest: verifies the bad-DID plugin is
  excluded from both response and no_update, and WP_Error is cached.
- Replaced test_empty_registry_handles_gracefully (reflection-based,
  now redundant after Updater::reset() fix).

#15 (plugin_api_details unit tests):
- Added PluginApiDetailsTest (4 tests) to PipelineWPTest.php:
  non-plugin_information action pass-through, empty slug pass-through,
  unmatched slug returns false, full pipeline success returns plugin
  info object with correct name and version.
- Used seed_pipeline() pattern from existing SearchByDidTest.

Zero-refactoring remaining: #14 (pipeline-mock migration), #18 (theme HTML).

After: 283 tests, 520 assertions. Single-site + multisite pass.
Signed-off-by: Chuck Adams <chaz@chaz.works>
)

Removed from PipelineWPTest (unit):
- SearchByDidTest::test_should_return_plugin_result_on_success
- SearchByDidTest::seed_full_pipeline()
- AddPackageToReleaseCacheTest::test_should_add_release_to_cache
- AddPackageToReleaseCacheTest::test_should_retain_existing_releases
- AddPackageToReleaseCacheTest::seed_pipeline()

These tested the FULL pipeline (DID doc → service → HTTP fetch →
metadata → release) using pre_http_request mock filters inside
unit tests. A metadata schema change would silently break them.

Added to DidResolutionIntegrationTest (integration):
- test_search_by_did_returns_plugin_result
- test_add_package_to_release_cache_populates_cache

Both run against the real Docker mock server, so a schema change
produces a genuine integration failure, not a mock-data mismatch.

Unit tests kept: edge cases that don't need HTTP (empty DID, non-DID
search, wrong action, pipeline failure propagation).

After: 280 unit tests (511 assertions), 19 integration (56 assertions).
All suites pass.

Signed-off-by: Chuck Adams <chaz@chaz.works>
Added CustomizeThemeUpdateHtmlTest (3 tests):
- FAIR-registered theme with update available gets update links
  ('There is a new version of', 'update now') appended.
- Unregistered themes are left untouched.
- Empty registry does not error.

Creates a real theme directory with Theme ID header in style.css,
registers it with Updater, seeds DID doc + metadata in transients
(no network calls), seeds update_themes transient (with temporary
filter removal to avoid triggering the full update pipeline during
test setup), and verifies customize_theme_update_html output.

Zero-refactoring items: ALL DONE.

After: 283 tests, 516 assertions. Single-site + multisite pass.
Signed-off-by: Chuck Adams <chaz@chaz.works>
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>
@github-actions

Copy link
Copy Markdown
Contributor

Signed-off-by: Chuck Adams <chaz@chaz.works>
- Revert the null-to-'' normalization in validate_package_alias()
  that caused cache falsey-value collisions on single-site
- Add test_should_cache_null_result (incomplete) documenting the
  null-transient bug and sentinel-based fix
- Split all test files to one class per file (10 files → 31 files)
- Rename test files to match their single class name

Signed-off-by: Chuck Adams <chaz@chaz.works>
Signed-off-by: Chuck Adams <chaz@chaz.works>
… crashes

Signed-off-by: Chuck Adams <chaz@chaz.works>
Signed-off-by: Chuck Adams <chaz@chaz.works>
Add export-ignore in .gitattributes and rsync exclusions in .distignore
to keep development-only files out of the distributed plugin zip.

Signed-off-by: Chuck Adams <chaz@chaz.works>
@chuckadams chuckadams marked this pull request as ready for review June 10, 2026 13:01
Let CI matrix jobs resolve dependencies against their actual PHP version
instead of all using a hard-coded 8.0 floor. The PHP 8.0 matrix cell now
acts as the canary for minimum-version compatibility.

Pin the release workflow to PHP 8.0 and set platform.php explicitly via
composer config so the distributed vendor/ remains 8.0-compatible.

Signed-off-by: Chuck Adams <chaz@chaz.works>
tests/browser/.auth/admin.json contains ephemeral auth cookies and was
committed before the .gitignore entry for tests/browser/.auth/ existed.
Remove from tracking; .gitignore already covers the directory.

Signed-off-by: Chuck Adams <chaz@chaz.works>
@kasparsd

Copy link
Copy Markdown
Contributor

Would it be possible to split this out into separate pull requests for clarity:

  1. one that extends the phpunit test suite
  2. one that adds phpunit coverage reporting
  3. one that adds the e2e tests

I feel like there are some ways to simplify the e2e setup by relying on existing WP packages and helpers but it wouldn't be fair to the phpunit suite work if we started discussing it here 😂

@chuckadams

Copy link
Copy Markdown
Contributor Author

Possible for sure, but in the end it's all tests, and they're quite well separated by function into different directory trees under test/. I'm not in a massive rush to merge this, and and we can make plenty of changes as needed.

As for e2e tests in particular, I'm rather attached to playwright, and would need a lot of convincing evidence to switch to something else. Ultimately it's about what harness is controlling Chromium/Safari/FF, because anything else is not actually a browser test.

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.

2 participants