Skip to content

fix(sso): guard against unpopulated $current_blog during early bootstrap#1185

Merged
superdav42 merged 1 commit into
Ultimate-Multisite:mainfrom
kenedytorcatt:fix/sso-race-current-blog-not-ready
May 12, 2026
Merged

fix(sso): guard against unpopulated $current_blog during early bootstrap#1185
superdav42 merged 1 commit into
Ultimate-Multisite:mainfrom
kenedytorcatt:fix/sso-race-current-blog-not-ready

Conversation

@kenedytorcatt

@kenedytorcatt kenedytorcatt commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Two defensive guards in inc/sso/class-sso.php that prevent SSO_Exception when convert_bearer_into_auth_cookies() runs before $current_blog is fully populated on multisite.

Bug

The SSO callback is attached to init. On some multisite bootstraps the $GLOBALS['current_blog'] object exists, but its properties (including registered) are still empty. get_broker() then calls calculate_secret_from_date($current_blog->registered) with an empty string, which throws SSO_Exception. Because the exception is uncaught at that level, the parent response becomes a 500 / blank page. The most user-visible symptom is admin pages loaded inside an iframe via SSO render blank.

Reproduced reliably on:

  • Mapped-domain subsites right after a cold cache flush (the page that misses cache renders blank; the next request, with $current_blog populated, works fine).
  • Admin pages loaded inside an iframe (custom dashboards, third-party "frontend admin" plugins) where the SSO bearer exchange races with WordPress finishing sunrise.php and ms-settings.php.
  • Hosts that warm object caches between sunrise and ms-settings.

Fix

  1. convert_bearer_into_auth_cookies() — bail out early when the global blog object is missing or its registered property is empty. The conversion is naturally retried on the next request once WordPress has finished populating $current_blog, so no SSO state is lost.

  2. calculate_secret_from_date() — when $date is empty, fall back to the main site's registered date instead of throwing. Any caller that reaches this method through a path other than (1) (custom code that builds secrets from $current_blog->registered before sunrise.php finishes) gets a stable, consistent secret for the network rather than a 500.

Both branches are pure early-return / fallback guards. When the data is present the behaviour is unchanged.

Production validation

Deployed on a 300+ subsite multisite (kursopro.com) since 2026-05-07. Zero regressions. Resolves a recurring "blank admin iframe" bug on mapped-domain subsites that was masked by full-page cache on healthy requests but reappeared whenever the cache was invalidated.

Test plan

  • PHP 8.3 syntax check (php -l).
  • No behavior change when $current_blog->registered is populated (default path).
  • Mapped-domain subsite admin loads inside an iframe via SSO after cache flush.
  • calculate_secret_from_date() returns the same hash for a given main-site registered date when called with empty $date (deterministic fallback).

Notes

Independent from #1169 / #1168 — does not touch password reset paths. Targets inc/sso/class-sso.php only.

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability of SSO authentication during multisite site initialization by preventing errors in specific bootstrap scenarios.
    • Enhanced SSO secret calculation with fallback logic to ensure consistent authentication across edge cases, particularly in complex multisite environments.

Review Change Stack

Two defensive guards in inc/sso/class-sso.php that prevent SSO_Exception
when convert_bearer_into_auth_cookies() runs before $current_blog is
fully populated.

Root cause
==========

The callback is attached to `init`, but on some multisite bootstraps the
$GLOBALS['current_blog'] object exists while its properties --- including
`registered` --- are still empty. Reproduced reliably on:

  * Mapped domains right after a cold cache flush.
  * Admin pages loaded inside an iframe (custom dashboards, third-party
    "frontend admin" plugins) where the SSO bearer exchange races with
    WordPress finishing sunrise.php and ms-settings.php.
  * Hosts that warm object caches between sunrise and ms-settings.

In those requests get_broker() ends up calling
calculate_secret_from_date($current_blog->registered) with an empty
string, which throws SSO_Exception. Because the exception is uncaught at
that level the parent iframe response becomes a 500 / blank page and the
admin UI never renders.

Fix
===

1. convert_bearer_into_auth_cookies(): bail out early when the global
   blog object is missing or its `registered` property is empty. The
   conversion is naturally retried on the next request once WordPress
   has finished populating $current_blog, so no SSO state is lost.

