From a37682e64d5788028138d26fce76b62c76d5b111 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 23 Sep 2022 17:10:15 +0200 Subject: [PATCH 01/12] record device client inforamtion events on app start --- src/DeviceListener.ts | 19 +++++ src/utils/device/clientInformation.ts | 62 +++++++++++++++ test/DeviceListener-test.ts | 50 ++++++++++++ test/utils/device/clientInformation-test.ts | 86 +++++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 src/utils/device/clientInformation.ts create mode 100644 test/utils/device/clientInformation-test.ts diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index cf9af5befc4..c6d920c2eac 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -40,6 +40,9 @@ import { isSecureBackupRequired } from './utils/WellKnownUtils'; import { ActionPayload } from "./dispatcher/payloads"; import { Action } from "./dispatcher/actions"; import { isLoggedIn } from "./utils/login"; +import SdkConfig from "./SdkConfig"; +import PlatformPeg from "./PlatformPeg"; +import { recordClientInformation } from "./utils/device/clientInformation"; const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000; @@ -78,6 +81,7 @@ export default class DeviceListener { MatrixClientPeg.get().on(RoomStateEvent.Events, this.onRoomStateEvents); this.dispatcherRef = dis.register(this.onAction); this.recheck(); + this.recordClientInformation(); } public stop() { @@ -200,6 +204,7 @@ export default class DeviceListener { private onAction = ({ action }: ActionPayload) => { if (action !== Action.OnLoggedIn) return; this.recheck(); + this.recordClientInformation(); }; // The server doesn't tell us when key backup is set up, so we poll @@ -343,4 +348,18 @@ export default class DeviceListener { dis.dispatch({ action: Action.ReportKeyBackupNotEnabled }); } }; + + private recordClientInformation = async () => { + try { + await recordClientInformation( + MatrixClientPeg.get(), + SdkConfig.get(), + PlatformPeg.get(), + ); + } catch (error) { + // this is a best effort operation + // log the error without rethrowing + logger.error('Failed to record client information', error); + } + }; } diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts new file mode 100644 index 00000000000..9dfa5c03ff4 --- /dev/null +++ b/src/utils/device/clientInformation.ts @@ -0,0 +1,62 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; + +import BasePlatform from "../../BasePlatform"; +import { IConfigOptions } from "../../IConfigOptions"; + +export type DeviceClientInformation = { + name?: string; + version?: string; + url?: string; +}; + +const formatUrl = (): string | undefined => { + // don't record url for electron clients + if (window.electron) { + return undefined; + } + + // strip query-string and fragment from uri + const url = new URL(window.location.href); + + return [ + url.host, + url.pathname.replace(/\/$/, ""), // Remove trailing slash if present + ].join(""); +}; + +export const getClientInformationEventType = (deviceId: string): string => + `io.element.matrix-client-information.${deviceId}`; + +export const recordClientInformation = async ( + matrixClient: MatrixClient, + sdkConfig: IConfigOptions, + platform: BasePlatform, +): Promise => { + const deviceId = matrixClient.getDeviceId(); + const { brand } = sdkConfig; + const version = await platform.getAppVersion(); + const type = getClientInformationEventType(deviceId); + const url = formatUrl(); + + await matrixClient.setAccountData(type, { + name: brand, + version, + url, + }); +}; diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts index 06405674416..6e7f68896cc 100644 --- a/test/DeviceListener-test.ts +++ b/test/DeviceListener-test.ts @@ -18,6 +18,7 @@ limitations under the License. import { EventEmitter } from "events"; import { mocked } from "jest-mock"; import { Room } from "matrix-js-sdk/src/matrix"; +import { logger } from "matrix-js-sdk/src/logger"; import DeviceListener from "../src/DeviceListener"; import { MatrixClientPeg } from "../src/MatrixClientPeg"; @@ -27,6 +28,7 @@ import * as BulkUnverifiedSessionsToast from "../src/toasts/BulkUnverifiedSessio import { isSecretStorageBeingAccessed } from "../src/SecurityManager"; import dis from "../src/dispatcher/dispatcher"; import { Action } from "../src/dispatcher/actions"; +import { mockPlatformPeg } from "./test-utils"; // don't litter test console with logs jest.mock("matrix-js-sdk/src/logger"); @@ -40,6 +42,8 @@ jest.mock("../src/SecurityManager", () => ({ isSecretStorageBeingAccessed: jest.fn(), accessSecretStorage: jest.fn(), })); +const deviceId = 'my-device-id'; + class MockClient extends EventEmitter { getUserId = jest.fn(); getKeyBackupVersion = jest.fn().mockResolvedValue(undefined); @@ -57,6 +61,8 @@ class MockClient extends EventEmitter { downloadKeys = jest.fn(); isRoomEncrypted = jest.fn(); getClientWellKnown = jest.fn(); + getDeviceId = jest.fn().mockReturnValue(deviceId); + setAccountData = jest.fn(); } const mockDispatcher = mocked(dis); const flushPromises = async () => await new Promise(process.nextTick); @@ -75,6 +81,9 @@ describe('DeviceListener', () => { beforeEach(() => { jest.resetAllMocks(); + mockPlatformPeg({ + getAppVersion: jest.fn().mockResolvedValue('1.2.3'), + }); mockClient = new MockClient(); jest.spyOn(MatrixClientPeg, 'get').mockReturnValue(mockClient); }); @@ -86,6 +95,47 @@ describe('DeviceListener', () => { return instance; }; + describe('client information', () => { + it('saves client information on start', async () => { + await createAndStart(); + + expect(mockClient.setAccountData).toHaveBeenCalledWith( + `io.element.matrix-client-information.${deviceId}`, + { name: 'Element', url: 'localhost', version: '1.2.3' }, + ); + }); + + it('catches error and logs when saving client information fails', async () => { + const errorLogSpy = jest.spyOn(logger, 'error'); + const error = new Error('oups'); + mockClient.setAccountData.mockRejectedValue(error); + + // doesn't throw + await createAndStart(); + + expect(errorLogSpy).toHaveBeenCalledWith( + 'Failed to record client information', + error, + ); + }); + + it('saves client information on logged in action', async () => { + const instance = await createAndStart(); + + mockClient.setAccountData.mockClear(); + + // @ts-ignore calling private function + instance.onAction({ action: Action.OnLoggedIn }); + + await flushPromises(); + + expect(mockClient.setAccountData).toHaveBeenCalledWith( + `io.element.matrix-client-information.${deviceId}`, + { name: 'Element', url: 'localhost', version: '1.2.3' }, + ); + }); + }); + describe('recheck', () => { it('does nothing when cross signing feature is not supported', async () => { mockClient.doesServerSupportUnstableFeature.mockResolvedValue(false); diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts new file mode 100644 index 00000000000..e6b470437e8 --- /dev/null +++ b/test/utils/device/clientInformation-test.ts @@ -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. +*/ + +import BasePlatform from "../../../src/BasePlatform"; +import { IConfigOptions } from "../../../src/IConfigOptions"; +import { recordClientInformation } from "../../../src/utils/device/clientInformation"; +import { getMockClientWithEventEmitter } from "../../test-utils"; + +describe('recordClientInformation()', () => { + const deviceId = 'my-device-id'; + const version = '1.2.3'; + const isElectron = window.electron; + + const mockClient = getMockClientWithEventEmitter({ + getDeviceId: jest.fn().mockReturnValue(deviceId), + setAccountData: jest.fn(), + }); + + const sdkConfig: IConfigOptions = { + brand: 'Test Brand', + element_call: { url: '' }, + }; + + const platform = { + getAppVersion: jest.fn().mockResolvedValue(version), + } as unknown as BasePlatform; + + beforeEach(() => { + jest.clearAllMocks(); + window.electron = false; + }); + + afterAll(() => { + // restore global + window.electron = isElectron; + }); + + it('saves client information without url for electron clients', async () => { + window.electron = true; + + await recordClientInformation( + mockClient, + sdkConfig, + platform, + ); + + expect(mockClient.setAccountData).toHaveBeenCalledWith( + `io.element.matrix-client-information.${deviceId}`, + { + name: sdkConfig.brand, + version, + url: undefined, + }, + ); + }); + + it('saves client information with url for non-electron clients', async () => { + await recordClientInformation( + mockClient, + sdkConfig, + platform, + ); + + expect(mockClient.setAccountData).toHaveBeenCalledWith( + `io.element.matrix-client-information.${deviceId}`, + { + name: sdkConfig.brand, + version, + url: 'localhost', + }, + ); + }); +}); From 94b027b9c94ef1732b8d1a0d06329ea35fc1797d Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 10:35:29 +0200 Subject: [PATCH 02/12] matrix-client-information -> matrix_client_information --- src/utils/device/clientInformation.ts | 2 +- test/DeviceListener-test.ts | 4 ++-- test/utils/device/clientInformation-test.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 9dfa5c03ff4..f45fafbd1e3 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -41,7 +41,7 @@ const formatUrl = (): string | undefined => { }; export const getClientInformationEventType = (deviceId: string): string => - `io.element.matrix-client-information.${deviceId}`; + `io.element.matrix_client_information.${deviceId}`; export const recordClientInformation = async ( matrixClient: MatrixClient, diff --git a/test/DeviceListener-test.ts b/test/DeviceListener-test.ts index 6e7f68896cc..466ea39c49b 100644 --- a/test/DeviceListener-test.ts +++ b/test/DeviceListener-test.ts @@ -100,7 +100,7 @@ describe('DeviceListener', () => { await createAndStart(); expect(mockClient.setAccountData).toHaveBeenCalledWith( - `io.element.matrix-client-information.${deviceId}`, + `io.element.matrix_client_information.${deviceId}`, { name: 'Element', url: 'localhost', version: '1.2.3' }, ); }); @@ -130,7 +130,7 @@ describe('DeviceListener', () => { await flushPromises(); expect(mockClient.setAccountData).toHaveBeenCalledWith( - `io.element.matrix-client-information.${deviceId}`, + `io.element.matrix_client_information.${deviceId}`, { name: 'Element', url: 'localhost', version: '1.2.3' }, ); }); diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index e6b470437e8..0c4a6cf2648 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -58,7 +58,7 @@ describe('recordClientInformation()', () => { ); expect(mockClient.setAccountData).toHaveBeenCalledWith( - `io.element.matrix-client-information.${deviceId}`, + `io.element.matrix_client_information.${deviceId}`, { name: sdkConfig.brand, version, @@ -75,7 +75,7 @@ describe('recordClientInformation()', () => { ); expect(mockClient.setAccountData).toHaveBeenCalledWith( - `io.element.matrix-client-information.${deviceId}`, + `io.element.matrix_client_information.${deviceId}`, { name: sdkConfig.brand, version, From 593465ae04d3f5cb671b3cca994cee9dd9a2376a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 11:16:46 +0200 Subject: [PATCH 03/12] fix types --- src/utils/device/clientInformation.ts | 2 +- test/utils/device/clientInformation-test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index f45fafbd1e3..d12d4cd8ae2 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -40,7 +40,7 @@ const formatUrl = (): string | undefined => { ].join(""); }; -export const getClientInformationEventType = (deviceId: string): string => +const getClientInformationEventType = (deviceId: string): string => `io.element.matrix_client_information.${deviceId}`; export const recordClientInformation = async ( diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index 0c4a6cf2648..628c9729d14 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -31,7 +31,7 @@ describe('recordClientInformation()', () => { const sdkConfig: IConfigOptions = { brand: 'Test Brand', - element_call: { url: '' }, + element_call: { url: '', use_exclusively: false }, }; const platform = { From c85865607fb354dc99345e21a4d528a77306155d Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 11:25:18 +0200 Subject: [PATCH 04/12] remove another unused export --- src/utils/device/clientInformation.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index d12d4cd8ae2..83277b11363 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -19,12 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import BasePlatform from "../../BasePlatform"; import { IConfigOptions } from "../../IConfigOptions"; -export type DeviceClientInformation = { - name?: string; - version?: string; - url?: string; -}; - const formatUrl = (): string | undefined => { // don't record url for electron clients if (window.electron) { From 950f61cdb09d210e4fa424a3ee69fd0a9cb4ca3c Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Tue, 27 Sep 2022 10:26:46 +0200 Subject: [PATCH 05/12] add docs link --- src/utils/device/clientInformation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 83277b11363..32445334f5a 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -37,6 +37,10 @@ const formatUrl = (): string | undefined => { const getClientInformationEventType = (deviceId: string): string => `io.element.matrix_client_information.${deviceId}`; +/** + * Record extra client information for the current device + * https://github.com/vector-im/element-meta/blob/develop/spec/matrix_client_information.md + */ export const recordClientInformation = async ( matrixClient: MatrixClient, sdkConfig: IConfigOptions, From 258d36488c8067474c07f156b5695da6723d33ed Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 23 Sep 2022 17:44:27 +0200 Subject: [PATCH 06/12] display device client information in device details --- .../views/settings/devices/DeviceDetails.tsx | 20 ++++++++++++++++--- .../views/settings/devices/types.ts | 8 +++++++- .../views/settings/devices/useOwnDevices.ts | 14 ++++++++++++- src/i18n/strings/en_EN.json | 3 +++ src/utils/device/clientInformation.ts | 20 +++++++++++++++++++ 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index 53c095a33bd..f6fd825964d 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -22,10 +22,10 @@ import AccessibleButton from '../../elements/AccessibleButton'; import Spinner from '../../elements/Spinner'; import { DeviceDetailHeading } from './DeviceDetailHeading'; import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard'; -import { DeviceWithVerification } from './types'; +import { ExtendedDevice } from './types'; interface Props { - device: DeviceWithVerification; + device: ExtendedDevice; isSigningOut: boolean; onVerifyDevice?: () => void; onSignOutDevice: () => void; @@ -54,13 +54,27 @@ const DeviceDetails: React.FC = ({ }, ], }, + { + heading: _t('Application'), + values: [ + { label: _t('Name'), value: device.clientName }, + { label: _t('Version'), value: device.clientVersion }, + { label: _t('URL'), value: device.url }, + ], + }, { heading: _t('Device'), values: [ { label: _t('IP address'), value: device.last_seen_ip }, ], }, - ]; + ].map(section => + // filter out falsy values + ({ ...section, values: section.values.filter(row => !!row.value) })) + .filter(section => + // then filter out sections with no values + section.values.length, + ); return
; +export type ExtendedDeviceInfo = { + clientName?: string; + clientVersion?: string; + url?: string; +}; +export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceInfo; +export type DevicesDictionary = Record; export enum DeviceSecurityVariation { Verified = 'Verified', diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index 0f7d1044da6..4ca0fface13 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -23,7 +23,8 @@ import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { _t } from "../../../../languageHandler"; -import { DevicesDictionary, DeviceWithVerification } from "./types"; +import { getDeviceClientInformation } from "../../../../utils/device/clientInformation"; +import { DevicesDictionary, DeviceWithVerification, ExtendedDeviceInfo } from "./types"; const isDeviceVerified = ( matrixClient: MatrixClient, @@ -51,6 +52,16 @@ const isDeviceVerified = ( } }; +const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceInfo => { + const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id); + + return { + clientName: name, + clientVersion: version, + url, + }; +}; + const fetchDevicesWithVerification = async ( matrixClient: MatrixClient, userId: string, @@ -64,6 +75,7 @@ const fetchDevicesWithVerification = async ( [device.device_id]: { ...device, isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device), + ...parseDeviceExtendedInformation(matrixClient, device), }, }), {}); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9bd1dd8124f..80031659e8b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1716,6 +1716,9 @@ "Please be aware that session names are also visible to people you communicate with": "Please be aware that session names are also visible to people you communicate with", "Session ID": "Session ID", "Last activity": "Last activity", + "Application": "Application", + "Version": "Version", + "URL": "URL", "Device": "Device", "IP address": "IP address", "Session details": "Session details", diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 32445334f5a..0eeed294652 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -58,3 +58,23 @@ export const recordClientInformation = async ( url, }); }; + +const sanitizeContentString = (value: unknown): string | undefined => + value && typeof value === 'string' ? value : undefined; + +export const getDeviceClientInformation = (matrixClient: MatrixClient, deviceId: string): DeviceClientInformation => { + const event = matrixClient.getAccountData(getClientInformationEventType(deviceId)); + + if (!event) { + return {}; + } + + const { name, version, url } = event.getContent(); + + return { + name: sanitizeContentString(name), + version: sanitizeContentString(version), + url: sanitizeContentString(url), + }; +}; + From a3cf263ed621dcab37dc644fd516a89429ae9b91 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 23 Sep 2022 17:46:44 +0200 Subject: [PATCH 07/12] update snapshots --- .../CurrentDeviceSection-test.tsx.snap | 33 ---------- .../__snapshots__/DeviceDetails-test.tsx.snap | 66 ------------------- .../tabs/user/SessionManagerTab-test.tsx | 1 + 3 files changed, 1 insertion(+), 99 deletions(-) diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index 65ed96604de..d270b0132f4 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -90,39 +90,6 @@ HTMLCollection [ alices_device - - - Last activity - - - - - - - - - - - - - - -
- Device -
- IP address - -
diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap index ce9655456ef..14ecdf7c356 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap @@ -78,39 +78,6 @@ exports[` renders a verified device 1`] = ` my-device - - - Last activity - - - - - - - - - - - - - - -
- Device -
- IP address - -
@@ -350,39 +317,6 @@ exports[` renders device without metadata 1`] = ` my-device - - - Last activity - - - - - - - - - - - - - - -
- Device -
- IP address - -
diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index c69e71c32cf..d15731994e7 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -67,6 +67,7 @@ describe('', () => { deleteMultipleDevices: jest.fn(), generateClientSecret: jest.fn(), setDeviceDetails: jest.fn(), + getAccountData: jest.fn(), }); const defaultProps = {}; From d316ec6ff66b13dbce270e7d1fba9552fc34c57c Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 23 Sep 2022 17:58:52 +0200 Subject: [PATCH 08/12] integration-ish test client information in metadata --- .../views/settings/devices/DeviceDetails.tsx | 7 ++- .../CurrentDeviceSection-test.tsx.snap | 1 + .../__snapshots__/DeviceDetails-test.tsx.snap | 4 ++ .../tabs/user/SessionManagerTab-test.tsx | 46 ++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/devices/DeviceDetails.tsx b/src/components/views/settings/devices/DeviceDetails.tsx index f6fd825964d..a59de991e30 100644 --- a/src/components/views/settings/devices/DeviceDetails.tsx +++ b/src/components/views/settings/devices/DeviceDetails.tsx @@ -33,6 +33,7 @@ interface Props { } interface MetadataTable { + id: string; heading?: string; values: { label: string, value?: string | React.ReactNode }[]; } @@ -46,6 +47,7 @@ const DeviceDetails: React.FC = ({ }) => { const metadata: MetadataTable[] = [ { + id: 'session', values: [ { label: _t('Session ID'), value: device.device_id }, { @@ -55,6 +57,7 @@ const DeviceDetails: React.FC = ({ ], }, { + id: 'application', heading: _t('Application'), values: [ { label: _t('Name'), value: device.clientName }, @@ -63,6 +66,7 @@ const DeviceDetails: React.FC = ({ ], }, { + id: 'device', heading: _t('Device'), values: [ { label: _t('IP address'), value: device.last_seen_ip }, @@ -88,9 +92,10 @@ const DeviceDetails: React.FC = ({

{ _t('Session details') }

- { metadata.map(({ heading, values }, index) =>
{ heading && diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap index d270b0132f4..a5930a42fa4 100644 --- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap @@ -76,6 +76,7 @@ HTMLCollection [

diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap index 14ecdf7c356..a774c8fac54 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap @@ -64,6 +64,7 @@ exports[` renders a verified device 1`] = `

@@ -165,6 +166,7 @@ exports[` renders device with metadata 1`] = `

@@ -195,6 +197,7 @@ exports[` renders device with metadata 1`] = `
@@ -303,6 +306,7 @@ exports[` renders device without metadata 1`] = `

diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index d15731994e7..28fd65f8351 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -22,7 +22,7 @@ import { logger } from 'matrix-js-sdk/src/logger'; import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import { VerificationRequest } from 'matrix-js-sdk/src/crypto/verification/request/VerificationRequest'; import { sleep } from 'matrix-js-sdk/src/utils'; -import { IMyDevice } from 'matrix-js-sdk/src/matrix'; +import { IMyDevice, MatrixEvent } from 'matrix-js-sdk/src/matrix'; import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab'; import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext'; @@ -102,6 +102,8 @@ describe('', () => { mockClient.getDevices .mockReset() .mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); + + mockClient.getAccountData.mockReturnValue(undefined); }); it('renders spinner while devices load', () => { @@ -167,6 +169,48 @@ describe('', () => { expect(getByTestId(`device-tile-${alicesDevice.device_id}`)).toMatchSnapshot(); }); + it('extends device with client information when available', async () => { + mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); + mockClient.getAccountData.mockImplementation((eventType: string) => { + const content = { + name: 'Element Web', + version: '1.2.3', + url: 'test.com', + }; + return new MatrixEvent({ + type: eventType, + content, + }); + }); + + const { getByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + // once for each device + expect(mockClient.getAccountData).toHaveBeenCalledTimes(2); + + toggleDeviceDetails(getByTestId, alicesDevice.device_id); + // application metadata section rendered + expect(getByTestId('device-detail-metadata-application')).toBeTruthy(); + }); + + it('renders devices without available client information without error', async () => { + mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); + + const { getByTestId, queryByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + toggleDeviceDetails(getByTestId, alicesDevice.device_id); + // application metadata section not rendered + expect(queryByTestId('device-detail-metadata-application')).toBeFalsy(); + }); + it('renders current session section with an unverified session', async () => { mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); const { getByTestId } = render(getComponent()); From f24e983842e6fe1ca78bae3572d770e62e95c200 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Fri, 23 Sep 2022 18:17:26 +0200 Subject: [PATCH 09/12] tests --- .../settings/devices/DeviceDetails-test.tsx | 1 + .../__snapshots__/DeviceDetails-test.tsx.snap | 26 ++++++++ .../tabs/user/SessionManagerTab-test.tsx | 8 +-- test/utils/device/clientInformation-test.ts | 62 ++++++++++++++++++- 4 files changed, 92 insertions(+), 5 deletions(-) diff --git a/test/components/views/settings/devices/DeviceDetails-test.tsx b/test/components/views/settings/devices/DeviceDetails-test.tsx index dad0ce625be..3fabc4737ef 100644 --- a/test/components/views/settings/devices/DeviceDetails-test.tsx +++ b/test/components/views/settings/devices/DeviceDetails-test.tsx @@ -51,6 +51,7 @@ describe('', () => { display_name: 'My Device', last_seen_ip: '123.456.789', last_seen_ts: now - 60000000, + clientName: 'Element Web', }; const { container } = render(getComponent({ device })); expect(container).toMatchSnapshot(); diff --git a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap index a774c8fac54..68f0bd7d59a 100644 --- a/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap +++ b/test/components/views/settings/devices/__snapshots__/DeviceDetails-test.tsx.snap @@ -195,6 +195,32 @@ exports[` renders device with metadata 1`] = `
+ + + + + + + + + + + + +
+ Application +
+ Name + + Element Web +
', () => { mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); mockClient.getAccountData.mockImplementation((eventType: string) => { const content = { - name: 'Element Web', - version: '1.2.3', - url: 'test.com', - }; + name: 'Element Web', + version: '1.2.3', + url: 'test.com', + }; return new MatrixEvent({ type: eventType, content, diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index 628c9729d14..1760c8a873e 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -14,9 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; + import BasePlatform from "../../../src/BasePlatform"; import { IConfigOptions } from "../../../src/IConfigOptions"; -import { recordClientInformation } from "../../../src/utils/device/clientInformation"; +import { + getDeviceClientInformation, + recordClientInformation, +} from "../../../src/utils/device/clientInformation"; import { getMockClientWithEventEmitter } from "../../test-utils"; describe('recordClientInformation()', () => { @@ -84,3 +89,58 @@ describe('recordClientInformation()', () => { ); }); }); + +describe('getDeviceClientInformation()', () => { + const deviceId = 'my-device-id'; + + const mockClient = getMockClientWithEventEmitter({ + getAccountData: jest.fn(), + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('returns an empty object when no event exists for the device', () => { + expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({}); + + expect(mockClient.getAccountData).toHaveBeenCalledWith( + `io.element.matrix-client-information.${deviceId}`, + ); + }); + + it('returns client information for the device', () => { + const eventContent = { + name: 'Element Web', + version: '1.2.3', + url: 'test.com', + }; + const event = new MatrixEvent({ + type: `io.element.matrix-client-information.${deviceId}`, + content: eventContent, + }); + mockClient.getAccountData.mockReturnValue(event); + expect(getDeviceClientInformation(mockClient, deviceId)).toEqual(eventContent); + }); + + it('excludes values with incorrect types', () => { + const eventContent = { + extraField: 'hello', + name: 'Element Web', + // wrong format + version: { value: '1.2.3' }, + url: 'test.com', + }; + const event = new MatrixEvent({ + type: `io.element.matrix-client-information.${deviceId}`, + content: eventContent, + }); + mockClient.getAccountData.mockReturnValue(event); + // invalid fields excluded + expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({ + name: eventContent.name, + url: eventContent.url, + }); + }); +}); + From e79934f3902ecaea6a68d5d194aefc07ef8efb90 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 11:13:38 +0200 Subject: [PATCH 10/12] fix tests --- test/utils/device/clientInformation-test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/device/clientInformation-test.ts b/test/utils/device/clientInformation-test.ts index 1760c8a873e..0f1d030e791 100644 --- a/test/utils/device/clientInformation-test.ts +++ b/test/utils/device/clientInformation-test.ts @@ -105,7 +105,7 @@ describe('getDeviceClientInformation()', () => { expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({}); expect(mockClient.getAccountData).toHaveBeenCalledWith( - `io.element.matrix-client-information.${deviceId}`, + `io.element.matrix_client_information.${deviceId}`, ); }); @@ -116,7 +116,7 @@ describe('getDeviceClientInformation()', () => { url: 'test.com', }; const event = new MatrixEvent({ - type: `io.element.matrix-client-information.${deviceId}`, + type: `io.element.matrix_client_information.${deviceId}`, content: eventContent, }); mockClient.getAccountData.mockReturnValue(event); @@ -132,7 +132,7 @@ describe('getDeviceClientInformation()', () => { url: 'test.com', }; const event = new MatrixEvent({ - type: `io.element.matrix-client-information.${deviceId}`, + type: `io.element.matrix_client_information.${deviceId}`, content: eventContent, }); mockClient.getAccountData.mockReturnValue(event); From 9e86e9e119cb509035e9ba771b9989c06c845cd5 Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 11:20:06 +0200 Subject: [PATCH 11/12] export helper --- src/utils/device/clientInformation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index 0eeed294652..f63fed6cbda 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -34,7 +34,7 @@ const formatUrl = (): string | undefined => { ].join(""); }; -const getClientInformationEventType = (deviceId: string): string => +export const getClientInformationEventType = (deviceId: string): string => `io.element.matrix_client_information.${deviceId}`; /** From ad81d2c6394bc587ff7dbb6c6bf20156dffbe06a Mon Sep 17 00:00:00 2001 From: Kerry Archibald Date: Mon, 26 Sep 2022 11:26:59 +0200 Subject: [PATCH 12/12] DeviceClientInformation type --- src/utils/device/clientInformation.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/device/clientInformation.ts b/src/utils/device/clientInformation.ts index f63fed6cbda..c31d1c690ea 100644 --- a/src/utils/device/clientInformation.ts +++ b/src/utils/device/clientInformation.ts @@ -19,6 +19,12 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import BasePlatform from "../../BasePlatform"; import { IConfigOptions } from "../../IConfigOptions"; +export type DeviceClientInformation = { + name?: string; + version?: string; + url?: string; +}; + const formatUrl = (): string | undefined => { // don't record url for electron clients if (window.electron) {