Add A-Element-R labels to rageshakes if rust (#12251)

* Add A-Element-R labels to rageshakes if rust

* fix import

* Add tests for rageshake  collect

* add ts-ignore in test

* refactor rageshake to match sonar Cognitive Complexity
This commit is contained in:
Valere 2024-02-15 20:45:46 +01:00 committed by GitHub
parent 342d0db153
commit db096b7986
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 810 additions and 122 deletions

View file

@ -17,7 +17,7 @@ limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { Method } from "matrix-js-sdk/src/matrix";
import { Method, MatrixClient, CryptoApi } from "matrix-js-sdk/src/matrix";
import type * as Pako from "pako";
import { MatrixClientPeg } from "../MatrixClientPeg";
@ -37,34 +37,70 @@ interface IOpts {
customFields?: Record<string, string>;
}
async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<FormData> {
const progressCallback = opts.progressCallback || ((): void => {});
/**
* Exported only for testing.
* @internal public for test
*/
export async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<FormData> {
const progressCallback = opts.progressCallback;
progressCallback(_t("bug_reporting|collecting_information"));
let version: string | undefined;
try {
version = await PlatformPeg.get()?.getAppVersion();
} catch (err) {} // PlatformPeg already logs this.
const userAgent = window.navigator?.userAgent ?? "UNKNOWN";
let installedPWA = "UNKNOWN";
try {
// Known to work at least for desktop Chrome
installedPWA = String(window.matchMedia("(display-mode: standalone)").matches);
} catch (e) {}
let touchInput = "UNKNOWN";
try {
// MDN claims broad support across browsers
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
} catch (e) {}
const client = MatrixClientPeg.get();
progressCallback?.(_t("bug_reporting|collecting_information"));
logger.log("Sending bug report.");
const body = new FormData();
await collectBaseInformation(body, opts);
const client = MatrixClientPeg.get();
if (client) {
await collectClientInfo(client, body);
}
collectLabels(client, opts, body);
collectSettings(body);
await collectStorageStatInfo(body);
collectMissingFeatures(body);
if (opts.sendLogs) {
await collectLogs(body, gzipLogs, progressCallback);
}
return body;
}
async function getAppVersion(): Promise<string | undefined> {
try {
return await PlatformPeg.get()?.getAppVersion();
} catch (err) {
// this happens if no version is set i.e. in dev
}
}
function matchesMediaQuery(query: string): string {
try {
return String(window.matchMedia(query).matches);
} catch (err) {
// if not supported in browser
}
return "UNKNOWN";
}
/**
* Collects base information about the user and the app to add to the report.
*/
async function collectBaseInformation(body: FormData, opts: IOpts): Promise<void> {
const version = await getAppVersion();
const userAgent = window.navigator?.userAgent ?? "UNKNOWN";
const installedPWA = matchesMediaQuery("(display-mode: standalone)");
const touchInput = matchesMediaQuery("(pointer: coarse)");
body.append("text", opts.userText || "User did not supply any additional text.");
body.append("app", opts.customApp || "element-web");
body.append("version", version ?? "UNKNOWN");
@ -77,98 +113,129 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append(key, opts.customFields[key]);
}
}
}
if (client) {
body.append("user_id", client.credentials.userId!);
body.append("device_id", client.deviceId!);
/**
* Collects client and crypto related info.
*/
async function collectClientInfo(client: MatrixClient, body: FormData): Promise<void> {
body.append("user_id", client.credentials.userId!);
body.append("device_id", client.deviceId!);
const cryptoApi = client.getCrypto();
const cryptoApi = client.getCrypto();
if (cryptoApi) {
body.append("crypto_version", cryptoApi.getVersion());
if (cryptoApi) {
await collectCryptoInfo(cryptoApi, body);
await collectRecoveryInfo(client, cryptoApi, body);
}
const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`];
body.append("device_keys", keys.join(", "));
// add cross-signing status information
const crossSigningStatus = await cryptoApi.getCrossSigningStatus();
const secretStorage = client.secretStorage;
body.append("cross_signing_ready", String(await cryptoApi.isCrossSigningReady()));
body.append("cross_signing_key", (await cryptoApi.getCrossSigningKeyId()) ?? "n/a");
body.append(
"cross_signing_privkey_in_secret_storage",
String(crossSigningStatus.privateKeysInSecretStorage),
);
body.append(
"cross_signing_master_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.masterKey),
);
body.append(
"cross_signing_self_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.selfSigningKey),
);
body.append(
"cross_signing_user_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.userSigningKey),
);
body.append("secret_storage_ready", String(await cryptoApi.isSecretStorageReady()));
body.append("secret_storage_key_in_account", String(await secretStorage.hasKey()));
body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored())));
const sessionBackupKeyFromCache = await cryptoApi.getSessionBackupPrivateKey();
body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache));
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
}
await collectSynapseSpecific(client, body);
}
/**
* Collects information about the home server.
*/
async function collectSynapseSpecific(client: MatrixClient, body: FormData): Promise<void> {
try {
// XXX: This is synapse-specific but better than nothing until MSC support for a server version endpoint
const data = await client.http.request<Record<string, any>>(
Method.Get,
"/server_version",
undefined,
undefined,
{
prefix: "/_synapse/admin/v1",
},
);
Object.keys(data).forEach((key) => {
body.append(`matrix_hs_${key}`, data[key]);
});
} catch {
try {
// XXX: This is synapse-specific but better than nothing until MSC support for a server version endpoint
const data = await client.http.request<Record<string, any>>(
Method.Get,
"/server_version",
undefined,
undefined,
{
prefix: "/_synapse/admin/v1",
},
);
Object.keys(data).forEach((key) => {
body.append(`matrix_hs_${key}`, data[key]);
});
// XXX: This relies on the federation listener being delegated via well-known
// or at the same place as the client server endpoint
const data = await getServerVersionFromFederationApi(client);
body.append("matrix_hs_name", data.server.name);
body.append("matrix_hs_version", data.server.version);
} catch {
try {
// XXX: This relies on the federation listener being delegated via well-known
// or at the same place as the client server endpoint
const data = await getServerVersionFromFederationApi(client);
body.append("matrix_hs_name", data.server.name);
body.append("matrix_hs_version", data.server.version);
} catch {
try {
// If that fails we'll hit any endpoint and look at the server response header
const res = await window.fetch(client.http.getUrl("/login"), {
method: "GET",
mode: "cors",
});
if (res.headers.has("server")) {
body.append("matrix_hs_server", res.headers.get("server")!);
}
} catch {
// Could not determine server version
// If that fails we'll hit any endpoint and look at the server response header
const res = await window.fetch(client.http.getUrl("/login"), {
method: "GET",
mode: "cors",
});
if (res.headers.has("server")) {
body.append("matrix_hs_server", res.headers.get("server")!);
}
} catch {
// Could not determine server version
}
}
}
}
/**
* Collects crypto related information.
*/
async function collectCryptoInfo(cryptoApi: CryptoApi, body: FormData): Promise<void> {
body.append("crypto_version", cryptoApi.getVersion());
const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`];
body.append("device_keys", keys.join(", "));
// add cross-signing status information
const crossSigningStatus = await cryptoApi.getCrossSigningStatus();
body.append("cross_signing_ready", String(await cryptoApi.isCrossSigningReady()));
body.append("cross_signing_key", (await cryptoApi.getCrossSigningKeyId()) ?? "n/a");
body.append("cross_signing_privkey_in_secret_storage", String(crossSigningStatus.privateKeysInSecretStorage));
body.append("cross_signing_master_privkey_cached", String(crossSigningStatus.privateKeysCachedLocally.masterKey));
body.append(
"cross_signing_self_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.selfSigningKey),
);
body.append(
"cross_signing_user_signing_privkey_cached",
String(crossSigningStatus.privateKeysCachedLocally.userSigningKey),
);
}
/**
* Collects information about secret storage and backup.
*/
async function collectRecoveryInfo(client: MatrixClient, cryptoApi: CryptoApi, body: FormData): Promise<void> {
const secretStorage = client.secretStorage;
body.append("secret_storage_ready", String(await cryptoApi.isSecretStorageReady()));
body.append("secret_storage_key_in_account", String(await secretStorage.hasKey()));
body.append("session_backup_key_in_secret_storage", String(!!(await client.isKeyBackupKeyStored())));
const sessionBackupKeyFromCache = await cryptoApi.getSessionBackupPrivateKey();
body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache));
body.append("session_backup_key_well_formed", String(sessionBackupKeyFromCache instanceof Uint8Array));
}
/**
* Collects labels to add to the report.
*/
export function collectLabels(client: MatrixClient | null, opts: IOpts, body: FormData): void {
if (client?.getCrypto()?.getVersion()?.startsWith(`Rust SDK`)) {
body.append("label", "A-Element-R");
}
if (opts.labels) {
for (const label of opts.labels) {
body.append("label", label);
}
}
}
/**
* Collects some settings (lab flags and more) to add to the report.
*/
export function collectSettings(body: FormData): void {
// add labs options
const enabledLabs = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.getValue(f));
if (enabledLabs.length) {
@ -179,6 +246,13 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("lowBandwidth", "enabled");
}
body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!);
}
/**
* Collects storage statistics to add to the report.
*/
async function collectStorageStatInfo(body: FormData): Promise<void> {
// add storage persistence/quota information
if (navigator.storage && navigator.storage.persisted) {
try {
@ -202,7 +276,9 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
}
} catch (e) {}
}
}
function collectMissingFeatures(body: FormData): void {
if (window.Modernizr) {
const missingFeatures = (Object.keys(window.Modernizr) as [keyof ModernizrStatic]).filter(
(key: keyof ModernizrStatic) => window.Modernizr[key] === false,
@ -211,33 +287,35 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("modernizr_missing_features", missingFeatures.join(", "));
}
}
body.append("mx_local_settings", localStorage.getItem("mx_local_settings")!);
if (opts.sendLogs) {
let pako: typeof Pako | undefined;
if (gzipLogs) {
pako = await import("pako");
}
progressCallback(_t("bug_reporting|collecting_logs"));
const logs = await rageshake.getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);
// compress
if (gzipLogs) {
buf = pako!.gzip(buf);
}
body.append("compressed-log", new Blob([buf]), entry.id);
}
}
return body;
}
/**
* Collects logs to add to the report if enabled.
*/
async function collectLogs(
body: FormData,
gzipLogs: boolean,
progressCallback: ((s: string) => void) | undefined,
): Promise<void> {
let pako: typeof Pako | undefined;
if (gzipLogs) {
pako = await import("pako");
}
progressCallback?.(_t("bug_reporting|collecting_logs"));
const logs = await rageshake.getLogsForReport();
for (const entry of logs) {
// encode as UTF-8
let buf = new TextEncoder().encode(entry.lines);
// compress
if (gzipLogs) {
buf = pako!.gzip(buf);
}
body.append("compressed-log", new Blob([buf]), entry.id);
}
}
/**
* Send a bug report.
*

View file

@ -0,0 +1,608 @@
/*
Copyright 2024 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 { Mocked, mocked } from "jest-mock";
import {
HttpApiEvent,
HttpApiEventHandlerMap,
IHttpOpts,
MatrixClient,
TypedEventEmitter,
MatrixHttpApi,
} from "matrix-js-sdk/src/matrix";
import fetchMock from "fetch-mock-jest";
import { getMockClientWithEventEmitter, mockClientMethodsCrypto, mockPlatformPeg } from "./test-utils";
import { collectBugReport } from "../src/rageshake/submit-rageshake";
import { MatrixClientPeg } from "../src/MatrixClientPeg";
import SettingsStore from "../src/settings/SettingsStore";
import { ConsoleLogger } from "../src/rageshake/rageshake";
describe("Rageshakes", () => {
const RUST_CRYPTO_VERSION = "Rust SDK 0.7.0 (691ec63), Vodozemac 0.5.0";
const OLM_CRYPTO_VERSION = "Olm 3.2.15";
let mockClient: Mocked<MatrixClient>;
const mockHttpAPI: MatrixHttpApi<IHttpOpts & { onlyData: true }> = new MatrixHttpApi(
new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(),
{
baseUrl: "https://alice-server.com",
prefix: "/_matrix/client/v3",
onlyData: true,
},
);
beforeEach(() => {
jest.spyOn(MatrixClientPeg, "getHomeserverName").mockReturnValue("alice-server.com");
mockClient = getMockClientWithEventEmitter({
credentials: { userId: "@test:example.com" },
deviceId: "AAAAAAAAAA",
baseUrl: "https://alice-server.com",
getHomeserverUrl: jest.fn().mockReturnValue("https://alice-server.com"),
...mockClientMethodsCrypto(),
http: mockHttpAPI,
});
mocked(mockClient.getCrypto()!.getOwnDeviceKeys).mockResolvedValue({
ed25519: "",
curve25519: "",
});
fetchMock.restore();
fetchMock.catch(404);
});
describe("Basic Information", () => {
let mockWindow: Mocked<Window>;
let windowSpy: jest.SpyInstance;
beforeEach(() => {
mockWindow = {
matchMedia: jest.fn().mockReturnValue({ matches: false }),
navigator: {
userAgent: "",
},
} as unknown as Mocked<Window>;
// @ts-ignore - We just need partial mock
windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow);
});
afterEach(() => {
windowSpy.mockRestore();
});
it("should include app version", async () => {
mockPlatformPeg({ getAppVersion: jest.fn().mockReturnValue("1.11.58") });
const formData = await collectBugReport();
const appVersion = formData.get("version");
expect(appVersion).toBe("1.11.58");
});
it("should put unknown app version if on dev", async () => {
mockPlatformPeg({ getAppVersion: jest.fn().mockRejectedValue(undefined) });
const formData = await collectBugReport();
const appVersion = formData.get("version");
expect(appVersion).toBe("UNKNOWN");
});
const mediaQueryTests: Array<[string, string, string, boolean]> = [
["if installed WPA", "(display-mode: standalone)", "installed_pwa", true],
["if not installed WPA", "(display-mode: standalone)", "installed_pwa", false],
["if touchInput", "(pointer: coarse)", "touch_input", true],
["if not touchInput", "(pointer: coarse)", "touch_input", false],
];
it.each(mediaQueryTests)("should collect %s", async (_, query, label, matches) => {
mocked(mockWindow.matchMedia).mockImplementation((q): MediaQueryList => {
if (q === query) {
return { matches: matches } as unknown as MediaQueryList;
}
return { matches: false } as unknown as MediaQueryList;
});
const formData = await collectBugReport();
const value = formData.get(label);
expect(value).toBe(String(matches));
});
const optionsTests: Array<[string, string, string, string]> = [
// [name, opt name, label, default]
["userText", "userText", "text", "User did not supply any additional text."],
["customApp", "customApp", "app", "element-web"],
];
it.each(optionsTests)("should collect %s", async (_, optName, label, defaultValue) => {
const formData = await collectBugReport();
const value = formData.get(label);
expect(value).toBe(defaultValue);
const formDataWithOpt = await collectBugReport({ [optName]: "SomethingSomething" });
expect(formDataWithOpt.get(label)).toBe("SomethingSomething");
});
it("should collect custom fields", async () => {
const formDataWithOpt = await collectBugReport({
customFields: {
something: "SomethingSomething",
another: "AnotherThing",
},
});
expect(formDataWithOpt.get("something")).toBe("SomethingSomething");
expect(formDataWithOpt.get("another")).toBe("AnotherThing");
});
it("should collect user agent", async () => {
jest.replaceProperty(mockWindow.navigator, "userAgent", "jest navigator");
const formData = await collectBugReport();
const userAgent = formData.get("user_agent");
expect(userAgent).toBe("jest navigator");
// @ts-ignore - Need to force navigator to be undefined for test
jest.replaceProperty(mockWindow, "navigator", undefined);
const formDataWithoutNav = await collectBugReport();
expect(formDataWithoutNav.get("user_agent")).toBe("UNKNOWN");
});
});
describe("Credentials", () => {
it("should collect user id", async () => {
const formData = await collectBugReport();
expect(formData.get("user_id")).toBe("@test:example.com");
});
it("should collect device id", async () => {
const formData = await collectBugReport();
expect(formData.get("device_id")).toBe("AAAAAAAAAA");
});
});
describe("Crypto info", () => {
it("should collect crypto version", async () => {
mocked(mockClient.getCrypto()!.getVersion).mockReturnValue("0.0.0");
const formData = await collectBugReport();
expect(formData.get("crypto_version")).toBe("0.0.0");
});
it("should collect device keys", async () => {
const ownDeviceKeys = {
curve25519: "curve25519b64",
ed25519: "ed25519b64",
};
mocked(mockClient.getCrypto()!.getOwnDeviceKeys).mockResolvedValue(ownDeviceKeys);
const keys = [`curve25519:${ownDeviceKeys.curve25519}`, `ed25519:${ownDeviceKeys.ed25519}`].join(", ");
const formData = await collectBugReport();
expect(formData.get("device_keys")).toBe(keys);
});
describe("Cross-Signing", () => {
it.each([true, false])("should collect cross-signing ready %s", async (ready) => {
mocked(mockClient.getCrypto()!.isCrossSigningReady).mockResolvedValue(ready);
const formData = await collectBugReport();
expect(formData.get("cross_signing_ready")).toBe(String(ready));
});
it("should collect cross-signing pub key if set", async () => {
const crossSigningPubKey = "crossSigningPubKey";
mocked(mockClient.getCrypto()!.getCrossSigningKeyId).mockImplementation(
async (type): Promise<string | null> => {
if (!type || type === "master") {
return crossSigningPubKey;
}
return null;
},
);
const formData = await collectBugReport();
expect(formData.get("cross_signing_key")).toBe(crossSigningPubKey);
});
it("should not collect cross-signing pub key if not set", async () => {
mocked(mockClient.getCrypto()!.getCrossSigningKeyId).mockResolvedValue(null);
expect((await collectBugReport()).get("cross_signing_key")).toBe("n/a");
});
describe("Cross-signing status", () => {
const baseDetails = {
masterKey: false,
selfSigningKey: false,
userSigningKey: false,
};
const baseStatus = {
privateKeysInSecretStorage: false,
publicKeysOnDevice: false,
privateKeysCachedLocally: {
...baseDetails,
},
};
it.each([true, false])("should collect if key cached locally %s", async (cached) => {
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
...baseStatus,
privateKeysInSecretStorage: cached,
});
const formData = await collectBugReport();
expect(formData.get("cross_signing_privkey_in_secret_storage")).toBe(String(cached));
});
// @ts-ignore
const detailsTests: Array<[string, string, string]> = [
["master", "masterKey", "cross_signing_master_privkey_cached"],
["ssk", "selfSigningKey", "cross_signing_self_signing_privkey_cached"],
["usk", "userSigningKey", "cross_signing_user_signing_privkey_cached"],
];
describe.each(detailsTests)("Cached locally %s", (_, objectKey, label) => {
it.each([true, false])("should collect if cached locally %s", async (cached) => {
mocked(mockClient.getCrypto()!.getCrossSigningStatus).mockResolvedValue({
...baseStatus,
privateKeysCachedLocally: {
...baseDetails,
[objectKey]: cached,
},
});
const formData = await collectBugReport();
expect(formData.get(label)).toBe(String(cached));
});
});
});
describe("Secret Storage and backup", () => {
it.each([true, false])("should collect secret storage ready %s", async (ready) => {
mocked(mockClient.getCrypto()!.isSecretStorageReady).mockResolvedValue(ready);
const formData = await collectBugReport();
expect(formData.get("secret_storage_ready")).toBe(String(ready));
});
it.each([true, false])("should collect secret storage key in account %s", async (stored) => {
mocked(mockClient.secretStorage.hasKey).mockResolvedValue(stored);
const formData = await collectBugReport();
expect(formData.get("secret_storage_key_in_account")).toBe(String(stored));
});
it("should collect backup version", async () => {
mocked(mockClient.isKeyBackupKeyStored).mockResolvedValue({});
const formData = await collectBugReport();
expect(formData.get("session_backup_key_in_secret_storage")).toBe(String(true));
{
mocked(mockClient.isKeyBackupKeyStored).mockResolvedValue(null);
const formData = await collectBugReport();
expect(formData.get("session_backup_key_in_secret_storage")).toBe(String(false));
}
});
it("should collect backup key cached", async () => {
mocked(mockClient.getCrypto()!.getSessionBackupPrivateKey).mockResolvedValue(
new Uint8Array([0, 0]),
);
const formData = await collectBugReport();
expect(formData.get("session_backup_key_cached")).toBe(String(true));
expect(formData.get("session_backup_key_well_formed")).toBe(String(true));
});
});
});
});
describe("Synapse info", () => {
beforeEach(() => {
fetchMock.reset();
});
it("should collect synapse admin keys if available", async () => {
fetchMock.get("path:/_synapse/admin/v1/server_version", {
server_version: "1.101.0 (b=matrix-org-hotfixes,6dbedcf601)",
python_version: "3.7.8",
});
const formData = await collectBugReport();
expect(formData.get("matrix_hs_server_version")).toBe("1.101.0 (b=matrix-org-hotfixes,6dbedcf601)");
expect(formData.get("matrix_hs_python_version")).toBe("3.7.8");
});
it("should collect synapse admin keys with federation", async () => {
fetchMock.get("path:/_synapse/admin/v1/server_version", {
status: 404,
});
fetchMock.get("path:/_matrix/client/v3/login", {
status: 404,
});
fetchMock.get("path:/.well-known/matrix/server", {
"m.server": "matrix-federation.example.com:443",
});
fetchMock.get("https://matrix-federation.example.com/_matrix/federation/v1/version", {
server: {
name: "Synapse",
version: "1.101.0 (b=matrix-org-hotfixes,6dbedcf601)",
},
});
const formData = await collectBugReport();
expect(formData.get("matrix_hs_name")).toBe("Synapse");
expect(formData.get("matrix_hs_version")).toBe("1.101.0 (b=matrix-org-hotfixes,6dbedcf601)");
});
it("should collect synapse admin keys with fallback", async () => {
fetchMock.get("path:/_synapse/admin/v1/server_version", {
status: 404,
});
fetchMock.get("path:/.well-known/matrix/server", {
status: 404,
});
fetchMock.get("path:/_matrix/client/v3/login", {
status: 200,
body: {},
headers: {
Server: "some_cdn",
},
});
const formData = await collectBugReport();
expect(formData.get("matrix_hs_server")).toBe("some_cdn");
});
});
describe("Settings Store", () => {
const mockSettingsStore = mocked(SettingsStore);
it("should collect labs from settings store", async () => {
const someFeatures: string[] = ["feature_video_rooms", "feature_notification_settings2", "feature_pinning"];
const enabledFeatures: string[] = ["feature_video_rooms", "feature_pinning"];
jest.spyOn(mockSettingsStore, "getFeatureSettingNames").mockReturnValue(someFeatures);
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
return enabledFeatures.includes(settingName);
});
const formData = await collectBugReport();
expect(formData.get("enabled_labs")).toBe(enabledFeatures.join(", "));
});
it("should collect low bandWidth enabled", async () => {
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
if (settingName == "lowBandwidth") {
return true;
}
});
const formData = await collectBugReport();
expect(formData.get("lowBandwidth")).toBe("enabled");
});
it("should collect low bandWidth disabled", async () => {
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
if (settingName == "lowBandwidth") {
return false;
}
});
const formData = await collectBugReport();
expect(formData.get("lowBandwidth")).toBeNull();
});
});
describe("Navigator Storage", () => {
let mockNavigator: Mocked<Navigator>;
let navigatorSpy: jest.SpyInstance;
beforeEach(() => {
mockNavigator = {
storage: {
estimate: jest.fn(),
persisted: jest.fn(),
},
} as unknown as Mocked<Navigator>;
// @ts-ignore - We just need partial mock
navigatorSpy = jest.spyOn(global, "navigator", "get").mockReturnValue(mockNavigator);
});
afterEach(() => {
navigatorSpy.mockRestore();
});
it("should collect navigator storage persisted", async () => {
mocked(mockNavigator.storage.persisted).mockResolvedValue(true);
const formData = await collectBugReport();
expect(formData.get("storageManager_persisted")).toBe("true");
});
it("should collect navigator storage safari", async () => {
mocked(mockNavigator.storage.persisted).mockResolvedValue(true);
// @ts-ignore - Need to mock the safari
jest.replaceProperty(mockNavigator, "storage", undefined);
const mockDocument = {
hasStorageAccess: jest.fn().mockReturnValue(true),
} as unknown as Mocked<Document>;
const spy = jest.spyOn(global, "document", "get").mockReturnValue(mockDocument);
const formData = await collectBugReport();
expect(formData.get("storageManager_persisted")).toBe("true");
spy.mockRestore();
});
it("should collect navigator storage estimate", async () => {
const estimate = {
quota: 596797550592,
usage: 9147087,
usageDetails: {
indexedDB: 9147045,
serviceWorkerRegistrations: 42,
},
};
mocked(mockNavigator.storage.estimate).mockResolvedValue(estimate);
const formData = await collectBugReport();
expect(formData.get("storageManager_quota")).toEqual(estimate.quota.toString());
expect(formData.get("storageManager_usage")).toEqual(estimate.usage.toString());
expect(formData.get("storageManager_usage_indexedDB")).toEqual(
estimate.usageDetails["indexedDB"].toString(),
);
expect(formData.get("storageManager_usage_serviceWorkerRegistrations")).toEqual(
estimate.usageDetails["serviceWorkerRegistrations"].toString(),
);
});
});
it("should collect modernizer", async () => {
const allFeatures = {
cssanimations: false,
flexbox: true,
d0: false,
d1: false,
crypto: true,
};
const disabledFeatures = ["cssanimations", "d0", "d1"];
const mockWindow = {
Modernizr: {
...allFeatures,
},
} as unknown as Mocked<Window>;
// @ts-ignore - We just need partial mock
const windowSpy = jest.spyOn(global, "window", "get").mockReturnValue(mockWindow);
const formData = await collectBugReport();
expect(formData.get("modernizr_missing_features")).toBe(disabledFeatures.join(", "));
windowSpy.mockRestore();
});
it("should collect localstorage settings", async () => {
const localSettings = {
language: "fr",
showHiddenEventsInTimeline: true,
activeCallRoomIds: [],
};
const spy = jest.spyOn(window.localStorage.__proto__, "getItem").mockImplementation((key) => {
return JSON.stringify(localSettings);
});
const formData = await collectBugReport();
expect(formData.get("mx_local_settings")).toBe(JSON.stringify(localSettings));
spy.mockRestore();
});
it("should collect logs", async () => {
const mockConsoleLogger = {
flush: jest.fn(),
consume: jest.fn(),
warn: jest.fn(),
} as unknown as Mocked<ConsoleLogger>;
// @ts-ignore - mock the console logger
global.mx_rage_logger = mockConsoleLogger;
// @ts-ignore
mockConsoleLogger.flush.mockReturnValue([
{
id: "instance-0",
line: "line 1",
},
{
id: "instance-1",
line: "line 2",
},
]);
const formData = await collectBugReport({ sendLogs: true });
expect(formData.get("compressed-log")).toBeDefined();
});
describe("A-Element-R label", () => {
test("should add A-Element-R label if rust crypto", async () => {
mocked(mockClient.getCrypto()!.getVersion).mockReturnValue(RUST_CRYPTO_VERSION);
const formData = await collectBugReport();
const labelNames = formData.getAll("label");
expect(labelNames).toContain("A-Element-R");
});
test("should add A-Element-R label if rust crypto and new version", async () => {
mocked(mockClient.getCrypto()!.getVersion).mockReturnValue("Rust SDK 0.9.3 (909d09fd), Vodozemac 0.8.1");
const formData = await collectBugReport();
const labelNames = formData.getAll("label");
expect(labelNames).toContain("A-Element-R");
});
test("should not add A-Element-R label if not rust crypto", async () => {
mocked(mockClient.getCrypto()!.getVersion).mockReturnValue(OLM_CRYPTO_VERSION);
const formData = await collectBugReport();
const labelNames = formData.getAll("label");
expect(labelNames).not.toContain("A-Element-R");
});
test("should add A-Element-R label to the set of requested labels", async () => {
mocked(mockClient.getCrypto()!.getVersion).mockReturnValue(RUST_CRYPTO_VERSION);
const formData = await collectBugReport({
labels: ["Z-UISI", "Foo"],
});
const labelNames = formData.getAll("label");
expect(labelNames).toContain("A-Element-R");
expect(labelNames).toContain("Z-UISI");
expect(labelNames).toContain("Foo");
});
test("should not panic if there is no crypto", async () => {
mocked(mockClient.getCrypto).mockReturnValue(undefined);
const formData = await collectBugReport();
const labelNames = formData.getAll("label");
expect(labelNames).not.toContain("A-Element-R");
});
});
it("should notify progress", () => {
const progressCallback = jest.fn();
collectBugReport({ progressCallback });
expect(progressCallback).toHaveBeenCalled();
});
});

View file

@ -170,5 +170,7 @@ export const mockClientMethodsCrypto = (): Partial<
isSecretStorageReady: jest.fn(),
getSessionBackupPrivateKey: jest.fn(),
getVersion: jest.fn().mockReturnValue("Version 0"),
getOwnDeviceKeys: jest.fn(),
getCrossSigningKeyId: jest.fn(),
}),
});