2. calculate_secret_from_date(): when $date is empty, fall back to the
   main site's registered date instead of throwing. Any caller that
   reaches this method through a path other than (1) (custom code that
   builds secrets from $current_blog->registered before sunrise.php
   finishes) gets a stable, consistent secret for the network rather
   than a 500.

Both branches are pure early-return / fallback guards: when the data is
present the behaviour is unchanged.

Impact
======

Production deploy on a 300+ subsite multisite (kursopro.com) since
2026-05-07 with zero regressions. Resolves a recurring "blank admin
iframe" bug on mapped-domain subsites that was masked by full-page cache
on healthy requests but reappeared whenever the cache was invalidated.

Tested on PHP 8.3, WordPress 6.9, multisite with sunrise.php enabled and
domain mapping active.
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51a05097-1964-4712-afcf-1b5cc9c537fe

📥 Commits

Reviewing files that changed from the base of the PR and between 370e2f6 and 5a2c207.

📒 Files selected for processing (1)
  • inc/sso/class-sso.php

📝 Walkthrough

Walkthrough

SSO authentication adds defensive guards in two methods to prevent exceptions when blog data is incompletely populated during early multisite bootstrap phases (init hook, iframes, mapped domains).

Changes

Bootstrap State Guards

Layer / File(s) Summary
Early-Return Guard on Init
inc/sso/class-sso.php
convert_bearer_into_auth_cookies() returns early when current_blog->registered is empty, preventing secret-calculation calls before site data is fully populated.
Fallback Secret Calculation
inc/sso/class-sso.php
calculate_secret_from_date() handles empty $date by reading the main site's registered value or using a default timestamp, ensuring stable secret computation if called during incomplete bootstrap.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Suggested labels

status:available, origin:worker

Poem

🐰 A rabbit hops through bootstrap's maze,
Guards catch gaps in early days,
When blogs aren't quite awake yet,
Fallbacks flow—no SSO sweat!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding guards against an unpopulated $current_blog during early bootstrap in the SSO module, which directly matches the file changes and PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@superdav42 superdav42 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved by pulse runner @superdav42 — author @kenedytorcatt confirmed collaborator, pre-merge gates passed.

@kenedytorcatt

Copy link
Copy Markdown
Contributor Author

Reproduction + validation on staging

Validated this fix on a clean staging copy of our production multisite. Findings below.

Setup

  • Staging: kpstage.com (clone of kursopro.com production)
  • WordPress 6.9, PHP 8.3, MariaDB, LiteSpeed
  • 200+ subsites, several with mapped domains (one used for the test: javieraacademy.com, blog_id 218)
  • UM 2.10.1 with this patch already applied in prod since 2026-05-07

Reproduction with UM 2.11.0 official (no patch)

Replaced UM on staging with the freshly-cut 2.11.0 release ZIP — vanilla, no modifications.

Test script (run via WP-CLI wp eval-file):

$blog_id = 218;
switch_to_blog($blog_id);

// Simulate the bootstrap race: $current_blog object exists, but
// `registered` is still empty (matches what we observed in real
// requests right after sunrise.php finishes but before ms-settings.php
// finishes hydrating $current_blog).
$GLOBALS['current_blog']->registered = '';

try {
    $sso = \WP_Ultimo\SSO\SSO::get_instance();
    $secret = $sso->calculate_secret_from_date('');
    echo 'OK: ' . $secret;
} catch (\Throwable $e) {
    echo 'EXCEPTION: ' . get_class($e) . ' :: ' . $e->getMessage();
}

Result on UM 2.11.0 official:

EXCEPTION: WP_Ultimo\SSO\Exception\SSO_Exception :: La creación del secreto SSO falló.

In a real iframe'd admin request this becomes a 500 response and the WPFA / frontend-admin panel renders blank for the end user.

Result with this PR applied

Patched only inc/sso/class-sso.php (the 41 inserted lines from this PR) on top of the same UM 2.11.0 install. Same test:

OK: secret=cc95d360e9559785ec2d094e951e54a2

No exception. The fallback path returns a deterministic, network-scoped secret as designed.

Side effects checked

  • Default code path (when $current_blog->registered is populated) is unchanged — verified by running the test with a non-empty registered date on the same subsite.
  • Smoke test on kpstage.com main and javieraacademy.com mapped domain both return HTTP 200 after restoring the staging environment.
  • No new PHP warnings in debug.log during the patched run.

