diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 4a1d5877d1..6ac72a2dd6 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -52,6 +52,7 @@ import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import { Skinner } from "../Skinner"; import AutoRageshakeStore from "../stores/AutoRageshakeStore"; +import { ConfigOptions } from "../SdkConfig"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -62,6 +63,7 @@ declare global { Olm: { init: () => Promise; }; + mxReactSdkConfig: ConfigOptions; // Needed for Safari, unknown to TypeScript webkitAudioContext: typeof AudioContext; diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index de73fcc051..85f0535851 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -58,6 +58,7 @@ import LazyLoadingDisabledDialog from "./components/views/dialogs/LazyLoadingDis import SessionRestoreErrorDialog from "./components/views/dialogs/SessionRestoreErrorDialog"; import StorageEvictedDialog from "./components/views/dialogs/StorageEvictedDialog"; import { setSentryUser } from "./sentry"; +import SdkConfig from "./SdkConfig"; const HOMESERVER_URL_KEY = "mx_hs_url"; const ID_SERVER_URL_KEY = "mx_is_url"; @@ -845,6 +846,13 @@ export async function onLoggedOut(): Promise { stopMatrixClient(); await clearStorage({ deleteEverything: true }); LifecycleCustomisations.onLoggedOutAndStorageCleared?.(); + + // Do this last so we can make sure all storage has been cleared and all + // customisations got the memo. + if (SdkConfig.get().logout_redirect_url) { + logger.log("Redirecting to external provider to finish logout"); + window.location.href = SdkConfig.get().logout_redirect_url; + } } /** diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 575b4a626d..e8ac0dcee3 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -20,13 +20,17 @@ export interface ISsoRedirectOptions { on_welcome_page?: boolean; // eslint-disable-line camelcase } +/* eslint-disable camelcase */ export interface ConfigOptions { [key: string]: any; + logout_redirect_url?: string; + // sso_immediate_redirect is deprecated in favour of sso_redirect_options.immediate - sso_immediate_redirect?: boolean; // eslint-disable-line camelcase - sso_redirect_options?: ISsoRedirectOptions; // eslint-disable-line camelcase + sso_immediate_redirect?: boolean; + sso_redirect_options?: ISsoRedirectOptions; } +/* eslint-enable camelcase*/ export const DEFAULTS: ConfigOptions = { // Brand name of the app @@ -56,14 +60,14 @@ export default class SdkConfig { SdkConfig.instance = i; // For debugging purposes - (window).mxReactSdkConfig = i; + window.mxReactSdkConfig = i; } - static get() { + public static get() { return SdkConfig.instance || {}; } - static put(cfg: ConfigOptions) { + public static put(cfg: ConfigOptions) { const defaultKeys = Object.keys(DEFAULTS); for (let i = 0; i < defaultKeys.length; ++i) { if (cfg[defaultKeys[i]] === undefined) { @@ -73,11 +77,11 @@ export default class SdkConfig { SdkConfig.setInstance(cfg); } - static unset() { + public static unset() { SdkConfig.setInstance({}); } - static add(cfg: ConfigOptions) { + public static add(cfg: ConfigOptions) { const liveConfig = SdkConfig.get(); const newConfig = Object.assign({}, liveConfig, cfg); SdkConfig.put(newConfig); diff --git a/test/end-to-end-tests/Windows.md b/test/end-to-end-tests/Windows.md index f6ea87d0af..f276843d17 100644 --- a/test/end-to-end-tests/Windows.md +++ b/test/end-to-end-tests/Windows.md @@ -6,26 +6,31 @@ and start following these steps to get going: 1. Navigate to your working directory (`cd /mnt/c/users/travisr/whatever/matrix-react-sdk` for example). 2. Run `sudo apt-get install unzip python3 virtualenv dos2unix` 3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/element/*.sh` -4. Install NodeJS for ubuntu: +4. Install NodeJS for ubuntu: ```bash - curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - + curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt-get update sudo apt-get install nodejs ``` -5. Start Element on Windows through `yarn start` -6. While that builds... Run: +5. Run `yarn link` and `yarn install` for all layers from WSL if you haven't already. If you want to switch back to + your Windows host after your tests then you'll need to re-run `yarn install` (and possibly `yarn link`) there too. + Though, do note that you can access `http://localhost:8080` in your Windows-based browser when running webpack in + the WSL environment (it does *not* work the other way around, annoyingly). +6. In WSL, run `yarn start` at the element-web layer to get things going. +7. While that builds... Run: ```bash sudo apt-get install x11-apps wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb sudo dpkg -i google-chrome-stable_current_amd64.deb sudo apt -f install ``` -7. Run: +8. Get the IP of your host machine out of WSL: `cat /etc/resolv.conf` - use the nameserver IP. +9. Run: ```bash cd ./test/end-to-end-tests ./synapse/install.sh ./install.sh - ./run.sh --app-url http://localhost:8080 --no-sandbox + ./run.sh --app-url http://localhost:8080 --log-directory ./logs ``` Note that using `yarn test:e2e` probably won't work for you. You might also have to use the config.json from the @@ -38,3 +43,4 @@ could probably fix this with enough effort, or you could run a headless Chrome i Reference material that isn't fully represented in the steps above (but snippets have been borrowed): * https://virtualizationreview.com/articles/2017/02/08/graphical-programs-on-windows-subsystem-on-linux.aspx * https://gist.github.com/drexler/d70ab957f964dbef1153d46bd853c775 +* https://docs.microsoft.com/en-us/windows/wsl/networking#accessing-windows-networking-apps-from-linux-host-ip diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts index 88b5a948c1..bfccd7fe78 100644 --- a/test/end-to-end-tests/src/scenario.ts +++ b/test/end-to-end-tests/src/scenario.ts @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +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. @@ -27,6 +28,7 @@ import { spacesScenarios } from './scenarios/spaces'; import { RestSession } from "./rest/session"; import { stickerScenarios } from './scenarios/sticker'; import { userViewScenarios } from "./scenarios/user-view"; +import { ssoCustomisationScenarios } from "./scenarios/sso-customisations"; export async function scenario(createSession: (s: string) => Promise, restCreator: RestSessionCreator): Promise { @@ -52,7 +54,7 @@ export async function scenario(createSession: (s: string) => Promise Promise { diff --git a/test/end-to-end-tests/src/scenarios/sso-customisations.ts b/test/end-to-end-tests/src/scenarios/sso-customisations.ts new file mode 100644 index 0000000000..957f45a682 --- /dev/null +++ b/test/end-to-end-tests/src/scenarios/sso-customisations.ts @@ -0,0 +1,50 @@ +/* +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. +*/ + +import { strict as assert } from "assert"; + +import { ElementSession } from "../session"; +import { logout } from "../usecases/logout"; +import { applyConfigChange } from "../util"; + +export async function ssoCustomisationScenarios(session: ElementSession): Promise { + console.log(" injecting logout customisations for SSO scenarios:"); + + await session.delay(1000); // wait for dialogs to close + await applyConfigChange(session, { + // we redirect to config.json because it's a predictable page that isn't Element + // itself. We could use example.org, matrix.org, or something else, however this + // puts dependency of external infrastructure on our tests. In the same vein, we + // don't really want to figure out how to ship a `test-landing.html` page when + // running with an uncontrolled Element (via `./run.sh --app-url http://localhost:8080`). + // Using the config.json is just as fine, and we can search for strategic names. + 'logout_redirect_url': '/config.json', + }); + + await logoutCanCauseRedirect(session); +} + +async function logoutCanCauseRedirect(session: ElementSession): Promise { + await logout(session, false); // we'll check the login page ourselves, so don't assert + + session.log.step("waits for redirect to config.json (as external page)"); + const foundLoginUrl = await session.poll(async () => { + const url = session.page.url(); + return url === session.url('/config.json'); + }); + assert(foundLoginUrl); + session.log.done(); +} diff --git a/test/end-to-end-tests/src/usecases/logout.ts b/test/end-to-end-tests/src/usecases/logout.ts new file mode 100644 index 0000000000..b422ff3d74 --- /dev/null +++ b/test/end-to-end-tests/src/usecases/logout.ts @@ -0,0 +1,43 @@ +/* +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. +*/ + +import { strict as assert } from 'assert'; + +import { ElementSession } from "../session"; + +export async function logout(session: ElementSession, assertLoginPage = true): Promise { + session.log.startGroup("logs out"); + + session.log.step("navigates to user menu"); + const userButton = await session.query('.mx_UserMenu > div.mx_AccessibleButton'); + await userButton.click(); + session.log.done(); + + session.log.step("clicks the 'Sign Out' button"); + const signOutButton = await session.query('.mx_UserMenu_contextMenu .mx_UserMenu_iconSignOut'); + await signOutButton.click(); + session.log.done(); + + if (assertLoginPage) { + const foundLoginUrl = await session.poll(async () => { + const url = session.page.url(); + return url === session.url('/#/login'); + }); + assert(foundLoginUrl); + } + + session.log.endGroup(); +} diff --git a/test/end-to-end-tests/src/util.ts b/test/end-to-end-tests/src/util.ts index 5c3c4bc6a2..cfb00394a9 100644 --- a/test/end-to-end-tests/src/util.ts +++ b/test/end-to-end-tests/src/util.ts @@ -1,6 +1,6 @@ /* Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 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. @@ -40,3 +40,14 @@ export const measureStop = function(session: ElementSession, name: string): Prom window.mxPerformanceMonitor.stop(_name); }, name); }; + +// TODO: Proper types on `config` - for some reason won't accept an import of ConfigOptions. +export async function applyConfigChange(session: ElementSession, config: any): Promise { + await session.page.evaluate((_config) => { + // note: we can't *set* the object because the window version is effectively a pointer. + for (const [k, v] of Object.entries(_config)) { + // @ts-ignore - for some reason it's not picking up on global.d.ts types. + window.mxReactSdkConfig[k] = v; + } + }, config); +} diff --git a/test/end-to-end-tests/start.ts b/test/end-to-end-tests/start.ts index f6c7400a15..b346f9165d 100644 --- a/test/end-to-end-tests/start.ts +++ b/test/end-to-end-tests/start.ts @@ -90,6 +90,10 @@ async function runTests() { // Collecting all performance monitoring data before closing the session const measurements = await session.page.evaluate(() => { let measurements; + + // Some tests do redirects away from the app, so don't count those sessions. + if (!window.mxPerformanceMonitor) return JSON.stringify([]); + window.mxPerformanceMonitor.addPerformanceDataCallback({ entryNames: [ window.mxPerformanceEntryNames.REGISTER, @@ -111,7 +115,7 @@ async function runTests() { performanceEntries = JSON.parse(measurements); return session.close(); })); - if (performanceEntries) { + if (performanceEntries?.length > 0) { fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries)); } if (failure) {