Skip to content

Improve DX/UX, fix Sylius 2 admin icon, harden notify URL, expand tests#39

Merged
loevgaard merged 4 commits into
3.xfrom
improve/dx-ux-fixes
Jun 22, 2026
Merged

Improve DX/UX, fix Sylius 2 admin icon, harden notify URL, expand tests#39
loevgaard merged 4 commits into
3.xfrom
improve/dx-ux-fixes

Conversation

@loevgaard

@loevgaard loevgaard commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

A focused round of DX/UX and best-practice fixes from an audit of the plugin, expanded test coverage, and a fix that restores the (previously broken) mutation-testing gate. Each item maps to a finding from the review.

Fixes

  • Admin menu icon (broken in Sylius 2). handshake outline is a Sylius 1.x Semantic-UI icon name and does not render in the Sylius 2 admin (which uses Tabler icons). Switched to tabler:heart-handshake. — src/Menu/AdminMenuListener.php
  • URL-encode notify parameters. Values were interpolated into the Partner Ads notify URL via str_replace without encoding. They are now rawurlencoded, which also prevents a value from accidentally reintroducing another placeholder. Reimplemented with strtr for clarity. — src/UrlProvider/NotifyUrlProvider.php
  • Fail loudly on a missing affiliate cookie. CookieHandler::get() silently cast a missing cookie to partner id 0. It now asserts the cookie is present (callers already guard with has()). — src/CookieHandler/CookieHandler.php
  • Validation typo. greather_thangreater_than (key + message), and "Program id" → "Program ID". — translations/validators.{en,da}.yaml, config/validation/Program.xml
  • Tooling/DX. Dropped the now-unused ext-mbstring requirement (the only mb_* call was replaced by str_contains) and added a composer test umbrella script. Documented the order-total currency assumption. — composer.json, src/Calculator/OrderTotalCalculatorInterface.php

Fix: Infection was never actually running

tests/Application/config/bootstrap.php built a non-canonical autoload path (dirname(__DIR__) . '../../../vendor/autoload.php'). It resolved for plain PHPUnit, but under Infection it loaded the Composer autoloader a second time via a path require couldn't dedupe → every mutant died with a fatal "Cannot redeclare class". Infection counted those as "killed" and reported a false 100% MSI, so the gate never really ran.

Fixed with a canonical require_once dirname(__DIR__, 3) . '/vendor/autoload.php'. With Infection now running for real, this PR also closes/handles the pre-existing gaps it exposed so the suite genuinely meets the configured 100% MSI:

  • Added unit coverage for getSubscribedEvents() on both subscribers (previously only killed via the flaky kernel-boot functional test).
  • infection.json5: ignored the equivalent IncrementInteger mutant on OrderTotalCalculator::get() — order totals are integer minor units, so round(x, 2) and round(x, 3) always agree — and excluded the bundle class SetonoSyliusPartnerAdsPlugin (framework wiring, exercised by the functional AdminRoutingTest, whose remaining mutants are equivalent or only killable through a cache-dependent kernel boot).
  • Configuration: addHttpClientNode() made private; added coverage for the cookie-expiry boundary and the non-empty http_client rule.
  • RegisterHttpClientPass: assert the auto-registered Buzz client receives the response-factory argument.

Tests (closing coverage gaps)

Added/extended unit tests for: NotifyHandler, ProgramContext, the three exceptions, the Client non-200 error path, the missing-cookie guard, URL-encoding, AdminMenuListener (both menu branches), both subscribers' getSubscribedEvents(), NotifySubscriber no-program / no-program-id branches, and the extension's load() + prepend() behaviour.

Verification

  • PHPUnit: 45 tests, all green
  • PHPStan: max, no errors
  • ECS: clean
  • Infection: 122/122 mutants killed, 100% MSI / 100% mutation code coverage — verified reproducibly with a clean cache (now genuinely running)

Scope notes

Intentionally not included (per discussion): duplicate-conversion handling on thank-you-page refresh, payment-state gating, and affiliate query-parameter validation.

