Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 77a437f

Browse files
authored
Create more cypress tests and utilities (#8494)
1 parent fd6498a commit 77a437f

File tree

9 files changed

+331
-53
lines changed

9 files changed

+331
-53
lines changed

cypress/integration/1-register/register.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ describe("Registration", () => {
2222
let synapse: SynapseInstance;
2323

2424
beforeEach(() => {
25+
cy.visit("/#/register");
2526
cy.startSynapse("consent").then(data => {
2627
synapse = data;
2728
});
28-
cy.visit("/#/register");
2929
});
3030

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

3535
it("registers an account and lands on the home screen", () => {
3636
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
37-
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(`http://localhost:${synapse.port}`);
37+
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
3838
cy.get(".mx_ServerPickerDialog_continue").click();
3939
// wait for the dialog to go away
4040
cy.get('.mx_ServerPickerDialog').should('not.exist');
41+
4142
cy.get("#mx_RegistrationForm_username").type("alice");
4243
cy.get("#mx_RegistrationForm_password").type("totally a great password");
4344
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
4445
cy.get(".mx_Login_submit").click();
46+
4547
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
4648
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
4749
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker";
20+
21+
describe("Login", () => {
22+
let synapse: SynapseInstance;
23+
24+
beforeEach(() => {
25+
cy.visit("/#/login");
26+
cy.startSynapse("consent").then(data => {
27+
synapse = data;
28+
});
29+
});
30+
31+
afterEach(() => {
32+
cy.stopSynapse(synapse);
33+
});
34+
35+
describe("m.login.password", () => {
36+
const username = "user1234";
37+
const password = "p4s5W0rD";
38+
39+
beforeEach(() => {
40+
cy.registerUser(synapse, username, password);
41+
});
42+
43+
it("logs in with an existing account and lands on the home screen", () => {
44+
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
45+
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
46+
cy.get(".mx_ServerPickerDialog_continue").click();
47+
// wait for the dialog to go away
48+
cy.get('.mx_ServerPickerDialog').should('not.exist');
49+
50+
cy.get("#mx_LoginForm_username").type(username);
51+
cy.get("#mx_LoginForm_password").type(password);
52+
cy.get(".mx_Login_submit").click();
53+
54+
cy.url().should('contain', '/#/home');
55+
});
56+
});
57+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker";
20+
import type { UserCredentials } from "../../support/login";
21+
22+
describe("UserMenu", () => {
23+
let synapse: SynapseInstance;
24+
let user: UserCredentials;
25+
26+
beforeEach(() => {
27+
cy.startSynapse("consent").then(data => {
28+
synapse = data;
29+
30+
cy.initTestUser(synapse, "Jeff").then(credentials => {
31+
user = credentials;
32+
});
33+
});
34+
});
35+
36+
afterEach(() => {
37+
cy.stopSynapse(synapse);
38+
});
39+
40+
it("should contain our name & userId", () => {
41+
cy.get('[aria-label="User menu"]', { timeout: 15000 }).click();
42+
cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
43+
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
44+
});
45+
});

cypress/plugins/synapsedocker/index.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import * as os from "os";
2121
import * as crypto from "crypto";
2222
import * as childProcess from "child_process";
2323
import * as fse from "fs-extra";
24+
import * as net from "net";
2425

2526
import PluginEvents = Cypress.PluginEvents;
2627
import PluginConfigOptions = Cypress.PluginConfigOptions;
@@ -31,11 +32,13 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
3132
interface SynapseConfig {
3233
configDir: string;
3334
registrationSecret: string;
35+
// Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage
36+
baseUrl: string;
37+
port: number;
3438
}
3539

3640
export interface SynapseInstance extends SynapseConfig {
3741
synapseId: string;
38-
port: number;
3942
}
4043

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

50+
async function getFreePort(): Promise<number> {
51+
return new Promise<number>(resolve => {
52+
const srv = net.createServer();
53+
srv.listen(0, () => {
54+
const port = (<net.AddressInfo>srv.address()).port;
55+
srv.close(() => resolve(port));
56+
});
57+
});
58+
}
59+
4760
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
4861
const templateDir = path.join(__dirname, "templates", template);
4962

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

67-
// now copy homeserver.yaml, applying sustitutions
80+
const port = await getFreePort();
81+
const baseUrl = `http://localhost:${port}`;
82+
83+
// now copy homeserver.yaml, applying substitutions
6884
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
6985
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
7086
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
7187
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
7288
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
89+
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
7390
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
7491

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

8299
return {
100+
port,
101+
baseUrl,
83102
configDir: tempDir,
84103
registrationSecret,
85104
};
@@ -101,7 +120,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
101120
"--name", containerName,
102121
"-d",
103122
"-v", `${synCfg.configDir}:/data`,
104-
"-p", "8008/tcp",
123+
"-p", `${synCfg.port}:8008/tcp`,
105124
"matrixdotorg/synapse:develop",
106125
"run",
107126
], (err, stdout) => {
@@ -110,26 +129,27 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
110129
});
111130
});
112131

113-
// Get the port that docker allocated: specifying only one
114-
// port above leaves docker to just grab a free one, although
115-
// in hindsight we need to put the port in public_baseurl in the
116-
// config really, so this will probably need changing to use a fixed
117-
// / configured port.
118-
const port = await new Promise<number>((resolve, reject) => {
119-
childProcess.execFile('docker', [
120-
"port", synapseId, "8008",
132+
synapses.set(synapseId, { synapseId, ...synCfg });
133+
134+
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
135+
136+
// Await Synapse healthcheck
137+
await new Promise<void>((resolve, reject) => {
138+
childProcess.execFile("docker", [
139+
"exec", synapseId,
140+
"curl",
141+
"--connect-timeout", "30",
142+
"--retry", "30",
143+
"--retry-delay", "1",
144+
"--retry-all-errors",
145+
"--silent",
146+
"http://localhost:8008/health",
121147
], { encoding: 'utf8' }, (err, stdout) => {
122148
if (err) reject(err);
123-
resolve(Number(stdout.trim().split(":")[1]));
149+
else resolve();
124150
});
125151
});
126152

127-
synapses.set(synapseId, Object.assign({
128-
port,
129-
synapseId,
130-
}, synCfg));
131-
132-
console.log(`Started synapse with id ${synapseId} on port ${port}.`);
133153
return synapses.get(synapseId);
134154
}
135155

cypress/plugins/synapsedocker/templates/consent/homeserver.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
server_name: "localhost"
22
pid_file: /data/homeserver.pid
3-
public_baseurl: http://localhost:5005/
3+
public_baseurl: "{{PUBLIC_BASEURL}}"
44
listeners:
55
- port: 8008
66
tls: false

cypress/support/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ limitations under the License.
1717
/// <reference types="cypress" />
1818

1919
import "./synapse";
20+
import "./login";

cypress/support/login.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
/// <reference types="cypress" />
18+
19+
import Chainable = Cypress.Chainable;
20+
import { SynapseInstance } from "../plugins/synapsedocker";
21+
22+
export interface UserCredentials {
23+
accessToken: string;
24+
userId: string;
25+
deviceId: string;
26+
password: string;
27+
homeServer: string;
28+
}
29+
30+
declare global {
31+
// eslint-disable-next-line @typescript-eslint/no-namespace
32+
namespace Cypress {
33+
interface Chainable {
34+
/**
35+
* Generates a test user and instantiates an Element session with that user.
36+
* @param synapse the synapse returned by startSynapse
37+
* @param displayName the displayName to give the test user
38+
*/
39+
initTestUser(synapse: SynapseInstance, displayName: string): Chainable<UserCredentials>;
40+
}
41+
}
42+
}
43+
44+
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string): Chainable<UserCredentials> => {
45+
const username = Cypress._.uniqueId("userId_");
46+
const password = Cypress._.uniqueId("password_");
47+
return cy.registerUser(synapse, username, password, displayName).then(() => {
48+
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
49+
return cy.request<{
50+
access_token: string;
51+
user_id: string;
52+
device_id: string;
53+
home_server: string;
54+
}>({
55+
url,
56+
method: "POST",
57+
body: {
58+
"type": "m.login.password",
59+
"identifier": {
60+
"type": "m.id.user",
61+
"user": username,
62+
},
63+
"password": password,
64+
},
65+
});
66+
}).then(response => {
67+
return cy.window().then(win => {
68+
// Seed the localStorage with the required credentials
69+
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
70+
win.localStorage.setItem("mx_user_id", response.body.user_id);
71+
win.localStorage.setItem("mx_access_token", response.body.access_token);
72+
win.localStorage.setItem("mx_device_id", response.body.device_id);
73+
win.localStorage.setItem("mx_is_guest", "false");
74+
win.localStorage.setItem("mx_has_pickle_key", "false");
75+
win.localStorage.setItem("mx_has_access_token", "true");
76+
77+
return cy.visit("/").then(() => ({
78+
password,
79+
accessToken: response.body.access_token,
80+
userId: response.body.user_id,
81+
deviceId: response.body.device_id,
82+
homeServer: response.body.home_server,
83+
}));
84+
});
85+
});
86+
});

0 commit comments

Comments
 (0)