mirror of https://github.com/vector-im/riot-web
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 Complexitypull/28217/head
parent
342d0db153
commit
db096b7986
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
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 type * as Pako from "pako";
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
@ -37,34 +37,70 @@ interface IOpts {
|
||||||
customFields?: Record<string, string>;
|
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"));
|
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();
|
|
||||||
|
|
||||||
logger.log("Sending bug report.");
|
logger.log("Sending bug report.");
|
||||||
|
|
||||||
const body = new FormData();
|
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("text", opts.userText || "User did not supply any additional text.");
|
||||||
body.append("app", opts.customApp || "element-web");
|
body.append("app", opts.customApp || "element-web");
|
||||||
body.append("version", version ?? "UNKNOWN");
|
body.append("version", version ?? "UNKNOWN");
|
||||||
|
@ -77,98 +113,129 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
|
||||||
body.append(key, opts.customFields[key]);
|
body.append(key, opts.customFields[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (client) {
|
/**
|
||||||
body.append("user_id", client.credentials.userId!);
|
* Collects client and crypto related info.
|
||||||
body.append("device_id", client.deviceId!);
|
*/
|
||||||
|
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) {
|
if (cryptoApi) {
|
||||||
body.append("crypto_version", cryptoApi.getVersion());
|
await collectCryptoInfo(cryptoApi, body);
|
||||||
|
await collectRecoveryInfo(client, cryptoApi, body);
|
||||||
|
}
|
||||||
|
|
||||||
const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys();
|
await collectSynapseSpecific(client, body);
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
try {
|
||||||
// XXX: This is synapse-specific but better than nothing until MSC support for a server version endpoint
|
// XXX: This relies on the federation listener being delegated via well-known
|
||||||
const data = await client.http.request<Record<string, any>>(
|
// or at the same place as the client server endpoint
|
||||||
Method.Get,
|
const data = await getServerVersionFromFederationApi(client);
|
||||||
"/server_version",
|
body.append("matrix_hs_name", data.server.name);
|
||||||
undefined,
|
body.append("matrix_hs_version", data.server.version);
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
prefix: "/_synapse/admin/v1",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Object.keys(data).forEach((key) => {
|
|
||||||
body.append(`matrix_hs_${key}`, data[key]);
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
// XXX: This relies on the federation listener being delegated via well-known
|
// If that fails we'll hit any endpoint and look at the server response header
|
||||||
// or at the same place as the client server endpoint
|
const res = await window.fetch(client.http.getUrl("/login"), {
|
||||||
const data = await getServerVersionFromFederationApi(client);
|
method: "GET",
|
||||||
body.append("matrix_hs_name", data.server.name);
|
mode: "cors",
|
||||||
body.append("matrix_hs_version", data.server.version);
|
});
|
||||||
} catch {
|
if (res.headers.has("server")) {
|
||||||
try {
|
body.append("matrix_hs_server", res.headers.get("server")!);
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
} 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) {
|
if (opts.labels) {
|
||||||
for (const label of opts.labels) {
|
for (const label of opts.labels) {
|
||||||
body.append("label", label);
|
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
|
// add labs options
|
||||||
const enabledLabs = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.getValue(f));
|
const enabledLabs = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.getValue(f));
|
||||||
if (enabledLabs.length) {
|
if (enabledLabs.length) {
|
||||||
|
@ -179,6 +246,13 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
|
||||||
body.append("lowBandwidth", "enabled");
|
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
|
// add storage persistence/quota information
|
||||||
if (navigator.storage && navigator.storage.persisted) {
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
try {
|
try {
|
||||||
|
@ -202,7 +276,9 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMissingFeatures(body: FormData): void {
|
||||||
if (window.Modernizr) {
|
if (window.Modernizr) {
|
||||||
const missingFeatures = (Object.keys(window.Modernizr) as [keyof ModernizrStatic]).filter(
|
const missingFeatures = (Object.keys(window.Modernizr) as [keyof ModernizrStatic]).filter(
|
||||||
(key: keyof ModernizrStatic) => window.Modernizr[key] === false,
|
(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("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.
|
* Send a bug report.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -170,5 +170,7 @@ export const mockClientMethodsCrypto = (): Partial<
|
||||||
isSecretStorageReady: jest.fn(),
|
isSecretStorageReady: jest.fn(),
|
||||||
getSessionBackupPrivateKey: jest.fn(),
|
getSessionBackupPrivateKey: jest.fn(),
|
||||||
getVersion: jest.fn().mockReturnValue("Version 0"),
|
getVersion: jest.fn().mockReturnValue("Version 0"),
|
||||||
|
getOwnDeviceKeys: jest.fn(),
|
||||||
|
getCrossSigningKeyId: jest.fn(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue