From 52e3e0de1ffcdd5270465afcef343825c193f936 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 22 Nov 2023 11:49:51 +0000 Subject: [PATCH] Add a login test against Synapse to Playwright (#11913) * Install playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add foundations for writing tests under Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * .gitignore juggling Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tsconfig and fix eslint rules Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add docker & synapse plugins Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add login.spec.ts Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Wire up fixture which sets up ElementAppPage & bakes config.json Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove launch test, it has served its purpose Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove test which has been ported to Playwright Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Fix test not cleaning up after itself Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Move registerUser to the Homeserver interface Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove unused fixture param Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove redundant launch test Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add newline --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: R Midhun Suresh --- cypress/e2e/login/login.spec.ts | 59 ----- cypress/e2e/login/soft_logout.spec.ts | 1 + package.json | 1 + playwright/.gitignore | 1 + playwright/e2e/launch.spec.ts | 29 --- playwright/e2e/login/login.spec.ts | 78 +++++++ playwright/element-web-test.ts | 64 ++++++ playwright/plugins/docker/index.ts | 153 +++++++++++++ playwright/plugins/synapse/index.ts | 205 ++++++++++++++++++ .../synapse/templates/COPYME/README.md | 3 + .../synapse/templates/COPYME/homeserver.yaml | 72 ++++++ .../synapse/templates/COPYME/log.config | 50 +++++ .../synapse/templates/consent/README.md | 1 + .../synapse/templates/consent/homeserver.yaml | 84 +++++++ .../synapse/templates/consent/log.config | 50 +++++ .../consent/res/templates/privacy/en/1.0.html | 19 ++ .../res/templates/privacy/en/success.html | 9 + .../synapse/templates/default/README.md | 1 + .../synapse/templates/default/homeserver.yaml | 94 ++++++++ .../synapse/templates/default/log.config | 50 +++++ .../plugins/synapse/templates/email/README.md | 1 + .../synapse/templates/email/homeserver.yaml | 44 ++++ .../synapse/templates/email/log.config | 50 +++++ playwright/plugins/utils/homeserver.ts | 57 +++++ playwright/plugins/utils/port.ts | 27 +++ yarn.lock | 24 +- 26 files changed, 1138 insertions(+), 89 deletions(-) delete mode 100644 playwright/e2e/launch.spec.ts create mode 100644 playwright/e2e/login/login.spec.ts create mode 100644 playwright/element-web-test.ts create mode 100644 playwright/plugins/docker/index.ts create mode 100644 playwright/plugins/synapse/index.ts create mode 100644 playwright/plugins/synapse/templates/COPYME/README.md create mode 100644 playwright/plugins/synapse/templates/COPYME/homeserver.yaml create mode 100644 playwright/plugins/synapse/templates/COPYME/log.config create mode 100644 playwright/plugins/synapse/templates/consent/README.md create mode 100644 playwright/plugins/synapse/templates/consent/homeserver.yaml create mode 100644 playwright/plugins/synapse/templates/consent/log.config create mode 100644 playwright/plugins/synapse/templates/consent/res/templates/privacy/en/1.0.html create mode 100644 playwright/plugins/synapse/templates/consent/res/templates/privacy/en/success.html create mode 100644 playwright/plugins/synapse/templates/default/README.md create mode 100644 playwright/plugins/synapse/templates/default/homeserver.yaml create mode 100644 playwright/plugins/synapse/templates/default/log.config create mode 100644 playwright/plugins/synapse/templates/email/README.md create mode 100644 playwright/plugins/synapse/templates/email/homeserver.yaml create mode 100644 playwright/plugins/synapse/templates/email/log.config create mode 100644 playwright/plugins/utils/homeserver.ts create mode 100644 playwright/plugins/utils/port.ts diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts index 2bb7d4c6a0..05dd6f7b8b 100644 --- a/cypress/e2e/login/login.spec.ts +++ b/cypress/e2e/login/login.spec.ts @@ -26,65 +26,6 @@ describe("Login", () => { cy.stopHomeserver(homeserver); }); - describe("m.login.password", () => { - const username = "user1234"; - const password = "p4s5W0rD"; - - beforeEach(() => { - cy.startHomeserver("consent").then((data) => { - homeserver = data; - cy.registerUser(homeserver, username, password); - cy.visit("/#/login"); - }); - }); - - it("logs in with an existing account and lands on the home screen", () => { - cy.injectAxe(); - - // first pick the homeserver, as otherwise the user picker won't be visible - cy.findByRole("button", { name: "Edit" }).click(); - cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl); - cy.findByRole("button", { name: "Continue" }).click(); - // wait for the dialog to go away - cy.get(".mx_ServerPickerDialog").should("not.exist"); - - cy.get(".mx_Spinner").should("not.exist"); - cy.get(".mx_ServerPicker_server").should("have.text", homeserver.baseUrl); - - cy.findByRole("button", { name: "Edit" }).click(); - - // select the default server again - cy.get(".mx_StyledRadioButton").first().click(); - cy.findByRole("button", { name: "Continue" }).click(); - cy.get(".mx_ServerPickerDialog").should("not.exist"); - cy.get(".mx_Spinner").should("not.exist"); - // name of default server - cy.get(".mx_ServerPicker_server").should("have.text", "server.invalid"); - - // switch back to the custom homeserver - - cy.findByRole("button", { name: "Edit" }).click(); - cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl); - cy.findByRole("button", { name: "Continue" }).click(); - // wait for the dialog to go away - cy.get(".mx_ServerPickerDialog").should("not.exist"); - - cy.get(".mx_Spinner").should("not.exist"); - cy.get(".mx_ServerPicker_server").should("have.text", homeserver.baseUrl); - - cy.findByRole("textbox", { name: "Username", timeout: 15000 }).should("be.visible"); - // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 - //cy.percySnapshot("Login"); - cy.checkA11y(); - - cy.findByRole("textbox", { name: "Username" }).type(username); - cy.findByPlaceholderText("Password").type(password); - cy.findByRole("button", { name: "Sign in" }).click(); - - cy.url().should("contain", "/#/home", { timeout: 30000 }); - }); - }); - // tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server describe("SSO login", () => { beforeEach(() => { diff --git a/cypress/e2e/login/soft_logout.spec.ts b/cypress/e2e/login/soft_logout.spec.ts index 43b3b03900..8a96b56e9c 100644 --- a/cypress/e2e/login/soft_logout.spec.ts +++ b/cypress/e2e/login/soft_logout.spec.ts @@ -33,6 +33,7 @@ describe("Soft logout", () => { afterEach(() => { cy.stopHomeserver(homeserver); + cy.task("stopOAuthServer"); }); describe("with password user", () => { diff --git a/package.json b/package.json index 5c4f081b14..95c0500a46 100644 --- a/package.json +++ b/package.json @@ -184,6 +184,7 @@ "@typescript-eslint/parser": "^5.6.0", "allchange": "^1.1.0", "axe-core": "4.8.2", + "axe-playwright": "^1.2.3", "babel-jest": "^29.0.0", "blob-polyfill": "^7.0.0", "cypress": "^12.0.0", diff --git a/playwright/.gitignore b/playwright/.gitignore index ab8ff9582a..7ee0f07a88 100644 --- a/playwright/.gitignore +++ b/playwright/.gitignore @@ -1,2 +1,3 @@ /test-results/ /html-report/ +/synapselogs/ diff --git a/playwright/e2e/launch.spec.ts b/playwright/e2e/launch.spec.ts deleted file mode 100644 index b14fac143d..0000000000 --- a/playwright/e2e/launch.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { test, expect } from "@playwright/test"; - -test.describe("App launch", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - }); - - test("should launch and render the welcome view successfully", async ({ page }) => { - await page.locator("#matrixchat").waitFor(); - await page.locator(".mx_Welcome").waitFor(); - await expect(page).toHaveURL("http://localhost:8080/#/welcome"); - }); -}); diff --git a/playwright/e2e/login/login.spec.ts b/playwright/e2e/login/login.spec.ts new file mode 100644 index 0000000000..e4a8ddd5ce --- /dev/null +++ b/playwright/e2e/login/login.spec.ts @@ -0,0 +1,78 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { checkA11y, injectAxe } from "axe-playwright"; + +import { test, expect } from "../../element-web-test"; + +test.describe("Consent", () => { + test.describe("m.login.password", () => { + test.use({ startHomeserverOpts: "consent" }); + + const username = "user1234"; + const password = "p4s5W0rD"; + + test.beforeEach(async ({ page, homeserver }) => { + await homeserver.registerUser(username, password); + await page.goto("/#/login"); + }); + + test("logs in with an existing account and lands on the home screen", async ({ page, homeserver }) => { + await injectAxe(page); + + // first pick the homeserver, as otherwise the user picker won't be visible + await page.getByRole("button", { name: "Edit" }).click(); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + + await expect(page.locator(".mx_Spinner")).toHaveCount(0); + await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl); + + await page.getByRole("button", { name: "Edit" }).click(); + + // select the default server again + await page.locator(".mx_StyledRadioButton").first().click(); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + await expect(page.locator(".mx_Spinner")).toHaveCount(0); + // name of default server + await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid"); + + // switch back to the custom homeserver + await page.getByRole("button", { name: "Edit" }).click(); + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0); + + await expect(page.locator(".mx_Spinner")).toHaveCount(0); + await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl); + + await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688 + // cy.percySnapshot("Login"); + await checkA11y(page); + + await page.getByRole("textbox", { name: "Username" }).fill(username); + await page.getByPlaceholder("Password").fill(password); + await page.getByRole("button", { name: "Sign in" }).click(); + + await expect(page).toHaveURL(/\/#\/home$/); + }); + }); +}); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts new file mode 100644 index 0000000000..28b97199dc --- /dev/null +++ b/playwright/element-web-test.ts @@ -0,0 +1,64 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { test as base } from "@playwright/test"; + +import { HomeserverInstance, StartHomeserverOpts } from "./plugins/utils/homeserver"; +import { Synapse } from "./plugins/synapse"; + +const CONFIG_JSON = { + // This is deliberately quite a minimal config.json, so that we can test that the default settings + // actually work. + // + // The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver. + // We point that to a guaranteed-invalid domain. + default_server_config: { + "m.homeserver": { + base_url: "https://server.invalid", + }, + }, + + // the location tests want a map style url. + map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx", +}; + +export const test = base.extend<{ + startHomeserverOpts: StartHomeserverOpts | string; + homeserver: HomeserverInstance; +}>({ + page: async ({ context, page }, use) => { + await context.route(`http://localhost:8080/config.json*`, async (route) => { + await route.fulfill({ json: CONFIG_JSON }); + }); + + await use(page); + }, + + startHomeserverOpts: "default", + homeserver: async ({ request, startHomeserverOpts: opts }, use) => { + if (typeof opts === "string") { + opts = { template: opts }; + } + + const server = new Synapse(request); + await use(await server.start(opts)); + await server.stop(); + }, +}); + +test.use({}); + +export { expect } from "@playwright/test"; diff --git a/playwright/plugins/docker/index.ts b/playwright/plugins/docker/index.ts new file mode 100644 index 0000000000..7b6793eaed --- /dev/null +++ b/playwright/plugins/docker/index.ts @@ -0,0 +1,153 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as os from "os"; +import * as crypto from "crypto"; +import * as childProcess from "child_process"; +import * as fse from "fs-extra"; + +export class Docker { + public id: string; + + async run(opts: { image: string; containerName: string; params?: string[]; cmd?: string[] }): Promise { + const userInfo = os.userInfo(); + const params = opts.params ?? []; + + if (params?.includes("-v") && userInfo.uid >= 0) { + // Run the docker container as our uid:gid to prevent problems with permissions. + if (await Docker.isPodman()) { + // Note: this setup is for podman rootless containers. + + // In podman, run as root in the container, which maps to the current + // user on the host. This is probably the default since Synapse's + // Dockerfile doesn't specify, but we're being explicit here + // because it's important for the permissions to work. + params.push("-u", "0:0"); + + // Tell Synapse not to switch UID + params.push("-e", "UID=0"); + params.push("-e", "GID=0"); + } else { + params.push("-u", `${userInfo.uid}:${userInfo.gid}`); + } + } + + const args = [ + "run", + "--name", + `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`, + "-d", + "--rm", + ...params, + opts.image, + ]; + + if (opts.cmd) args.push(...opts.cmd); + + this.id = await new Promise((resolve, reject) => { + childProcess.execFile("docker", args, (err, stdout) => { + if (err) reject(err); + resolve(stdout.trim()); + }); + }); + return this.id; + } + + stop(): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", ["stop", this.id], (err) => { + if (err) reject(err); + resolve(); + }); + }); + } + + exec(params: string[]): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile( + "docker", + ["exec", this.id, ...params], + { encoding: "utf8" }, + (err, stdout, stderr) => { + if (err) { + console.log(stdout); + console.log(stderr); + reject(err); + return; + } + resolve(); + }, + ); + }); + } + + rm(): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", ["rm", this.id], (err) => { + if (err) reject(err); + resolve(); + }); + }); + } + + getContainerIp(): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile( + "docker", + ["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", this.id], + (err, stdout) => { + if (err) reject(err); + else resolve(stdout.trim()); + }, + ); + }); + } + + async persistLogsToFile(args: { stdoutFile?: string; stderrFile?: string }): Promise { + const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore"; + const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore"; + await new Promise((resolve) => { + childProcess + .spawn("docker", ["logs", this.id], { + stdio: ["ignore", stdoutFile, stderrFile], + }) + .once("close", resolve); + }); + if (args.stdoutFile) await fse.close(stdoutFile); + if (args.stderrFile) await fse.close(stderrFile); + } + + /** + * Detects whether the docker command is actually podman. + * To do this, it looks for "podman" in the output of "docker --help". + */ + static isPodman(): Promise { + return new Promise((resolve, reject) => { + childProcess.execFile("docker", ["--help"], (err, stdout) => { + if (err) reject(err); + else resolve(stdout.toLowerCase().includes("podman")); + }); + }); + } + + /** + * Supply the right hostname to use to talk to the host machine. On Docker this + * is "host.docker.internal" and on Podman this is "host.containers.internal". + */ + static async hostnameOfHost(): Promise<"host.containers.internal" | "host.docker.internal"> { + return (await Docker.isPodman()) ? "host.containers.internal" : "host.docker.internal"; + } +} diff --git a/playwright/plugins/synapse/index.ts b/playwright/plugins/synapse/index.ts new file mode 100644 index 0000000000..8095266e75 --- /dev/null +++ b/playwright/plugins/synapse/index.ts @@ -0,0 +1,205 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; +import * as fse from "fs-extra"; +import { APIRequestContext } from "@playwright/test"; + +import { getFreePort } from "../utils/port"; +import { Docker } from "../docker"; +import { + HomeserverConfig, + HomeserverInstance, + Homeserver, + StartHomeserverOpts, + Credentials, +} from "../utils/homeserver"; + +function randB64Bytes(numBytes: number): string { + return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); +} + +async function cfgDirFromTemplate( + opts: StartHomeserverOpts, +): Promise { + const templateDir = path.join(__dirname, "templates", opts.template); + + const stats = await fse.stat(templateDir); + if (!stats?.isDirectory) { + throw new Error(`No such template: ${opts.template}`); + } + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-synapsedocker-")); + + // copy the contents of the template dir, omitting homeserver.yaml as we'll template that + console.log(`Copy ${templateDir} -> ${tempDir}`); + await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== "homeserver.yaml" }); + + const registrationSecret = randB64Bytes(16); + const macaroonSecret = randB64Bytes(16); + const formSecret = randB64Bytes(16); + + const port = await getFreePort(); + const baseUrl = `http://localhost:${port}`; + + // now copy homeserver.yaml, applying substitutions + const templateHomeserver = path.join(templateDir, "homeserver.yaml"); + const outputHomeserver = path.join(tempDir, "homeserver.yaml"); + console.log(`Gen ${templateHomeserver} -> ${outputHomeserver}`); + let hsYaml = await fse.readFile(templateHomeserver, "utf8"); + hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); + hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); + hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); + hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); + if (opts.oAuthServerPort) { + hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort.toString()); + } + hsYaml = hsYaml.replace(/{{HOST_DOCKER_INTERNAL}}/g, await Docker.hostnameOfHost()); + if (opts.variables) { + let fetchedHostContainer: Awaited> | null = null; + for (const key in opts.variables) { + let value = String(opts.variables[key]); + + if (value === "{{HOST_DOCKER_INTERNAL}}") { + if (!fetchedHostContainer) { + fetchedHostContainer = await Docker.hostnameOfHost(); + } + value = fetchedHostContainer; + } + + hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value); + } + } + + await fse.writeFile(outputHomeserver, hsYaml); + + // now generate a signing key (we could use synapse's config generation for + // this, or we could just do this...) + // NB. This assumes the homeserver.yaml specifies the key in this location + const signingKey = randB64Bytes(32); + const outputSigningKey = path.join(tempDir, "localhost.signing.key"); + console.log(`Gen -> ${outputSigningKey}`); + await fse.writeFile(outputSigningKey, `ed25519 x ${signingKey}`); + + return { + port, + baseUrl, + configDir: tempDir, + registrationSecret, + }; +} + +export class Synapse implements Homeserver, HomeserverInstance { + private docker: Docker = new Docker(); + public config: HomeserverConfig & { serverId: string; registrationSecret: string }; + + public constructor(private readonly request: APIRequestContext) {} + + /** + * Start a synapse instance: the template must be the name of + * one of the templates in the playwright/plugins/synapsedocker/templates + * directory. + * + * Any value in opts.variables that is set to `{{HOST_DOCKER_INTERNAL}}' + * will be replaced with 'host.docker.internal' (if we are on Docker) or + * 'host.containers.internal' if we are on Podman. + */ + public async start(opts: StartHomeserverOpts): Promise { + if (this.config) await this.stop(); + + const synCfg = await cfgDirFromTemplate(opts); + console.log(`Starting synapse with config dir ${synCfg.configDir}...`); + const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; + if (await Docker.isPodman()) { + // Make host.containers.internal work to allow Synapse to talk to the test OIDC server. + dockerSynapseParams.push("--network"); + dockerSynapseParams.push("slirp4netns:allow_host_loopback=true"); + } else { + // Make host.docker.internal work to allow Synapse to talk to the test OIDC server. + dockerSynapseParams.push("--add-host"); + dockerSynapseParams.push("host.docker.internal:host-gateway"); + } + const synapseId = await this.docker.run({ + image: "matrixdotorg/synapse:develop", + containerName: `react-sdk-playwright-synapse`, + params: dockerSynapseParams, + cmd: ["run"], + }); + console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`); + // Await Synapse healthcheck + await this.docker.exec([ + "curl", + "--connect-timeout", + "30", + "--retry", + "30", + "--retry-delay", + "1", + "--retry-all-errors", + "--silent", + "http://localhost:8008/health", + ]); + + this.config = { + ...synCfg, + serverId: synapseId, + }; + return this; + } + + public async stop(): Promise { + if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?"); + const id = this.config.serverId; + const synapseLogsPath = path.join("playwright", "synapselogs", id); + await fse.ensureDir(synapseLogsPath); + await this.docker.persistLogsToFile({ + stdoutFile: path.join(synapseLogsPath, "stdout.log"), + stderrFile: path.join(synapseLogsPath, "stderr.log"), + }); + await this.docker.stop(); + await fse.remove(this.config.configDir); + console.log(`Stopped synapse id ${id}.`); + } + + public async registerUser(username: string, password: string, displayName?: string): Promise { + const url = `${this.config.baseUrl}/_synapse/admin/v1/register`; + const { nonce } = await this.request.get(url).then((r) => r.json()); + const mac = crypto + .createHmac("sha1", this.config.registrationSecret) + .update(`${nonce}\0${username}\0${password}\0notadmin`) + .digest("hex"); + const res = await this.request.post(url, { + data: { + nonce, + username, + password, + mac, + admin: false, + displayname: displayName, + }, + }); + + const data = await res.json(); + return { + homeServer: data.home_server, + accessToken: data.access_token, + userId: data.user_id, + deviceId: data.device_id, + password, + }; + } +} diff --git a/playwright/plugins/synapse/templates/COPYME/README.md b/playwright/plugins/synapse/templates/COPYME/README.md new file mode 100644 index 0000000000..df1ed89e6e --- /dev/null +++ b/playwright/plugins/synapse/templates/COPYME/README.md @@ -0,0 +1,3 @@ +# Meta-template for synapse templates + +To make another template, you can copy this directory diff --git a/playwright/plugins/synapse/templates/COPYME/homeserver.yaml b/playwright/plugins/synapse/templates/COPYME/homeserver.yaml new file mode 100644 index 0000000000..cb58dc8661 --- /dev/null +++ b/playwright/plugins/synapse/templates/COPYME/homeserver.yaml @@ -0,0 +1,72 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +# XXX: This won't actually be right: it lets docker allocate an ephemeral port, +# so we have a chicken-and-egg problem +public_baseurl: http://localhost:8008/ +# Listener is always port 8008 (configured in the container) +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +# An sqlite in-memory database is fast & automatically wipes each time +database: + name: "sqlite3" + args: + database: ":memory:" + +# Needs to be configured to log to the console like a good docker process +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +# These placeholders will be be replaced with values generated at start +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +# Signing key must be here: it will be generated to this file +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/playwright/plugins/synapse/templates/COPYME/log.config b/playwright/plugins/synapse/templates/COPYME/log.config new file mode 100644 index 0000000000..ac232762da --- /dev/null +++ b/playwright/plugins/synapse/templates/COPYME/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/plugins/synapse/templates/consent/README.md b/playwright/plugins/synapse/templates/consent/README.md new file mode 100644 index 0000000000..713e55f9d5 --- /dev/null +++ b/playwright/plugins/synapse/templates/consent/README.md @@ -0,0 +1 @@ +A synapse configured with user privacy consent enabled diff --git a/playwright/plugins/synapse/templates/consent/homeserver.yaml b/playwright/plugins/synapse/templates/consent/homeserver.yaml new file mode 100644 index 0000000000..d3a4fa520c --- /dev/null +++ b/playwright/plugins/synapse/templates/consent/homeserver.yaml @@ -0,0 +1,84 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +user_consent: + template_dir: /data/res/templates/privacy + version: 1.0 + server_notice_content: + msgtype: m.text + body: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + send_server_notice_to_guests: True + block_events_error: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + require_at_registration: true + +server_notices: + system_mxid_localpart: notices + system_mxid_display_name: "Server Notices" + system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ" + room_name: "Server Notices" +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/playwright/plugins/synapse/templates/consent/log.config b/playwright/plugins/synapse/templates/consent/log.config new file mode 100644 index 0000000000..b9123d0f5b --- /dev/null +++ b/playwright/plugins/synapse/templates/consent/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/1.0.html b/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/1.0.html new file mode 100644 index 0000000000..8ee888518a --- /dev/null +++ b/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/1.0.html @@ -0,0 +1,19 @@ + + + + Test Privacy policy + + + {% if has_consented %} +

Thank you, you've already accepted the license.

+ {% else %} +

Please accept the license!

+
+ + + + +
+ {% endif %} + + diff --git a/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/success.html b/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 0000000000..8db01e8a6e --- /dev/null +++ b/playwright/plugins/synapse/templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + + Test Privacy policy + + +

Danke schoen

+ + diff --git a/playwright/plugins/synapse/templates/default/README.md b/playwright/plugins/synapse/templates/default/README.md new file mode 100644 index 0000000000..8f6b11f999 --- /dev/null +++ b/playwright/plugins/synapse/templates/default/README.md @@ -0,0 +1 @@ +A synapse configured with user privacy consent disabled diff --git a/playwright/plugins/synapse/templates/default/homeserver.yaml b/playwright/plugins/synapse/templates/default/homeserver.yaml new file mode 100644 index 0000000000..e51ac1918f --- /dev/null +++ b/playwright/plugins/synapse/templates/default/homeserver.yaml @@ -0,0 +1,94 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 +rc_joins: + local: + per_second: 9999 + burst_count: 9999 + remote: + per_second: 9999 + burst_count: 9999 +rc_joins_per_room: + per_second: 9999 + burst_count: 9999 +rc_3pid_validation: + per_second: 1000 + burst_count: 1000 + +rc_invites: + per_room: + per_second: 1000 + burst_count: 1000 + per_user: + per_second: 1000 + burst_count: 1000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +oidc_providers: + - idp_id: test + idp_name: "OAuth test" + issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" + authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" + # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. + # Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to + # host.docker.internal on Docker and host.containers.internal on Podman. + token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + client_id: "synapse" + discover: false + scopes: ["profile"] + skip_verification: true + user_mapping_provider: + config: + display_name_template: "{{ user.name }}" diff --git a/playwright/plugins/synapse/templates/default/log.config b/playwright/plugins/synapse/templates/default/log.config new file mode 100644 index 0000000000..b9123d0f5b --- /dev/null +++ b/playwright/plugins/synapse/templates/default/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: DEBUG + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: DEBUG + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/plugins/synapse/templates/email/README.md b/playwright/plugins/synapse/templates/email/README.md new file mode 100644 index 0000000000..40c23ba0be --- /dev/null +++ b/playwright/plugins/synapse/templates/email/README.md @@ -0,0 +1 @@ +A synapse configured to require an email for registration diff --git a/playwright/plugins/synapse/templates/email/homeserver.yaml b/playwright/plugins/synapse/templates/email/homeserver.yaml new file mode 100644 index 0000000000..fc20641ab4 --- /dev/null +++ b/playwright/plugins/synapse/templates/email/homeserver.yaml @@ -0,0 +1,44 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +registrations_require_3pid: + - email +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +email: + smtp_host: "%SMTP_HOST%" + smtp_port: %SMTP_PORT% + notif_from: "Your Friendly %(app)s homeserver " + app_name: my_branded_matrix_server diff --git a/playwright/plugins/synapse/templates/email/log.config b/playwright/plugins/synapse/templates/email/log.config new file mode 100644 index 0000000000..ac232762da --- /dev/null +++ b/playwright/plugins/synapse/templates/email/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/playwright/plugins/utils/homeserver.ts b/playwright/plugins/utils/homeserver.ts new file mode 100644 index 0000000000..e369fb2142 --- /dev/null +++ b/playwright/plugins/utils/homeserver.ts @@ -0,0 +1,57 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface HomeserverConfig { + readonly configDir: string; + readonly baseUrl: string; + readonly port: number; +} + +export interface HomeserverInstance { + readonly config: HomeserverConfig; + + /** + * Register a user on the given Homeserver using the shared registration secret. + * @param username the username of the user to register + * @param password the password of the user to register + * @param displayName optional display name to set on the newly registered user + */ + registerUser(username: string, password: string, displayName?: string): Promise; +} + +export interface StartHomeserverOpts { + /** path to template within playwright/plugins/{homeserver}docker/template/ directory. */ + template: string; + + /** Port of an OAuth server to configure the homeserver to use */ + oAuthServerPort?: number; + + /** Additional variables to inject into the configuration template **/ + variables?: Record; +} + +export interface Homeserver { + start(opts: StartHomeserverOpts): Promise; + stop(): Promise; +} + +export interface Credentials { + accessToken: string; + userId: string; + deviceId: string; + homeServer: string; + password: string; +} diff --git a/playwright/plugins/utils/port.ts b/playwright/plugins/utils/port.ts new file mode 100644 index 0000000000..156ba866d5 --- /dev/null +++ b/playwright/plugins/utils/port.ts @@ -0,0 +1,27 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as net from "net"; + +export async function getFreePort(): Promise { + return new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, () => { + const port = (srv.address()).port; + srv.close(() => resolve(port)); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index a53f4062d5..02f8435f5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3434,7 +3434,7 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== -axe-core@4.8.2: +axe-core@4.8.2, axe-core@^4.5.1: version "4.8.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae" integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== @@ -3444,6 +3444,23 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axe-html-reporter@2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/axe-html-reporter/-/axe-html-reporter-2.2.3.tgz#2d56e239fe9bd1f09ba0735d94596bf79dd389a7" + integrity sha512-io8aCEt4fJvv43W+33n3zEa8rdplH5Ti2v5fOnth3GBKLhLHarNs7jj46xGfpnGnpaNrz23/tXPHC3HbwTzwwA== + dependencies: + mustache "^4.0.1" + rimraf "^3.0.2" + +axe-playwright@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/axe-playwright/-/axe-playwright-1.2.3.tgz#b590b4edf3898ed5784c4932cbad2937115b31f2" + integrity sha512-bTxCTNp3kx6sQRMjmuLv8pG3+v+kOCvFXATim1+XUXzW6ykulbbuJdQfgB+vQPNAF9uvYbW2qrv9pg81ZSFV/A== + dependencies: + axe-core "^4.5.1" + axe-html-reporter "2.2.3" + picocolors "^1.0.0" + axios-retry@^3.7.0: version "3.9.1" resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.9.1.tgz#c8924a8781c8e0a2c5244abf773deb7566b3830d" @@ -7748,6 +7765,11 @@ murmurhash-js@^1.0.0: resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51" integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw== +mustache@^4.0.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"