Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b1c56a1
test: add phpunit harness with characterization tests
YvesCesar Sep 14, 2026
29699fc
test: keep the suite out of the web server and tighten the assertions
YvesCesar Sep 14, 2026
a9d345f
ci: name the phpunit workflow after the tool it runs
YvesCesar Sep 17, 2026
7d54de5
test: run the unit suite without WordPress or a database
YvesCesar Sep 17, 2026
4a7230c
fix: deploy the site only when a post changes publish status
YvesCesar Sep 17, 2026
2fd8034
fix: encrypt the deploy token and the webhook secret only once
YvesCesar Sep 17, 2026
dbbcd1c
refactor: move the plugin decisions into src
YvesCesar Sep 14, 2026
4316f3b
docs: describe the layout of src and tests
YvesCesar Sep 14, 2026
1acae43
refactor: name the variables after what they hold
YvesCesar Sep 14, 2026
fb784d1
fix: keep an unreadable deploy token from being sent to github
YvesCesar Sep 15, 2026
efa6685
fix: store a setting of "0" instead of taking it for a blank field
YvesCesar Sep 15, 2026
3ecec39
refactor: let an impossible decision fail loudly instead of on null
YvesCesar Sep 15, 2026
a8eb740
refactor: type the plugin classes and cut the comments they replaced
YvesCesar Sep 15, 2026
b492a89
fix: do not ask github for a deploy without a token
YvesCesar Sep 17, 2026
7933a8a
test: cover the account and subscription flows with playwright
YvesCesar Sep 15, 2026
37f0245
ci: run the playwright suite on every pull request
YvesCesar Sep 15, 2026
d0e9284
test: refuse to seed a site that is not disposable
YvesCesar Sep 15, 2026
67298d5
test: locate the cancel action by its class instead of its label
YvesCesar Sep 15, 2026
33a4edf
test: fall back when an environment variable is set to an empty string
YvesCesar Sep 15, 2026
22130bf
ci: pin woocommerce subscriptions and map the mu-plugin as a directory
YvesCesar Sep 15, 2026
c4ad0df
chore: keep node_modules out of the php tooling
YvesCesar Sep 15, 2026
618bc97
ci: rename the e2e workflow to playwright
YvesCesar Sep 17, 2026
a553fcf
Merge pull request #12 from LibreSign/test/e2e-playwright
vitormattos Sep 18, 2026
c53dce4
Merge pull request #11 from LibreSign/refactor/decision-logic-in-src
vitormattos Sep 18, 2026
2bdbbf2
fix: declare the minimum wordpress version the suite runs against
YvesCesar Sep 19, 2026
fa44698
Merge branch 'chore/static-analysis' into test/integration-harness
YvesCesar Sep 22, 2026
e6a9627
ci: run phpunit on the php versions composer.json supports
YvesCesar Sep 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: PHPUnit

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
php-matrix:
name: PHP matrix
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.php-matrix.outputs.versions }}
steps:
- uses: actions/checkout@v5

- uses: typisttech/php-matrix-action@v3
id: php-matrix

unit:
name: Unit (PHP ${{ matrix.php }})
needs: php-matrix
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ${{ fromJSON(needs.php-matrix.outputs.versions) }}
steps:
- uses: actions/checkout@v5

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
tools: composer

- uses: ramsey/composer-install@v3

- run: composer test:unit

integration:
name: Integration (PHP ${{ matrix.php }})
needs: php-matrix
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ${{ fromJSON(needs.php-matrix.outputs.versions) }}
services:
mariadb:
image: mariadb:11
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: wordpress_test
ports:
- 3306:3306
options: >-
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=5s
--health-timeout=5s
--health-retries=10
env:
WP_TESTS_DB_HOST: 127.0.0.1
WP_TESTS_DB_NAME: wordpress_test
WP_TESTS_DB_USER: root
WP_TESTS_DB_PASSWORD: root
steps:
- uses: actions/checkout@v5

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mysqli
coverage: none
tools: composer

- uses: ramsey/composer-install@v3

- run: composer test:integration
40 changes: 40 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Playwright

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
playwright:
name: Playwright
runs-on: ubuntu-latest
env:
WP_BASE_URL: http://localhost:8888
WP_CLI: npx wp-env run cli wp
steps:
- uses: actions/checkout@v5

- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm

- run: npm ci

- run: npx playwright install --with-deps chromium

- run: npm run env:start

- run: npm run test:e2e

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: tests/E2E/.report
retention-days: 7
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
/vendor/
/.phpunit.result.cache
node_modules/
/tests/E2E/.state/
/tests/E2E/.results/
/tests/E2E/.report/
16 changes: 16 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"phpVersion": "8.3",
"plugins": [
".",
"https://downloads.wordpress.org/plugin/woocommerce.10.9.4.zip",
"pronamic/woocommerce-subscriptions#v9.0.0"
],
"config": {
"WP_DEBUG": true,
"WP_DEBUG_DISPLAY": false
},
"mappings": {
"wp-content/mu-plugins": "./tests/E2E/support/mu-plugins"
}
}
129 changes: 129 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,132 @@ Customizations at WordPress relative to website libresign.coop
- Go to Configurations of this plugin
- Create the GitHub token and add at configuration page
- Set the organization and repository that have the deploy action

## Architecture

`src/` holds the decisions and `includes/` plus the main plugin file hold the
wiring: the hooks, the options and the effects. A decision receives values and
returns values — the navigation receives the query vars instead of reading
`global $wp`, the webhook gate receives the headers instead of a
`WP_REST_Request` — so it is covered by a data provider and no test double.

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

