Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

953 lines
35 KiB
TypeScript
Raw Permalink Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
*/
import EventEmitter from "events";
2025-02-05 13:25:06 +00:00
import { mocked, type MockedObject } from "jest-mock";
2022-02-23 12:21:11 +01:00
import {
MatrixEvent,
2025-02-05 13:25:06 +00:00
type Room,
type User,
type IContent,
type IEvent,
type RoomMember,
type MatrixClient,
type EventTimeline,
2022-02-23 12:21:11 +01:00
EventType,
2025-02-05 13:25:06 +00:00
type IEventRelation,
type IUnsigned,
type IPusher,
RoomType,
KNOWN_SAFE_ROOM_VERSION,
ConditionKind,
2025-02-05 13:25:06 +00:00
type IPushRules,
RelationType,
JoinRule,
2025-02-05 13:25:06 +00:00
type OidcClientConfig,
type GroupCall,
2026-01-12 21:13:15 +00:00
type EventStatus,
type ICreateRoomOpts,
RoomState,
HistoryVisibility,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
2022-07-20 09:26:25 +02:00
import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
2025-02-05 13:25:06 +00:00
import { type MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
2022-10-13 18:22:25 +01:00
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
2025-02-05 13:25:06 +00:00
import { type MapperOpts } from "matrix-js-sdk/src/event-mapper";
import { type MatrixRTCSessionManager, type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
2021-10-22 17:23:32 -05:00
import type { Membership } from "matrix-js-sdk/src/types";
2022-02-23 12:21:11 +01:00
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
2025-02-05 13:25:06 +00:00
import { type ValidatedServerConfig } from "../../src/utils/ValidatedServerConfig";
2022-02-23 12:21:11 +01:00
import { EnhancedMap } from "../../src/utils/maps";
2025-02-05 13:25:06 +00:00
import { type AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler";
2017-03-16 17:26:42 +00:00
2016-03-28 22:59:34 +01:00
/**
* Stub out the MatrixClient, and configure the MatrixClientPeg object to
* return it when get() is called.
*
* TODO: once the components are updated to get their MatrixClients from
* the react context, we can get rid of this and just inject a test client
* via the context instead.
*
* See also {@link getMockClientWithEventEmitter} which does something similar but different.
2016-03-28 22:59:34 +01:00
*/
2022-09-21 18:46:28 +02:00
export function stubClient(): MatrixClient {
2017-10-11 17:56:17 +01:00
const client = createTestClient();
// stub out the methods in MatrixClientPeg
//
// 'sandbox.restore()' doesn't work correctly on inherited methods,
// so we do this for each method
2022-03-09 14:23:58 +01:00
jest.spyOn(peg, "get");
jest.spyOn(peg, "safeGet");
2022-03-09 14:23:58 +01:00
jest.spyOn(peg, "unset");
jest.spyOn(peg, "replaceUsingCreds");
// MatrixClientPeg.safeGet() is called a /lot/, so implement it with our own
// fast stub function rather than a sinon stub
peg.get = () => client;
peg.safeGet = () => client;
MatrixClientBackedSettingsHandler.matrixClient = client;
2022-09-21 18:46:28 +02:00
return client;
}
/**
* Create a stubbed-out MatrixClient
*
* @returns {object} MatrixClient stub
*/
2022-03-09 14:23:58 +01:00
export function createTestClient(): MatrixClient {
const eventEmitter = new EventEmitter();
2023-10-30 16:14:27 +01:00
2022-07-13 07:56:36 +02:00
let txnId = 1;
let createdRoom: Room | undefined;
const client = {
2019-12-16 11:12:48 +00:00
getHomeserverUrl: jest.fn(),
getIdentityServerUrl: jest.fn(),
2022-07-13 07:56:36 +02:00
getDomain: jest.fn().mockReturnValue("matrix.org"),
getUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
2022-12-16 12:01:16 +01:00
getSafeUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
2022-11-22 07:58:37 +01:00
getUserIdLocalpart: jest.fn().mockResolvedValue("userId"),
2024-10-10 15:08:43 +01:00
getUser: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn() }),
getDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
2022-11-08 10:02:07 +01:00
deviceId: "ABCDEFGHI",
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
2022-07-13 07:56:36 +02:00
credentials: { userId: "@userId:matrix.org" },
getAccessToken: jest.fn(),
secretStorage: {
get: jest.fn(),
isStored: jest.fn().mockReturnValue(false),
checkKey: jest.fn().mockResolvedValue(false),
hasKey: jest.fn().mockReturnValue(false),
getDefaultKeyId: jest.fn().mockResolvedValue(null),
},
2022-06-14 21:29:24 +01:00
store: {
getPendingEvents: jest.fn().mockResolvedValue([]),
setPendingEvents: jest.fn().mockResolvedValue(undefined),
2022-07-13 07:56:36 +02:00
storeRoom: jest.fn(),
removeRoom: jest.fn(),
2022-06-14 21:29:24 +01:00
},
getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn().mockResolvedValue({ ed25519: "ed25519", curve25519: "curve25519" }),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
isStateEncryptionEnabledInRoom: jest.fn().mockResolvedValue(false),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
setDeviceIsolationMode: jest.fn(),
prepareToEncrypt: jest.fn(),
2024-10-22 12:42:07 +01:00
bootstrapCrossSigning: jest.fn(),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
isKeyBackupTrusted: jest.fn().mockResolvedValue({}),
createRecoveryKeyFromPassphrase: jest.fn().mockResolvedValue({
privateKey: new Uint8Array(32),
encodedPrivateKey: "encoded private key",
}),
bootstrapSecretStorage: jest.fn(),
isDehydrationSupported: jest.fn().mockResolvedValue(false),
restoreKeyBackup: jest.fn(),
restoreKeyBackupWithPassphrase: jest.fn(),
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
storeSessionBackupPrivateKey: jest.fn(),
2024-12-17 14:50:48 +00:00
checkKeyBackupAndEnable: jest.fn().mockResolvedValue(null),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
getCrossSigningStatus: jest.fn().mockResolvedValue({
publicKeysOnDevice: false,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: {
masterKey: false,
selfSigningKey: false,
userSigningKey: false,
},
}),
isCrossSigningReady: jest.fn().mockResolvedValue(false),
disableKeyStorage: jest.fn(),
resetEncryption: jest.fn(),
getSessionBackupPrivateKey: jest.fn().mockResolvedValue(null),
isSecretStorageReady: jest.fn().mockResolvedValue(false),
deleteKeyBackupVersion: jest.fn(),
crossSignDevice: jest.fn(),
}),
2019-12-16 11:12:48 +00:00
getPushActionsForEvent: jest.fn(),
getRoom: jest.fn().mockImplementation((roomId) => {
// If the test called `createRoom`, return the mocked room it created.
if (createdRoom) {
return createdRoom;
} else {
return mkStubRoom(roomId, "My room", client);
}
}),
2019-12-16 11:12:48 +00:00
getRooms: jest.fn().mockReturnValue([]),
getVisibleRooms: jest.fn().mockReturnValue([]),
loginFlows: jest.fn(),
on: eventEmitter.on.bind(eventEmitter),
once: eventEmitter.once.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
2019-12-16 11:12:48 +00:00
isRoomEncrypted: jest.fn().mockReturnValue(false),
2022-03-09 14:23:58 +01:00
peekInRoom: jest.fn().mockResolvedValue(mkStubRoom(undefined, undefined, undefined)),
stopPeeking: jest.fn(),
getEventTimeline: jest.fn().mockResolvedValue([]),
2019-12-16 11:12:48 +00:00
paginateEventTimeline: jest.fn().mockResolvedValue(undefined),
sendReadReceipt: jest.fn().mockResolvedValue(undefined),
getRoomIdForAlias: jest.fn().mockResolvedValue(undefined),
getRoomDirectoryVisibility: jest.fn().mockResolvedValue(undefined),
getProfileInfo: jest.fn().mockResolvedValue({}),
2021-04-23 14:39:39 +01:00
getThirdpartyProtocols: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue(null),
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
2021-04-23 14:39:39 +01:00
supportsVoip: jest.fn().mockReturnValue(true),
getTurnServers: jest.fn().mockReturnValue([]),
2022-03-09 14:23:58 +01:00
getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32),
2021-04-23 14:39:39 +01:00
getThirdpartyUser: jest.fn().mockResolvedValue([]),
getAccountData: jest.fn().mockImplementation((type) => {
2016-09-09 18:07:42 +05:30
return mkEvent({
user: "@user:example.com",
2022-03-09 14:23:58 +01:00
room: undefined,
2016-09-09 18:07:42 +05:30
type,
event: true,
content: {},
});
}),
getAccountDataFromServer: jest.fn(),
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
2019-12-16 11:12:48 +00:00
setAccountData: jest.fn(),
deleteAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
setRoomName: jest.fn(),
setRoomTopic: jest.fn(),
setRoomReadMarkers: jest.fn().mockResolvedValue({}),
2019-12-16 11:12:48 +00:00
sendTyping: jest.fn().mockResolvedValue({}),
2022-07-13 07:56:36 +02:00
sendMessage: jest.fn().mockResolvedValue({}),
2022-03-09 14:23:58 +01:00
sendStateEvent: jest.fn().mockResolvedValue(undefined),
sendRtcDecline: jest.fn().mockResolvedValue(undefined),
getSyncState: jest.fn().mockReturnValue("SYNCING"),
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: jest.fn().mockReturnValue(false),
getRoomHierarchy: jest.fn().mockReturnValue({
2021-04-23 14:45:22 +01:00
rooms: [],
}),
createRoom: jest.fn(async (createOpts?: ICreateRoomOpts) => {
const initialState = createOpts?.initial_state?.map((event, i) =>
mkEvent({
...event,
room: "!1:example.org",
user: "@user:example.com",
event: true,
}),
);
createdRoom = mkStubRoom(
"!1:example.org",
"My room",
client,
initialState && mkRoomState("!1:example.org", initialState),
);
return { room_id: "!1:example.org" };
}),
setPowerLevel: jest.fn().mockResolvedValue(undefined),
2021-08-03 15:43:56 +02:00
pushRules: {},
2021-05-18 13:46:47 +01:00
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
2021-07-06 10:34:50 +01:00
getCapabilities: jest.fn().mockResolvedValue({}),
supportsThreads: jest.fn().mockReturnValue(false),
supportsIntentionalMentions: jest.fn().mockReturnValue(false),
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
2022-03-09 14:23:58 +01:00
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
2022-10-12 18:59:07 +01:00
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
agreeToTerms: jest.fn(),
2022-03-09 14:23:58 +01:00
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
isVersionSupported: jest.fn().mockResolvedValue(undefined),
2022-03-09 14:23:58 +01:00
getPushRules: jest.fn().mockResolvedValue(undefined),
2022-01-06 10:47:03 +01:00
getPushers: jest.fn().mockResolvedValue({ pushers: [] }),
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
bulkLookupThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
setAvatarUrl: jest.fn().mockResolvedValue(undefined),
setDisplayName: jest.fn().mockResolvedValue(undefined),
2022-03-09 14:23:58 +01:00
setPusher: jest.fn().mockResolvedValue(undefined),
setPushRuleEnabled: jest.fn().mockResolvedValue(undefined),
setPushRuleActions: jest.fn().mockResolvedValue(undefined),
relations: jest.fn().mockResolvedValue({
events: [],
}),
hasLazyLoadMembersEnabled: jest.fn().mockReturnValue(false),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
fetchRoomEvent: jest.fn().mockRejectedValue({}),
2022-07-13 07:56:36 +02:00
makeTxnId: jest.fn().mockImplementation(() => `t${txnId++}`),
sendToDevice: jest.fn().mockResolvedValue(undefined),
queueToDevice: jest.fn().mockResolvedValue(undefined),
2022-10-21 19:26:33 +02:00
cancelPendingEvent: jest.fn(),
getMediaHandler: jest.fn().mockReturnValue({
setVideoInput: jest.fn(),
setAudioInput: jest.fn(),
2022-11-09 21:14:55 +01:00
setAudioSettings: jest.fn(),
stopAllStreams: jest.fn(),
} as unknown as MediaHandler),
2022-10-12 18:59:07 +01:00
uploadContent: jest.fn(),
2023-01-27 11:00:06 +00:00
getEventMapper: (_options?: MapperOpts) => (event: Partial<IEvent>) => new MatrixEvent(event),
2022-11-07 13:45:34 +00:00
leaveRoomChain: jest.fn((roomId) => ({ [roomId]: null })),
2022-11-22 07:58:37 +01:00
requestPasswordEmailToken: jest.fn().mockRejectedValue({}),
setPassword: jest.fn().mockRejectedValue({}),
groupCallEventHandler: { groupCalls: new Map<string, GroupCall>() },
redactEvent: jest.fn(),
createMessagesRequest: jest.fn().mockResolvedValue({
chunk: [],
}),
sendEvent: jest.fn().mockImplementation((roomId, type, content) => {
return new MatrixEvent({
type,
sender: "@me:localhost",
content,
event_id: "$9999999999999999999999999999999999999999999",
room_id: roomId,
});
}),
2026-01-12 21:13:15 +00:00
resendEvent: jest.fn().mockResolvedValue({}),
_unstable_sendDelayedEvent: jest.fn(),
_unstable_sendDelayedStateEvent: jest.fn(),
_unstable_cancelScheduledDelayedEvent: jest.fn(),
_unstable_restartScheduledDelayedEvent: jest.fn(),
_unstable_sendScheduledDelayedEvent: jest.fn(),
_unstable_sendStickyEvent: jest.fn(),
_unstable_sendStickyDelayedEvent: jest.fn(),
_unstable_getRTCTransports: jest.fn(),
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
setDeviceVerified: jest.fn(),
joinRoom: jest.fn(),
getSyncStateData: jest.fn(),
getDehydratedDevice: jest.fn(),
exportRoomKeys: jest.fn(),
2023-08-07 08:27:09 +02:00
knockRoom: jest.fn(),
leave: jest.fn(),
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
requestAdd3pidEmailToken: jest.fn(),
requestAdd3pidMsisdnToken: jest.fn(),
submitMsisdnTokenOtherUrl: jest.fn(),
deleteThreePid: jest.fn().mockResolvedValue({}),
bindThreePid: jest.fn().mockResolvedValue({}),
unbindThreePid: jest.fn().mockResolvedValue({}),
requestEmailToken: jest.fn(),
addThreePidOnly: jest.fn(),
requestMsisdnToken: jest.fn(),
submitMsisdnToken: jest.fn(),
getMediaConfig: jest.fn(),
baseUrl: "https://matrix-client.matrix.org",
2023-10-30 16:14:27 +01:00
matrixRTC: createStubMatrixRTC(),
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
2024-06-06 09:57:28 +01:00
getAuthIssuer: jest.fn(),
getOrCreateFilter: jest.fn(),
sendStickerMessage: jest.fn(),
getLocalAliases: jest.fn().mockReturnValue([]),
uploadDeviceSigningKeys: jest.fn(),
isKeyBackupKeyStored: jest.fn().mockResolvedValue(null),
getIgnoredUsers: jest.fn().mockReturnValue([]),
setIgnoredUsers: jest.fn(),
reportRoom: jest.fn(),
pushProcessor: {
getPushRuleById: jest.fn(),
},
search: jest.fn().mockResolvedValue({}),
processRoomEventsSearch: jest.fn().mockResolvedValue({ highlights: [], results: [] }),
invite: jest.fn(),
kick: jest.fn(),
ban: jest.fn(),
sendTextMessage: jest.fn(),
deleteRoomTag: jest.fn().mockResolvedValue({}),
setRoomTag: jest.fn().mockResolvedValue({}),
2022-03-09 14:23:58 +01:00
} as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client);
2022-10-13 18:22:25 +01:00
client.canSupport = new Map();
Object.keys(Feature).forEach((feature) => {
client.canSupport.set(feature as Feature, ServerSupport.Stable);
});
Object.defineProperty(client, "pollingTurnServers", {
configurable: true,
get: () => true,
});
return client;
2016-03-28 22:59:34 +01:00
}
2023-10-30 16:14:27 +01:00
export function createStubMatrixRTC(): MatrixRTCSessionManager {
const eventEmitterMatrixRTCSessionManager = new EventEmitter();
const mockGetRoomSession = jest.fn();
mockGetRoomSession.mockImplementation((roomId) => {
const session = new EventEmitter() as MatrixRTCSession;
session.memberships = [];
session.getOldestMembership = () => undefined;
session.getConsensusCallIntent = () => "video";
2023-10-30 16:14:27 +01:00
return session;
});
return {
start: jest.fn(),
stop: jest.fn(),
getActiveRoomSession: jest.fn(),
getRoomSession: mockGetRoomSession,
on: eventEmitterMatrixRTCSessionManager.on.bind(eventEmitterMatrixRTCSessionManager),
off: eventEmitterMatrixRTCSessionManager.off.bind(eventEmitterMatrixRTCSessionManager),
removeListener: eventEmitterMatrixRTCSessionManager.removeListener.bind(eventEmitterMatrixRTCSessionManager),
emit: eventEmitterMatrixRTCSessionManager.emit.bind(eventEmitterMatrixRTCSessionManager),
} as unknown as MatrixRTCSessionManager;
}
2022-02-23 12:21:11 +01:00
type MakeEventPassThruProps = {
user: User["userId"];
2022-03-09 14:23:58 +01:00
relatesTo?: IEventRelation;
2022-02-23 12:21:11 +01:00
event?: boolean;
ts?: number;
skey?: string;
};
type MakeEventProps = MakeEventPassThruProps & {
2023-03-21 10:23:20 +01:00
/** If provided will be used as event Id. Else an Id is generated. */
id?: string;
2022-02-23 12:21:11 +01:00
type: string;
redacts?: string;
2022-02-23 12:21:11 +01:00
content: IContent;
room?: Room["roomId"]; // to-device messages are roomless
2022-02-23 12:21:11 +01:00
// eslint-disable-next-line camelcase
prev_content?: IContent;
unsigned?: IUnsigned;
2026-01-12 21:13:15 +00:00
status?: EventStatus;
2022-02-23 12:21:11 +01:00
};
2023-10-25 12:08:10 +02:00
export const mkRoomCreateEvent = (userId: string, roomId: string, content?: IContent): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomCreate,
content: {
creator: userId,
room_version: KNOWN_SAFE_ROOM_VERSION,
2023-10-25 12:08:10 +02:00
...content,
},
skey: "",
user: userId,
room: roomId,
});
};
2016-03-31 00:48:46 +01:00
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
2021-04-22 14:45:13 +01:00
* @param {string=} opts.skey Optional. The state key (auto inserts empty string)
* @param {number=} opts.ts Optional. Timestamp for the event
2016-03-31 00:48:46 +01:00
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
2021-08-10 12:25:11 +05:30
* @param {unsigned=} opts.unsigned
2016-03-31 00:48:46 +01:00
* @return {Object} a JSON object representing this event.
*/
2022-02-23 12:21:11 +01:00
export function mkEvent(opts: MakeEventProps): MatrixEvent {
2016-03-31 00:48:46 +01:00
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
2022-02-23 12:21:11 +01:00
const event: Partial<IEvent> = {
2016-03-31 00:48:46 +01:00
type: opts.type,
room_id: opts.room,
sender: opts.user,
content: opts.content,
2023-03-21 10:23:20 +01:00
event_id: opts.id ?? "$" + Math.random() + "-" + Math.random(),
origin_server_ts: opts.ts ?? 0,
unsigned: {
...opts.unsigned,
prev_content: opts.prev_content,
},
redacts: opts.redacts,
2016-03-31 00:48:46 +01:00
};
if (opts.skey !== undefined) {
2016-03-31 00:48:46 +01:00
event.state_key = opts.skey;
} else if (
[
"m.room.name",
"m.room.topic",
"m.room.create",
"m.room.join_rules",
"m.room.power_levels",
"m.room.topic",
"m.room.history_visibility",
"m.room.encryption",
"m.room.member",
"com.example.state",
"m.room.guest_access",
"m.room.tombstone",
].indexOf(opts.type) !== -1
) {
2016-03-31 00:48:46 +01:00
event.state_key = "";
}
const mxEvent = opts.event ? new MatrixEvent(event) : (event as unknown as MatrixEvent);
if (!mxEvent.sender && opts.user && opts.room) {
mxEvent.sender = {
userId: opts.user,
membership: KnownMembership.Join,
name: opts.user,
rawDisplayName: opts.user,
roomId: opts.room,
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
} as unknown as RoomMember;
}
2026-01-12 21:13:15 +00:00
if (opts.status !== undefined) {
mxEvent.status = opts.status;
}
return mxEvent;
2017-10-11 17:56:17 +01:00
}
2016-03-31 00:48:46 +01:00
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
2017-01-18 11:53:17 +01:00
* @param {string} opts.prevMship The prev_content.membership for the event.
2021-08-10 12:25:11 +05:30
* @param {number=} opts.ts Optional. Timestamp for the event
2016-03-31 00:48:46 +01:00
* @param {string} opts.user The user ID for the event.
2017-01-18 11:53:17 +01:00
* @param {RoomMember} opts.target The target of the event.
2021-08-10 12:25:11 +05:30
* @param {string=} opts.skey The other user ID for the event if applicable
2016-03-31 00:48:46 +01:00
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
2021-08-10 12:25:11 +05:30
* @param {string=} opts.url The content.avatar_url for the event.
2016-03-31 00:48:46 +01:00
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
2022-02-23 12:21:11 +01:00
export function mkMembership(
opts: MakeEventPassThruProps & {
room: Room["roomId"];
2024-03-11 17:16:53 +00:00
mship: Membership;
prevMship?: Membership;
2022-02-23 12:21:11 +01:00
name?: string;
url?: string;
skey?: string;
target?: RoomMember;
},
): MatrixEvent {
const event: MakeEventProps = {
...opts,
type: "m.room.member",
content: {
membership: opts.mship,
},
};
2016-03-31 00:48:46 +01:00
if (!opts.skey) {
2022-02-23 12:21:11 +01:00
event.skey = opts.user;
2016-03-31 00:48:46 +01:00
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
2022-02-23 12:21:11 +01:00
2017-01-18 11:53:17 +01:00
if (opts.prevMship) {
2022-02-23 12:21:11 +01:00
event.prev_content = { membership: opts.prevMship };
2017-01-18 11:53:17 +01:00
}
2022-02-23 12:21:11 +01:00
if (opts.name) {
event.content.displayname = opts.name;
}
if (opts.url) {
event.content.avatar_url = opts.url;
}
const e = mkEvent(event);
2017-01-18 11:53:17 +01:00
if (opts.target) {
e.target = opts.target;
}
return e;
2017-10-11 17:56:17 +01:00
}
2016-03-31 00:48:46 +01:00
2023-09-19 14:24:35 +03:00
export function mkRoomMember(
roomId: string,
userId: string,
membership = KnownMembership.Join,
2023-09-19 14:24:35 +03:00
isKicked = false,
prevMemberContent: Partial<IContent> = {},
): RoomMember {
return {
userId,
membership,
name: userId,
rawDisplayName: userId,
roomId,
2023-09-19 14:24:35 +03:00
events: {
member: {
getSender: () => undefined,
getPrevContent: () => prevMemberContent,
},
},
isKicked: () => isKicked,
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
2022-09-16 11:12:27 -04:00
getDMInviter: () => {},
off: () => {},
} as unknown as RoomMember;
}
2022-02-23 17:12:48 +01:00
export type MessageEventProps = MakeEventPassThruProps & {
room: Room["roomId"];
relatesTo?: IEventRelation;
msg?: string;
};
/**
* Creates a "🙃" reaction for the given event.
* Uses the same room and user as for the event.
*
* @returns The reaction event
*/
export const mkReaction = (event: MatrixEvent, opts: Partial<MakeEventProps> = {}): MatrixEvent => {
return mkEvent({
event: true,
room: event.getRoomId(),
type: EventType.Reaction,
user: event.getSender()!,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: event.getId(),
key: "🙃",
},
},
...opts,
});
};
2016-03-31 00:48:46 +01:00
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
2021-08-03 14:36:21 +05:30
* @param {number} opts.ts The timestamp for the event.
2016-03-31 00:48:46 +01:00
* @param {boolean} opts.event True to make a MatrixEvent.
2021-08-03 14:36:21 +05:30
* @param {string=} opts.msg Optional. The content.body for the event.
* @param {string=} opts.format Optional. The content.format for the event.
* @param {string=} opts.formattedMsg Optional. The content.formatted_body for the event.
2016-03-31 00:48:46 +01:00
* @return {Object|MatrixEvent} The event
*/
2022-03-09 14:23:58 +01:00
export function mkMessage({
msg,
format,
formattedMsg,
2022-03-09 14:23:58 +01:00
relatesTo,
...opts
2023-03-21 10:23:20 +01:00
}: MakeEventPassThruProps &
Pick<MakeEventProps, "id"> & {
room: Room["roomId"];
msg?: string;
format?: string;
formattedMsg?: string;
}): MatrixEvent {
2016-03-31 00:48:46 +01:00
if (!opts.room || !opts.user) {
2022-02-23 12:21:11 +01:00
throw new Error("Missing .room or .user from options");
2016-03-31 00:48:46 +01:00
}
2022-02-23 17:12:48 +01:00
const message = msg ?? "Random->" + Math.random();
2022-02-23 12:21:11 +01:00
const event: MakeEventProps = {
ts: 0,
2022-02-23 12:21:11 +01:00
...opts,
type: "m.room.message",
content: {
msgtype: "m.text",
body: message,
...(format && formattedMsg ? { format, formatted_body: formattedMsg } : {}),
2022-02-23 17:12:48 +01:00
["m.relates_to"]: relatesTo,
2022-02-23 12:21:11 +01:00
},
2016-03-31 00:48:46 +01:00
};
2022-02-23 12:21:11 +01:00
return mkEvent(event);
2016-09-09 18:07:42 +05:30
}
2016-06-17 12:20:26 +01:00
export function mkStubRoom(
roomId: string | null | undefined = null,
name: string | undefined,
client: MatrixClient | undefined,
state?: RoomState | undefined,
): Room {
2024-09-11 13:16:52 +02:00
const stubTimeline = {
getEvents: (): MatrixEvent[] => [],
getState: (): RoomState | undefined => state,
2024-09-11 13:16:52 +02:00
} as unknown as EventTimeline;
const eventEmitter = new EventEmitter();
2016-06-17 12:20:26 +01:00
return {
canInvite: jest.fn().mockReturnValue(false),
2023-01-27 11:00:06 +00:00
client,
findThreadForEvent: jest.fn(),
2023-01-27 11:00:06 +00:00
createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})),
2016-06-17 12:20:26 +01:00
currentState: {
getStateEvents: jest.fn((_type, key) => (key === undefined ? [] : null)),
2021-05-19 15:01:05 +05:30
getMember: jest.fn(),
2019-12-16 11:12:48 +00:00
mayClientSendStateEvent: jest.fn().mockReturnValue(true),
maySendStateEvent: jest.fn().mockReturnValue(true),
2022-03-04 18:15:03 -05:00
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
2019-12-16 11:12:48 +00:00
maySendEvent: jest.fn().mockReturnValue(true),
maySendMessage: jest.fn().mockReturnValue(true),
2022-02-23 12:21:11 +01:00
members: {},
getHistoryVisibility: jest.fn().mockReturnValue(HistoryVisibility.Shared),
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
on: jest.fn(),
off: jest.fn(),
removeListener: jest.fn(),
2022-02-23 12:21:11 +01:00
} as unknown as RoomState,
eventShouldLiveIn: jest.fn().mockReturnValue({ shouldLiveInRoom: true, shouldLiveInThread: false }),
2023-01-27 11:00:06 +00:00
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
2023-03-22 13:27:24 +01:00
findEventById: jest.fn().mockReturnValue(undefined),
2023-01-27 11:00:06 +00:00
findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }),
getAltAliases: jest.fn().mockReturnValue([]),
2021-02-03 15:18:19 +00:00
getAvatarUrl: () => "mxc://avatar.url/room.png",
2023-01-27 11:00:06 +00:00
getCanonicalAlias: jest.fn(),
getDMInviter: jest.fn(),
getEventReadUpTo: jest.fn(() => null),
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinRule: jest.fn().mockReturnValue("invite"),
getJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinedMembers: jest.fn().mockReturnValue([]),
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
2024-03-28 17:38:21 +00:00
getLastLiveEvent: jest.fn().mockReturnValue(undefined),
2025-11-05 07:24:26 +00:00
getLastActiveTimestamp: jest.fn().mockReturnValue(1183140000),
2023-01-27 11:00:06 +00:00
getMember: jest.fn().mockReturnValue({
userId: "@member:domain.bla",
name: "Member",
rawDisplayName: "Member",
roomId: roomId,
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
2023-09-19 14:24:35 +03:00
events: {},
isKicked: () => false,
2023-01-27 11:00:06 +00:00
}),
getMembers: jest.fn().mockReturnValue([]),
getEncryptionTargetMembers: jest.fn().mockReturnValue([]),
2023-01-27 11:00:06 +00:00
getMembersWithMembership: jest.fn().mockReturnValue([]),
2021-03-11 09:42:55 -07:00
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join),
2026-01-12 21:13:15 +00:00
getPendingEvents: jest.fn().mockReturnValue([]),
2023-01-27 11:00:06 +00:00
getReceiptsForEvent: jest.fn().mockReturnValue([]),
getRecommendedVersion: jest.fn().mockReturnValue(Promise.resolve("")),
getThreads: jest.fn().mockReturnValue([]),
getType: jest.fn().mockReturnValue(undefined),
2023-01-27 11:00:06 +00:00
getUnfilteredTimelineSet: jest.fn(),
2021-04-23 12:19:08 +01:00
getUnreadNotificationCount: jest.fn(() => 0),
getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0),
2023-01-27 11:00:06 +00:00
getVersion: jest.fn().mockReturnValue("1"),
2025-03-18 17:54:32 +00:00
getBumpStamp: jest.fn().mockReturnValue(0),
getAccountData: jest.fn(),
2023-01-27 11:00:06 +00:00
hasMembershipState: () => false,
isElementVideoRoom: jest.fn().mockReturnValue(false),
isSpaceRoom: jest.fn().mockReturnValue(false),
2024-03-25 19:35:31 +01:00
isCallRoom: jest.fn().mockReturnValue(false),
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
loadMembersIfNeeded: jest.fn(),
2023-01-27 11:00:06 +00:00
maySendMessage: jest.fn().mockReturnValue(true),
myUserId: client?.getUserId(),
2023-01-27 11:00:06 +00:00
name,
normalizedName: normalize(name || ""),
on: eventEmitter.on.bind(eventEmitter),
once: eventEmitter.once.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
2023-01-27 11:00:06 +00:00
roomId,
setBlacklistUnverifiedDevices: jest.fn(),
setUnreadNotificationCount: jest.fn(),
tags: {},
timeline: [],
2022-02-23 12:21:11 +01:00
} as unknown as Room;
2016-09-09 18:07:42 +05:30
}
2017-05-24 16:56:13 +01:00
export function mkRoomState(
roomId: string = "!1:example.org",
stateEvents: MatrixEvent[] = [],
members: RoomMember[] = [],
): RoomState {
const roomState = new RoomState(roomId);
roomState.setStateEvents(stateEvents);
for (const member of members) {
roomState.members[member.userId] = member;
}
return roomState;
}
export function mkServerConfig(
hsUrl: string,
isUrl: string,
2023-10-12 10:44:46 +13:00
delegatedAuthentication?: OidcClientConfig,
): ValidatedServerConfig {
return {
2019-05-02 23:46:43 -06:00
hsUrl,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl,
delegatedAuthentication,
} as ValidatedServerConfig;
2019-05-02 23:46:43 -06:00
}
2022-02-23 12:21:11 +01:00
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
2018-02-06 17:50:53 +00:00
2024-10-16 16:43:07 +01:00
export const setupAsyncStoreWithClient = async <T extends object = any>(
store: AsyncStoreWithClient<T>,
client: MatrixClient,
) => {
// @ts-ignore protected access
2022-02-23 12:21:11 +01:00
store.readyStore.useUnitTestClient(client);
// @ts-ignore protected access
2022-02-23 12:21:11 +01:00
await store.onReady();
};
2024-10-16 16:43:07 +01:00
export const resetAsyncStoreWithClient = async <T extends object = any>(store: AsyncStoreWithClient<T>) => {
// @ts-ignore protected access
2022-02-23 12:21:11 +01:00
await store.onNotReady();
};
2022-03-09 14:23:58 +01:00
export const mockStateEventImplementation = (events: MatrixEvent[]) => {
2022-02-23 12:21:11 +01:00
const stateMap = new EnhancedMap<string, Map<string, MatrixEvent>>();
events.forEach((event) => {
stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey()!, event);
2022-02-23 12:21:11 +01:00
});
2018-02-06 17:50:53 +00:00
2022-03-09 14:23:58 +01:00
// recreate the overloading in RoomState
function getStateEvents(eventType: EventType | string): MatrixEvent[];
function getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent;
function getStateEvents(eventType: EventType | string, stateKey?: string) {
2022-02-23 12:21:11 +01:00
if (stateKey || stateKey === "") {
return stateMap.get(eventType)?.get(stateKey) || null;
2018-02-06 17:50:53 +00:00
}
2022-02-23 12:21:11 +01:00
return Array.from(stateMap.get(eventType)?.values() || []);
2022-03-09 14:23:58 +01:00
}
return getStateEvents;
2022-02-23 12:21:11 +01:00
};
2022-03-11 17:03:33 +01:00
export const mkRoom = (
client: MatrixClient,
roomId: string,
rooms?: ReturnType<typeof mkStubRoom>[],
): MockedObject<Room> => {
const room = mocked(mkStubRoom(roomId, roomId, client));
2022-02-23 12:21:11 +01:00
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([]));
rooms?.push(room);
return room;
};
2018-05-02 11:19:01 +01:00
/**
2022-02-23 12:21:11 +01:00
* Upserts given events into room.currentState
* @param room
* @param events
2018-05-02 11:19:01 +01:00
*/
2022-02-23 12:21:11 +01:00
export const upsertRoomStateEvents = (room: Room, events: MatrixEvent[]): void => {
const eventsMap = events.reduce((acc, event) => {
const eventType = event.getType();
if (!acc.has(eventType)) {
acc.set(eventType, new Map());
}
acc.get(eventType)?.set(event.getStateKey()!, event);
2022-02-23 12:21:11 +01:00
return acc;
}, room.currentState.events || new Map<string, Map<string, MatrixEvent>>());
2018-05-02 11:19:01 +01:00
2022-02-23 12:21:11 +01:00
room.currentState.events = eventsMap;
};
2018-05-02 11:19:01 +01:00
2022-02-23 12:21:11 +01:00
export const mkSpace = (
client: MatrixClient,
spaceId: string,
rooms?: ReturnType<typeof mkStubRoom>[],
children: string[] = [],
2022-03-11 17:03:33 +01:00
): MockedObject<Room> => {
const space = mocked(mkRoom(client, spaceId, rooms));
space.isSpaceRoom.mockReturnValue(true);
space.getType.mockReturnValue(RoomType.Space);
2022-02-23 12:21:11 +01:00
mocked(space.currentState).getStateEvents.mockImplementation(
mockStateEventImplementation(
children.map((roomId) =>
mkEvent({
event: true,
type: EventType.SpaceChild,
room: spaceId,
user: "@user:server",
skey: roomId,
content: { via: [] },
ts: Date.now(),
}),
2022-12-12 12:24:14 +01:00
),
),
2022-02-23 12:21:11 +01:00
);
return space;
};
2023-03-08 13:06:50 +01:00
export const mkRoomMemberJoinEvent = (user: string, room: string, content?: IContent): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomMember,
content: {
membership: KnownMembership.Join,
2023-03-08 13:06:50 +01:00
...content,
},
skey: user,
user,
room,
});
};
2023-03-08 13:06:50 +01:00
export const mkRoomCanonicalAliasEvent = (userId: string, roomId: string, alias: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomCanonicalAlias,
content: {
alias,
},
skey: "",
user: userId,
room: roomId,
});
};
export const mkThirdPartyInviteEvent = (user: string, displayName: string, room: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomThirdPartyInvite,
content: {
display_name: displayName,
public_key: "foo",
key_validity_url: "bar",
},
skey: "test" + Math.random(),
user,
room,
});
};
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
/** Add a mute rule for a room. */
export function muteRoom(room: Room): void {
const client = room.client!;
client.pushRules = client.pushRules ?? ({ global: [] } as IPushRules);
client.pushRules.global = client.pushRules.global ?? {};
client.pushRules.global.override = [
{
default: true,
enabled: true,
rule_id: "rule_id",
conditions: [
{
kind: ConditionKind.EventMatch,
key: "room_id",
pattern: room.roomId,
},
],
actions: [],
},
];
}