Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cypress/integration/1-register/register.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ describe("Registration", () => {
let synapse: SynapseInstance;

beforeEach(() => {
cy.visit("/#/register");
cy.startSynapse("consent").then(data => {
synapse = data;
});
cy.visit("/#/register");
});

afterEach(() => {
Expand All @@ -34,14 +34,16 @@ describe("Registration", () => {

it("registers an account and lands on the home screen", () => {
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(`http://localhost:${synapse.port}`);
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');

cy.get("#mx_RegistrationForm_username").type("alice");
cy.get("#mx_RegistrationForm_password").type("totally a great password");
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
cy.get(".mx_Login_submit").click();

cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
Expand Down
57 changes: 57 additions & 0 deletions cypress/integration/2-login/login.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
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.
*/

/// <reference types="cypress" />

import { SynapseInstance } from "../../plugins/synapsedocker";

describe("Login", () => {
let synapse: SynapseInstance;

beforeEach(() => {
cy.visit("/#/login");
cy.startSynapse("consent").then(data => {
synapse = data;
});
});

afterEach(() => {
cy.stopSynapse(synapse);
});

describe("m.login.password", () => {
const username = "user1234";
const password = "p4s5W0rD";

beforeEach(() => {
cy.registerUser(synapse, username, password);
});

it("logs in with an existing account and lands on the home screen", () => {
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');

cy.get("#mx_LoginForm_username").type(username);
cy.get("#mx_LoginForm_password").type(password);
cy.get(".mx_Login_submit").click();

cy.url().should('contain', '/#/home');
});
});
});
45 changes: 45 additions & 0 deletions cypress/integration/3-user-menu/user-menu.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
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.
*/

/// <reference types="cypress" />

import { SynapseInstance } from "../../plugins/synapsedocker";
import type { UserCredentials } from "../../support/login";

describe("UserMenu", () => {
let synapse: SynapseInstance;
let user: UserCredentials;

beforeEach(() => {
cy.startSynapse("consent").then(data => {
synapse = data;

cy.initTestUser(synapse, "Jeff").then(credentials => {
user = credentials;
});
});
});

afterEach(() => {
cy.stopSynapse(synapse);
});

it("should contain our name & userId", () => {
cy.get('[aria-label="User menu"]', { timeout: 15000 }).click();
cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
});
});
56 changes: 38 additions & 18 deletions cypress/plugins/synapsedocker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import * as os from "os";
import * as crypto from "crypto";
import * as childProcess from "child_process";
import * as fse from "fs-extra";
import * as net from "net";

import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
Expand All @@ -31,11 +32,13 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
interface SynapseConfig {
configDir: string;
registrationSecret: string;
// Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage
baseUrl: string;
port: number;
}

export interface SynapseInstance extends SynapseConfig {
synapseId: string;
port: number;
}

const synapses = new Map<string, SynapseInstance>();
Expand All @@ -44,6 +47,16 @@ function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
}

async function getFreePort(): Promise<number> {
return new Promise<number>(resolve => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port;
srv.close(() => resolve(port));
});
});
}

async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
const templateDir = path.join(__dirname, "templates", template);

Expand All @@ -64,12 +77,16 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
const macaroonSecret = randB64Bytes(16);
const formSecret = randB64Bytes(16);

// now copy homeserver.yaml, applying sustitutions
const port = await getFreePort();
const baseUrl = `http://localhost:${port}`;

// now copy homeserver.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "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);
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);

// now generate a signing key (we could use synapse's config generation for
Expand All @@ -80,6 +97,8 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);

return {
port,
baseUrl,
configDir: tempDir,
registrationSecret,
};
Expand All @@ -101,7 +120,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
"--name", containerName,
"-d",
"-v", `${synCfg.configDir}:/data`,
"-p", "8008/tcp",
"-p", `${synCfg.port}:8008/tcp`,
"matrixdotorg/synapse:develop",
"run",
], (err, stdout) => {
Expand All @@ -110,26 +129,27 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
});
});

// Get the port that docker allocated: specifying only one
// port above leaves docker to just grab a free one, although
// in hindsight we need to put the port in public_baseurl in the
// config really, so this will probably need changing to use a fixed
// / configured port.
const port = await new Promise<number>((resolve, reject) => {
childProcess.execFile('docker', [
"port", synapseId, "8008",
synapses.set(synapseId, { synapseId, ...synCfg });

console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);

// Await Synapse healthcheck
await new Promise<void>((resolve, reject) => {
childProcess.execFile("docker", [
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to run this as a curl in the docker container? Couldn't we just poll in javascript?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I guess you get the retries / timeout for free, and you know there's a curl in the docker container. If I've guessed correctly please comment. :)

Copy link
Member Author

@t3chguy t3chguy May 4, 2022

Choose a reason for hiding this comment

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

Yes correct, we can't use cy.request as it doesn't retry on total connection failure
We could use http but that's horribly unusable, don't want to have to bring in another dep like fetch or axios where it isn't needed
This is a price of leaning into the cypress anti-pattern of standing up servers in tasks

"exec", synapseId,
"curl",
"--connect-timeout", "30",
"--retry", "30",
"--retry-delay", "1",
"--retry-all-errors",
"--silent",
"http://localhost:8008/health",
], { encoding: 'utf8' }, (err, stdout) => {
if (err) reject(err);
resolve(Number(stdout.trim().split(":")[1]));
else resolve();
});
});

synapses.set(synapseId, Object.assign({
port,
synapseId,
}, synCfg));

console.log(`Started synapse with id ${synapseId} on port ${port}.`);
return synapses.get(synapseId);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: http://localhost:5005/
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
Expand Down
1 change: 1 addition & 0 deletions cypress/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ limitations under the License.
/// <reference types="cypress" />

import "./synapse";
import "./login";
86 changes: 86 additions & 0 deletions cypress/support/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
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.
*/

/// <reference types="cypress" />

import Chainable = Cypress.Chainable;
import { SynapseInstance } from "../plugins/synapsedocker";

export interface UserCredentials {
accessToken: string;
userId: string;
deviceId: string;
password: string;
homeServer: string;
}

declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Generates a test user and instantiates an Element session with that user.
* @param synapse the synapse returned by startSynapse
* @param displayName the displayName to give the test user
*/
initTestUser(synapse: SynapseInstance, displayName: string): Chainable<UserCredentials>;
}
}
}

Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

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

This is fun - is this a cypress internal or something and what's it doing?

Copy link
Member Author

@t3chguy t3chguy May 4, 2022

Choose a reason for hiding this comment

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

Its a re-exported lodash https://lodash.com/docs/4.17.15#uniqueId - sequential id generator, nothing fancy

return cy.registerUser(synapse, username, password, displayName).then(() => {
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
return cy.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>({
url,
method: "POST",
body: {
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": username,
},
"password": password,
},
});
}).then(response => {
return cy.window().then(win => {
// Seed the localStorage with the required credentials
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
win.localStorage.setItem("mx_user_id", response.body.user_id);
win.localStorage.setItem("mx_access_token", response.body.access_token);
win.localStorage.setItem("mx_device_id", response.body.device_id);
win.localStorage.setItem("mx_is_guest", "false");
win.localStorage.setItem("mx_has_pickle_key", "false");
win.localStorage.setItem("mx_has_access_token", "true");

return cy.visit("/").then(() => ({
password,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
homeServer: response.body.home_server,
}));
});
});
});
Loading