The plugin is installed by cloning the repository, so Composer never runs on the
server: `src/Autoloader.php` maps the namespace to `src/` and is the only file
the plugin requires by hand.

## Development

Every check is a Composer script:

```bash
composer lint # php -l on every file
composer cs # PHPCS
composer stan # PHPStan
composer test # PHPUnit
composer ci # all of the above, in this order
```

### Tests

`composer install` brings in WordPress itself (`vendor/wordpress`) and the
WordPress test suite, so the only thing the tests need from outside is a
MySQL/MariaDB server and a database they are allowed to wipe on every run.

| Variable | Default |
|---|---|
| `WP_TESTS_DB_NAME` | `wordpress_test` |
| `WP_TESTS_DB_USER` | `root` |
| `WP_TESTS_DB_PASSWORD` | `root` |
| `WP_TESTS_DB_HOST` | `mariadb` |
| `WP_TESTS_TABLE_PREFIX` | `wptests_` |
| `WP_CORE_DIR` | `vendor/wordpress` |

The defaults are the ones of the local SaaS stack, where both the database and
Composer already live inside the containers:

```bash
docker exec wordpress-docker-mariadb-1 \
mariadb -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS wordpress_test;'

docker exec -w /var/www/html/wp-content/plugins/libresign-wp-customizations \
wordpress-docker-wordpress-1 composer test
```

`tests/Unit/` mirrors `src/` and `tests/Integration/` mirrors the plugin files,
in both cases file by file with `Test.php` appended:
`src/Github/WebhookGate.php` is covered by
`tests/Unit/Github/WebhookGateTest.php`, and the endpoint wiring it serves,
`includes/github-site-webhook.php`, by
`tests/Integration/Includes/GithubSiteWebhookTest.php`. A decision is covered by
a unit test, and the wiring by an integration test going through WordPress:
options, hooks, the REST server or the database.

Nothing is mocked. Outgoing HTTP is answered through the `pre_http_request`
filter (`tests/Support/FakeHttp.php`), which is WordPress' own extension point,
and any request that is not answered that way fails the test instead of
reaching the network.

WooCommerce is not installed in this suite, so the screens that only exist with
WooCommerce loaded are covered by the browser tests instead.

### Browser tests

`tests/E2E/` mirrors `src/` the same way, with `.spec.ts` in place of
`Test.php`: `src/Account/Navigation.php` is covered end to end by
`tests/E2E/Account/Navigation.spec.ts`. What lives here is what PHPUnit cannot
reach without WooCommerce, the rewrite rules and a browser — the account
navigation, the account screens served from the site root and the confirmation
of a subscription status change.

The suite runs against a WordPress that is already up, described by four
variables:

| Variable | Default |
|---|---|
| `WP_BASE_URL` | `http://localhost` |
| `WP_CLI` | `docker exec -i -u www-data wordpress-docker-wordpress-1 wp --path=/var/www/html` |
| `WP_E2E_CUSTOMER_PASSWORD` | `libresign-e2e` |
| `WP_E2E_ALLOW_ANY_SITE` | unset |

The defaults are the local SaaS stack again, so there it takes no arguments:

```bash
npm install
npx playwright install chromium
npm run test:e2e
```

`tests/E2E/support/seed.php` puts the site in the state the specs expect, and
runs again before each test that changes the subscription. It is destructive and
nothing is restored afterwards: it makes My Account the front page, rebuilds the
rewrite rules, and creates `libresign_e2e_customer` with the password above,
which this repository publishes. That is why it refuses to run against anything
but `localhost` unless `WP_E2E_ALLOW_ANY_SITE=1` says so — point it at a site you
can throw away, never at production or staging.

Anywhere else, point the first two at the site under test. `.wp-env.json`
describes a disposable one, which is what CI runs:

```bash
npm run env:start
WP_BASE_URL=http://localhost:8888 WP_CLI="npx wp-env run cli wp" npm run test:e2e
```

WooCommerce only offers the billing screen when one of the available gateways
keeps payment methods. The local stack has Stripe for that; `.wp-env.json` maps
a mu-plugin that declares one.
37 changes: 33 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,53 @@
"php-stubs/woocommerce-stubs": "^9.0",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpunit/phpunit": "^9.6",
"roots/wordpress": "^7.0",
"squizlabs/php_codesniffer": "^3.13",
"szepeviktor/phpstan-wordpress": "^2.0",
"wp-coding-standards/wpcs": "^3.2"
"wp-coding-standards/wpcs": "^3.2",
"wp-phpunit/wp-phpunit": "^7.0",
"yoast/phpunit-polyfills": "^4.0"
},
"autoload": {
"psr-4": {
"LibreSign\\WPCustomizations\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LibreSign\\WPCustomizations\\Tests\\": "tests/"
}
},
"config": {
"platform": {
"php": "8.3"
},
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
"dealerdirect/phpcodesniffer-composer-installer": true,
"roots/wordpress-core-installer": true
}
},
"extra": {
"wordpress-install-dir": "vendor/wordpress"
},
"scripts": {
"lint": "parallel-lint --exclude vendor .",
"lint": "parallel-lint --exclude vendor --exclude node_modules .",
"cs": "phpcs",
"cs:fix": "phpcbf",
"stan": "phpstan analyse",
"test": [
"@test:unit",
"@test:integration"
],
"test:unit": "phpunit",
"test:integration": "phpunit -c phpunit-integration.xml.dist",
"ci": [
"@lint",
"@cs",
"@stan"
"@stan",
"@test"
]
}
}
Loading
Loading