Recommendation

This is what 2.11.0 needs to be drop-in safe for networks that use SSO + iframe'd admin (WPFA, frontend admin plugins, custom dashboards on mapped domains). Without it, anyone updating 2.10.1 → 2.11.0 will hit blank admin panels on cache miss for mapped-domain subsites — the same symptom that drove us to write this patch back in 2026-05-07.

Happy to add unit tests for calculate_secret_from_date('') and the bail-out in convert_bearer_into_auth_cookies() if you want them in the same PR — let me know your preferred test harness.

@kenedytorcatt

Copy link
Copy Markdown
Contributor Author

Full integration brief for the 2.11.x release

Detailed breakdown so this can ship as part of the next 2.11.x without follow-up. The patch touches a single file (inc/sso/class-sso.php) and adds 41 lines, zero deletions, zero behavior changes on the happy path.


Patch 1 — convert_bearer_into_auth_cookies()

File: inc/sso/class-sso.php
Method: public function convert_bearer_into_auth_cookies(): void
Inserted: 20 lines (comment block + 3-line guard) at the very top of the method body, before any other statement.

public function convert_bearer_into_auth_cookies(): void {

    /*
     * Bail out early when $current_blog has not been fully populated yet.
     *
     * This callback runs on `init`, but on some multisite bootstraps
     * (mapped domains, iframed admin requests, hosts that warm caches
     * before sunrise.php finishes) `$GLOBALS['current_blog']` is present
     * as an object but its properties -- including `registered` -- are
     * still empty. `get_broker()` then calls
     * `calculate_secret_from_date($current_blog->registered)` with an
     * empty string, which throws SSO_Exception and breaks any admin UI
     * loaded through an iframe via SSO.
     *
     * Skipping this request is safe: the next request in the same
     * session re-runs this callback with a fully populated
     * `$current_blog` and completes the conversion.
     */
    if (empty($GLOBALS['current_blog']) || empty($GLOBALS['current_blog']->registered)) {
        return;
    }

    $broker = $this->get_broker();
    // ... unchanged ...
}

Behavior:

  • When $current_blog is fully populated → behavior unchanged (guard does not trigger).
  • When $current_blog->registered is empty → method returns silently. The bearer-to-cookie conversion is re-attempted on the next request once WP has finished hydrating the blog. No SSO state is lost, no transient is touched.

Why empty() and not isset(): isset($GLOBALS['current_blog']->registered) returns true even when the property exists with an empty string (which is what WP sets it to during the race). empty() catches null, empty string, and missing-object cases in one check.


Patch 2 — calculate_secret_from_date()

File: inc/sso/class-sso.php
Method: public function calculate_secret_from_date($date)
Inserted: 21 lines (comment block + 4-line fallback) at the top of the method body, before the DateTimeZone instantiation.

public function calculate_secret_from_date($date) {

    /*
     * Fall back to the main site registration date when $date is empty.
     *
     * This guards against the same multisite bootstrap race that
     * `convert_bearer_into_auth_cookies()` skips: a caller can still
     * reach this method with an empty $date (for example, custom code
     * that builds an SSO secret from `$current_blog->registered` before
     * sunrise.php finishes populating it). Throwing SSO_Exception here
     * breaks the parent request, which on iframed admin pages renders
     * the whole panel blank.
     *
     * Using the main site registration date as a fallback keeps the
     * secret stable across requests for the same network and avoids
     * cascading failures during early boot.
     */
    if (empty($date)) {
        $main_site = function_exists('get_site') ? get_site(function_exists('get_main_site_id') ? get_main_site_id() : 1) : null;
        $date      = ($main_site && ! empty($main_site->registered)) ? $main_site->registered : '2024-01-01 00:00:00';
    }

    $tz = new \DateTimeZone('GMT');
    // ... unchanged ...
}

Why a defense-in-depth second guard:

  • Patch 1 covers the only known production path that triggers the bug (the init callback).
  • Patch 2 protects every other caller of calculate_secret_from_date() — third-party add-ons, custom integrations, and any future code path that might build secrets from $current_blog->registered outside the SSO bootstrap. Throwing in this method renders the whole parent request as a 500.

