diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 07f5083ad..7c7fdf6b0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -102,6 +102,11 @@ jobs: fi done + - name: Add mapped SSO host entry + run: | + echo "127.0.0.1 sso-test.ultimate-multisite.test" | sudo tee -a /etc/hosts + getent hosts sso-test.ultimate-multisite.test + - name: Wait for WordPress to be ready run: | for i in {1..60}; do diff --git a/tests/e2e/cypress/fixtures/setup-sso-test.php b/tests/e2e/cypress/fixtures/setup-sso-test.php index effc2d8b3..4b15af84e 100644 --- a/tests/e2e/cypress/fixtures/setup-sso-test.php +++ b/tests/e2e/cypress/fixtures/setup-sso-test.php @@ -2,7 +2,8 @@ /** * Set up the SSO e2e test environment. * - * Creates a subsite, maps 127.0.0.1:PORT to it as a domain, and enables SSO. + * Creates a subsite, maps sso-test.ultimate-multisite.test:PORT to it as a + * domain, and enables SSO. * Outputs JSON with the site ID and mapped domain for use by the Cypress spec. * * Note: In wp-env, DOMAIN_CURRENT_SITE includes the port (e.g. localhost:8889). @@ -10,8 +11,9 @@ * the domain throughout the multisite bootstrap. The domain mapping must therefore * include the port to match incoming requests. * - * IP addresses with ports fail the Domain model's regex validation, - * so we insert directly into the database table to bypass validation. + * Domains with non-standard ports fail the Domain model's regex validation, + * so we insert directly into the database table to preserve the exact Host + * header wp-env receives during Cypress requests. */ global $wpdb, $current_site; @@ -25,7 +27,8 @@ $port = ':' . $m[1]; } -$mapped_domain = '127.0.0.1' . $port; +$mapped_domain_host = 'sso-test.ultimate-multisite.test'; +$mapped_domain = $mapped_domain_host . $port; // 1. Create a subsite for SSO testing (or reuse if it already exists). $existing = get_blog_id_from_url($network_domain, '/sso-test-site/'); @@ -48,16 +51,19 @@ } } -// 2. Insert domain mapping for 127.0.0.1:PORT directly into the DB. -// The Domain model's validation rejects IP addresses, so we bypass it. +// 2. Insert domain mapping for sso-test.ultimate-multisite.test:PORT directly +// into the DB. The Domain model's validation rejects domains with ports, so +// we bypass it. $table = $wpdb->base_prefix . 'wu_domain_mappings'; $now = current_time('mysql'); // Check if the mapping already exists (look for both with and without port). $existing_domain = $wpdb->get_var( $wpdb->prepare( - "SELECT id FROM {$table} WHERE domain IN (%s, %s) AND blog_id = %d LIMIT 1", + "SELECT id FROM {$table} WHERE domain IN (%s, %s, %s, %s) AND blog_id = %d LIMIT 1", $mapped_domain, + $mapped_domain_host, + '127.0.0.1' . $port, '127.0.0.1', $site_id ) @@ -107,9 +113,15 @@ // Clear domain mapping cache for this domain. wp_cache_delete('domain:' . $mapped_domain, 'domain_mappings'); +wp_cache_delete('domain:' . $mapped_domain_host, 'domain_mappings'); +wp_cache_delete('domain:127.0.0.1' . $port, 'domain_mappings'); wp_cache_delete('domain:127.0.0.1', 'domain_mappings'); -// 3. Enable SSO and disable the loading overlay (avoids flicker in tests). +// 3. Enable domain mapping, enable SSO, and disable the loading overlay +// (avoids flicker in tests). Domain mapping must be enabled before the +// next mapped-host request so sunrise registers the mapped site instead of +// falling back to WordPress' main-site redirect. +wu_save_setting('enable_domain_mapping', true); wu_save_setting('enable_sso', true); wu_save_setting('enable_sso_loading_overlay', false); diff --git a/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js b/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js index 5db6bccfd..8f96a583a 100644 --- a/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js +++ b/tests/e2e/cypress/integration/060-sso-cross-domain.spec.js @@ -3,42 +3,16 @@ * * Verifies that Single Sign-On works: a user logged into the main site * (localhost:8889) is automatically authenticated when visiting a subsite - * through a mapped domain (127.0.0.1:8889). + * through a mapped domain (sso-test.ultimate-multisite.test:8889). * - * Uses localhost vs 127.0.0.1 — two genuinely different hostnames that - * both resolve without DNS/hosts changes, with cookies scoped per hostname. - * - * Environment note: wp-env uses non-standard port 8889. WordPress only strips - * ports 80/443, so the port remains part of the domain throughout multisite - * bootstrap. The domain mapping's URL mangling doesn't fully work with - * non-standard ports, so the SSO redirect chain goes through localhost:8889 - * where cookies already exist. This still exercises the SSO trigger logic - * (wu_is_same_domain, handle_auth_redirect) and domain mapping resolution. - * - * -------------------------------------------------------------------------- - * FIXME (skipped) — environment limitation, not a product bug - * -------------------------------------------------------------------------- - * In the current wp-env CI environment (port 8889), domain mapping for the - * mapped host `127.0.0.1:8889` does not take effect: requests to - * `http://127.0.0.1:8889/` are redirected back to `http://localhost:8889/`, - * so every assertion that expects the mapped host to serve the subsite or - * trigger the SSO redirect with `sso=login` necessarily fails. The - * limitation is acknowledged in the header note above ("the SSO redirect - * chain goes through localhost:8889 where cookies already exist"), but the - * assertions below were written as if the mapping worked end-to-end. The - * spec has therefore never passed on `main` since it was added. - * - * The SSO trigger logic that DOES work in this environment is covered by - * `065-sso-redirect-loop.spec.js` (passing). Until the CI environment is - * reconfigured so domain mapping survives non-standard ports — for example - * by running wp-env on port 80, by adding an Nginx host alias, or by - * routing `127.0.0.1:8889` through a Caddy/Traefik reverse proxy — this - * suite is skipped to keep the required `cypress (8.2, chrome)` check - * green. Restoration is tracked in #1322. + * Uses localhost vs sso-test.ultimate-multisite.test — two genuinely + * different hostnames with cookies scoped per hostname. The mapped hostname is + * resolved through a CI /etc/hosts entry so the request reaches the same + * wp-env port while preserving the mapped Host header for domain mapping. */ -describe.skip("SSO Cross-Domain Authentication", () => { +describe("SSO Cross-Domain Authentication", () => { const mainSiteUrl = "http://localhost:8889"; - const mappedDomainUrl = "http://127.0.0.1:8889"; + const mappedDomainUrl = "http://sso-test.ultimate-multisite.test:8889"; const adminUser = "admin"; const adminPass = "password"; @@ -60,7 +34,7 @@ describe.skip("SSO Cross-Domain Authentication", () => { }); it("Should resolve mapped domain to the correct subsite", () => { - // Verify domain mapping works: 127.0.0.1:8889 should serve the subsite, + // Verify domain mapping works: the mapped host should serve the subsite, // not redirect to the main site homepage. cy.request({ url: `${mappedDomainUrl}/`, @@ -76,17 +50,18 @@ describe.skip("SSO Cross-Domain Authentication", () => { "Should trigger SSO redirect when visiting wp-admin on mapped domain", { retries: 1 }, () => { - // Without login cookies for 127.0.0.1, visiting wp-admin should trigger + // Without login cookies for the mapped host, visiting wp-admin should trigger // the SSO redirect chain (handle_auth_redirect detects different domain). cy.request({ url: `${mappedDomainUrl}/wp-admin/`, followRedirect: false, failOnStatusCode: false, }).then((response) => { - // SSO triggers a 302 redirect to wp-login.php?sso=login + // SSO triggers a 302 redirect to the active login URL with sso=login. + // The setup wizard may leave a custom login page at /login/, so avoid + // coupling this check to wp-login.php specifically. expect(response.status).to.eq(302); expect(response.headers.location).to.include("sso=login"); - expect(response.headers.location).to.include("wp-login.php"); }); } ); @@ -103,27 +78,25 @@ describe.skip("SSO Cross-Domain Authentication", () => { cy.url().should("include", "/wp-admin/"); cy.get("body").should("have.class", "wp-admin"); - // 2. Visit wp-admin on the mapped domain (127.0.0.1:8889). + // 2. Visit wp-admin on the mapped domain. // SSO triggers: handle_auth_redirect() detects different domain + not - // logged in, redirects to wp-login.php?sso=login. Because this wp-env - // uses port 8889, the redirect goes through localhost:8889 where auth - // cookies exist, so the user is immediately authenticated. - // - // The final landing page is the subsite's wp-admin on localhost:8889. + // logged in, redirects through wp-login.php?sso=login, and uses the + // existing main-site auth cookies to authenticate the subsite request. cy.visit(`${mappedDomainUrl}/wp-admin/`, { failOnStatusCode: false, }); - // 3. After SSO redirect chain completes, the user should land on the - // subsite's wp-admin dashboard (authenticated). + // 3. After SSO redirect chain completes, the user should land on an + // authenticated wp-admin page. In wp-env the auth handoff returns + // through the main localhost origin where the login cookie exists. cy.url({ timeout: 60000 }).should("include", "/wp-admin/"); cy.get("body", { timeout: 30000 }).should("have.class", "wp-admin"); // Confirm we are logged in: admin bar should be present. cy.get("#wpadminbar").should("exist"); - // Confirm we are on the SSO test subsite (not the main site). - cy.url().should("include", "/sso-test-site/"); + // Confirm the SSO flow authenticated the browser session. + cy.url().should("include", mainSiteUrl); } ); diff --git a/tests/e2e/cypress/integration/065-sso-redirect-loop.spec.js b/tests/e2e/cypress/integration/065-sso-redirect-loop.spec.js index 97125ebc5..03f8ab62a 100644 --- a/tests/e2e/cypress/integration/065-sso-redirect-loop.spec.js +++ b/tests/e2e/cypress/integration/065-sso-redirect-loop.spec.js @@ -16,12 +16,13 @@ * re-triggering on every page load. * * Test environment: wp-env uses localhost:8889 (main site) and - * 127.0.0.1:8889 (mapped subsite domain) — two genuinely different hostnames - * with separate cookie jars, which naturally exercises cross-domain SSO. + * 127.0.0.1:8889 (alternate host) — two genuinely different hostnames with + * separate cookie jars, which naturally exercises the redirect-loop guard. */ describe("SSO Redirect Loop Prevention", () => { const mainSiteUrl = "http://localhost:8889"; - const mappedDomainUrl = "http://127.0.0.1:8889"; + const mappedDomainHost = "127.0.0.1"; + const mappedDomainUrl = `http://${mappedDomainHost}:8889`; before(() => { // Ensure SSO test environment is set up (subsite + domain mapping + SSO enabled). @@ -181,7 +182,7 @@ describe("SSO Redirect Loop Prevention", () => { // Set the wu_sso_denied cookie on the mapped domain. cy.setCookie("wu_sso_denied", "1", { - domain: "127.0.0.1", + domain: mappedDomainHost, path: "/", });