Create more cypress tests and utilities (#8494)
This commit is contained in:
committed by
GitHub
parent
fd6498a821
commit
77a437f30a
@@ -22,10 +22,10 @@ describe("Registration", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/#/register");
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
});
|
||||
cy.visit("/#/register");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -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();
|
||||
|
||||
57
cypress/integration/2-login/login.spec.ts
Normal file
57
cypress/integration/2-login/login.spec.ts
Normal file
@@ -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
cypress/integration/3-user-menu/user-menu.spec.ts
Normal file
45
cypress/integration/3-user-menu/user-menu.spec.ts
Normal file
@@ -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 childProcess from "child_process";
|
||||
import * as fse from "fs-extra";
|
||||
import * as net from "net";
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
@@ -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>();
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) => {
|
||||
@@ -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", [
|
||||
"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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,3 +17,4 @@ limitations under the License.
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import "./synapse";
|
||||
import "./login";
|
||||
|
||||
86
cypress/support/login.ts
Normal file
86
cypress/support/login.ts
Normal file
@@ -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" />
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
@@ -29,12 +31,27 @@ declare global {
|
||||
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
|
||||
*/
|
||||
startSynapse(template: string): Chainable<SynapseInstance>;
|
||||
|
||||
/**
|
||||
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
|
||||
* for if Synapse stopping races with the app's background sync loop.
|
||||
* @param synapse the synapse instance returned by startSynapse
|
||||
*/
|
||||
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("stopSynapse", stopSynapse);
|
||||
Cypress.Commands.add("registerUser", registerUser);
|
||||
|
||||
Reference in New Issue
Block a user