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

Commit 655bca6

Browse files
authored
Move Enterprise Erin tests from Puppeteer to Cypress (#8569)
* Move Enterprise Erin tests from Puppeteer to Cypress * delint * types * Fix double space * Better handle logout in Lifecycle * Fix test by awaiting the network request * Improve some logout handlings * Try try try again * Delint * Fix tests * Delint
1 parent 7efd7b6 commit 655bca6

File tree

12 files changed

+131
-150
lines changed

12 files changed

+131
-150
lines changed

cypress/integration/2-login/login.spec.ts

+41
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,45 @@ describe("Login", () => {
5959
cy.stopMeasuring("from-submit-to-home");
6060
});
6161
});
62+
63+
describe("logout", () => {
64+
beforeEach(() => {
65+
cy.initTestUser(synapse, "Erin");
66+
});
67+
68+
it("should go to login page on logout", () => {
69+
cy.get('[aria-label="User menu"]').click();
70+
71+
// give a change for the outstanding requests queue to settle before logging out
72+
cy.wait(500);
73+
74+
cy.get(".mx_UserMenu_contextMenu").within(() => {
75+
cy.get(".mx_UserMenu_iconSignOut").click();
76+
});
77+
78+
cy.url().should("contain", "/#/login");
79+
});
80+
81+
it("should respect logout_redirect_url", () => {
82+
cy.tweakConfig({
83+
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
84+
// We could use example.org, matrix.org, or something else, however this puts dependency of external
85+
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
86+
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
87+
// Using the decoder-ring is just as fine, and we can search for strategic names.
88+
logout_redirect_url: "/decoder-ring/",
89+
});
90+
91+
cy.get('[aria-label="User menu"]').click();
92+
93+
// give a change for the outstanding requests queue to settle before logging out
94+
cy.wait(500);
95+
96+
cy.get(".mx_UserMenu_contextMenu").within(() => {
97+
cy.get(".mx_UserMenu_iconSignOut").click();
98+
});
99+
100+
cy.url().should("contains", "decoder-ring");
101+
});
102+
});
62103
});

