diff --git a/Taskfile.yml b/Taskfile.yml index e15fb8660..9b6c498c0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -189,7 +189,7 @@ tasks: dev:enable-dev-tools: desc: Enable dev modules and settings, which are not to be used in Prod. They are config-ignored cmds: - - task dev:cli -- drush install -y devel dpl_example_content field_ui restui uuid_url views_ui dblog + - task dev:cli -- drush install -y devel dpl_example_content field_ui purge_ui restui uuid_url views_ui dblog dev:enable-xdebug: desc: "Enable xdebug within the container." diff --git a/assets/all.settings.php b/assets/all.settings.php index 5303940a7..279633fcd 100644 --- a/assets/all.settings.php +++ b/assets/all.settings.php @@ -40,6 +40,7 @@ 'dpl_example_breadcrumb', 'devel', 'field_ui', + 'purge_ui', 'views_ui', 'restui', 'upgrade_status', diff --git a/composer.json b/composer.json index 64b91b097..042b712ff 100644 --- a/composer.json +++ b/composer.json @@ -150,7 +150,7 @@ "drupal/password_policy": "^4.0", "drupal/pathauto": "^1.12", "drupal/potion": "dev-2.x#e48d6ee336d960b9d2efb5d88e68402db6d229a6", - "drupal/purge": "^3.2", + "drupal/purge": "^3.6", "drupal/recurring_events": "^2.0@RC", "drupal/redirect": "^1.9", "drupal/redis": "^1.7", @@ -350,8 +350,8 @@ "3413314: Include .links.menu.yml files in YamlExtractor.php": "https://www.drupal.org/files/issues/2024-01-08/potion-add_links_menu_yml_title_and_description_to_yaml_extractor-3413314-3.patch" }, "drupal/purge": { - "3391383: Uninstalling module triggers ServiceNotFoundException exception": "https://git.drupalcode.org/project/purge/-/commit/88546a6e6c043f10832be43e02ae4ae65cf5ceb2.patch", - "Combat NoCorrespondingEntityClassException": "patches/purge-5340bcf-combat-nocorresponding-entity-exception.patch" + "Combat NoCorrespondingEntityClassException": "patches/purge-5340bcf-combat-nocorresponding-entity-exception.patch", + "3462078: Disabling late runtime processor when using CLI": "https://git.drupalcode.org/project/purge/-/commit/14c09e0ec4f1f46df0f19876ad6b3e8c93921e00.patch" }, "drupal/recurring_events": { "3178696: Make compatible with Scheduler": "https://git.drupalcode.org/project/recurring_events/-/merge_requests/86.patch", diff --git a/composer.lock b/composer.lock index bca12694b..06ba9be61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "75a25c022bda0c981cd4e420738f8791", + "content-hash": "866e32c1241163882e64b92d29697b2b", "packages": [ { "name": "amazeeio/drupal_integrations", @@ -6396,26 +6396,26 @@ }, { "name": "drupal/purge", - "version": "3.5.0", + "version": "3.6.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/purge.git", - "reference": "8.x-3.5" + "reference": "8.x-3.6" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/purge-8.x-3.5.zip", - "reference": "8.x-3.5", - "shasum": "3772c156be0a2fc1df179710d23b9d7bf645b112" + "url": "https://ftp.drupal.org/files/projects/purge-8.x-3.6.zip", + "reference": "8.x-3.6", + "shasum": "f01d53c5a1d34301e86371c70a1d237a517b2897" }, "require": { - "drupal/core": "^9.2 || ^10" + "drupal/core": "^9.5 || ^10 || ^11" }, "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-3.5", - "datestamp": "1697142035", + "version": "8.x-3.6", + "datestamp": "1719557519", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -6426,7 +6426,7 @@ }, "drush": { "services": { - "drush.services.yml": ">=9" + "drush.services.yml": ">=10" } } }, diff --git a/config/sync/purge_processor_lateruntime.settings.yml b/config/sync/purge_processor_lateruntime.settings.yml new file mode 100644 index 000000000..491a5aa4c --- /dev/null +++ b/config/sync/purge_processor_lateruntime.settings.yml @@ -0,0 +1 @@ +cli_disable: true diff --git a/cypress/e2e/campaign.cy.ts b/cypress/e2e/campaign.cy.ts index 2feca7d66..d00d697a2 100644 --- a/cypress/e2e/campaign.cy.ts +++ b/cypress/e2e/campaign.cy.ts @@ -288,8 +288,6 @@ describe("Campaign creation and endpoint", () => { }); before(() => { - // Login as admin. - cy.clearCookies(); cy.drupalLogin(); // Create campaigns. @@ -298,13 +296,10 @@ describe("Campaign creation and endpoint", () => { createRankingAndCampaign(); createRankingOrCampaign(); - // Logout (obviously). - cy.drupalLogout(); + cy.anonymousUser(); }); after(() => { - // Login as admin. - cy.clearCookies(); cy.drupalLogin(); // Delete campaigns. @@ -313,8 +308,7 @@ describe("Campaign creation and endpoint", () => { deleteCampaign(campaigns.rankingAndCampaign); deleteCampaign(campaigns.rankingOrCampaign); - // Logout (obviously). - cy.drupalLogout(); + cy.anonymousUser(); }); beforeEach(() => { diff --git a/cypress/e2e/varnish.cy.ts b/cypress/e2e/varnish.cy.ts new file mode 100644 index 000000000..e49e27a14 --- /dev/null +++ b/cypress/e2e/varnish.cy.ts @@ -0,0 +1,95 @@ +import "cypress-if"; + +const node = { + title: "Varnish test", + subtitle: "A subtitle", + path: "/articles/varnish-test", +}; + +const varnishCacheHeader = "x-varnish-cache"; + +describe("Varnish", () => { + it("is caching responses for anonymous users", () => { + cy.anonymousUser(); + // Query the front page twice to ensure that Varnish has had a chance to + // cache the response. + cy.request("/"); + cy.request("/").then((response) => { + cy.log("Headers", response.headers); + expect(response.headers).to.have.property(varnishCacheHeader, "HIT"); + }); + }); + + it("is purged when updating content", () => { + // Create a node as admin. + cy.drupalLogin("/node/add/article"); + cy.findByLabelText("Title").type(node.title); + cy.findByRole("button", { name: "Save" }).click(); + cy.contains(node.title); + cy.should("not.contain", node.subtitle); + // We do not have a good way to store the current path between tests so + // instead we ensure that the expected path is correct. + cy.url().should("include", node.path); + + // Check that the node is accessible and rendered with the expected content + // for anonymous users. + cy.anonymousUser(); + cy.visit(node.path); + cy.contains(node.title); + cy.should("not.contain", node.subtitle); + + // Edit the page as admin and ensure that it is updated. + cy.drupalLogin(); + cy.visit(node.path); + cy.findByRole("link", { + name: `Edit ${node.title}`, + }).click({ + // Use force as the toolbar may cover the Edit link. + force: true, + }); + cy.findByLabelText("Subtitle").type(node.subtitle); + cy.findByRole("button", { name: "Save" }).click(); + cy.contains(node.title); + cy.contains(node.subtitle); + + // Ensure that the cache is purged and update shown immediately to + // anonymous users. + cy.anonymousUser(); + cy.request(node.path).then((response) => { + cy.log("Headers", response.headers); + expect(response.headers).to.have.property(varnishCacheHeader, "MISS"); + }); + cy.visit(node.path); + cy.contains(node.title); + cy.contains(node.subtitle); + }); + + before(() => { + cy.drupalLogin("/admin/content"); + // Delete all preexisting instances of the node. + cy.get("a") + .contains(node.title) + .if() + .each(() => { + // We have to repeat the selector as Cypress will otherwise complain about + // missing references to elements when clicking the page. + cy.findAllByRole("link", { name: node.title }).first().click(); + cy.findByRole("link", { + name: `Edit ${node.title}`, + }).click(); + cy.findByRole("button", { name: "More actions" }) + .click() + .parent() + .findByRole("link", { name: "Delete" }) + .click(); + cy.findByRole("dialog") + .findByRole("button", { name: "Delete" }) + .click(); + + // Return to the node list to prepare for the next iteration. + cy.visit("/admin/content"); + }); + + cy.anonymousUser(); + }); +}); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 3073d7200..9b7bf294b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -78,19 +78,27 @@ Cypress.Commands.add("logRequests", () => { }); Cypress.Commands.add("drupalLogin", (url?: string) => { - cy.clearCookies(); - cy.visit("/user/login"); - cy.get('[name="name"]') - .type(Cypress.env("DRUPAL_USERNAME")) - .parent() - .get('[name="pass"]') - .type(Cypress.env("DRUPAL_PASSWORD")); - cy.get('[value="Log in"]').click(); + const username = Cypress.env("DRUPAL_USERNAME"); + const password = Cypress.env("DRUPAL_PASSWORD"); + cy.session({ username, password }, () => { + cy.visit("/user/login"); + cy.get('[name="name"]') + .type(username) + .parent() + .get('[name="pass"]') + .type(password); + cy.get('[value="Log in"]').click(); + }); + if (url) { cy.visit(url); } }); +Cypress.Commands.add("anonymousUser", () => { + cy.session("anonymous", () => {}); +}); + Cypress.Commands.add("drupalLogout", () => { cy.visit("/logout"); }); @@ -334,6 +342,7 @@ declare global { logRequests(): Chainable; getRequestCount(request: RequestPattern): Chainable; resetRequests(): Chainable; + anonymousUser(): Chainable; drupalLogin(url?: string): Chainable; drupalLogout(): Chainable; drupalCron(): Chainable;