Skip to content

refactor: move the plugin decisions into src - #11

Merged
vitormattos merged 17 commits into
test/integration-harnessfrom
refactor/decision-logic-in-src
Sep 18, 2026
Merged

vitormattos merged 17 commits into
test/integration-harnessfrom
refactor/decision-logic-in-src

Conversation

@YvesCesar

@YvesCesar YvesCesar commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

The plugin was entirely procedural: hook callbacks that read global $wp, $_SERVER and the options, and decided in the middle of applying the effect. This separates the two, on top of the characterization suite from #10 (hence the base branch; it retargets to main once #9 and #10 are merged).

src/

src/ holds the decisions, includes/ and the main plugin file hold the wiring. A decision receives values and returns values, so it is covered by a data provider and no test double.

src/Account/Navigation.php        # entries, labels and the active one
src/Account/RootEndpoint.php      # account screens served from the site root
src/Github/DeployDispatch.php     # when publishing asks GitHub for a deploy
src/Github/SiteDeploy.php         # which run publishes the site
src/Github/WebhookDecision.php    # what to answer a delivery
src/Github/WebhookGate.php        # inspection of a delivery
src/Github/WebhookRequest.php     # headers and body of a delivery
src/Github/WebhookSignature.php   # the HMAC GitHub signs with
src/Github/WorkflowRun.php        # the run a payload describes
src/Settings/Secret.php           # the cipher of the token and the secret
src/Subscription/StatusChange.php # changes that ask for a confirmation

The plugin is installed by cloning the repository, so Composer never runs on the server and its autoloader does not exist at runtime: src/Autoloader.php maps the namespace to src/ and is the only file the plugin requires by hand. The autoload section of composer.json describes the same mapping for PHPStan and PHPUnit.

What each rewired file kept:

  • libresign_receive_github_site_deploy_webhook() went from seven nested branches to building a WebhookGate, asking it for a decision and turning that decision into a response. The transient guarding duplicated deliveries, the fragment sync and the recorded result stayed, since those are effects.
  • libresign_filter_account_menu_items(), libresign_filter_account_menu_item_classes() and libresign_filter_account_endpoint_title() became one line each, passing libresign_get_current_query_vars() to Navigation instead of having it read global $wp for them.
  • libresign_is_root_my_account_endpoint_request() keeps reading and sanitizing $_SERVER['REQUEST_URI'] and hands the path to RootEndpoint::matches().
  • The settings registered on admin_init moved from an anonymous function to libresign_register_settings(). tests/Support/RegistersPluginSettings.php no longer has to walk $wp_filter with Reflection to find that closure, which its own docblock asked for.

The cipher was written out three times — decrypting the webhook secret, decrypting the deploy token and encrypting both on save — and only the webhook copy handled a value stored in plain text. Settings\Secret is now the single copy, and Secret::from_salts() takes the salts as arguments instead of reading the constants, which is what makes it testable.

The two uses do not read a stored value the same way, so there are two methods. decrypt() returns a value it cannot decipher untouched, which is what the webhook secret has always done and is safe because that value never leaves the site. decrypt_or_discard(), used for the deploy token, returns it only when it was never encrypted at all — a token saved by hand has an underscore, which base64 does not, so it still works — and drops it when it is base64 that does not decipher, which means it was encrypted with salts this installation no longer has. That value is not the token and it is on its way to GitHub, so it must not leave the site.

DeployDispatch::triggers_deploy() carries the current condition with the parentheses PHP already applies, && binding tighter than ||. The behaviour is unchanged and the test names it; the bug is still there to be decided on separately.

Tests

227 tests, up from 143. tests/Unit/ now mirrors src/ and tests/Integration/ mirrors the plugin files, in both cases file by file with Test.php appended.

The matrices that used to go through WordPress moved to unit tests with a data provider: the endpoint titles and the highlighted entry no longer need global $wp, and every answer the webhook endpoint can give — unconfigured secret, another client, wrong signature, ping, unsupported event, unparsable payload, a run that is not the production deploy, and the deploy itself — is decided without a REST server. The integration tests kept one case each, which is what proves the wiring hands over the right values.

New coverage that did not exist: WorkflowRun against payloads GitHub fills in only partially, SiteDeploy against a different configured repository, RootEndpoint against the paths of the front page setup, and Secret round tripping, refusing another installation's salts and passing through a value stored in plain text.

Behaviour

Unchanged, except for two details the suite does not cover: the fields echoed back in the ignored and synced responses are now trimmed, like the fields already compared, and a GitHub response without a message no longer raises an undefined index while building the failure notice.

Two bugs are fixed, each with a test that fails without the fix:

  • A deploy token that cannot be deciphered is no longer sent to GitHub. It was sent as the bearer token on a site whose salts had been rotated, which is how the value of an encrypted setting ended up leaving the installation.
  • A setting submitted as exactly 0 is stored. empty() took it for a blank field and silently kept the previous value, which predates this branch.

WebhookDecision::workflow_run() no longer returns null. Only a deploy carries a run and the four outcomes are exhaustive, so the endpoint dereferences it without a guard; asking any other decision for the run now throws instead of failing on null, and the accessor stops being documented as nullable.

Verification

composer ci is green: lint, PHPCS, PHPStan level 5 over src/ as well, and 239 tests passing. In the local stack the plugin still loads without a Composer autoloader, the webhook endpoint answers the same, the cipher round trips and the My Account navigation is still filtered.

@YvesCesar
YvesCesar marked this pull request as draft September 14, 2026 22:31
@YvesCesar
YvesCesar marked this pull request as ready for review September 15, 2026 21:23
Comment thread libresign-wp-customizations.php
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
@YvesCesar
YvesCesar force-pushed the refactor/decision-logic-in-src branch from 011fb74 to a8eb740 Compare September 17, 2026 20:58
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
YvesCesar and others added 2 commits September 17, 2026 17:29
Signed-off-by: YvesCesar <yvesamorim73@gmail.com>
test: add a playwright suite for the account and subscription flows
@vitormattos
vitormattos merged commit c53dce4 into test/integration-harness Sep 18, 2026
12 checks passed
@vitormattos
vitormattos deleted the refactor/decision-logic-in-src branch September 18, 2026 20:49
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