Determinism: the fallback uses get_main_site_id()get_site()->registered, which is the canonical, stable network-scoped value. Two requests that hit the fallback in the same network will produce the same secret. Two requests that hit the fallback on different networks produce different secrets (correct isolation).

The literal '2024-01-01 00:00:00' is only reached if get_site() itself returns null (extremely degraded state — would mean WP core is half-loaded). Included only to avoid throwing inside a guard whose entire purpose is to prevent throwing.

function_exists() guards keep the fallback safe to call from contexts where wp-includes/ms-load.php may not yet be loaded.


Edge cases verified

Scenario Behavior with patch
Healthy request, $current_blog->registered populated Unchanged — guards do not fire
$GLOBALS['current_blog'] is null Returns early in convert_bearer_into_auth_cookies()
$GLOBALS['current_blog'] is object, registered = '' Returns early; if calculate_secret_from_date() is invoked directly with '', falls back to main site date
$GLOBALS['current_blog'] is object, registered = null Same as above (empty() covers both)
Main site has empty registered (broken install) Falls back to literal '2024-01-01 00:00:00' instead of throwing
Subsite is suspended / deleted No change — the patches do not interact with site status
User is not logged in No change — the inner is_user_logged_in() && $broker && $broker->isAttached() block is reached only after the guard, same as before

Suggested unit tests

// tests/SSO/SSOTest.php

public function test_calculate_secret_from_date_returns_main_site_fallback_for_empty_date(): void {
    $sso  = SSO::get_instance();
    $main = get_site(get_main_site_id());

    $expected = wp_hash((int) (new \DateTime($main->registered, new \DateTimeZone('GMT')))->format('mdisY'));
    $actual   = $sso->calculate_secret_from_date('');

    $this->assertSame($expected, $actual, 'Empty date should fall back to the main site registered date.');
}

public function test_calculate_secret_from_date_unchanged_for_valid_date(): void {
    $sso = SSO::get_instance();

    $a = $sso->calculate_secret_from_date('2024-11-06 15:24:32');
    $b = $sso->calculate_secret_from_date('2024-11-06 15:24:32');

    $this->assertSame($a, $b, 'Same date must produce same secret.');
    $this->assertNotSame($a, $sso->calculate_secret_from_date('2024-11-07 15:24:32'));
}

public function test_convert_bearer_skips_when_current_blog_not_populated(): void {
    global $current_blog;
    $original = $current_blog;

    $current_blog            = new \stdClass();
    $current_blog->registered = '';

    $sso = SSO::get_instance();
    $this->assertNull($sso->convert_bearer_into_auth_cookies(), 'Must return void without throwing when current_blog is half-populated.');

    $current_blog = $original;
}

Reproduction on staging (see previous comment for the WP-CLI script)

  • UM 2.11.0 official (no patch): SSO_Exception :: La creación del secreto SSO falló when $current_blog->registered = ''.
  • UM 2.11.0 + this PR: returns deterministic secret cc95d360e9559785ec2d094e951e54a2, no exception.

Staging is kpstage.com, full clone of kursopro.com production: 200+ subsites, several mapped domains (test ran against javieraacademy.com, blog_id 218), PHP 8.3, WP 6.9, LiteSpeed + Cloudflare.


Why this matters for 2.11.x

Networks that combine UM SSO with any iframed admin (WPFA / WP Frontend Admin, frontend admin panels, custom dashboards on mapped domains) will hit blank panel pages immediately after updating 2.10.1 → 2.11.0 on the first cache-miss request to a mapped-domain subsite. The bug is invisible in dev (no Cloudflare, no LiteSpeed, no SaaS-scale cache invalidation) but surfaces predictably in production whenever the page cache cycles.

The patch is 41 lines, single file, zero behavior change on healthy requests, validated in production for 5+ days. It is the minimum surface needed to make 2.11.0 drop-in safe for SaaS-style multisite installs.

Let me know if you want this rebased on any specific branch, or if you prefer the unit tests in a follow-up PR.

@superdav42

Copy link
Copy Markdown
Collaborator

Stuck-merge detector: PR has been merge-eligible but unmerged past the threshold

The pulse merge pass has classified PR #1185 as STUCK_OTHER and it has been sitting unmerged longer than AIDEVOPS_MERGE_STUCK_AGE_MINUTES (currently 240m). The deterministic merge gates are evaluated every cycle (~120s) and this PR has consistently failed them.

