From 9f80ce6458594202b432fca3b56dcd27903df2e1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Aug 2023 08:17:24 +0100 Subject: [PATCH 1/8] Simplify registration with email validation Instead of having the email validation flow redirect back to the client which is error-prone due to 2 clients racing on the /register request we only encourage a single tab of Element (the one you started registering on) as this fixes both the flakiness and the double-registration. --- src/components/structures/MatrixChat.tsx | 10 ------- .../structures/auth/Registration.tsx | 26 +++---------------- .../components/structures/MatrixChat-test.tsx | 1 - .../structures/auth/Registration-test.tsx | 1 - 4 files changed, 3 insertions(+), 35 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 1da54addf45..1d8603d0970 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -176,8 +176,6 @@ interface IProps { initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. defaultDeviceDisplayName?: string; - // A function that makes a registration URL - makeRegistrationUrl: (params: QueryDict) => string; } interface IState { @@ -1987,13 +1985,6 @@ export default class MatrixChat extends React.PureComponent { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict): string => { - if (this.props.startingFragmentQueryParams?.referrer) { - params.referrer = this.props.startingFragmentQueryParams.referrer; - } - return this.props.makeRegistrationUrl(params); - }; - /** * After registration or login, we run various post-auth steps before entering the app * proper, such setting up cross-signing or verifying the new session. @@ -2104,7 +2095,6 @@ export default class MatrixChat extends React.PureComponent { idSid={this.state.register_id_sid} email={email} brand={this.props.config.brand} - makeRegistrationUrl={this.makeRegistrationUrl} onLoggedIn={this.onRegisterFlowComplete} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 2a8f89a5cdb..71b34462fd1 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -73,15 +73,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: IMatrixClientCreds, password: string): void; - makeRegistrationUrl(params: { - /* eslint-disable camelcase */ - client_secret: string; - hs_url: string; - is_url?: string; - session_id: string; - /* eslint-enable camelcase */ - }): string; + onLoggedIn(params: IMatrixClientCreds, password: string): Promise; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -302,17 +294,7 @@ export default class Registration extends React.Component { sessionId: string, ): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); - return this.state.matrixClient.requestRegisterEmailToken( - emailAddress, - clientSecret, - sendAttempt, - this.props.makeRegistrationUrl({ - client_secret: clientSecret, - hs_url: this.state.matrixClient.getHomeserverUrl(), - is_url: this.state.matrixClient.getIdentityServerUrl(), - session_id: sessionId, - }), - ); + return this.state.matrixClient.requestRegisterEmailToken(emailAddress, clientSecret, sendAttempt); }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { @@ -401,9 +383,7 @@ export default class Registration extends React.Component { const hasAccessToken = Boolean(accessToken); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user - if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { - // we'll only try logging in if we either have no email to verify at all or we're the client that verified - // the email, not the client that started the registration flow + if (hasAccessToken && !newState.differentLoggedInUserId) { await this.props.onLoggedIn( { userId, diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index 563fafafd63..e78c71f89a7 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -130,7 +130,6 @@ describe("", () => { }, onNewScreen: jest.fn(), onTokenLoginCompleted: jest.fn(), - makeRegistrationUrl: jest.fn(), realQueryParams: {}, }; const getComponent = (props: Partial> = {}) => diff --git a/test/components/structures/auth/Registration-test.tsx b/test/components/structures/auth/Registration-test.tsx index e72ffc58b98..d2ac6b4c23c 100644 --- a/test/components/structures/auth/Registration-test.tsx +++ b/test/components/structures/auth/Registration-test.tsx @@ -74,7 +74,6 @@ describe("Registration", function () { const defaultProps = { defaultDeviceDisplayName: "test-device-display-name", - makeRegistrationUrl: jest.fn(), onLoggedIn: jest.fn(), onLoginClick: jest.fn(), onServerConfigChange: jest.fn(), From dee499d921ae6f58d29d73a4057604bde0c800fd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Aug 2023 10:01:04 +0100 Subject: [PATCH 2/8] Add cypress test for registration with email validation --- cypress/e2e/register/email.spec.ts | 79 ++++++++++++++++ cypress/plugins/index.ts | 2 + cypress/plugins/mailhog/index.ts | 91 +++++++++++++++++++ cypress/plugins/synapsedocker/index.ts | 18 +++- .../synapsedocker/templates/email/README.md | 1 + .../templates/email/homeserver.yaml | 44 +++++++++ .../synapsedocker/templates/email/log.config | 50 ++++++++++ cypress/support/e2e.ts | 1 + cypress/support/homeserver.ts | 9 +- cypress/support/mailhog.ts | 54 +++++++++++ package.json | 1 + yarn.lock | 49 ++++++---- 12 files changed, 373 insertions(+), 26 deletions(-) create mode 100644 cypress/e2e/register/email.spec.ts create mode 100644 cypress/plugins/mailhog/index.ts create mode 100644 cypress/plugins/synapsedocker/templates/email/README.md create mode 100644 cypress/plugins/synapsedocker/templates/email/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/email/log.config create mode 100644 cypress/support/mailhog.ts diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts new file mode 100644 index 00000000000..f718abbe586 --- /dev/null +++ b/cypress/e2e/register/email.spec.ts @@ -0,0 +1,79 @@ +/* +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 { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { Mailhog } from "../../support/mailhog"; + +describe("Email Registration", () => { + let homeserver: HomeserverInstance; + let mailhog: Mailhog; + + beforeEach(() => { + cy.visit("/#/register"); + cy.startMailhog().then((_mailhog) => { + mailhog = _mailhog; + cy.startHomeserver("email", { + SMTP_HOST: "host.docker.internal", + SMTP_PORT: _mailhog.instance.smtpPort, + }).then((_homeserver) => { + homeserver = _homeserver; + }); + }); + cy.injectAxe(); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + cy.stopMailhog(mailhog); + }); + + it("registers an account and lands on the use case selection screen", () => { + cy.findByRole("button", { name: "Edit", timeout: 15000 }).click(); + cy.findByRole("button", { name: "Continue" }).should("be.visible"); + + 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.findByRole("textbox", { name: "Username" }).should("be.visible"); + // Hide the server text as it contains the randomly allocated Homeserver port + const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }"; + + cy.findByRole("textbox", { name: "Username" }).type("alice"); + cy.findByPlaceholderText("Password").type("totally a great password"); + cy.findByPlaceholderText("Confirm password").type("totally a great password"); + cy.findByPlaceholderText("Email").type("alice@email.com"); + cy.findByRole("button", { name: "Register" }).click(); + + cy.findByText("Check your email to continue").should("be.visible"); + cy.percySnapshot("Registration check your email", { percyCSS }); + cy.checkA11y(); + + // Unfortunately the email is not available immediately, so we have a magic wait here + cy.wait(1000).then(async () => { + const messages = await mailhog.api.messages(); + expect(messages.items).to.have.length(1); + expect(messages.items[0].to).to.eq("alice@email.com"); + const [link] = messages.items[0].text.match(/http.+/); + cy.request(link); + }); + + cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist"); + }); +}); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index 8ef9aac7883..743a0bd3b47 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -25,6 +25,7 @@ import { slidingSyncProxyDocker } from "./sliding-sync"; import { webserver } from "./webserver"; import { docker } from "./docker"; import { log } from "./log"; +import { mailhogDocker } from "./mailhog"; /** * @type {Cypress.PluginConfig} @@ -39,4 +40,5 @@ export default function (on: PluginEvents, config: PluginConfigOptions) { installLogsPrinter(on, { // printLogsToConsole: "always", }); + mailhogDocker(on, config); } diff --git a/cypress/plugins/mailhog/index.ts b/cypress/plugins/mailhog/index.ts new file mode 100644 index 00000000000..a156e939818 --- /dev/null +++ b/cypress/plugins/mailhog/index.ts @@ -0,0 +1,91 @@ +/* +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 PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerIp, dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to manage an instance of Mailhog in Docker + +export interface Instance { + host: string; + smtpPort: number; + httpPort: number; + containerId: string; +} + +const instances = new Map(); + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function mailhogStart(): Promise { + const smtpPort = await getFreePort(); + const httpPort = await getFreePort(); + + console.log(`Starting mailhog...`); + + const containerId = await dockerRun({ + image: "mailhog/mailhog:latest", + containerName: `react-sdk-cypress-mailhog`, + params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], + }); + + console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); + + const host = await dockerIp({ containerId }); + const instance: Instance = { smtpPort, httpPort, containerId, host }; + instances.set(containerId, instance); + return instance; +} + +async function mailhogStop(id: string): Promise { + const synCfg = instances.get(id); + + if (!synCfg) throw new Error("Unknown mailhog ID"); + + await dockerStop({ + containerId: id, + }); + + instances.delete(id); + + console.log(`Stopped mailhog id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function mailhogDocker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + mailhogStart, + mailhogStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining instances after a spec run + for (const synId of instances.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await mailhogStop(synId); + } + }); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 3615e4d5117..dc33d146f43 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -36,7 +36,10 @@ function randB64Bytes(numBytes: number): string { return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); } -async function cfgDirFromTemplate(template: string): Promise { +async function cfgDirFromTemplate( + template: string, + variables?: Record, +): Promise { const templateDir = path.join(__dirname, "templates", template); const stats = await fse.stat(templateDir); @@ -63,6 +66,12 @@ async function cfgDirFromTemplate(template: string): Promise { hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); + if (variables) { + for (const key in variables) { + hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), String(variables[key])); + } + } + await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); // now generate a signing key (we could use synapse's config generation for @@ -83,8 +92,11 @@ async function cfgDirFromTemplate(template: string): Promise { // Start a synapse instance: the template must be the name of // one of the templates in the cypress/plugins/synapsedocker/templates // directory -async function synapseStart(template: string): Promise { - const synCfg = await cfgDirFromTemplate(template); +async function synapseStart([template, variables]: [ + string, + Record | undefined, +]): Promise { + const synCfg = await cfgDirFromTemplate(template, variables); console.log(`Starting synapse with config dir ${synCfg.configDir}...`); diff --git a/cypress/plugins/synapsedocker/templates/email/README.md b/cypress/plugins/synapsedocker/templates/email/README.md new file mode 100644 index 00000000000..40c23ba0be4 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/README.md @@ -0,0 +1 @@ +A synapse configured to require an email for registration diff --git a/cypress/plugins/synapsedocker/templates/email/homeserver.yaml b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml new file mode 100644 index 00000000000..97967367add --- /dev/null +++ b/cypress/plugins/synapsedocker/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/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/cypress/plugins/synapsedocker/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/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2ff0197ba65..f8371739987 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -40,6 +40,7 @@ import "./network"; import "./composer"; import "./proxy"; import "./axe"; +import "./mailhog"; installLogsCollector({ // specify the types of logs to collect (and report to the node console at the end of the test) diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index f233c4d41e6..58f4e54b33f 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -30,7 +30,10 @@ declare global { * Start a homeserver instance with a given config template. * @param template path to template within cypress/plugins/{homeserver}docker/template/ directory. */ - startHomeserver(template: string): Chainable; + startHomeserver( + template: string, + variables?: Record, + ): Chainable; /** * Custom command wrapping task:{homeserver}Stop whilst preventing uncaught exceptions @@ -56,9 +59,9 @@ declare global { } } -function startHomeserver(template: string): Chainable { +function startHomeserver(template: string, variables?: Record): Chainable { const homeserverName = Cypress.env("HOMESERVER"); - return cy.task(homeserverName + "Start", template, { log: false }).then((x) => { + return cy.task(homeserverName + "Start", [template, variables], { log: false }).then((x) => { Cypress.log({ name: "startHomeserver", message: `Started homeserver instance ${x.serverId}` }); }); } diff --git a/cypress/support/mailhog.ts b/cypress/support/mailhog.ts new file mode 100644 index 00000000000..86efc5e0f66 --- /dev/null +++ b/cypress/support/mailhog.ts @@ -0,0 +1,54 @@ +/* +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 mailhog from "mailhog"; + +import Chainable = Cypress.Chainable; +import { Instance } from "../plugins/mailhog"; + +export interface Mailhog { + api: mailhog.API; + instance: Instance; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + startMailhog(): Chainable; + stopMailhog(instance: Mailhog): Chainable; + } + } +} + +Cypress.Commands.add("startMailhog", (): Chainable => { + return cy.task("mailhogStart", { log: false }).then((x) => { + Cypress.log({ name: "startHomeserver", message: `Started mailhog instance ${x.containerId}` }); + return { + api: mailhog({ + host: "localhost", + port: x.httpPort, + }), + instance: x, + }; + }); +}); + +Cypress.Commands.add("stopMailhog", (mailhog: Mailhog): Chainable => { + return cy.task("mailhogStop", mailhog.instance.containerId); +}); diff --git a/package.json b/package.json index adc6362b8a1..e241d8f9573 100644 --- a/package.json +++ b/package.json @@ -207,6 +207,7 @@ "jest-mock": "^29.2.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "mailhog": "^4.16.0", "matrix-mock-request": "^2.5.0", "matrix-web-i18n": "^1.4.0", "mocha-junit-reporter": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 3d0f5319bd1..08e3ac736f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,9 +1389,9 @@ integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== "@cypress/request@^2.88.11": - version "2.88.11" - resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.11.tgz#5a4c7399bc2d7e7ed56e92ce5acb620c8b187047" - integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w== + version "2.88.12" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" + integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -1408,7 +1408,7 @@ performance-now "^2.1.0" qs "~6.10.3" safe-buffer "^5.1.2" - tough-cookie "~2.5.0" + tough-cookie "^4.1.3" tunnel-agent "^0.6.0" uuid "^8.3.2" @@ -2615,16 +2615,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== -"@types/node@^14.14.31": - version "14.18.54" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.54.tgz#fc304bd66419030141fa997dc5a9e0e374029ae8" - integrity sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw== - "@types/node@^16": version "16.18.39" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.39.tgz#aa39a1a87a40ef6098ee69689a1acb0c1b034832" integrity sha512-8q9ZexmdYYyc5/cfujaXb4YOucpQxAV4RMG0himLyDUOEr8Mr79VrqsFI+cQ2M2h89YIuy95lbxuYjxT4Hk4kQ== +"@types/node@^16.18.39": + version "16.18.40" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.40.tgz#968d64746d20cac747a18ca982c0f1fe518c031c" + integrity sha512-+yno3ItTEwGxXiS/75Q/aHaa5srkpnJaH+kdkTVJ3DtJEwv92itpKbxU+FjPoh2m/5G9zmUQfrL4A4C13c+iGA== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -3993,13 +3993,13 @@ cypress-terminal-report@^5.3.2: tv4 "^1.3.0" cypress@^12.0.0: - version "12.17.2" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.2.tgz#040ac55de1e811f6e037d231a2869d5ab8c29c85" - integrity sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg== + version "12.17.3" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.3.tgz#1e2b19037236fc60e4a4b3a14f0846be17a1fc0e" + integrity sha512-/R4+xdIDjUSLYkiQfwJd630S81KIgicmQOLXotFxVXkl+eTeVO+3bHXxdi5KBh/OgC33HWN33kHX+0tQR/ZWpg== dependencies: "@cypress/request" "^2.88.11" "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" + "@types/node" "^16.18.39" "@types/sinonjs__fake-timers" "8.1.1" "@types/sizzle" "^2.3.2" arch "^2.2.0" @@ -5619,7 +5619,7 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.6.3: +iconv-lite@0.6.3, iconv-lite@^0.6: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -6917,6 +6917,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +mailhog@^4.16.0: + version "4.16.0" + resolved "https://registry.yarnpkg.com/mailhog/-/mailhog-4.16.0.tgz#1ad4dda104505399f3f17824737a962696e7d240" + integrity sha512-wXrGik+0MaAy4dbYTImxa8niX9a4aRpZTzC/b1GzCvQs09khhs0aKZgHjgScakI4Y18WInDvvF48hhEz9ifN4g== + optionalDependencies: + iconv-lite "^0.6" + make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" @@ -7835,7 +7842,7 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== @@ -8987,13 +8994,15 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: - psl "^1.1.28" + psl "^1.1.33" punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" tr46@^1.0.1: version "1.0.1" From 1d1b5ee31e583c0713d9884f69ec36a4dcc71eb5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Aug 2023 10:16:10 +0100 Subject: [PATCH 3/8] Bump wait --- cypress/e2e/register/email.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts index f718abbe586..6e3989bc61d 100644 --- a/cypress/e2e/register/email.spec.ts +++ b/cypress/e2e/register/email.spec.ts @@ -66,7 +66,7 @@ describe("Email Registration", () => { cy.checkA11y(); // Unfortunately the email is not available immediately, so we have a magic wait here - cy.wait(1000).then(async () => { + cy.wait(5000).then(async () => { const messages = await mailhog.api.messages(); expect(messages.items).to.have.length(1); expect(messages.items[0].to).to.eq("alice@email.com"); From caff0c5e7133b831eccd88feb8042a7a71af6269 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 11 Aug 2023 11:13:17 +0100 Subject: [PATCH 4/8] Iterate --- cypress/e2e/register/email.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts index 6e3989bc61d..0dc2d3864b1 100644 --- a/cypress/e2e/register/email.spec.ts +++ b/cypress/e2e/register/email.spec.ts @@ -28,7 +28,7 @@ describe("Email Registration", () => { cy.startMailhog().then((_mailhog) => { mailhog = _mailhog; cy.startHomeserver("email", { - SMTP_HOST: "host.docker.internal", + SMTP_HOST: _mailhog.instance.host, SMTP_PORT: _mailhog.instance.smtpPort, }).then((_homeserver) => { homeserver = _homeserver; From c02fdae6b0ecb2b3cf9c6e57c52e9f46b1292fdd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Aug 2023 09:30:43 +0100 Subject: [PATCH 5/8] Crank the log level --- cypress/plugins/synapsedocker/templates/email/log.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config index ac232762da3..fc15f06a730 100644 --- a/cypress/plugins/synapsedocker/templates/email/log.config +++ b/cypress/plugins/synapsedocker/templates/email/log.config @@ -36,7 +36,7 @@ loggers: propagate: false root: - level: INFO + level: DEBUG # Write logs to the `buffer` handler, which will buffer them together in memory, # then write them to a file. From 5d98a4039aa47d78fac02a2bcf4bf3ff1a25a2c9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Aug 2023 11:32:35 +0100 Subject: [PATCH 6/8] Iterate --- cypress/e2e/register/email.spec.ts | 4 +++- cypress/plugins/synapsedocker/templates/email/homeserver.yaml | 2 +- cypress/plugins/synapsedocker/templates/email/log.config | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts index 0dc2d3864b1..fd76a612c14 100644 --- a/cypress/e2e/register/email.spec.ts +++ b/cypress/e2e/register/email.spec.ts @@ -28,7 +28,7 @@ describe("Email Registration", () => { cy.startMailhog().then((_mailhog) => { mailhog = _mailhog; cy.startHomeserver("email", { - SMTP_HOST: _mailhog.instance.host, + SMTP_HOST: "172.17.0.1", SMTP_PORT: _mailhog.instance.smtpPort, }).then((_homeserver) => { homeserver = _homeserver; @@ -65,6 +65,8 @@ describe("Email Registration", () => { cy.percySnapshot("Registration check your email", { percyCSS }); cy.checkA11y(); + cy.findByText("An error was encountered when sending the email").should("not.exist"); + // Unfortunately the email is not available immediately, so we have a magic wait here cy.wait(5000).then(async () => { const messages = await mailhog.api.messages(); diff --git a/cypress/plugins/synapsedocker/templates/email/homeserver.yaml b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml index 97967367add..fc20641ab40 100644 --- a/cypress/plugins/synapsedocker/templates/email/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml @@ -38,7 +38,7 @@ ui_auth: session_timeout: "300s" email: - smtp_host: %SMTP_HOST% + smtp_host: "%SMTP_HOST%" smtp_port: %SMTP_PORT% notif_from: "Your Friendly %(app)s homeserver " app_name: my_branded_matrix_server diff --git a/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config index fc15f06a730..ac232762da3 100644 --- a/cypress/plugins/synapsedocker/templates/email/log.config +++ b/cypress/plugins/synapsedocker/templates/email/log.config @@ -36,7 +36,7 @@ loggers: propagate: false root: - level: DEBUG + level: INFO # Write logs to the `buffer` handler, which will buffer them together in memory, # then write them to a file. From 5679405441dfcf502af87ed038cd0c7c645e5fc4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 14 Aug 2023 11:38:01 +0100 Subject: [PATCH 7/8] Iterate --- cypress/e2e/register/email.spec.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts index fd76a612c14..afa4bf78f5c 100644 --- a/cypress/e2e/register/email.spec.ts +++ b/cypress/e2e/register/email.spec.ts @@ -24,7 +24,6 @@ describe("Email Registration", () => { let mailhog: Mailhog; beforeEach(() => { - cy.visit("/#/register"); cy.startMailhog().then((_mailhog) => { mailhog = _mailhog; cy.startHomeserver("email", { @@ -32,9 +31,26 @@ describe("Email Registration", () => { SMTP_PORT: _mailhog.instance.smtpPort, }).then((_homeserver) => { homeserver = _homeserver; + + cy.intercept( + { method: "GET", pathname: "/config.json" }, + { + body: { + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "m.identity_server": { + base_url: "https://server.invalid", + }, + }, + }, + }, + ); + cy.visit("/#/register"); + cy.injectAxe(); }); }); - cy.injectAxe(); }); afterEach(() => { @@ -43,14 +59,6 @@ describe("Email Registration", () => { }); it("registers an account and lands on the use case selection screen", () => { - cy.findByRole("button", { name: "Edit", timeout: 15000 }).click(); - cy.findByRole("button", { name: "Continue" }).should("be.visible"); - - 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.findByRole("textbox", { name: "Username" }).should("be.visible"); // Hide the server text as it contains the randomly allocated Homeserver port const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }"; From fa0ec5d20142af040d2b606f0286969c2cea4db8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 15 Aug 2023 12:53:43 +0100 Subject: [PATCH 8/8] Iterate --- cypress/e2e/register/email.spec.ts | 9 ++--- cypress/support/e2e.ts | 1 + cypress/support/promise.ts | 58 ++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 cypress/support/promise.ts diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts index 1428f82703b..a93c05c6eef 100644 --- a/cypress/e2e/register/email.spec.ts +++ b/cypress/e2e/register/email.spec.ts @@ -78,14 +78,15 @@ describe("Email Registration", () => { cy.findByText("An error was encountered when sending the email").should("not.exist"); - // Unfortunately the email is not available immediately, so we have a magic wait here - cy.wait(5000).then(async () => { + cy.waitForPromise(async () => { const messages = await mailhog.api.messages(); expect(messages.items).to.have.length(1); expect(messages.items[0].to).to.eq("alice@email.com"); const [link] = messages.items[0].text.match(/http.+/); - cy.request(link); - }); + return link; + }).as("emailLink"); + + cy.get("@emailLink").then((link) => cy.request(link)); cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist"); }); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index f8371739987..11eae401f9e 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -41,6 +41,7 @@ import "./composer"; import "./proxy"; import "./axe"; import "./mailhog"; +import "./promise"; installLogsCollector({ // specify the types of logs to collect (and report to the node console at the end of the test) diff --git a/cypress/support/promise.ts b/cypress/support/promise.ts new file mode 100644 index 00000000000..4baaf75e8ea --- /dev/null +++ b/cypress/support/promise.ts @@ -0,0 +1,58 @@ +/* +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 Chainable = Cypress.Chainable; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Utility wrapper around promises to help control flow in tests + * Calls `fn` function `tries` times, with a sleep of `interval` between calls. + * Ensure you do not rely on any effects of calling any `cy.*` functions within the body of `fn` + * as the calls will not happen until after waitForPromise returns. + * @param fn the function to retry + * @param tries the number of tries to call it + * @param interval the time interval between tries + */ + waitForPromise(fn: () => Promise, tries?: number, interval?: number): Chainable; + } + } +} + +function waitForPromise(fn: () => Promise, tries = 10, interval = 1000): Chainable { + return cy.then( + () => + new Cypress.Promise(async (resolve, reject) => { + for (let i = 0; i < tries; i++) { + try { + const v = await fn(); + resolve(v); + } catch { + await new Cypress.Promise((resolve) => setTimeout(resolve, interval)); + } + } + reject(); + }), + ); +} + +Cypress.Commands.add("waitForPromise", waitForPromise); + +export {};