Migrate more tests to vitest (#34349)

This commit is contained in:
Michael Telatynski
2026-07-20 13:07:35 +00:00
committed by GitHub
parent 6c2c962588
commit b719f531dd
20 changed files with 310 additions and 271 deletions
@@ -1,743 +0,0 @@
/*
Copyright 2018-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { mocked, type Mocked, type MockedObject } from "jest-mock";
import { HttpApiEvent, type MatrixClient, type MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
import {
type CryptoApi,
DecryptionFailureCode,
UserVerificationStatus,
CryptoEvent,
} from "matrix-js-sdk/src/crypto-api";
import { sleep } from "matrix-js-sdk/src/utils";
import { DecryptionFailureTracker, type ErrorProperties } from "../../src/DecryptionFailureTracker";
import { stubClient } from "../test-utils";
import * as Lifecycle from "../../src/Lifecycle";
async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) {
return await mkDecryptionFailureMatrixEvent({
roomId: "!room:id",
sender: opts.sender ?? "@alice:example.com",
code: opts.code ?? DecryptionFailureCode.UNKNOWN_ERROR,
msg: ":(",
});
}
// wrap tracker.eventDecrypted so that we don't need to have so many `ts-ignore`s
function eventDecrypted(tracker: DecryptionFailureTracker, e: MatrixEvent, nowTs: number): void {
// @ts-ignore access to private member
return tracker.eventDecrypted(e, nowTs);
}
describe("DecryptionFailureTracker", function () {
afterEach(() => {
localStorage.clear();
});
it("tracks a failed decryption for a visible event", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
false,
);
tracker.addVisibleEvent(failedDecryptionEvent);
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
});
it("tracks a failed decryption with expected raw error for a visible event", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent({
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
});
let count = 0;
let reportedRawCode = "";
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(_errCode: string, rawCode: string) => {
count++;
reportedRawCode = rawCode;
},
() => "UnknownError",
false,
);
tracker.addVisibleEvent(failedDecryptionEvent);
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
// Should add the rawCode to the event context
expect(reportedRawCode).toBe("OLM_UNKNOWN_MESSAGE_INDEX");
});
it("tracks a failed decryption for an event that becomes visible later", async function () {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
false,
);
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
tracker.addVisibleEvent(failedDecryptionEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// should track a failure for an event that failed decryption
expect(count).not.toBe(0);
});
it("tracks visible vs. not visible events", async () => {
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string, rawError: string, properties: ErrorProperties) => {
propertiesByErrorCode[errorCode] = properties;
},
(error: string) => error,
false,
);
// use three different errors so that we can distinguish the reports
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
const error2 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
const error3 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
// event that will be marked as visible before it's marked as undecryptable
const markedVisibleFirst = await createFailedDecryptionEvent({ code: error1 });
// event that will be marked as undecryptable before it's marked as visible
const markedUndecryptableFirst = await createFailedDecryptionEvent({ code: error2 });
// event that is never marked as visible
const neverVisible = await createFailedDecryptionEvent({ code: error3 });
tracker.addVisibleEvent(markedVisibleFirst);
const now = Date.now();
eventDecrypted(tracker, markedVisibleFirst, now);
eventDecrypted(tracker, markedUndecryptableFirst, now);
eventDecrypted(tracker, neverVisible, now);
tracker.addVisibleEvent(markedUndecryptableFirst);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(propertiesByErrorCode[error1].wasVisibleToUser).toBe(true);
expect(propertiesByErrorCode[error2].wasVisibleToUser).toBe(true);
expect(propertiesByErrorCode[error3].wasVisibleToUser).toBe(false);
});
it("does not track a failed decryption where the event is subsequently successfully decrypted", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => {
// should not track an event that has since been decrypted correctly
expect(true).toBe(false);
},
() => "UnknownError",
false,
);
tracker.addVisibleEvent(decryptedEvent);
eventDecrypted(tracker, decryptedEvent, Date.now());
// Indicate successful decryption.
await decryptExistingEvent(decryptedEvent, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
eventDecrypted(tracker, decryptedEvent, Date.now());
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
});
it(
"does not track a failed decryption where the event is subsequently successfully decrypted " +
"and later becomes visible",
async () => {
const decryptedEvent = await createFailedDecryptionEvent();
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => {
// should not track an event that has since been decrypted correctly
expect(true).toBe(false);
},
() => "UnknownError",
false,
);
eventDecrypted(tracker, decryptedEvent, Date.now());
// Indicate successful decryption.
await decryptExistingEvent(decryptedEvent, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
eventDecrypted(tracker, decryptedEvent, Date.now());
tracker.addVisibleEvent(decryptedEvent);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
},
);
it("only tracks a single failure per event, despite multiple failed decryptions for multiple events", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
const decryptedEvent2 = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
false,
);
tracker.addVisibleEvent(decryptedEvent);
// Arbitrary number of failed decryptions for both events
const now = Date.now();
eventDecrypted(tracker, decryptedEvent, now);
eventDecrypted(tracker, decryptedEvent, now);
eventDecrypted(tracker, decryptedEvent, now);
eventDecrypted(tracker, decryptedEvent, now);
eventDecrypted(tracker, decryptedEvent, now);
eventDecrypted(tracker, decryptedEvent2, now);
eventDecrypted(tracker, decryptedEvent2, now);
tracker.addVisibleEvent(decryptedEvent2);
eventDecrypted(tracker, decryptedEvent2, now);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Simulated polling of `checkFailures`, an arbitrary number ( > 2 ) times
tracker.checkFailures(Infinity);
tracker.checkFailures(Infinity);
// should only track a single failure per event
expect(count).toBe(2);
});
it("should not track a failure for an event that was tracked previously", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
eventDecrypted(tracker, decryptedEvent, Date.now());
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// Indicate a second decryption, after having tracked the failure
eventDecrypted(tracker, decryptedEvent, Date.now());
tracker.checkFailures(Infinity);
// should only track a single failure per event
expect(count).toBe(1);
});
it("should not report a failure for an event that was reported in a previous session", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
eventDecrypted(tracker, decryptedEvent, Date.now());
// Pretend "now" is Infinity
// NB: This saves to localStorage specific to DFT
tracker.checkFailures(Infinity);
// Simulate the browser refreshing by destroying tracker and creating a new tracker
// @ts-ignore access to private constructor
const secondTracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await secondTracker.start(mockClient());
secondTracker.addVisibleEvent(decryptedEvent);
eventDecrypted(secondTracker, decryptedEvent, Date.now());
secondTracker.checkFailures(Infinity);
// should only track a single failure per event
expect(count).toBe(1);
});
it("should report a failure for an event that was tracked but not reported in a previous session", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
eventDecrypted(tracker, decryptedEvent, Date.now());
// we do *not* call `checkFailures` here
expect(count).toBe(0);
// Simulate the browser refreshing by destroying tracker and creating a new tracker
// @ts-ignore access to private constructor
const secondTracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await secondTracker.start(mockClient());
secondTracker.addVisibleEvent(decryptedEvent);
eventDecrypted(secondTracker, decryptedEvent, Date.now());
secondTracker.checkFailures(Infinity);
expect(count).toBe(1);
});
it("should report a failure for an event that was reported before a logout/login cycle", async () => {
const decryptedEvent = await createFailedDecryptionEvent();
let count = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
() => count++,
() => "UnknownError",
);
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent);
// Indicate decryption
eventDecrypted(tracker, decryptedEvent, Date.now());
tracker.checkFailures(Infinity);
expect(count).toBe(1);
// Simulate a logout/login cycle
await Lifecycle.onLoggedOut();
await tracker.start(mockClient());
tracker.addVisibleEvent(decryptedEvent);
eventDecrypted(tracker, decryptedEvent, Date.now());
tracker.checkFailures(Infinity);
expect(count).toBe(2);
});
it("should count different error codes separately for multiple failures with different error codes", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
(error: DecryptionFailureCode) =>
error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError",
false,
);
const decryptedEvent1 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.UNKNOWN_ERROR,
});
const decryptedEvent2 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
});
const decryptedEvent3 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
});
tracker.addVisibleEvent(decryptedEvent1);
tracker.addVisibleEvent(decryptedEvent2);
tracker.addVisibleEvent(decryptedEvent3);
// One failure of UNKNOWN_ERROR, and effectively two for MEGOLM_UNKNOWN_INBOUND_SESSION_ID
const now = Date.now();
eventDecrypted(tracker, decryptedEvent1, now);
eventDecrypted(tracker, decryptedEvent2, now);
eventDecrypted(tracker, decryptedEvent2, now);
eventDecrypted(tracker, decryptedEvent3, now);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
//expect(counts['UnknownError']).toBe(1, 'should track one UnknownError');
expect(counts["OlmKeysNotSentError"]).toBe(2);
});
it("should aggregate error codes correctly", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
(_errorCode: string) => "OlmUnspecifiedError",
false,
);
const decryptedEvent1 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
});
const decryptedEvent2 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
});
const decryptedEvent3 = await createFailedDecryptionEvent({
code: DecryptionFailureCode.UNKNOWN_ERROR,
});
tracker.addVisibleEvent(decryptedEvent1);
tracker.addVisibleEvent(decryptedEvent2);
tracker.addVisibleEvent(decryptedEvent3);
const now = Date.now();
eventDecrypted(tracker, decryptedEvent1, now);
eventDecrypted(tracker, decryptedEvent2, now);
eventDecrypted(tracker, decryptedEvent3, now);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(counts["OlmUnspecifiedError"]).toBe(3);
});
it("should remap error codes correctly", async () => {
const counts: Record<string, number> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
(errorCode: string) => Array.from(errorCode).reverse().join(""),
false,
);
const decryptedEvent = await createFailedDecryptionEvent({
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
});
tracker.addVisibleEvent(decryptedEvent);
eventDecrypted(tracker, decryptedEvent, Date.now());
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// should track remapped error code
expect(counts["XEDNI_EGASSEM_NWONKNU_MLO"]).toBe(1);
});
it("default error code mapper maps error codes correctly", async () => {
const errorCodes: string[] = [];
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string) => {
errorCodes.push(errorCode);
},
// @ts-ignore access to private member
DecryptionFailureTracker.instance.errorCodeMapFn,
false,
);
const now = Date.now();
async function createAndTrackEventWithError(code: DecryptionFailureCode) {
const event = await createFailedDecryptionEvent({ code });
tracker.addVisibleEvent(event);
eventDecrypted(tracker, event, now);
return event;
}
await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
await createAndTrackEventWithError(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED);
await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
await createAndTrackEventWithError(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD);
await createAndTrackEventWithError(DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE);
await createAndTrackEventWithError(DecryptionFailureCode.SENDER_IDENTITY_PREVIOUSLY_VERIFIED);
await createAndTrackEventWithError(DecryptionFailureCode.UNSIGNED_SENDER_DEVICE);
await createAndTrackEventWithError(DecryptionFailureCode.UNKNOWN_ERROR);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(errorCodes).toEqual([
"OlmKeysNotSentError",
"OlmIndexError",
"HistoricalMessage",
"HistoricalMessage",
"HistoricalMessage",
"ExpectedDueToMembership",
"OlmKeysNotSentError",
"RoomKeysWithheldForUnverifiedDevice",
"ExpectedVerificationViolation",
"ExpectedSentByInsecureDevice",
"UnknownError",
]);
});
it("tracks late decryptions vs. undecryptable", async () => {
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string, rawError: string, properties: ErrorProperties) => {
propertiesByErrorCode[errorCode] = properties;
},
(error: string) => error,
false,
);
// use three different errors so that we can distinguish the reports
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
const error2 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
const error3 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
// event that will be slow to decrypt
const lateDecryption = await createFailedDecryptionEvent({ code: error1 });
// event that will be so slow to decrypt, it gets counted as undecryptable
const veryLateDecryption = await createFailedDecryptionEvent({ code: error2 });
// event that never gets decrypted
const neverDecrypted = await createFailedDecryptionEvent({ code: error3 });
tracker.addVisibleEvent(lateDecryption);
tracker.addVisibleEvent(veryLateDecryption);
tracker.addVisibleEvent(neverDecrypted);
const now = Date.now();
eventDecrypted(tracker, lateDecryption, now);
eventDecrypted(tracker, veryLateDecryption, now);
eventDecrypted(tracker, neverDecrypted, now);
await decryptExistingEvent(lateDecryption, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
await decryptExistingEvent(veryLateDecryption, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
eventDecrypted(tracker, lateDecryption, now + 40000);
eventDecrypted(tracker, veryLateDecryption, now + 100000);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(propertiesByErrorCode[error1].timeToDecryptMillis).toEqual(40000);
expect(propertiesByErrorCode[error2].timeToDecryptMillis).toEqual(-1);
expect(propertiesByErrorCode[error3].timeToDecryptMillis).toEqual(-1);
});
it("listens for client events", async () => {
// Test that the decryption failure tracker registers the right event
// handlers on start, and unregisters them when the client logs out.
const client = mockClient();
let errorCount: number = 0;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string, rawError: string, properties: ErrorProperties) => {
errorCount++;
},
(error: string) => error,
false,
);
// Calling .start will start some intervals. This test shouldn't run
// long enough for the timers to fire, but we'll use fake timers just
// to be safe.
jest.useFakeTimers();
await tracker.start(client);
// If the client fails to decrypt, it should get tracked
const failedDecryption = await createFailedDecryptionEvent();
client.emit(MatrixEventEvent.Decrypted, failedDecryption);
tracker.checkFailures(Infinity);
expect(errorCount).toEqual(1);
client.emit(HttpApiEvent.SessionLoggedOut, {} as any);
// After the client has logged out, we shouldn't be listening to events
// any more, so even if the client emits an event regarding a failed
// decryption, we won't track it.
const anotherFailedDecryption = await createFailedDecryptionEvent();
client.emit(MatrixEventEvent.Decrypted, anotherFailedDecryption);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(errorCount).toEqual(1);
jest.useRealTimers();
});
it("tracks client information", async () => {
const client = mockClient();
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string, rawError: string, properties: ErrorProperties) => {
propertiesByErrorCode[errorCode] = properties;
},
(error: string) => error,
false,
);
// @ts-ignore access to private method
await tracker.calculateClientProperties(client);
// @ts-ignore access to private method
await tracker.registerHandlers(client);
// use three different errors so that we can distinguish the reports
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
const error2 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD_FOR_UNVERIFIED_DEVICE;
const error3 = DecryptionFailureCode.MEGOLM_KEY_WITHHELD;
// event from a federated user (@alice:example.com)
const federatedDecryption = await createFailedDecryptionEvent({
code: error1,
});
// event from a local user
const localDecryption = await createFailedDecryptionEvent({
sender: "@bob:matrix.org",
code: error2,
});
tracker.addVisibleEvent(federatedDecryption);
tracker.addVisibleEvent(localDecryption);
const now = Date.now();
eventDecrypted(tracker, federatedDecryption, now);
mocked(client.getCrypto()!.getUserVerificationStatus).mockResolvedValue(
new UserVerificationStatus(true, true, false),
);
client.emit(CryptoEvent.KeysChanged, {});
await sleep(100);
eventDecrypted(tracker, localDecryption, now);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
expect(propertiesByErrorCode[error1].isMatrixDotOrg).toBe(true);
expect(propertiesByErrorCode[error1].cryptoSDK).toEqual("Rust");
expect(propertiesByErrorCode[error1].isFederated).toBe(true);
expect(propertiesByErrorCode[error1].userTrustsOwnIdentity).toEqual(false);
expect(propertiesByErrorCode[error2].isFederated).toBe(false);
expect(propertiesByErrorCode[error2].userTrustsOwnIdentity).toEqual(true);
// change client params, and make sure the reports the right values
client.getDomain.mockReturnValue("example.com");
mocked(client.getCrypto()!.getVersion).mockReturnValue("Olm 0.0.0");
// @ts-ignore access to private method
await tracker.calculateClientProperties(client);
const anotherFailure = await createFailedDecryptionEvent({
code: error3,
});
tracker.addVisibleEvent(anotherFailure);
eventDecrypted(tracker, anotherFailure, now);
tracker.checkFailures(Infinity);
expect(propertiesByErrorCode[error3].isMatrixDotOrg).toBe(false);
expect(propertiesByErrorCode[error3].cryptoSDK).toEqual("Legacy");
});
it("keeps the original timestamp after repeated decryption failures", async () => {
const failedDecryptionEvent = await createFailedDecryptionEvent();
let failure: ErrorProperties | undefined;
// @ts-ignore access to private constructor
const tracker = new DecryptionFailureTracker(
(errorCode: string, rawError: string, properties: ErrorProperties) => {
failure = properties;
},
() => "UnknownError",
false,
);
tracker.addVisibleEvent(failedDecryptionEvent);
const now = Date.now();
eventDecrypted(tracker, failedDecryptionEvent, now);
eventDecrypted(tracker, failedDecryptionEvent, now + 20000);
await decryptExistingEvent(failedDecryptionEvent, {
plainType: "m.room.message",
plainContent: { body: "success" },
});
eventDecrypted(tracker, failedDecryptionEvent, now + 50000);
// Pretend "now" is Infinity
tracker.checkFailures(Infinity);
// the time to decrypt should be relative to the first time we failed
// to decrypt, not the second
expect(failure?.timeToDecryptMillis).toEqual(50000);
});
});
function mockClient(): MockedObject<MatrixClient> {
const client = mocked(stubClient());
const mockCrypto = {
getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"),
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)),
} as unknown as Mocked<CryptoApi>;
client.getCrypto.mockReturnValue(mockCrypto);
// @ts-ignore
client.stopClient = jest.fn(() => {});
// @ts-ignore
client.removeAllListeners = jest.fn(() => {});
client.store = { destroy: jest.fn(() => {}) } as any;
return client;
}
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,7 @@ import EventEmitter from "events";
import { SimpleObservable } from "matrix-widget-api";
import { PlaybackState } from "../../../src/audio/Playback";
import { vi } from "../../setup/adapter.ts";
/**
* A mocked playback implementation for testing purposes.
@@ -51,8 +52,8 @@ export class MockedPlayback extends EventEmitter {
return this.waveformObservable;
}
public prepare = jest.fn().mockResolvedValue(undefined);
public skipTo = jest.fn();
public toggle = jest.fn();
public destroy = jest.fn().mockResolvedValue(undefined);
public prepare = vi.fn().mockResolvedValue(undefined);
public skipTo = vi.fn();
public toggle = vi.fn();
public destroy = vi.fn().mockResolvedValue(undefined);
}
@@ -1,95 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { formatList, formatCount, formatCountLong } from "../../../src/utils/FormattingUtils";
import SettingsStore from "../../../src/settings/SettingsStore";
jest.mock("../../../src/dispatcher/dispatcher");
describe("FormattingUtils", () => {
describe("formatCount", () => {
it.each([
{ count: 999, expectedCount: "999" },
{ count: 9999, expectedCount: "10K" },
{ count: 99999, expectedCount: "100K" },
{ count: 999999, expectedCount: "1M" },
{ count: 9999999, expectedCount: "10M" },
{ count: 99999999, expectedCount: "100M" },
{ count: 999999999, expectedCount: "1B" },
{ count: 9999999999, expectedCount: "10B" },
])("formats $count as $expectedCount", ({ count, expectedCount }) => {
expect(formatCount(count)).toBe(expectedCount);
});
});
describe("formatCountLong", () => {
it("formats numbers according to the locale", () => {
expect(formatCountLong(1000)).toBe("1,000");
});
});
describe("formatList", () => {
beforeEach(() => {
jest.resetAllMocks();
jest.spyOn(SettingsStore, "getValue").mockReturnValue("en-GB");
});
it("should return empty string when given empty list", () => {
expect(formatList([])).toEqual("");
});
it("should return only item when given list of length 1", () => {
expect(formatList(["abc"])).toEqual("abc");
});
it("should return expected sentence in English without item limit", () => {
expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def and ghi");
});
it("should return expected sentence in German without item limit", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue("de");
expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def und ghi");
});
it("should return expected sentence in English with item limit", () => {
expect(formatList(["abc", "def", "ghi", "jkl"], 2)).toEqual("abc, def and 2 others");
expect(formatList(["abc", "def", "ghi", "jkl"], 3)).toEqual("abc, def, ghi and one other");
});
it("should return expected sentence in English with item limit and includeCount", () => {
expect(formatList(["abc", "def", "ghi", "jkl"], 3, true)).toEqual("abc, def and 2 others");
expect(formatList(["abc", "def", "ghi", "jkl"], 4, true)).toEqual("abc, def, ghi and jkl");
});
it("should return expected sentence in ReactNode when given 2 React children", () => {
expect(formatList([<span key="a">a</span>, <span key="b">b</span>])).toMatchSnapshot();
});
it("should return expected sentence in ReactNode when given more React children", () => {
expect(
formatList([
<span key="a">a</span>,
<span key="b">b</span>,
<span key="c">c</span>,
<span key="d">d</span>,
]),
).toMatchSnapshot();
});
it("should return expected sentence in ReactNode when using itemLimit", () => {
expect(
formatList(
[<span key="a">a</span>, <span key="b">b</span>, <span key="c">c</span>, <span key="d">d</span>],
2,
),
).toMatchSnapshot();
});
});
});
@@ -1,83 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { render } from "jest-matrix-react";
import type { IContent } from "matrix-js-sdk/src/matrix";
import type React from "react";
import { editBodyDiffToHtml } from "../../../src/utils/MessageDiffUtils";
describe("editBodyDiffToHtml", () => {
function buildContent(message: string): IContent {
return {
body: message,
format: "org.matrix.custom.html",
formatted_body: message,
msgtype: "m.text",
};
}
function renderDiff(before: string, after: string) {
const node = editBodyDiffToHtml(buildContent(before), buildContent(after));
return render(node as React.ReactElement);
}
it.each([
["simple word changes", "hello", "world"],
["central word changes", "beginning middle end", "beginning :smile: end"],
["text deletions", "<b>hello</b> world", "<b>hello</b>"],
["text additions", "<b>hello</b>", "<b>hello</b> world"],
["block element additions", "hello", "hello <p>world</p>"],
["inline element additions", "hello", "hello <q>world</q>"],
["block element deletions", `hi <blockquote>there</blockquote>`, "hi"],
["inline element deletions", `hi <em>there</em>`, "hi"],
["element replacements", `hi <i>there</i>`, "hi <em>there</em>"],
["attribute modifications", `<a href="#hi">hi</a>`, `<a href="#bye">hi</a>`],
["attribute deletions", `<a href="#hi">hi</a>`, `<a>hi</a>`],
["attribute additions", `<a>hi</a>`, `<a href="#/room/!123">hi</a>`],
["handles empty tags", `<a>hi</a>`, `<a><h1></h1></a> hi`],
])("renders %s", (_label, before, after) => {
const { container } = renderDiff(before, after);
expect(container).toMatchSnapshot();
});
// see https://github.com/fiduswriter/diffDOM/issues/90
// fixed in diff-dom in 4.2.2+
it("deduplicates diff steps", () => {
const { container } = renderDiff("<div><em>foo</em> bar baz</div>", "<div><em>foo</em> bar bay</div>");
expect(container).toMatchSnapshot();
});
it("handles non-html input", () => {
const before: IContent = {
body: "who knows what's going on <strong>here</strong>",
format: "org.exotic.encoding",
formatted_body: "who knows what's going on <strong>here</strong>",
msgtype: "m.text",
};
const after: IContent = {
...before,
body: "who knows what's going on <strong>there</strong>",
formatted_body: "who knows what's going on <strong>there</strong>",
};
const { container } = render(editBodyDiffToHtml(before, after) as React.ReactElement);
expect(container).toMatchSnapshot();
});
// see https://github.com/vector-im/element-web/issues/23665
it("handles complex transformations", () => {
const { container } = renderDiff(
'<span data-mx-maths="{☃️}^\\infty"><code>{☃️}^\\infty</code></span>',
'<span data-mx-maths="{😃}^\\infty"><code>{😃}^\\infty</code></span>',
);
expect(container).toMatchSnapshot();
});
});
@@ -1,72 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`FormattingUtils formatList should return expected sentence in ReactNode when given 2 React children 1`] = `
<React.Fragment>
<React.Fragment>
<span>
a
</span>
</React.Fragment>
<React.Fragment>
and
</React.Fragment>
<React.Fragment>
<span>
b
</span>
</React.Fragment>
</React.Fragment>
`;
exports[`FormattingUtils formatList should return expected sentence in ReactNode when given more React children 1`] = `
<React.Fragment>
<React.Fragment>
<span>
a
</span>
</React.Fragment>
<React.Fragment>
,
</React.Fragment>
<React.Fragment>
<span>
b
</span>
</React.Fragment>
<React.Fragment>
,
</React.Fragment>
<React.Fragment>
<span>
c
</span>
</React.Fragment>
<React.Fragment>
and
</React.Fragment>
<React.Fragment>
<span>
d
</span>
</React.Fragment>
</React.Fragment>
`;
exports[`FormattingUtils formatList should return expected sentence in ReactNode when using itemLimit 1`] = `
<span>
<React.Fragment>
<React.Fragment>
<span>
a
</span>
,
</React.Fragment>
<React.Fragment>
<span>
b
</span>
</React.Fragment>
</React.Fragment>
and 2 others
</span>
`;
@@ -1,500 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`editBodyDiffToHtml deduplicates diff steps 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<div>
<em>
foo
</em>
<span>
bar ba
<span
class="mx_EditHistoryMessage_deletion"
>
z
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
y
</span>
</span>
</div>
</span>
</div>
`;
exports[`editBodyDiffToHtml handles complex transformations 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<span
data-mx-maths="{<span class='mx_Emoji' title=':snowman:'>☃️</span>}^\\infty"
>
<code>
{
<span
class="mx_Emoji"
title=":snowman:"
>
☃️
</span>
}^\\infty
</code>
</span>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<span
data-mx-maths="{<span class='mx_Emoji' title=':smiley:'>😃</span>}^\\infty"
>
<code>
{
<span
class="mx_Emoji"
title=":snowman:"
>
☃️
</span>
}^\\infty
</code>
</span>
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml handles non-html input 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
who knows what's going on &lt;strong&gt;
<span
class="mx_EditHistoryMessage_insertion"
>
t
</span>
here&lt;/strong&gt;
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders attribute additions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<a
rel="noreferrer noopener"
>
hi
</a>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<a
href="undefined"
rel="noreferrer noopener"
>
hi
</a>
</span>
</span>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<span
target="undefined"
>
<span
class="mx_EditHistoryMessage_deletion"
>
<a
rel="noreferrer noopener"
>
hi
</a>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<a
href="undefined"
rel="noreferrer noopener"
>
hi
</a>
</span>
</span>
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders attribute deletions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<a
href="#hi"
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<a
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
</span>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<a
href="#hi"
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<a
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
</span>
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders attribute modifications 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<a
href="#hi"
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<a
href="#bye"
rel="noreferrer noopener"
target="_blank"
>
hi
</a>
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders block element additions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
hello
<span
class="mx_EditHistoryMessage_insertion"
>
</span>
</span>
<div
class="mx_EditHistoryMessage_insertion"
>
<p>
world
</p>
</div>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders block element deletions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
hi
<span
class="mx_EditHistoryMessage_deletion"
>
</span>
</span>
<div
class="mx_EditHistoryMessage_deletion"
>
<blockquote>
there
</blockquote>
</div>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders central word changes 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
beginning
<span
class="mx_EditHistoryMessage_insertion"
>
:s
</span>
mi
<span
class="mx_EditHistoryMessage_deletion"
>
dd
</span>
le
<span
class="mx_EditHistoryMessage_insertion"
>
:
</span>
end
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders element replacements 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
hi
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
<i>
there
</i>
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
<em>
there
</em>
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders handles empty tags 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<a
rel="noreferrer noopener"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
hi
</span>
<div
class="mx_EditHistoryMessage_insertion"
>
<h1 />
</div>
</span>
</a>
<span
class="mx_EditHistoryMessage_insertion"
>
hi
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders inline element additions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
hello
<span
class="mx_EditHistoryMessage_insertion"
>
world
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders inline element deletions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
hi
<span
class="mx_EditHistoryMessage_deletion"
>
</span>
</span>
<span
class="mx_EditHistoryMessage_deletion"
>
<em>
there
</em>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders simple word changes 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<span>
<span
class="mx_EditHistoryMessage_deletion"
>
hello
</span>
<span
class="mx_EditHistoryMessage_insertion"
>
world
</span>
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders text additions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<b>
hello
</b>
<span
class="mx_EditHistoryMessage_insertion"
>
world
</span>
</span>
</div>
`;
exports[`editBodyDiffToHtml renders text deletions 1`] = `
<div>
<span
class="mx_EventTile_body markdown-body"
dir="auto"
>
<b>
hello
</b>
<span
class="mx_EditHistoryMessage_deletion"
>
world
</span>
</span>
</div>
`;
@@ -1,32 +0,0 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`createVoiceMessageContent should create a voice message content 1`] = `
{
"body": "Voice message",
"file": {},
"info": {
"duration": 23000,
"mimetype": "ogg/opus",
"size": 42000,
},
"msgtype": "m.audio",
"org.matrix.msc1767.audio": {
"duration": 23000,
"waveform": [
1,
2,
3,
],
},
"org.matrix.msc1767.file": {
"file": {},
"mimetype": "ogg/opus",
"name": "Voice message.ogg",
"size": 42000,
"url": "mxc://example.com/file",
},
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc3245.voice": {},
"url": "mxc://example.com/file",
}
`;
@@ -1,26 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type EncryptedFile } from "matrix-js-sdk/src/types";
import { createVoiceMessageContent } from "../../../src/utils/createVoiceMessageContent";
describe("createVoiceMessageContent", () => {
it("should create a voice message content", () => {
expect(
createVoiceMessageContent(
"mxc://example.com/file",
"ogg/opus",
23000,
42000,
{} as unknown as EncryptedFile,
[1, 2, 3],
),
).toMatchSnapshot();
});
});
@@ -1,22 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { PermalinkParts } from "../../../../src/utils/permalinks/PermalinkConstructor";
import MatrixSchemePermalinkConstructor from "../../../../src/utils/permalinks/MatrixSchemePermalinkConstructor";
describe("MatrixSchemePermalinkConstructor", () => {
const peramlinkConstructor = new MatrixSchemePermalinkConstructor();
describe("parsePermalink", () => {
it("should strip ?action=chat from user links", () => {
expect(peramlinkConstructor.parsePermalink("matrix:u/user:example.com?action=chat")).toEqual(
new PermalinkParts(null, null, "@user:example.com", null),
);
});
});
});
@@ -1,52 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import MatrixToPermalinkConstructor from "../../../../src/utils/permalinks/MatrixToPermalinkConstructor";
import { PermalinkParts } from "../../../../src/utils/permalinks/PermalinkConstructor";
describe("MatrixToPermalinkConstructor", () => {
const peramlinkConstructor = new MatrixToPermalinkConstructor();
describe("parsePermalink", () => {
it.each([
["empty URL", ""],
["something that is not an URL", "hello"],
["should raise an error for a non-matrix.to URL", "https://example.com/#/@user:example.com"],
])("should raise an error for %s", (name: string, url: string) => {
expect(() => peramlinkConstructor.parsePermalink(url)).toThrow(
new Error("Does not appear to be a permalink"),
);
});
it.each([
["(https)", "https://matrix.to/#/@user:example.com"],
["(http)", "http://matrix.to/#/@user:example.com"],
["without protocol", "matrix.to/#/@user:example.com"],
])("should parse an MXID %s", (name: string, url: string) => {
expect(peramlinkConstructor.parsePermalink(url)).toEqual(
new PermalinkParts(null, null, "@user:example.com", null),
);
});
});
describe("forRoom", () => {
it("constructs a link given a room ID and via servers", () => {
expect(peramlinkConstructor.forRoom("!myroom:example.com", ["one.example.com", "two.example.com"])).toEqual(
"https://matrix.to/#/!myroom:example.com?via=one.example.com&via=two.example.com",
);
});
});
describe("forEvent", () => {
it("constructs a link given an event ID, room ID and via servers", () => {
expect(
peramlinkConstructor.forEvent("!myroom:example.com", "$event4", ["one.example.com", "two.example.com"]),
).toEqual("https://matrix.to/#/!myroom:example.com/$event4?via=one.example.com&via=two.example.com");
});
});
});
@@ -1,462 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type EventEmitter } from "events";
import { Room, RoomMember, EventType, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { PermalinkParts } from "../../../../src/utils/permalinks/PermalinkConstructor";
import {
makeRoomPermalink,
makeUserPermalink,
parsePermalink,
RoomPermalinkCreator,
} from "../../../../src/utils/permalinks/Permalinks";
import { type IConfigOptions } from "../../../../src/IConfigOptions";
import SdkConfig from "../../../../src/SdkConfig";
import { getMockClientWithEventEmitter } from "../../../test-utils";
describe("Permalinks", function () {
const userId = "@test:example.com";
const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(userId),
getRoom: jest.fn(),
});
mockClient.credentials = { userId };
const makeMemberWithPL = (roomId: Room["roomId"], userId: string, powerLevel: number): RoomMember => {
const member = new RoomMember(roomId, userId);
member.powerLevel = powerLevel;
return member;
};
function mockRoom(
roomId: Room["roomId"],
members: RoomMember[],
serverACLContent?: { deny?: string[]; allow?: string[] },
): Room {
members.forEach((m) => (m.membership = KnownMembership.Join));
const powerLevelsUsers = members.reduce<Record<string, number>>((pl, member) => {
if (Number.isFinite(member.powerLevel)) {
pl[member.userId] = member.powerLevel;
}
return pl;
}, {});
const room = new Room(roomId, mockClient, userId);
const powerLevels = new MatrixEvent({
type: EventType.RoomPowerLevels,
room_id: roomId,
state_key: "",
content: {
users: powerLevelsUsers,
users_default: 0,
},
});
const serverACL = serverACLContent
? new MatrixEvent({
type: EventType.RoomServerAcl,
room_id: roomId,
state_key: "",
content: serverACLContent,
})
: undefined;
const stateEvents = serverACL ? [powerLevels, serverACL] : [powerLevels];
room.currentState.setStateEvents(stateEvents);
jest.spyOn(room, "getCanonicalAlias").mockReturnValue(null);
jest.spyOn(room, "getJoinedMembers").mockReturnValue(members);
jest.spyOn(room, "getMember").mockImplementation((userId) => members.find((m) => m.userId === userId) || null);
return room;
}
beforeEach(function () {
jest.clearAllMocks();
});
afterAll(() => {
jest.spyOn(MatrixClientPeg, "get").mockRestore();
});
it("should not clean up listeners even if start was called multiple times", () => {
const room = mockRoom("!fake:example.org", []);
const getListenerCount = (emitter: EventEmitter) =>
emitter
.eventNames()
.map((e) => emitter.listenerCount(e))
.reduce((a, b) => a + b, 0);
const listenerCountBefore = getListenerCount(room.currentState);
const creator = new RoomPermalinkCreator(room);
creator.start();
creator.start();
creator.start();
creator.start();
expect(getListenerCount(room.currentState)).toBeGreaterThan(listenerCountBefore);
creator.stop();
expect(getListenerCount(room.currentState)).toBe(listenerCountBefore);
});
it("should pick no candidate servers when the room has no members", function () {
const room = mockRoom("!fake:example.org", []);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should gracefully handle invalid MXIDs", () => {
const roomId = "!fake:example.org";
const alice50 = makeMemberWithPL(roomId, "@alice:pl-50:org", 50);
const room = mockRoom(roomId, [alice50]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
});
it("should pick a candidate server for the highest power level user in the room", function () {
const roomId = "!fake:example.org";
const alice50 = makeMemberWithPL(roomId, "@alice:pl_50", 50);
const alice75 = makeMemberWithPL(roomId, "@alice:pl_75", 75);
const alice95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
const room = mockRoom("!fake:example.org", [alice50, alice75, alice95]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(3);
expect(creator.serverCandidates![0]).toBe("pl_95");
// we don't check the 2nd and 3rd servers because that is done by the next test
});
it("should change candidate server when highest power level user leaves the room", function () {
const roomId = "!fake:example.org";
const member95 = makeMemberWithPL(roomId, "@alice:pl_95", 95);
const room = mockRoom(roomId, [
makeMemberWithPL(roomId, "@alice:pl_50", 50),
makeMemberWithPL(roomId, "@alice:pl_75", 75),
member95,
]);
const creator = new RoomPermalinkCreator(room, null);
creator.load();
expect(creator.serverCandidates![0]).toBe("pl_95");
member95.membership = KnownMembership.Leave;
// @ts-ignore illegal private property
creator.onRoomStateUpdate();
expect(creator.serverCandidates![0]).toBe("pl_75");
member95.membership = KnownMembership.Join;
// @ts-ignore illegal private property
creator.onRoomStateUpdate();
expect(creator.serverCandidates![0]).toBe("pl_95");
});
it("should pick candidate servers based on user population", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [
makeMemberWithPL(roomId, "@alice:first", 0),
makeMemberWithPL(roomId, "@bob:first", 0),
makeMemberWithPL(roomId, "@charlie:first", 0),
makeMemberWithPL(roomId, "@alice:second", 0),
makeMemberWithPL(roomId, "@bob:second", 0),
makeMemberWithPL(roomId, "@charlie:third", 0),
]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(3);
expect(creator.serverCandidates![0]).toBe("first");
expect(creator.serverCandidates![1]).toBe("second");
expect(creator.serverCandidates![2]).toBe("third");
});
it("should pick prefer candidate servers with higher power levels", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [
makeMemberWithPL(roomId, "@alice:first", 100),
makeMemberWithPL(roomId, "@alice:second", 0),
makeMemberWithPL(roomId, "@bob:second", 0),
makeMemberWithPL(roomId, "@charlie:third", 0),
]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates!.length).toBe(3);
expect(creator.serverCandidates![0]).toBe("first");
expect(creator.serverCandidates![1]).toBe("second");
expect(creator.serverCandidates![2]).toBe("third");
});
it("should pick a maximum of 3 candidate servers", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [
makeMemberWithPL(roomId, "@alice:alpha", 100),
makeMemberWithPL(roomId, "@alice:bravo", 0),
makeMemberWithPL(roomId, "@alice:charlie", 0),
makeMemberWithPL(roomId, "@alice:delta", 0),
makeMemberWithPL(roomId, "@alice:echo", 0),
]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(3);
});
it("should not consider IPv4 hosts", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:127.0.0.1", 100)]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should not consider IPv6 hosts", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:[::1]", 100)]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should not consider IPv4 hostnames with ports", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:127.0.0.1:8448", 100)]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should not consider IPv6 hostnames with ports", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:[::1]:8448", 100)]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should work with hostnames with ports", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:example.org:8448", 100)]);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(1);
expect(creator.serverCandidates![0]).toBe("example.org:8448");
});
it("should not consider servers explicitly denied by ACLs", function () {
const roomId = "!fake:example.org";
const room = mockRoom(
roomId,
[
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
],
{
deny: ["evilcorp.com", "*.evilcorp.com"],
allow: ["*"],
},
);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should not consider servers not allowed by ACLs", function () {
const roomId = "!fake:example.org";
const room = mockRoom(
roomId,
[
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
],
{
deny: [],
allow: [], // implies "ban everyone"
},
);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should consider servers not explicitly banned by ACLs", function () {
const roomId = "!fake:example.org";
const room = mockRoom(
roomId,
[
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
],
{
deny: ["*.evilcorp.com"], // evilcorp.com is still good though
allow: ["*"],
},
);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(1);
expect(creator.serverCandidates![0]).toEqual("evilcorp.com");
});
it("should consider servers not disallowed by ACLs", function () {
const roomId = "!fake:example.org";
const room = mockRoom(
"!fake:example.org",
[
makeMemberWithPL(roomId, "@alice:evilcorp.com", 100),
makeMemberWithPL(roomId, "@bob:chat.evilcorp.com", 0),
],
{
deny: [],
allow: ["evilcorp.com"], // implies "ban everyone else"
},
);
const creator = new RoomPermalinkCreator(room);
creator.load();
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(1);
expect(creator.serverCandidates![0]).toEqual("evilcorp.com");
});
it("should handle when ACL allow is not an array", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:goodcorp.com", 100)], {
deny: ["*.evilcorp.com"],
allow: "not-an-array" as any, // Test malformed data
});
const creator = new RoomPermalinkCreator(room);
creator.load();
// Should fall back to default behavior (no allowed servers list = allow none)
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(0);
});
it("should handle when ACL deny is not an array", function () {
const roomId = "!fake:example.org";
const room = mockRoom(roomId, [makeMemberWithPL(roomId, "@alice:goodcorp.com", 100)], {
deny: "not-an-array" as any, // Test malformed data
allow: ["*"],
});
const creator = new RoomPermalinkCreator(room);
creator.load();
// Should not crash and still allow the server
expect(creator.serverCandidates).toBeTruthy();
expect(creator.serverCandidates!.length).toBe(1);
expect(creator.serverCandidates![0]).toEqual("goodcorp.com");
});
it("should generate an event permalink for room IDs with no candidate servers", function () {
const room = mockRoom("!somewhere:example.org", []);
const creator = new RoomPermalinkCreator(room);
creator.load();
const result = creator.forEvent("$something:example.com");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com");
});
it("should generate an event permalink for room IDs with some candidate servers", function () {
const roomId = "!somewhere:example.org";
const room = mockRoom(roomId, [
makeMemberWithPL(roomId, "@alice:first", 100),
makeMemberWithPL(roomId, "@bob:second", 0),
]);
const creator = new RoomPermalinkCreator(room);
creator.load();
const result = creator.forEvent("$something:example.com");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org/$something:example.com?via=first&via=second");
});
it("should generate a room permalink for room IDs with some candidate servers", function () {
mockClient.getRoom.mockImplementation((roomId?: string) => {
return mockRoom(roomId!, [
makeMemberWithPL(roomId!, "@alice:first", 100),
makeMemberWithPL(roomId!, "@bob:second", 0),
]);
});
const result = makeRoomPermalink(mockClient, "!somewhere:example.org");
expect(result).toBe("https://matrix.to/#/!somewhere:example.org?via=first&via=second");
});
it("should generate a room permalink for room aliases with no candidate servers", function () {
mockClient.getRoom.mockReturnValue(null);
const result = makeRoomPermalink(mockClient, "#somewhere:example.org");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
});
it("should generate a room permalink for room aliases without candidate servers", function () {
mockClient.getRoom.mockImplementation((roomId?: string) => {
return mockRoom(roomId!, [
makeMemberWithPL(roomId!, "@alice:first", 100),
makeMemberWithPL(roomId!, "@bob:second", 0),
]);
});
const result = makeRoomPermalink(mockClient, "#somewhere:example.org");
expect(result).toBe("https://matrix.to/#/#somewhere:example.org");
});
it("should generate a user permalink", function () {
const result = makeUserPermalink("@someone:example.org");
expect(result).toBe("https://matrix.to/#/@someone:example.org");
});
it("should use permalink_prefix for permalinks", function () {
const sdkConfigGet = SdkConfig.get;
jest.spyOn(SdkConfig, "get").mockImplementation((key: keyof IConfigOptions, altCaseName?: string) => {
if (key === "permalink_prefix") {
return "https://element.fs.tld";
} else return sdkConfigGet(key, altCaseName);
});
const result = makeUserPermalink("@someone:example.org");
expect(result).toBe("https://element.fs.tld/#/user/@someone:example.org");
});
describe("parsePermalink", () => {
it("should correctly parse room permalinks with a via argument", () => {
const result = parsePermalink("https://matrix.to/#/!room_id:server?via=some.org");
expect(result?.roomIdOrAlias).toBe("!room_id:server");
expect(result?.viaServers).toEqual(["some.org"]);
});
it("should correctly parse room permalink via arguments", () => {
const result = parsePermalink("https://matrix.to/#/!room_id:server?via=foo.bar&via=bar.foo");
expect(result?.roomIdOrAlias).toBe("!room_id:server");
expect(result?.viaServers).toEqual(["foo.bar", "bar.foo"]);
});
it("should correctly parse event permalink via arguments", () => {
const result = parsePermalink(
"https://matrix.to/#/!room_id:server/$event_id/some_thing_here/foobar" + "?via=m1.org&via=m2.org",
);
expect(result?.eventId).toBe("$event_id/some_thing_here/foobar");
expect(result?.roomIdOrAlias).toBe("!room_id:server");
expect(result?.viaServers).toEqual(["m1.org", "m2.org"]);
});
it("should correctly parse permalinks with http protocol", () => {
expect(parsePermalink("http://matrix.to/#/@user:example.com")).toEqual(
new PermalinkParts(null, null, "@user:example.com", null),
);
});
it("should correctly parse permalinks without protocol", () => {
expect(parsePermalink("matrix.to/#/@user:example.com")).toEqual(
new PermalinkParts(null, null, "@user:example.com", null),
);
});
});
});
@@ -1,148 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type Mocked } from "jest-mock";
import { type IIdentityServerProvider, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { DirectoryMember, ThreepidMember } from "../../../src/utils/direct-messages";
import { lookupThreePids, resolveThreePids } from "../../../src/utils/threepids";
import { stubClient } from "../../test-utils";
describe("threepids", () => {
let client: Mocked<MatrixClient>;
const accessToken = "s3cr3t";
let identityServer: Mocked<IIdentityServerProvider>;
beforeEach(() => {
client = stubClient() as Mocked<MatrixClient>;
identityServer = {
getAccessToken: jest.fn().mockResolvedValue(accessToken),
} as unknown as Mocked<IIdentityServerProvider>;
});
describe("resolveThreePids", () => {
const userId = "@user1:example.com";
const directoryMember = new DirectoryMember({
user_id: userId,
});
const threePid1Id = "three1@example.com";
const threePid1MXID = "@three1:example.com";
const threePid1Member = new ThreepidMember(threePid1Id);
const threePid1Displayname = "Three Pid 1";
const threePid2Id = "three2@example.com";
const threePid2MXID = "@three2:example.com";
const threePid2Member = new ThreepidMember(threePid2Id);
const threePid3Id = "three3@example.com";
const threePid3Member = new ThreepidMember(threePid3Id);
const threePidPhoneId = "8801500121121";
const threePidPhoneMember = new ThreepidMember(threePidPhoneId);
it("should return an empty list for an empty input", async () => {
expect(await resolveThreePids([], client)).toEqual([]);
});
it("should return the same list for non-3rd-party members", async () => {
expect(await resolveThreePids([directoryMember], client)).toEqual([directoryMember]);
});
it("should return the same list for if no identity server is configured", async () => {
expect(await resolveThreePids([directoryMember, threePid1Member], client)).toEqual([
directoryMember,
threePid1Member,
]);
});
describe("when an identity server is configured", () => {
beforeEach(() => {
client.identityServer = identityServer;
});
it("should return the same list if the lookup doesn't return any results", async () => {
expect(
await resolveThreePids(
[directoryMember, threePid1Member, threePid2Member, threePidPhoneMember],
client,
),
).toEqual([directoryMember, threePid1Member, threePid2Member, threePidPhoneMember]);
expect(client.bulkLookupThreePids).toHaveBeenCalledWith(
[
["email", threePid1Id],
["email", threePid2Id],
["msisdn", threePidPhoneId],
],
accessToken,
);
});
describe("and some 3-rd party members can be resolved", () => {
beforeEach(() => {
client.bulkLookupThreePids.mockResolvedValue({
threepids: [
["email", threePid1Id, threePid1MXID],
["email", threePid2Id, threePid2MXID],
],
});
});
it("should return the resolved members", async () => {
expect(
await resolveThreePids(
[directoryMember, threePid1Member, threePid2Member, threePid3Member],
client,
),
).toEqual([
directoryMember,
new DirectoryMember({ user_id: threePid1MXID }),
new DirectoryMember({ user_id: threePid2MXID }),
threePid3Member,
]);
expect(client.bulkLookupThreePids).toHaveBeenCalledWith(
[
["email", threePid1Id],
["email", threePid2Id],
["email", threePid3Id],
],
accessToken,
);
});
describe("and some 3rd-party members have a profile", () => {
beforeEach(() => {
client.getProfileInfo.mockImplementation((matrixId: string) => {
if (matrixId === threePid1MXID)
return Promise.resolve({ displayname: threePid1Displayname });
throw new Error("Profile not found");
});
});
it("should resolve the profiles", async () => {
expect(
await resolveThreePids(
[directoryMember, threePid1Member, threePid2Member, threePid3Member],
client,
),
).toEqual([
directoryMember,
new DirectoryMember({ user_id: threePid1MXID, display_name: threePid1Displayname }),
new DirectoryMember({ user_id: threePid2MXID }),
threePid3Member,
]);
});
});
});
});
});
describe("lookupThreePids", () => {
it("should return an empty list for an empty list", async () => {
client.identityServer = identityServer;
expect(await lookupThreePids([], client)).toEqual([]);
});
});
});
@@ -1,153 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import {
clearUserStatus,
fetchUserStatus,
setUserStatus,
userStatusFromProfile,
userStatusTextWithinMaxLength,
} from "../../../src/utils/userStatus";
import { stubClient } from "../../test-utils";
describe("userStatus utils", () => {
describe("userStatusFromProfile", () => {
it("returns the user status if it is valid", () => {
expect(userStatusFromProfile({ emoji: "🐳", text: "Feeling a little blue" }, undefined)).toEqual({
emoji: "🐳",
text: "Feeling a little blue",
});
});
it("returns undefined if the user status is invalid and there is no call status", () => {
expect(userStatusFromProfile({ text: "Feeling a little blue" }, undefined)).toBeUndefined();
});
it("returns the call status if the user status is invalid but the call status is valid", () => {
expect(userStatusFromProfile({ text: "Feeling a little blue" }, { call_joined_ts: 12345 })).toEqual({
emoji: "📞",
text: "On a call",
});
});
it("prefers the user status over the call status if both are valid", () => {
expect(
userStatusFromProfile({ emoji: "🐳", text: "Feeling a little blue" }, { call_joined_ts: 12345 }),
).toEqual({
emoji: "🐳",
text: "Feeling a little blue",
});
});
it("returns undefined if the call status has a non-positive call_joined_ts", () => {
expect(userStatusFromProfile(undefined, { call_joined_ts: 0 })).toBeUndefined();
});
it("returns undefined if neither status is valid", () => {
expect(userStatusFromProfile(undefined, undefined)).toBeUndefined();
});
});
describe("userStatusTextWithinMaxLength", () => {
it("returns true for text within the max length", () => {
const text = "a".repeat(256);
expect(userStatusTextWithinMaxLength(text)).toBe(true);
});
it("returns false for text exceeding the max length", () => {
const text = "a".repeat(257);
expect(userStatusTextWithinMaxLength(text)).toBe(false);
});
});
describe("setUserStatus", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
});
it("sets the user status with valid input", async () => {
setUserStatus(client, { emoji: "🐳", text: "Feeling a little blue" });
expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", {
emoji: "🐳",
text: "Feeling a little blue",
});
});
});
describe("fetchUserStatus", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
client.doesServerSupportExtendedProfiles = jest.fn();
});
it("returns undefined if the server does not support extended profiles", async () => {
mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(false);
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
expect(client.getExtendedProfileProperty).not.toHaveBeenCalled();
});
it("returns the validated status if the server supports extended profiles and has a status set", async () => {
mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
mocked(client.getExtendedProfileProperty).mockResolvedValue({ emoji: "🐳", text: "Feeling a little blue" });
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toEqual({
emoji: "🐳",
text: "Feeling a little blue",
});
expect(client.getExtendedProfileProperty).toHaveBeenCalledWith(
"@alice:example.com",
"org.matrix.msc4426.status",
);
});
it("returns undefined if the status is invalid", async () => {
mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
mocked(client.getExtendedProfileProperty).mockResolvedValue({ text: "Feeling a little blue" });
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
});
it("returns undefined if the user has no status set", async () => {
mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
mocked(client.getExtendedProfileProperty).mockRejectedValue(
new MatrixError({ errcode: "M_NOT_FOUND" }, 404),
);
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
});
it("returns undefined and logs a warning if fetching the status fails unexpectedly", async () => {
mocked(client.doesServerSupportExtendedProfiles).mockResolvedValue(true);
const error = new Error("network error");
mocked(client.getExtendedProfileProperty).mockRejectedValue(error);
await expect(fetchUserStatus(client, "@alice:example.com")).resolves.toBeUndefined();
});
});
describe("clearUserStatus", () => {
let client: MatrixClient;
beforeEach(() => {
client = stubClient();
});
it("clears the user status", async () => {
clearUserStatus(client);
expect(client.setExtendedProfileProperty).toHaveBeenCalledWith("org.matrix.msc4426.status", null);
});
});
});
@@ -1,65 +0,0 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import fetchMock from "@fetch-mock/jest";
import type { Mocked } from "jest-mock";
import type { ConsoleLogger } from "../../../src/rageshake/rageshake";
import SdkConfig from "../../../src/SdkConfig";
import "../../../src/vector/rageshakesetup";
import { BugReportEndpointURLLocal } from "../../../src/IConfigOptions";
const RAGESHAKE_URL = "https://logs.example.org/logtome";
describe("mxSendRageshake", () => {
let prevLogger: ConsoleLogger;
beforeEach(() => {
fetchMock.mockGlobal();
SdkConfig.put({ bug_report_endpoint_url: RAGESHAKE_URL });
fetchMock.postOnce(RAGESHAKE_URL, { status: 200, body: {} });
const mockConsoleLogger = {
flush: jest.fn(),
consume: jest.fn(),
warn: jest.fn(),
} as unknown as Mocked<ConsoleLogger>;
prevLogger = global.mx_rage_logger;
mockConsoleLogger.flush.mockReturnValue("line 1\nline 2\n");
global.mx_rage_logger = mockConsoleLogger;
});
afterEach(() => {
global.mx_rage_logger = prevLogger;
jest.restoreAllMocks();
fetchMock.unmockGlobal();
SdkConfig.reset();
});
it("Does not send a rageshake if the URL is not configured", async () => {
SdkConfig.put({ bug_report_endpoint_url: undefined });
await window.mxSendRageshake("test");
expect(fetchMock).not.toHaveFetched();
});
it.each(["", " ", undefined, null])("Does not send a rageshake if text is '%s'", async (text) => {
await window.mxSendRageshake(text as string);
expect(fetchMock).not.toHaveFetched();
});
it("Sends a rageshake via URL", async () => {
await window.mxSendRageshake("Hello world");
expect(fetchMock).toHaveFetched(RAGESHAKE_URL);
});
it("Provides a rageshake locally", async () => {
SdkConfig.put({ bug_report_endpoint_url: BugReportEndpointURLLocal });
const urlSpy = jest.spyOn(URL, "createObjectURL");
await window.mxSendRageshake("Hello world");
expect(fetchMock).not.toHaveFetched(RAGESHAKE_URL);
expect(urlSpy).toHaveBeenCalledTimes(1);
});
});