element-web/test/test-utils/test-utils.ts

466 lines
17 KiB
TypeScript
Raw Normal View History

import EventEmitter from "events";
import { mocked } from 'jest-mock';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
import {
Room,
User,
IContent,
IEvent,
RoomMember,
MatrixClient,
EventTimeline,
RoomState,
EventType,
IEventRelation,
IUnsigned,
} from 'matrix-js-sdk/src/matrix';
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
import dis from '../../src/dispatcher/dispatcher';
import { makeType } from "../../src/utils/TypeUtils";
import { ValidatedServerConfig } from "../../src/utils/AutoDiscoveryUtils";
import { EnhancedMap } from "../../src/utils/maps";
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler";
2016-03-28 21:59:34 +00: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.
2016-03-28 21:59:34 +00:00
*/
export function stubClient() {
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
jest.spyOn(peg, 'get');
jest.spyOn(peg, 'unset');
jest.spyOn(peg, 'replaceUsingCreds');
// MatrixClientPeg.get() is called a /lot/, so implement it with our own
// fast stub function rather than a sinon stub
peg.get = function() { return client; };
MatrixClientBackedSettingsHandler.matrixClient = client;
}
/**
* Create a stubbed-out MatrixClient
*
* @returns {object} MatrixClient stub
*/
export function createTestClient(): MatrixClient {
const eventEmitter = new EventEmitter();
return {
2019-12-16 11:12:48 +00:00
getHomeserverUrl: jest.fn(),
getIdentityServerUrl: jest.fn(),
getDomain: jest.fn().mockReturnValue("matrix.rog"),
getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"),
getUser: jest.fn().mockReturnValue({ on: jest.fn() }),
credentials: { userId: "@userId:matrix.rog" },
2019-12-16 11:12:48 +00:00
getPushActionsForEvent: jest.fn(),
getRoom: jest.fn().mockImplementation(mkStubRoom),
2019-12-16 11:12:48 +00:00
getRooms: jest.fn().mockReturnValue([]),
getVisibleRooms: jest.fn().mockReturnValue([]),
getGroups: jest.fn().mockReturnValue([]),
loginFlows: jest.fn(),
on: eventEmitter.on.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),
peekInRoom: jest.fn().mockResolvedValue(mkStubRoom(undefined, undefined, undefined)),
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({}),
getThirdpartyProtocols: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue(null),
supportsVoip: jest.fn().mockReturnValue(true),
getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32),
getThirdpartyUser: jest.fn().mockResolvedValue([]),
getAccountData: (type) => {
return mkEvent({
user: undefined,
room: undefined,
type,
event: true,
content: {},
});
},
mxcUrlToHttp: (mxc) => `http://this.is.a.url/${mxc.substring(6)}`,
2019-12-16 11:12:48 +00:00
setAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
2019-12-16 11:12:48 +00:00
sendTyping: jest.fn().mockResolvedValue({}),
sendMessage: () => jest.fn().mockResolvedValue({}),
sendStateEvent: jest.fn().mockResolvedValue(undefined),
getSyncState: () => "SYNCING",
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: jest.fn().mockReturnValue(false),
getRoomHierarchy: jest.fn().mockReturnValue({
2021-04-23 13:45:22 +00:00
rooms: [],
}),
2021-04-21 22:45:21 +00:00
// Used by various internal bits we aren't concerned with (yet)
sessionStore: {
2021-04-21 22:45:21 +00:00
store: {
getItem: jest.fn(),
setItem: jest.fn(),
2021-04-21 22:45:21 +00:00
},
},
pushRules: {},
2021-05-18 12:46:47 +00:00
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
2021-07-06 09:34:50 +00:00
getCapabilities: jest.fn().mockResolvedValue({}),
supportsExperimentalThreads: () => false,
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValueOnce(undefined),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
getPushRules: jest.fn().mockResolvedValue(undefined),
getPushers: jest.fn().mockResolvedValue({ pushers: [] }),
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
setPusher: jest.fn().mockResolvedValue(undefined),
setPushRuleEnabled: jest.fn().mockResolvedValue(undefined),
setPushRuleActions: jest.fn().mockResolvedValue(undefined),
isCryptoEnabled: jest.fn().mockReturnValue(false),
} as unknown as MatrixClient;
2016-03-28 21:59:34 +00:00
}
type MakeEventPassThruProps = {
user: User["userId"];
relatesTo?: IEventRelation;
event?: boolean;
ts?: number;
skey?: string;
};
type MakeEventProps = MakeEventPassThruProps & {
type: string;
content: IContent;
room: Room["roomId"];
// eslint-disable-next-line camelcase
prev_content?: IContent;
unsigned?: IUnsigned;
};
/**
* 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 13:45:13 +00:00
* @param {string=} opts.skey Optional. The state key (auto inserts empty string)
* @param {number=} opts.ts Optional. Timestamp for the event
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
2021-08-10 06:55:11 +00:00
* @param {unsigned=} opts.unsigned
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: MakeEventProps): MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event: Partial<IEvent> = {
type: opts.type,
room_id: opts.room,
sender: opts.user,
content: opts.content,
2017-01-18 10:53:17 +00:00
prev_content: opts.prev_content,
event_id: "$" + Math.random() + "-" + Math.random(),
origin_server_ts: opts.ts ?? 0,
unsigned: opts.unsigned,
};
if (opts.skey !== undefined) {
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) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event as unknown as MatrixEvent;
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
content: {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
}
/**
* 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 10:53:17 +00:00
* @param {string} opts.prevMship The prev_content.membership for the event.
2021-08-10 06:55:11 +00:00
* @param {number=} opts.ts Optional. Timestamp for the event
* @param {string} opts.user The user ID for the event.
2017-01-18 10:53:17 +00:00
* @param {RoomMember} opts.target The target of the event.
2021-08-10 06:55:11 +00:00
* @param {string=} opts.skey The other user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
2021-08-10 06:55:11 +00:00
* @param {string=} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(opts: MakeEventPassThruProps & {
room: Room["roomId"];
mship: string;
prevMship?: string;
name?: string;
url?: string;
skey?: string;
target?: RoomMember;
}): MatrixEvent {
const event: MakeEventProps = {
...opts,
type: "m.room.member",
content: {
membership: opts.mship,
},
};
if (!opts.skey) {
event.skey = opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
2017-01-18 10:53:17 +00:00
if (opts.prevMship) {
event.prev_content = { membership: opts.prevMship };
2017-01-18 10:53:17 +00: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 10:53:17 +00:00
if (opts.target) {
e.target = opts.target;
}
return e;
}
export type MessageEventProps = MakeEventPassThruProps & {
room: Room["roomId"];
relatesTo?: IEventRelation;
msg?: string;
};
/**
* 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 09:06:21 +00:00
* @param {number} opts.ts The timestamp for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
2021-08-03 09:06:21 +00:00
* @param {string=} opts.msg Optional. The content.body for the event.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage({ msg, relatesTo, ...opts }: MakeEventPassThruProps & {
room: Room["roomId"];
msg?: string;
}): MatrixEvent {
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from options");
}
const message = msg ?? "Random->" + Math.random();
const event: MakeEventProps = {
...opts,
type: "m.room.message",
content: {
msgtype: "m.text",
body: message,
['m.relates_to']: relatesTo,
},
};
return mkEvent(event);
}
2016-06-17 11:20:26 +00:00
export function mkStubRoom(roomId: string = null, name: string, client: MatrixClient): Room {
const stubTimeline = { getEvents: () => [] } as unknown as EventTimeline;
2016-06-17 11:20:26 +00:00
return {
roomId,
2019-12-16 11:12:48 +00:00
getReceiptsForEvent: jest.fn().mockReturnValue([]),
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',
}),
2019-12-16 11:12:48 +00:00
getMembersWithMembership: jest.fn().mockReturnValue([]),
getJoinedMembers: jest.fn().mockReturnValue([]),
getJoinedMemberCount: jest.fn().mockReturnValue(1),
2021-05-19 11:34:27 +00:00
getMembers: jest.fn().mockReturnValue([]),
getPendingEvents: () => [],
getLiveTimeline: () => stubTimeline,
getUnfilteredTimelineSet: () => null,
findEventById: () => null,
getAccountData: () => null,
hasMembershipState: () => null,
getVersion: () => '1',
2018-08-17 14:15:53 +00:00
shouldUpgradeToVersion: () => null,
2021-04-23 11:19:08 +00:00
getMyMembership: jest.fn().mockReturnValue("join"),
2019-12-16 11:12:48 +00:00
maySendMessage: jest.fn().mockReturnValue(true),
2016-06-17 11:20:26 +00:00
currentState: {
2019-12-16 11:12:48 +00:00
getStateEvents: jest.fn(),
2021-05-19 09:31:05 +00:00
getMember: jest.fn(),
2019-12-16 11:12:48 +00:00
mayClientSendStateEvent: jest.fn().mockReturnValue(true),
maySendStateEvent: jest.fn().mockReturnValue(true),
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
2019-12-16 11:12:48 +00:00
maySendEvent: jest.fn().mockReturnValue(true),
members: {},
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
on: jest.fn(),
} as unknown as RoomState,
2021-04-23 11:19:08 +00:00
tags: {},
2019-12-16 11:12:48 +00:00
setBlacklistUnverifiedDevices: jest.fn(),
on: jest.fn(),
off: jest.fn(),
2019-12-16 11:12:48 +00:00
removeListener: jest.fn(),
2020-11-05 16:27:41 +00:00
getDMInviter: jest.fn(),
name,
getAvatarUrl: () => 'mxc://avatar.url/room.png',
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
2021-04-22 13:45:13 +00:00
isSpaceRoom: jest.fn(() => false),
2021-04-23 11:19:08 +00:00
getUnreadNotificationCount: jest.fn(() => 0),
getEventReadUpTo: jest.fn(() => null),
getCanonicalAlias: jest.fn(),
getAltAliases: jest.fn().mockReturnValue([]),
2021-04-23 11:19:08 +00:00
timeline: [],
2021-07-06 09:44:09 +00:00
getJoinRule: jest.fn().mockReturnValue("invite"),
loadMembersIfNeeded: jest.fn(),
client,
} as unknown as Room;
}
2019-05-03 05:46:43 +00:00
export function mkServerConfig(hsUrl, isUrl) {
return makeType(ValidatedServerConfig, {
hsUrl,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl,
});
}
export function getDispatchForStore(store) {
// Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a
// dispatcher `_isDispatching` is true.
return (payload) => {
// these are private properties in flux dispatcher
// fool ts
(dis as any)._isDispatching = true;
(dis as any)._callbacks[store._dispatchToken](payload);
(dis as any)._isDispatching = false;
};
}
// 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.
export const setupAsyncStoreWithClient = async <T = unknown>(store: AsyncStoreWithClient<T>, client: MatrixClient) => {
// @ts-ignore
store.readyStore.useUnitTestClient(client);
// @ts-ignore
await store.onReady();
};
export const resetAsyncStoreWithClient = async <T = unknown>(store: AsyncStoreWithClient<T>) => {
// @ts-ignore
await store.onNotReady();
};
export const mockStateEventImplementation = (events: MatrixEvent[]) => {
const stateMap = new EnhancedMap<string, Map<string, MatrixEvent>>();
events.forEach(event => {
stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey(), event);
});
// 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) {
if (stateKey || stateKey === "") {
return stateMap.get(eventType)?.get(stateKey) || null;
}
return Array.from(stateMap.get(eventType)?.values() || []);
}
return getStateEvents;
};
export const mkRoom = (client: MatrixClient, roomId: string, rooms?: ReturnType<typeof mkStubRoom>[]) => {
const room = mkStubRoom(roomId, roomId, client);
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([]));
rooms?.push(room);
return room;
};
2018-05-02 10:19:01 +00:00
/**
* Upserts given events into room.currentState
* @param room
* @param events
2018-05-02 10:19:01 +00: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);
return acc;
}, room.currentState.events || new Map<string, Map<string, MatrixEvent>>());
2018-05-02 10:19:01 +00:00
room.currentState.events = eventsMap;
};
2018-05-02 10:19:01 +00:00
export const mkSpace = (
client: MatrixClient,
spaceId: string,
rooms?: ReturnType<typeof mkStubRoom>[],
children: string[] = [],
) => {
const space = mkRoom(client, spaceId, rooms);
mocked(space).isSpaceRoom.mockReturnValue(true);
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(),
}),
)));
return space;
};