cypress/integration/3-user-menu/user-menu.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe("User Menu", () => {
3939

4040
it("should contain our name & userId", () => {
4141
cy.get('[aria-label="User menu"]').click();
42-
cy.get(".mx_ContextualMenu").within(() => {
42+
cy.get(".mx_UserMenu_contextMenu").within(() => {
4343
cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
4444
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
4545
});

cypress/support/app.ts

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 "./client"; // XXX: without an (any) import here, types break down
20+
import Chainable = Cypress.Chainable;
21+
import AUTWindow = Cypress.AUTWindow;
22+
23+
declare global {
24+
// eslint-disable-next-line @typescript-eslint/no-namespace
25+
namespace Cypress {
26+
interface Chainable {
27+
/**
28+
* Applies tweaks to the config read from config.json
29+
*/
30+
tweakConfig(tweaks: Record<string, any>): Chainable<AUTWindow>;
31+
}
32+
}
33+
}
34+
35+
Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUTWindow> => {
36+
return cy.window().then(win => {
37+
// note: we can't *set* the object because the window version is effectively a pointer.
38+
for (const [k, v] of Object.entries(tweaks)) {
39+
// @ts-ignore - for some reason it's not picking up on global.d.ts types.
40+
win.mxReactSdkConfig[k] = v;
41+
}
42+
});
43+
});

cypress/support/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ import "./settings";
2727
import "./bot";
2828
import "./clipboard";
2929
import "./util";
30+
import "./app";

src/DeviceListener.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
1818
import { logger } from "matrix-js-sdk/src/logger";
1919
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
2020
import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
21+
import { SyncState } from "matrix-js-sdk/src/sync";
2122

2223
import { MatrixClientPeg } from './MatrixClientPeg';
2324
import dis from "./dispatcher/dispatcher";
@@ -58,13 +59,15 @@ export default class DeviceListener {
5859
private ourDeviceIdsAtStart: Set<string> = null;
5960
// The set of device IDs we're currently displaying toasts for
6061
private displayingToastsForDeviceIds = new Set<string>();
62+
private running = false;
6163

62-
static sharedInstance() {
64+
public static sharedInstance() {
6365
if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener();
6466
return window.mxDeviceListener;
6567
}
6668

67-
start() {
69+
public start() {
70+
this.running = true;
6871
MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
6972
MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
7073
MatrixClientPeg.get().on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
@@ -77,7 +80,8 @@ export default class DeviceListener {
7780
this.recheck();
7881
}
7982

80-
stop() {
83+
public stop() {
84+
this.running = false;
8185
if (MatrixClientPeg.get()) {
8286
MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
8387
MatrixClientPeg.get().removeListener(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
@@ -109,7 +113,7 @@ export default class DeviceListener {
109113
*
110114
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
111115
*/
112-
async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
116+
public async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
113117
logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(','));
114118
for (const d of deviceIds) {
115119
this.dismissed.add(d);
@@ -118,7 +122,7 @@ export default class DeviceListener {
118122
this.recheck();
119123
}
120124

121-
dismissEncryptionSetup() {
125+
public dismissEncryptionSetup() {
122126
this.dismissedThisDeviceToast = true;
123127
this.recheck();
124128
}
@@ -179,8 +183,10 @@ export default class DeviceListener {
179183
}
180184
};
181185

182-
private onSync = (state, prevState) => {
183-
if (state === 'PREPARED' && prevState === null) this.recheck();
186+
private onSync = (state: SyncState, prevState?: SyncState) => {
187+
if (state === 'PREPARED' && prevState === null) {
188+
this.recheck();
189+
}
184190
};
185191

186192
private onRoomStateEvents = (ev: MatrixEvent) => {
@@ -217,6 +223,7 @@ export default class DeviceListener {
217223
}
218224

219225
private async recheck() {
226+
if (!this.running) return; // we have been stopped
220227
const cli = MatrixClientPeg.get();
221228

222229
if (!(await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))) return;

src/Lifecycle.ts

+24-24
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
168168
* Gets the user ID of the persisted session, if one exists. This does not validate
169169
* that the user's credentials still work, just that they exist and that a user ID
170170
* is associated with them. The session is not loaded.
171-
* @returns {[String, bool]} The persisted session's owner and whether the stored
171+
* @returns {[string, boolean]} The persisted session's owner and whether the stored
172172
* session is for a guest user, if an owner exists. If there is no stored session,
173173
* return [null, null].
174174
*/
@@ -494,7 +494,7 @@ async function handleLoadSessionFailure(e: Error): Promise<boolean> {
494494
* Also stops the old MatrixClient and clears old credentials/etc out of
495495
* storage before starting the new client.
496496
*
497-
* @param {MatrixClientCreds} credentials The credentials to use
497+
* @param {IMatrixClientCreds} credentials The credentials to use
498498
*
499499
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
500500
*/
@@ -525,7 +525,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
525525
* If the credentials belong to a different user from the session already stored,
526526
* the old session will be cleared automatically.
527527
*
528-
* @param {MatrixClientCreds} credentials The credentials to use
528+
* @param {IMatrixClientCreds} credentials The credentials to use
529529
*
530530
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
531531
*/
@@ -731,27 +731,25 @@ export function logout(): void {
731731
if (MatrixClientPeg.get().isGuest()) {
732732
// logout doesn't work for guest sessions
733733
// Also we sometimes want to re-log in a guest session if we abort the login.
734-
// defer until next tick because it calls a synchronous dispatch and we are likely here from a dispatch.
734+
// defer until next tick because it calls a synchronous dispatch, and we are likely here from a dispatch.
735735
setImmediate(() => onLoggedOut());
736736
return;
737737
}
738738

739739
_isLoggingOut = true;
740740
const client = MatrixClientPeg.get();
741741
PlatformPeg.get().destroyPickleKey(client.getUserId(), client.getDeviceId());
742-
client.logout().then(onLoggedOut,
743-
(err) => {
744-
// Just throwing an error here is going to be very unhelpful
745-
// if you're trying to log out because your server's down and
746-
// you want to log into a different server, so just forget the
747-
// access token. It's annoying that this will leave the access
748-
// token still valid, but we should fix this by having access
749-
// tokens expire (and if you really think you've been compromised,
750-
// change your password).
751-
logger.log("Failed to call logout API: token will not be invalidated");
752-
onLoggedOut();
753-
},
754-
);
742+
client.logout(undefined, true).then(onLoggedOut, (err) => {
743+
// Just throwing an error here is going to be very unhelpful
744+
// if you're trying to log out because your server's down and
745+
// you want to log into a different server, so just forget the
746+
// access token. It's annoying that this will leave the access
747+
// token still valid, but we should fix this by having access
748+
// tokens expire (and if you really think you've been compromised,
749+
// change your password).
750+
logger.warn("Failed to call logout API: token will not be invalidated", err);
751+
onLoggedOut();
752+
});
755753
}
756754

757755
export function softLogout(): void {
@@ -856,9 +854,8 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
856854
* storage. Used after a session has been logged out.
857855
*/
858856
export async function onLoggedOut(): Promise<void> {
859-
_isLoggingOut = false;
860857
// Ensure that we dispatch a view change **before** stopping the client,
861-
// so that React components unmount first. This avoids React soft crashes
858+
// that React components unmount first. This avoids React soft crashes
862859
// that can occur when components try to use a null client.
863860
dis.fire(Action.OnLoggedOut, true);
864861
stopMatrixClient();
@@ -869,8 +866,13 @@ export async function onLoggedOut(): Promise<void> {
869866
// customisations got the memo.
870867
if (SdkConfig.get().logout_redirect_url) {
871868
logger.log("Redirecting to external provider to finish logout");
872-
window.location.href = SdkConfig.get().logout_redirect_url;
869+
// XXX: Defer this so that it doesn't race with MatrixChat unmounting the world by going to /#/login
870+
setTimeout(() => {
871+
window.location.href = SdkConfig.get().logout_redirect_url;
872+
}, 100);
873873
}
874+
// Do this last to prevent racing `stopMatrixClient` and `on_logged_out` with MatrixChat handling Session.logged_out
875+
_isLoggingOut = false;
874876
}
875877

876878
/**
@@ -908,9 +910,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
908910
}
909911
}
910912

911-
if (window.sessionStorage) {
912-
window.sessionStorage.clear();
913-
}
913+
window.sessionStorage?.clear();
914914

915915
// create a temporary client to clear out the persistent stores.
916916
const cli = createMatrixClient({
@@ -937,7 +937,7 @@ export function stopMatrixClient(unsetClient = true): void {
937937
IntegrationManagers.sharedInstance().stopWatching();
938938
Mjolnir.sharedInstance().stop();
939939
DeviceListener.sharedInstance().stop();
940-
if (DMRoomMap.shared()) DMRoomMap.shared().stop();
940+
DMRoomMap.shared()?.stop();
941941
EventIndexPeg.stop();
942942
const cli = MatrixClientPeg.get();
943943
if (cli) {

src/SecurityManager.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ async function onSecretRequested(
257257
if (userId !== client.getUserId()) {
258258
return;
259259
}
260-
if (!deviceTrust || !deviceTrust.isVerified()) {
260+
if (!deviceTrust?.isVerified()) {
261261
logger.log(`Ignoring secret request from untrusted device ${deviceId}`);
262262
return;
263263
}
@@ -296,7 +296,7 @@ export const crossSigningCallbacks: ICryptoCallbacks = {
296296
};
297297

298298
export async function promptForBackupPassphrase(): Promise<Uint8Array> {
299-
let key;
299+
let key: Uint8Array;
300300

301301
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
302302
showSummary: false, keyCallback: k => key = k,

src/stores/SetupEncryptionStore.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -89,16 +89,15 @@ export class SetupEncryptionStore extends EventEmitter {
8989
return;
9090
}
9191
this.started = false;
92-
if (this.verificationRequest) {
93-
this.verificationRequest.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
94-
}
92+
this.verificationRequest?.off(VerificationRequestEvent.Change, this.onVerificationRequestChange);
9593
if (MatrixClientPeg.get()) {
9694
MatrixClientPeg.get().removeListener(CryptoEvent.VerificationRequest, this.onVerificationRequest);
9795
MatrixClientPeg.get().removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserTrustStatusChanged);
9896
}
9997
}
10098

10199
public async fetchKeyInfo(): Promise<void> {
100+
if (!this.started) return; // bail if we were stopped
102101
const cli = MatrixClientPeg.get();
103102
const keys = await cli.isSecretStored('m.cross_signing.master');
104103
if (keys === null || Object.keys(keys).length === 0) {
@@ -270,6 +269,7 @@ export class SetupEncryptionStore extends EventEmitter {
270269
}
271270

272271
private async setActiveVerificationRequest(request: VerificationRequest): Promise<void> {
272+
if (!this.started) return; // bail if we were stopped
273273
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
274274

275275
if (this.verificationRequest) {

test/end-to-end-tests/src/scenario.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,12 @@ import { RestMultiSession } from "./rest/multi";
2727
import { RestSession } from "./rest/session";
2828
import { stickerScenarios } from './scenarios/sticker';
2929
import { userViewScenarios } from "./scenarios/user-view";
30-
import { ssoCustomisationScenarios } from "./scenarios/sso-customisations";
3130
import { updateScenarios } from "./scenarios/update";
3231

3332
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
3433
restCreator: RestSessionCreator): Promise<void> {
3534
let firstUser = true;
36-
async function createUser(username) {
35+
async function createUser(username: string) {
3736
const session = await createSession(username);
3837
if (firstUser) {
3938
// only show browser version for first browser opened
@@ -65,12 +64,6 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
6564
const stickerSession = await createSession("sally");
6665
await stickerScenarios("sally", "ilikestickers", stickerSession, restCreator);
6766

68-
// we spawn yet another session for SSO stuff because it involves authentication and
69-
// logout, which can/does affect other tests dramatically. See notes above regarding
70-
// stickers for the performance loss of doing this.
71-
const ssoSession = await createUser("enterprise_erin");
72-
await ssoCustomisationScenarios(ssoSession);
73-
7467
// Create a new window to test app auto-updating
7568
const updateSession = await createSession("update");
7669
await updateScenarios(updateSession);

0 commit comments

Comments
 (0)