Create more cypress tests and utilities (#8494)
parent
fd6498a821
commit
77a437f30a
|
@ -22,10 +22,10 @@ describe("Registration", () => {
|
||||||
let synapse: SynapseInstance;
|
let synapse: SynapseInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
cy.visit("/#/register");
|
||||||
cy.startSynapse("consent").then(data => {
|
cy.startSynapse("consent").then(data => {
|
||||||
synapse = data;
|
synapse = data;
|
||||||
});
|
});
|
||||||
cy.visit("/#/register");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -34,14 +34,16 @@ describe("Registration", () => {
|
||||||
|
|
||||||
it("registers an account and lands on the home screen", () => {
|
it("registers an account and lands on the home screen", () => {
|
||||||
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
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();
|
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||||
// wait for the dialog to go away
|
// wait for the dialog to go away
|
||||||
cy.get('.mx_ServerPickerDialog').should('not.exist');
|
cy.get('.mx_ServerPickerDialog').should('not.exist');
|
||||||
|
|
||||||
cy.get("#mx_RegistrationForm_username").type("alice");
|
cy.get("#mx_RegistrationForm_username").type("alice");
|
||||||
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
||||||
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
|
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
|
||||||
cy.get(".mx_Login_submit").click();
|
cy.get(".mx_Login_submit").click();
|
||||||
|
|
||||||
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
||||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
||||||
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -21,6 +21,7 @@ import * as os from "os";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
|
import * as net from "net";
|
||||||
|
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
@ -31,11 +32,13 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
interface SynapseConfig {
|
interface SynapseConfig {
|
||||||
configDir: string;
|
configDir: string;
|
||||||
registrationSecret: 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 {
|
export interface SynapseInstance extends SynapseConfig {
|
||||||
synapseId: string;
|
synapseId: string;
|
||||||
port: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const synapses = new Map<string, SynapseInstance>();
|
const synapses = new Map<string, SynapseInstance>();
|
||||||
|
@ -44,6 +47,16 @@ function randB64Bytes(numBytes: number): string {
|
||||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
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> {
|
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
const templateDir = path.join(__dirname, "templates", template);
|
const templateDir = path.join(__dirname, "templates", template);
|
||||||
|
|
||||||
|
@ -64,12 +77,16 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
const macaroonSecret = randB64Bytes(16);
|
const macaroonSecret = randB64Bytes(16);
|
||||||
const formSecret = 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")}`);
|
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
|
||||||
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
|
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
|
||||||
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
|
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
|
||||||
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
|
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
|
||||||
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
|
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
|
||||||
|
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
|
||||||
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
|
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
|
||||||
|
|
||||||
// now generate a signing key (we could use synapse's config generation for
|
// now generate a signing key (we could use synapse's config generation for
|
||||||
|
@ -80,6 +97,8 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
|
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
port,
|
||||||
|
baseUrl,
|
||||||
configDir: tempDir,
|
configDir: tempDir,
|
||||||
registrationSecret,
|
registrationSecret,
|
||||||
};
|
};
|
||||||
|
@ -101,7 +120,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
"--name", containerName,
|
"--name", containerName,
|
||||||
"-d",
|
"-d",
|
||||||
"-v", `${synCfg.configDir}:/data`,
|
"-v", `${synCfg.configDir}:/data`,
|
||||||
"-p", "8008/tcp",
|
"-p", `${synCfg.port}:8008/tcp`,
|
||||||
"matrixdotorg/synapse:develop",
|
"matrixdotorg/synapse:develop",
|
||||||
"run",
|
"run",
|
||||||
], (err, stdout) => {
|
], (err, stdout) => {
|
||||||
|
@ -110,26 +129,27 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the port that docker allocated: specifying only one
|
synapses.set(synapseId, { synapseId, ...synCfg });
|
||||||
// port above leaves docker to just grab a free one, although
|
|
||||||
// in hindsight we need to put the port in public_baseurl in the
|
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||||
// config really, so this will probably need changing to use a fixed
|
|
||||||
// / configured port.
|
// Await Synapse healthcheck
|
||||||
const port = await new Promise<number>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
childProcess.execFile('docker', [
|
childProcess.execFile("docker", [
|
||||||
"port", synapseId, "8008",
|
"exec", synapseId,
|
||||||
|
"curl",
|
||||||
|
"--connect-timeout", "30",
|
||||||
|
"--retry", "30",
|
||||||
|
"--retry-delay", "1",
|
||||||
|
"--retry-all-errors",
|
||||||
|
"--silent",
|
||||||
|
"http://localhost:8008/health",
|
||||||
], { encoding: 'utf8' }, (err, stdout) => {
|
], { encoding: 'utf8' }, (err, stdout) => {
|
||||||
if (err) reject(err);
|
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);
|
return synapses.get(synapseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
server_name: "localhost"
|
server_name: "localhost"
|
||||||
pid_file: /data/homeserver.pid
|
pid_file: /data/homeserver.pid
|
||||||
public_baseurl: http://localhost:5005/
|
public_baseurl: "{{PUBLIC_BASEURL}}"
|
||||||
listeners:
|
listeners:
|
||||||
- port: 8008
|
- port: 8008
|
||||||
tls: false
|
tls: false
|
||||||
|
|
|
@ -17,3 +17,4 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import "./synapse";
|
import "./synapse";
|
||||||
|
import "./login";
|
||||||
|
|
|
@ -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_");
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import AUTWindow = Cypress.AUTWindow;
|
import AUTWindow = Cypress.AUTWindow;
|
||||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||||
|
@ -29,12 +31,27 @@ declare global {
|
||||||
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
|
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
|
||||||
*/
|
*/
|
||||||
startSynapse(template: string): Chainable<SynapseInstance>;
|
startSynapse(template: string): Chainable<SynapseInstance>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
|
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
|
||||||
* for if Synapse stopping races with the app's background sync loop.
|
* for if Synapse stopping races with the app's background sync loop.
|
||||||
* @param synapse the synapse instance returned by startSynapse
|
* @param synapse the synapse instance returned by startSynapse
|
||||||
*/
|
*/
|
||||||
stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow>;
|
stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a user on the given Synapse using the shared registration secret.
|
||||||
|
* @param synapse the synapse instance returned by startSynapse
|
||||||
|
* @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(
|
||||||
|
synapse: SynapseInstance,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
): Chainable<Credentials>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,5 +68,54 @@ function stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Credentials {
|
||||||
|
accessToken: string;
|
||||||
|
userId: string;
|
||||||
|
deviceId: string;
|
||||||
|
homeServer: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerUser(
|
||||||
|
synapse: SynapseInstance,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
displayName?: string,
|
||||||
|
): Chainable<Credentials> {
|
||||||
|
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
||||||
|
return cy.then(() => {
|
||||||
|
// get a nonce
|
||||||
|
return cy.request<{ nonce: string }>({ url });
|
||||||
|
}).then(response => {
|
||||||
|
const { nonce } = response.body;
|
||||||
|
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
|
||||||
|
`${nonce}\0${username}\0${password}\0notadmin`,
|
||||||
|
).digest('hex');
|
||||||
|
|
||||||
|
return cy.request<{
|
||||||
|
access_token: string;
|
||||||
|
user_id: string;
|
||||||
|
home_server: string;
|
||||||
|
device_id: string;
|
||||||
|
}>({
|
||||||
|
url,
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
nonce,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
mac,
|
||||||
|
admin: false,
|
||||||
|
displayname: displayName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).then(response => ({
|
||||||
|
homeServer: response.body.home_server,
|
||||||
|
accessToken: response.body.access_token,
|
||||||
|
userId: response.body.user_id,
|
||||||
|
deviceId: response.body.device_id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("startSynapse", startSynapse);
|
Cypress.Commands.add("startSynapse", startSynapse);
|
||||||
Cypress.Commands.add("stopSynapse", stopSynapse);
|
Cypress.Commands.add("stopSynapse", stopSynapse);
|
||||||
|
Cypress.Commands.add("registerUser", registerUser);
|
||||||
|
|
|
@ -9,7 +9,7 @@ It aims to cover:
|
||||||
|
|
||||||
## Running the Tests
|
## Running the Tests
|
||||||
Our Cypress tests run automatically as part of our CI along with our other tests,
|
Our Cypress tests run automatically as part of our CI along with our other tests,
|
||||||
on every pull request and on every merge to develop.
|
on every pull request and on every merge to develop & master.
|
||||||
|
|
||||||
However the Cypress tests are run, an element-web must be running on
|
However the Cypress tests are run, an element-web must be running on
|
||||||
http://localhost:8080 (this is configured in `cypress.json`) - this is what will
|
http://localhost:8080 (this is configured in `cypress.json`) - this is what will
|
||||||
|
@ -53,17 +53,17 @@ Synapse can be launched with different configurations in order to test element
|
||||||
in different configurations. `cypress/plugins/synapsedocker/templates` contains
|
in different configurations. `cypress/plugins/synapsedocker/templates` contains
|
||||||
template configuration files for each different configuration.
|
template configuration files for each different configuration.
|
||||||
|
|
||||||
Each test suite can then launch whatever Syanpse instances it needs it whatever
|
Each test suite can then launch whatever Synapse instances it needs it whatever
|
||||||
configurations.
|
configurations.
|
||||||
|
|
||||||
Note that although tests should stop the Synapse instances after running and the
|
Note that although tests should stop the Synapse instances after running and the
|
||||||
plugin also stop any remaining instances after all tests have run, it is possible
|
plugin also stop any remaining instances after all tests have run, it is possible
|
||||||
to be left with some stray containers if, for example, you terminate a test such
|
to be left with some stray containers if, for example, you terminate a test such
|
||||||
that the `after()` does not run and also exit Cypress uncleanly. All the containers
|
that the `after()` does not run and also exit Cypress uncleanly. All the containers
|
||||||
it starts are prefixed so they are easy to recognise. They can be removed safely.
|
it starts are prefixed, so they are easy to recognise. They can be removed safely.
|
||||||
|
|
||||||
After each test run, logs from the Syanpse instances are saved in `cypress/synapselogs`
|
After each test run, logs from the Synapse instances are saved in `cypress/synapselogs`
|
||||||
with each instance in a separate directory named after it's ID. These logs are removed
|
with each instance in a separate directory named after its ID. These logs are removed
|
||||||
at the start of each test run.
|
at the start of each test run.
|
||||||
|
|
||||||
## Writing Tests
|
## Writing Tests
|
||||||
|
@ -73,23 +73,29 @@ https://docs.cypress.io/guides/references/best-practices .
|
||||||
|
|
||||||
### Getting a Synapse
|
### Getting a Synapse
|
||||||
The key difference is in starting Synapse instances. Tests use this plugin via
|
The key difference is in starting Synapse instances. Tests use this plugin via
|
||||||
`cy.task()` to provide a Synapse instance to log into:
|
`cy.startSynapse()` to provide a Synapse instance to log into:
|
||||||
|
|
||||||
```
|
```javascript
|
||||||
cy.task<SynapseInstance>("synapseStart", "consent").then(result => {
|
cy.startSynapse("consent").then(result => {
|
||||||
synapseId = result.synapseId;
|
synapse = result;
|
||||||
synapsePort = result.port;
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
This returns an object with information about the Synapse instance, including what port
|
This returns an object with information about the Synapse instance, including what port
|
||||||
it was started on and the ID that needs to be passed to shut it down again. It also
|
it was started on and the ID that needs to be passed to shut it down again. It also
|
||||||
returns the registration shared secret (`registrationSecret`) that can be used to
|
returns the registration shared secret (`registrationSecret`) that can be used to
|
||||||
register users via the REST API.
|
register users via the REST API. The Synapse has been ensured ready to go by awaiting
|
||||||
|
its internal health-check.
|
||||||
|
|
||||||
Synapse instances should be reasonably cheap to start (you may see the first one take a
|
Synapse instances should be reasonably cheap to start (you may see the first one take a
|
||||||
while as it pulls the Docker image), so it's generally expected that tests will start a
|
while as it pulls the Docker image), so it's generally expected that tests will start a
|
||||||
Synapse instance for each test suite, ie. in `before()`, and then tear it down in `after()`.
|
Synapse instance for each test suite, i.e. in `before()`, and then tear it down in `after()`.
|
||||||
|
|
||||||
|
To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
|
||||||
|
object you received when starting it.
|
||||||
|
```javascript
|
||||||
|
cy.stopSynapse(synapse);
|
||||||
|
```
|
||||||
|
|
||||||
### Synapse Config Templates
|
### Synapse Config Templates
|
||||||
When a Synapse instance is started, it's given a config generated from one of the config
|
When a Synapse instance is started, it's given a config generated from one of the config
|
||||||
|
@ -100,6 +106,7 @@ in these templates:
|
||||||
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||||
* `MACAROON_SECRET_KEY`: Generated each time for security
|
* `MACAROON_SECRET_KEY`: Generated each time for security
|
||||||
* `FORM_SECRET`: Generated each time for security
|
* `FORM_SECRET`: Generated each time for security
|
||||||
|
* `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||||
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||||
Config templates should not contain a signing key and instead assume that one will exist
|
Config templates should not contain a signing key and instead assume that one will exist
|
||||||
in this file.
|
in this file.
|
||||||
|
@ -108,24 +115,18 @@ All other files in the template are copied recursively to `/data/`, so the file
|
||||||
in a template can be referenced in the config as `/data/foo.html`.
|
in a template can be referenced in the config as `/data/foo.html`.
|
||||||
|
|
||||||
### Logging In
|
### Logging In
|
||||||
This doesn't quite exist yet. Most tests will just want to start with the client in a 'logged in'
|
There exists a basic utility to start the app with a random user already logged in:
|
||||||
state, so we should provide an easy way to start a test with element in this state. The
|
```javascript
|
||||||
`registrationSecret` provided when starting a Synapse can be used to create a user (porting
|
cy.initTestUser(synapse, "Jeff");
|
||||||
the code from https://github.com/matrix-org/matrix-react-sdk/blob/develop/test/end-to-end-tests/src/rest/creator.ts#L49).
|
```
|
||||||
We'd then need to log in as this user. Ways of doing this would be:
|
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
|
||||||
|
This custom command will register a random userId using the registrationSecret with a random password
|
||||||
|
and the given display name. The returned Chainable will contain details about the credentials for if
|
||||||
|
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
||||||
|
and the app loaded (path `/`).
|
||||||
|
|
||||||
1. Fill in the login form. This isn't ideal as it's effectively testing the login process in each
|
The internals of how this custom command run may be swapped out later,
|
||||||
test, and will just be slower.
|
but the signature can be maintained for simpler maintenance.
|
||||||
1. Mint an access token using https://matrix-org.github.io/synapse/develop/admin_api/user_admin_api.html#login-as-a-user
|
|
||||||
then inject this into element-web. This would probably be fastest, although also relies on correctly
|
|
||||||
setting up localstorage
|
|
||||||
1. Mint a login token, inject the Homeserver URL into localstorage and then load element, passing the login
|
|
||||||
token as a URL parameter. This is a supported way of logging in to element-web, but there's no API
|
|
||||||
on Synapse to make such a token currently. It would be fairly easy to add a synapse-specific admin API
|
|
||||||
to do so. We should write tests for token login (and the rest of SSO) at some point anyway though.
|
|
||||||
|
|
||||||
If we make this as a convenience API, it can easily be swapped out later: we could start with option 1
|
|
||||||
and then switch later.
|
|
||||||
|
|
||||||
### Joining a Room
|
### Joining a Room
|
||||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||||
|
@ -141,9 +142,9 @@ This section mostly summarises general good Cypress testing practice, and should
|
||||||
already familiar with Cypress.
|
already familiar with Cypress.
|
||||||
|
|
||||||
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
|
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
|
||||||
wrong when they fail.
|
wrong when they fail.
|
||||||
1. Don't depend on state from other tests: any given test should be able to run in isolation.
|
1. Don't depend on state from other tests: any given test should be able to run in isolation.
|
||||||
1. Try to avoid driving the UI for anything other than the UI you're trying to test. eg. if you're
|
1. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're
|
||||||
testing that the user can send a reaction to a message, it's best to send a message using a REST
|
testing that the user can send a reaction to a message, it's best to send a message using a REST
|
||||||
API, then react to it using the UI, rather than using the element-web UI to send the message.
|
API, then react to it using the UI, rather than using the element-web UI to send the message.
|
||||||
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
|
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
|
||||||
|
|
Loading…
Reference in New Issue