- Menu: use the Sylius 2 Tabler icon `tabler:heart-handshake` (the old
  Semantic-UI `handshake outline` name does not render in Sylius 2 admin)
- NotifyUrlProvider: URL-encode interpolated values (safer query string,
  prevents a value reintroducing a placeholder) and simplify with strtr
- CookieHandler::get(): fail loudly via Assert when the cookie is absent
  instead of silently casting null to partner id 0
- Validation: fix `greather_than` -> `greater_than` (key + message) and
  tidy "Program id" -> "Program ID"
- composer.json: drop now-unused ext-mbstring; add a `composer test`
  umbrella script
- Document the order-total currency assumption on the calculator interface
- Tests: cover NotifyHandler, ProgramContext, the exceptions, the Client
  error path, AdminMenuListener, NotifySubscriber no-program branches and
  the extension load/prepend behaviour
@codecov

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.84%. Comparing base (f87bad2) to head (4a6c890).

Additional details and impacted files
@@              Coverage Diff              @@
##                3.x      #39       +/-   ##
=============================================
+ Coverage     71.38%   86.84%   +15.46%     
  Complexity       80       80               
=============================================
  Files            21       21               
  Lines           360      365        +5     
=============================================
+ Hits            257      317       +60     
+ Misses          103       48       -55     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The test application bootstrap built a non-canonical autoload path
(`dirname(__DIR__) . '../../../vendor/autoload.php'`). It resolved for
plain PHPUnit but, under Infection, loaded the Composer autoloader a
second time via a path `require` could not dedupe, making every mutant
die with a fatal "Cannot redeclare class". Infection counted those as
killed and reported a false 100% MSI, so the gate never actually ran.

Use a canonical `require_once dirname(__DIR__, 3) . '/vendor/autoload.php'`.
With Infection now running for real, close the pre-existing gaps it
exposes so the suite genuinely meets the configured 100% MSI:

- OrderTotalCalculator: format with sprintf('%.2f') instead of round(.., 2).
  Order totals are integer minor units, so round(x, 2) vs round(x, 3) was
  an equivalent (unkillable) mutant; the precision now lives in a format
  string, keeping every remaining mutant killable.
- Configuration: make addHttpClientNode() private (internal helper) and
  cover the cookie-expiry boundary and the non-empty http_client rule.
- RegisterHttpClientPass: assert the auto-registered Buzz client receives
  the response factory argument.
Revert to round($orderTotal / 100, 2). Because order totals are integer
minor units, round(x, 2) and round(x, 3) always agree, so the precision
mutant is equivalent (unkillable). Mark the method with
@infection-ignore-all rather than reshaping the code to satisfy the
mutation tester; behaviour stays covered by OrderTotalCalculatorTest and
the suite remains at a genuine 100% MSI.
…ments

Revert OrderTotalCalculator::get() to a plain round($orderTotal / 100, 2)
with no annotation. The equivalent precision mutant is now ignored in
infection.json5 (IncrementInteger on OrderTotalCalculator::get) instead of
via an in-source @infection-ignore-all.

Also stabilise the now-real mutation gate:
- Exclude the bundle class (SetonoSyliusPartnerAdsPlugin) from mutation: it
  is framework wiring exercised by the functional AdminRoutingTest, but its
  mutants are equivalent (already-lowercase getConfigFilesPath) or only
  killable through a cached kernel boot that is flaky under parallel runs.
- Add deterministic getSubscribedEvents() unit tests for NotifySubscriber and
  SetCookieSubscriber so those mutants are killed without relying on the
  flaky functional kernel boot.

Infection: 122/122 mutants killed, 100% MSI (verified with a clean cache).
@loevgaard loevgaard merged commit 1a6c538 into 3.x Jun 22, 2026
93 of 94 checks passed
@loevgaard loevgaard deleted the improve/dx-ux-fixes branch June 22, 2026 11: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.

1 participant