Failing checks on PR #1185

  • (no FAILURE entries in rollup; check rollup manually)

Worker guidance for the next attempt

  1. Read PR fix(sso): guard against unpopulated $current_blog during early bootstrap #1185 body + the latest check run logs:
    gh pr view 1185 --repo Ultimate-Multisite/ultimate-multisite --json statusCheckRollup
    gh pr checks 1185 --repo Ultimate-Multisite/ultimate-multisite
  2. If the failing checks are environment/Setup-step (Format, Lint, Typecheck all FAIL at the same step), the canonical default branch likely has a broken lockfile or a CI infra change — fix at the base, not on this PR. Look for a sibling outage meta-issue in this repo (filed by the same detector) before forking off here.
  3. If the failures are PR-specific (e.g. a Typecheck error introduced by this PR's code), rebase onto the latest default branch and address the diagnosed errors. Use full-loop-helper.sh start from the linked PR's worktree.
  4. If the linked issue body lacks the worker-ready file paths and verification commands required by t1900, post a comment naming the missing context before dispatching another worker — the next attempt will burn tokens on exploration otherwise.

Why you're seeing this

Every pulse cycle (~120s) the deterministic merge pass re-evaluates open PRs. PRs that pass APPROVED + MERGEABLE but fail required checks have historically been re-evaluated silently every cycle until a human noticed. The stuck-merge detector (t3193) surfaces them after AIDEVOPS_MERGE_STUCK_AGE_MINUTES minutes idle. This comment is posted exactly once per linked issue — repeated stuck cycles will NOT spam the thread. If the PR merges and the issue is reopened later with a fresh stuck PR, the marker will allow a second comment.

Posted automatically by pulse-merge-stuck.sh (t3193 / GH#21895). Threshold env: AIDEVOPS_MERGE_STUCK_AGE_MINUTES=240.


aidevops.sh v3.15.34 automated scan.

@superdav42 superdav42 merged commit 1728d68 into Ultimate-Multisite:main May 12, 2026
11 checks passed
superdav42 pushed a commit that referenced this pull request May 12, 2026
…1169 (subsite password reset) (#1198)

Two Cypress specs + matching WP-CLI fixtures that lock in the behaviour of two
recently merged fixes so future refactors of the SSO bootstrap chain or the
password reset rewrite can't silently regress them.

## 066-sso-bootstrap-race.spec.js (guards PR #1185)

Verifies:
- `calculate_secret_from_date('')` does NOT throw and returns a hash.
- Two consecutive calls with empty input return the SAME hash (deterministic
  fallback — important so SSO state stays consistent across requests during
  a bootstrap window).
- `convert_bearer_into_auth_cookies()` does NOT throw when `$current_blog`
  exists with an empty `registered` property.

Driven via fixture `setup-sso-bootstrap-race.php` which calls both methods
through the live `SSO::get_instance()` singleton and emits JSON.

## 011-password-reset-subsite-domain.spec.js (guards PR #1169)

Verifies:
- The URL produced by `retrieve_password_message` on a subsite uses the
  subsite host (or at least no longer points at `/wp-login.php` on a
  different host).
- The reset query args `action / key / login / wp_lang` are preserved so
  WooCommerce my-account, BuddyPress, custom themes, and the default
  wp-login fallback can still pick the request up.
- The new `wu_subsite_password_reset_url` filter (added by #1169) is
  reachable for integration overrides.

Driven via fixture `setup-password-reset-subsite.php` which creates a test
subsite, switches into it, and applies the filter chain directly with a
synthetic raw message — no SMTP / Mailpit / cron dependency.

Co-authored-by: Kenedy Torcatt <kursopro7@gmail.com>
@superdav42 superdav42 added the review-feedback-scanned Merged PR already scanned for quality feedback label May 13, 2026
superdav42 added a commit that referenced this pull request May 26, 2026
…1280)

* chore: upgrade planning templates

* ci(e2e): run the 9 cypress specs that existed but were never invoked

The e2e workflow listed only 6 of the 16 spec files in
tests/e2e/cypress/integration/. The other 10 were committed and
syntactically valid but never executed in CI, so any regression they
guarded against could slip through unnoticed.

Audit findings (16 specs total):

  Already in CI (6, unchanged):
    - 000-setup.spec.js
    - 010-manual-checkout-flow.spec.js
    - 020-free-trial-flow.spec.js
    - 030-stripe-checkout-flow.spec.js (gated, STRIPE_TEST_SK_KEY)
    - 040-stripe-renewal-flow.spec.js (gated, STRIPE_TEST_SK_KEY)
    - 065-sso-redirect-loop.spec.js

  Added to CI in this change (9):
    - login.spec.js                                (sanity)
    - mail.spec.js                                 (sanity)
    - wizard.spec.js                               (drives Setup Wizard UI)
    - 011-password-reset-subsite-domain.spec.js    (regression: PR #1169)
    - 030-modal-form-error-handling.spec.js        (UI: AJAX error handling)
    - 050-password-strength-enforcement.spec.js    (UI: zxcvbn scoring)
    - 060-sso-cross-domain.spec.js                 (SSO: domain mapping)
    - 066-sso-bootstrap-race.spec.js               (regression: PR #1185)
    - 035-paypal-checkout-flow.spec.js             (gated, new
                                                    PAYPAL_SANDBOX_*)

  Intentionally excluded (1):
    - installation.spec.js — single ‘cy.visit(/wp-admin/)’ call with
      no assertions; doesn’t actually test anything. Recommend deletion
      in a follow-up; not removed here to keep this change ci-only.

Why wizard.spec.js matters specifically: 000-setup.spec.js bypasses
the Setup Wizard UI by calling the installer + setup-finished flag
directly. The path Setup_Wizard_Admin_Page::handle_save_settings ->
Settings::save_settings -> Field::set_value -> validate_textarea_field
was therefore never exercised in CI. The recent
fix/textarea-array-coercion fix added unit-test coverage for the
validator itself; running wizard.spec.js now covers the integration
path.

Step organisation (after the existing ‘Run Setup Test’):

  1. Sanity Tests       — login, mail            (smoke)
  2. Wizard Test        — wizard
  3. Regression & UI    — 011, 030, 050, 066
  4. Checkout Tests     — 010, 020 (existing)
  5. SSO Tests          — 060 (new), 065         (was: only 065)
  6. Stripe Tests       — 030, 040 (existing, gated)
  7. PayPal Tests       — 035 (new, gated on PAYPAL_SANDBOX_*)

All new groups follow the existing pattern: ‘set +e’ + per-spec loop +
aggregated failure count + final exit 1 if any failed, so a single
flake doesn’t mask other failures.

PayPal step mirrors the Stripe gating: if PAYPAL_SANDBOX_CLIENT_SECRET
is unavailable (typical for fork PRs) the step prints the same
diagnostic message Stripe uses and exits 0. The PAYPAL_SANDBOX_* repo
secrets are not currently configured upstream, so PayPal will skip
until they are added; Stripe continues to run.

Verification performed in this change:
  - YAML parse: python3 -c 'import yaml; yaml.safe_load(open(path))' OK
  - Per-spec node --check on all 9 added specs               OK
  - All referenced fixture .php files exist in tests/e2e/cypress/fixtures
  - Inventory diff: workflow now references 15/16 specs

Local Cypress execution is not attempted in this commit because each
matrix variant requires a fresh wp-env (Docker download + WP install
+ plugin activation + multi-spec run) and would take longer than the
CI run that gates the merge. CI is the canonical verification surface
for this change.

* ci(e2e): disable custom login page after wizard spec

The wizard's Default Content step creates a Login page and points
Ultimate Multisite's custom-login feature at it, which redirects
/wp-login.php -> /login/. The custom page has no #rememberme checkbox,
so cy.loginByForm() fails in every subsequent spec's session-setup hook.

000-setup.spec.js already calls setup-disable-custom-login.php
defensively in its before() hook for the same reason; this CI step
runs the same fixture right after wizard.spec.js so the regression,
checkout, SSO, and gated gateway groups inherit a clean login state.

Observed on PR #1280 cypress matrix run 26456938227 where
030-modal-form-error-handling failed in its session-setup hook with
'Expected to find element: #rememberme, but never found it', cascading
into every subsequent step being skipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-feedback-scanned Merged PR already scanned for quality feedback

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants