Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0f670b8dc0
commit
b084ff2313
807 changed files with 0 additions and 0 deletions
27
test/unit-tests/utils/AnimationUtils-test.ts
Normal file
27
test/unit-tests/utils/AnimationUtils-test.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { lerp } from "../../src/utils/AnimationUtils";
|
||||
|
||||
describe("lerp", () => {
|
||||
it("correctly interpolates", () => {
|
||||
expect(lerp(0, 100, 0.5)).toBe(50);
|
||||
expect(lerp(50, 100, 0.5)).toBe(75);
|
||||
expect(lerp(0, 1, 0.1)).toBe(0.1);
|
||||
});
|
||||
|
||||
it("clamps the interpolant", () => {
|
||||
expect(lerp(0, 100, 50)).toBe(100);
|
||||
expect(lerp(0, 100, -50)).toBe(0);
|
||||
});
|
||||
|
||||
it("handles negative numbers", () => {
|
||||
expect(lerp(-100, 0, 0.5)).toBe(-50);
|
||||
expect(lerp(100, -100, 0.5)).toBe(0);
|
||||
});
|
||||
});
|
386
test/unit-tests/utils/AutoDiscoveryUtils-test.tsx
Normal file
386
test/unit-tests/utils/AutoDiscoveryUtils-test.tsx
Normal file
|
@ -0,0 +1,386 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { AutoDiscovery, AutoDiscoveryAction, ClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
|
||||
import AutoDiscoveryUtils from "../../src/utils/AutoDiscoveryUtils";
|
||||
import { mockOpenIdConfiguration } from "../test-utils/oidc";
|
||||
|
||||
describe("AutoDiscoveryUtils", () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.catch({
|
||||
status: 404,
|
||||
body: '{"errcode": "M_UNRECOGNIZED", "error": "Unrecognized request"}',
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildValidatedConfigFromDiscovery()", () => {
|
||||
const serverName = "my-server";
|
||||
|
||||
beforeEach(() => {
|
||||
// don't litter console with expected errors
|
||||
jest.spyOn(logger, "error")
|
||||
.mockClear()
|
||||
.mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
const validIsConfig = {
|
||||
"m.identity_server": {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
base_url: "identity.com",
|
||||
},
|
||||
};
|
||||
const validHsConfig = {
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
base_url: "https://matrix.org",
|
||||
},
|
||||
};
|
||||
|
||||
const expectedValidatedConfig = {
|
||||
hsName: serverName,
|
||||
hsNameIsDifferent: true,
|
||||
hsUrl: "https://matrix.org",
|
||||
isDefault: false,
|
||||
isNameResolvable: true,
|
||||
isUrl: "identity.com",
|
||||
};
|
||||
|
||||
it("throws an error when discovery result is falsy", async () => {
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, undefined as any),
|
||||
).rejects.toThrow("Unexpected error resolving homeserver configuration");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws an error when discovery result does not include homeserver config", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
} as unknown as ClientConfig;
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Unexpected error resolving homeserver configuration");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws an error when identity server config has fail error and recognised error string", async () => {
|
||||
const discoveryResult = {
|
||||
...validHsConfig,
|
||||
"m.identity_server": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: "GenericFailure",
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Unexpected error resolving identity server configuration");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws an error when homeserver config has fail error and recognised error string", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Homeserver URL does not appear to be a valid Matrix homeserver");
|
||||
expect(logger.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws an error with fallback message identity server config has fail error", async () => {
|
||||
const discoveryResult = {
|
||||
...validHsConfig,
|
||||
"m.identity_server": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Unexpected error resolving identity server configuration");
|
||||
});
|
||||
|
||||
it("throws an error when error is ERROR_INVALID_HOMESERVER", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Homeserver URL does not appear to be a valid Matrix homeserver");
|
||||
});
|
||||
|
||||
it("throws an error when homeserver base_url is falsy", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
base_url: "",
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Unexpected error resolving homeserver configuration");
|
||||
expect(logger.error).toHaveBeenCalledWith("No homeserver URL configured");
|
||||
});
|
||||
|
||||
it("throws an error when homeserver base_url is not a valid URL", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.SUCCESS,
|
||||
base_url: "banana",
|
||||
},
|
||||
};
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).rejects.toThrow("Invalid URL: banana");
|
||||
});
|
||||
|
||||
it("uses hs url hostname when serverName is falsy in args and config", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
...validHsConfig,
|
||||
};
|
||||
await expect(AutoDiscoveryUtils.buildValidatedConfigFromDiscovery("", discoveryResult)).resolves.toEqual({
|
||||
...expectedValidatedConfig,
|
||||
hsNameIsDifferent: false,
|
||||
hsName: "matrix.org",
|
||||
warning: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses serverName from props", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
...validHsConfig["m.homeserver"],
|
||||
server_name: "should not use this name",
|
||||
},
|
||||
};
|
||||
const syntaxOnly = true;
|
||||
await expect(
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
|
||||
).resolves.toEqual({
|
||||
...expectedValidatedConfig,
|
||||
hsNameIsDifferent: true,
|
||||
hsName: serverName,
|
||||
warning: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores liveliness error when checking syntax only", async () => {
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
...validHsConfig["m.homeserver"],
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
|
||||
},
|
||||
};
|
||||
const syntaxOnly = true;
|
||||
await expect(
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
|
||||
).resolves.toEqual({
|
||||
...expectedValidatedConfig,
|
||||
warning: "Homeserver URL does not appear to be a valid Matrix homeserver",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles homeserver too old error", async () => {
|
||||
const discoveryResult: ClientConfig = {
|
||||
...validIsConfig,
|
||||
"m.homeserver": {
|
||||
state: AutoDiscoveryAction.FAIL_ERROR,
|
||||
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
|
||||
base_url: "https://matrix.org",
|
||||
},
|
||||
};
|
||||
const syntaxOnly = true;
|
||||
await expect(() =>
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, syntaxOnly),
|
||||
).rejects.toThrow(
|
||||
"Your homeserver is too old and does not support the minimum API version required. Please contact your server owner, or upgrade your server.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should validate delegated oidc auth", async () => {
|
||||
const issuer = "https://auth.matrix.org/";
|
||||
fetchMock.get(
|
||||
`${validHsConfig["m.homeserver"].base_url}/_matrix/client/unstable/org.matrix.msc2965/auth_issuer`,
|
||||
{
|
||||
issuer,
|
||||
},
|
||||
);
|
||||
fetchMock.get(`${issuer}.well-known/openid-configuration`, {
|
||||
...mockOpenIdConfiguration(issuer),
|
||||
"scopes_supported": ["openid", "email"],
|
||||
"response_modes_supported": ["form_post", "query", "fragment"],
|
||||
"token_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none",
|
||||
],
|
||||
"token_endpoint_auth_signing_alg_values_supported": [
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES256K",
|
||||
],
|
||||
"revocation_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none",
|
||||
],
|
||||
"revocation_endpoint_auth_signing_alg_values_supported": [
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES256K",
|
||||
],
|
||||
"introspection_endpoint": `${issuer}oauth2/introspect`,
|
||||
"introspection_endpoint_auth_methods_supported": [
|
||||
"client_secret_basic",
|
||||
"client_secret_post",
|
||||
"client_secret_jwt",
|
||||
"private_key_jwt",
|
||||
"none",
|
||||
],
|
||||
"introspection_endpoint_auth_signing_alg_values_supported": [
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES256K",
|
||||
],
|
||||
"userinfo_endpoint": `${issuer}oauth2/userinfo`,
|
||||
"subject_types_supported": ["public"],
|
||||
"id_token_signing_alg_values_supported": [
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256K",
|
||||
],
|
||||
"userinfo_signing_alg_values_supported": [
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256K",
|
||||
],
|
||||
"display_values_supported": ["page"],
|
||||
"claim_types_supported": ["normal"],
|
||||
"claims_supported": ["iss", "sub", "aud", "iat", "exp", "nonce", "auth_time", "at_hash", "c_hash"],
|
||||
"claims_parameter_supported": false,
|
||||
"request_parameter_supported": false,
|
||||
"request_uri_parameter_supported": false,
|
||||
"prompt_values_supported": ["none", "login", "create"],
|
||||
"device_authorization_endpoint": `${issuer}oauth2/device`,
|
||||
"org.matrix.matrix-authentication-service.graphql_endpoint": `${issuer}graphql`,
|
||||
"account_management_uri": `${issuer}account/`,
|
||||
"account_management_actions_supported": [
|
||||
"org.matrix.profile",
|
||||
"org.matrix.sessions_list",
|
||||
"org.matrix.session_view",
|
||||
"org.matrix.session_end",
|
||||
"org.matrix.cross_signing_reset",
|
||||
],
|
||||
});
|
||||
fetchMock.get(`${issuer}jwks`, {
|
||||
keys: [],
|
||||
});
|
||||
|
||||
const discoveryResult = {
|
||||
...validIsConfig,
|
||||
...validHsConfig,
|
||||
};
|
||||
await expect(
|
||||
AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult),
|
||||
).resolves.toEqual({
|
||||
...expectedValidatedConfig,
|
||||
hsNameIsDifferent: true,
|
||||
hsName: serverName,
|
||||
delegatedAuthentication: expect.objectContaining({
|
||||
accountManagementActionsSupported: [
|
||||
"org.matrix.profile",
|
||||
"org.matrix.sessions_list",
|
||||
"org.matrix.session_view",
|
||||
"org.matrix.session_end",
|
||||
"org.matrix.cross_signing_reset",
|
||||
],
|
||||
accountManagementEndpoint: "https://auth.matrix.org/account/",
|
||||
authorizationEndpoint: "https://auth.matrix.org/auth",
|
||||
metadata: expect.objectContaining({
|
||||
issuer,
|
||||
}),
|
||||
registrationEndpoint: "https://auth.matrix.org/registration",
|
||||
signingKeys: [],
|
||||
tokenEndpoint: "https://auth.matrix.org/token",
|
||||
}),
|
||||
warning: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("authComponentStateForError", () => {
|
||||
const error = new Error("TEST");
|
||||
|
||||
it("should return expected error for the registration page", () => {
|
||||
expect(AutoDiscoveryUtils.authComponentStateForError(error, "register")).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
185
test/unit-tests/utils/DMRoomMap-test.ts
Normal file
185
test/unit-tests/utils/DMRoomMap-test.ts
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ClientEvent, EventType, IContent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
import { mkEvent, stubClient } from "../test-utils";
|
||||
describe("DMRoomMap", () => {
|
||||
const roomId1 = "!room1:example.com";
|
||||
const roomId2 = "!room2:example.com";
|
||||
const roomId3 = "!room3:example.com";
|
||||
const roomId4 = "!room4:example.com";
|
||||
|
||||
const validMDirectContent = {
|
||||
"user@example.com": [roomId1, roomId2],
|
||||
"@user:example.com": [roomId1, roomId3, roomId4],
|
||||
"@user2:example.com": [] as string[],
|
||||
} as IContent;
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let dmRoomMap: DMRoomMap;
|
||||
|
||||
const mkMDirectEvent = (content: any): MatrixEvent => {
|
||||
return mkEvent({
|
||||
event: true,
|
||||
type: EventType.Direct,
|
||||
user: client.getSafeUserId(),
|
||||
content: content,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client = mocked(stubClient());
|
||||
jest.spyOn(logger, "warn");
|
||||
});
|
||||
|
||||
describe("when m.direct has valid content", () => {
|
||||
beforeEach(() => {
|
||||
client.getAccountData.mockReturnValue(mkMDirectEvent(validMDirectContent));
|
||||
dmRoomMap = new DMRoomMap(client);
|
||||
dmRoomMap.start();
|
||||
});
|
||||
|
||||
it("getRoomIds should return the room Ids", () => {
|
||||
expect(dmRoomMap.getRoomIds()).toEqual(new Set([roomId1, roomId2, roomId3, roomId4]));
|
||||
});
|
||||
|
||||
describe("and there is an update with valid data", () => {
|
||||
beforeEach(() => {
|
||||
client.emit(
|
||||
ClientEvent.AccountData,
|
||||
mkMDirectEvent({
|
||||
"@user:example.com": [roomId1, roomId3],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("getRoomIds should return the new room Ids", () => {
|
||||
expect(dmRoomMap.getRoomIds()).toEqual(new Set([roomId1, roomId3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("and there is an update with invalid data", () => {
|
||||
const partiallyInvalidContent = {
|
||||
"@user1:example.com": [roomId1, roomId3],
|
||||
"@user2:example.com": "room2, room3",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.emit(ClientEvent.AccountData, mkMDirectEvent(partiallyInvalidContent));
|
||||
});
|
||||
|
||||
it("getRoomIds should return the valid room Ids", () => {
|
||||
expect(dmRoomMap.getRoomIds()).toEqual(new Set([roomId1, roomId3]));
|
||||
});
|
||||
|
||||
it("should log the invalid content", () => {
|
||||
expect(logger.warn).toHaveBeenCalledWith("Invalid m.direct content occurred", partiallyInvalidContent);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when m.direct content contains the entire event", () => {
|
||||
const mDirectContentContent = {
|
||||
type: EventType.Direct,
|
||||
content: validMDirectContent,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.getAccountData.mockReturnValue(mkMDirectEvent(mDirectContentContent));
|
||||
dmRoomMap = new DMRoomMap(client);
|
||||
});
|
||||
|
||||
it("should log the invalid content", () => {
|
||||
expect(logger.warn).toHaveBeenCalledWith("Invalid m.direct content occurred", mDirectContentContent);
|
||||
});
|
||||
|
||||
it("getRoomIds should return an empty list", () => {
|
||||
expect(dmRoomMap.getRoomIds()).toEqual(new Set([]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("when partially crap m.direct content appears", () => {
|
||||
const partiallyCrapContent = {
|
||||
"hello": 23,
|
||||
"@user1:example.com": [] as string[],
|
||||
"@user2:example.com": [roomId1, roomId2],
|
||||
"@user3:example.com": "room1, room2, room3",
|
||||
"@user4:example.com": [roomId4],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.getAccountData.mockReturnValue(mkMDirectEvent(partiallyCrapContent));
|
||||
dmRoomMap = new DMRoomMap(client);
|
||||
});
|
||||
|
||||
it("should log the invalid content", () => {
|
||||
expect(logger.warn).toHaveBeenCalledWith("Invalid m.direct content occurred", partiallyCrapContent);
|
||||
});
|
||||
|
||||
it("getRoomIds should only return the valid items", () => {
|
||||
expect(dmRoomMap.getRoomIds()).toEqual(new Set([roomId1, roomId2, roomId4]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUniqueRoomsWithIndividuals()", () => {
|
||||
const bigRoom = {
|
||||
roomId: "!bigRoom:server.org",
|
||||
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(5000),
|
||||
} as unknown as Room;
|
||||
const dmWithBob = {
|
||||
roomId: "!dmWithBob:server.org",
|
||||
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(2),
|
||||
} as unknown as Room;
|
||||
const dmWithCharlie = {
|
||||
roomId: "!dmWithCharlie:server.org",
|
||||
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(2),
|
||||
} as unknown as Room;
|
||||
const smallRoom = {
|
||||
roomId: "!smallRoom:server.org",
|
||||
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(3),
|
||||
} as unknown as Room;
|
||||
|
||||
const mDirectContent = {
|
||||
"@bob:server.org": [bigRoom.roomId, dmWithBob.roomId, smallRoom.roomId],
|
||||
"@charlie:server.org": [dmWithCharlie.roomId, smallRoom.roomId],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
client.getAccountData.mockReturnValue(mkMDirectEvent(mDirectContent));
|
||||
client.getRoom.mockImplementation(
|
||||
(roomId: string) =>
|
||||
[bigRoom, smallRoom, dmWithCharlie, dmWithBob].find((room) => room.roomId === roomId) ?? null,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an empty object when room map has not been populated", () => {
|
||||
const instance = new DMRoomMap(client);
|
||||
expect(instance.getUniqueRoomsWithIndividuals()).toEqual({});
|
||||
});
|
||||
|
||||
it("returns map of users to rooms with 2 members", () => {
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
dmRoomMap.start();
|
||||
expect(dmRoomMap.getUniqueRoomsWithIndividuals()).toEqual({
|
||||
"@bob:server.org": dmWithBob,
|
||||
"@charlie:server.org": dmWithCharlie,
|
||||
});
|
||||
});
|
||||
|
||||
it("excludes rooms that are not found by matrixClient", () => {
|
||||
client.getRoom.mockReset().mockReturnValue(null);
|
||||
const dmRoomMap = new DMRoomMap(client);
|
||||
dmRoomMap.start();
|
||||
expect(dmRoomMap.getUniqueRoomsWithIndividuals()).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
392
test/unit-tests/utils/DateUtils-test.ts
Normal file
392
test/unit-tests/utils/DateUtils-test.ts
Normal file
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
formatSeconds,
|
||||
formatRelativeTime,
|
||||
formatDuration,
|
||||
formatFullDateNoDayISO,
|
||||
formatDateForInput,
|
||||
formatTimeLeft,
|
||||
formatPreciseDuration,
|
||||
formatLocalDateShort,
|
||||
getDaysArray,
|
||||
getMonthsArray,
|
||||
formatFullDateNoDayNoTime,
|
||||
formatTime,
|
||||
formatFullTime,
|
||||
formatFullDate,
|
||||
formatFullDateNoTime,
|
||||
formatDate,
|
||||
HOUR_MS,
|
||||
MINUTE_MS,
|
||||
DAY_MS,
|
||||
} from "../../src/DateUtils";
|
||||
import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../test-utils";
|
||||
import * as languageHandler from "../../src/languageHandler";
|
||||
|
||||
describe("getDaysArray", () => {
|
||||
it("should return Sunday-Saturday in long mode", () => {
|
||||
expect(getDaysArray("long")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return Sun-Sat in short mode", () => {
|
||||
expect(getDaysArray("short")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Sun",
|
||||
"Mon",
|
||||
"Tue",
|
||||
"Wed",
|
||||
"Thu",
|
||||
"Fri",
|
||||
"Sat",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return S-S in narrow mode", () => {
|
||||
expect(getDaysArray("narrow")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"S",
|
||||
"M",
|
||||
"T",
|
||||
"W",
|
||||
"T",
|
||||
"F",
|
||||
"S",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonthsArray", () => {
|
||||
it("should return January-December in long mode", () => {
|
||||
expect(getMonthsArray("long")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return Jan-Dec in short mode", () => {
|
||||
expect(getMonthsArray("short")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return J-D in narrow mode", () => {
|
||||
expect(getMonthsArray("narrow")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"J",
|
||||
"F",
|
||||
"M",
|
||||
"A",
|
||||
"M",
|
||||
"J",
|
||||
"J",
|
||||
"A",
|
||||
"S",
|
||||
"O",
|
||||
"N",
|
||||
"D",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return 1-12 in numeric mode", () => {
|
||||
expect(getMonthsArray("numeric")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
]
|
||||
`);
|
||||
});
|
||||
it("should return 01-12 in 2-digit mode", () => {
|
||||
expect(getMonthsArray("2-digit")).toMatchInlineSnapshot(`
|
||||
[
|
||||
"01",
|
||||
"02",
|
||||
"03",
|
||||
"04",
|
||||
"05",
|
||||
"06",
|
||||
"07",
|
||||
"08",
|
||||
"09",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDate", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.setSystemTime(jest.getRealSystemTime());
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should return time string if date is within same day", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"19:10"`);
|
||||
});
|
||||
|
||||
it("should return time string with weekday if date is within last 6 days", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 6 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Fri 19:10"`);
|
||||
});
|
||||
|
||||
it("should return time & date string without year if it is within the same year", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 66 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Mon, 12 Sept, 19:10"`);
|
||||
});
|
||||
|
||||
it("should return full time & date string otherwise", () => {
|
||||
const date = new Date(REPEATABLE_DATE.getTime() - 666 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
|
||||
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Wed, 20 Jan 2021, 19:10"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoTime", () => {
|
||||
it("should match given locale en-GB", () => {
|
||||
expect(formatFullDateNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"Thu, 17 Nov 2022"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDate", () => {
|
||||
it("correctly formats with seconds", () => {
|
||||
expect(formatFullDate(REPEATABLE_DATE, true, true, "en-GB")).toMatchInlineSnapshot(
|
||||
`"Thu, 17 Nov 2022, 4:58:32 pm"`,
|
||||
);
|
||||
});
|
||||
it("correctly formats without seconds", () => {
|
||||
expect(formatFullDate(REPEATABLE_DATE, false, false, "en-GB")).toMatchInlineSnapshot(
|
||||
`"Thu, 17 Nov 2022, 16:58"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullTime", () => {
|
||||
it("correctly formats 12 hour mode", () => {
|
||||
expect(formatFullTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58:32 pm"`);
|
||||
});
|
||||
it("correctly formats 24 hour mode", () => {
|
||||
expect(formatFullTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58:32"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTime", () => {
|
||||
it("correctly formats 12 hour mode", () => {
|
||||
expect(formatTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58 pm"`);
|
||||
});
|
||||
it("correctly formats 24 hour mode", () => {
|
||||
expect(formatTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSeconds", () => {
|
||||
it("correctly formats time with hours", () => {
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 31 + 55)).toBe("03:31:55");
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 0 + 55)).toBe("03:00:55");
|
||||
expect(formatSeconds(60 * 60 * 3 + 60 * 31 + 0)).toBe("03:31:00");
|
||||
expect(formatSeconds(-(60 * 60 * 3 + 60 * 31 + 0))).toBe("-03:31:00");
|
||||
});
|
||||
|
||||
it("correctly formats time without hours", () => {
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 31 + 55)).toBe("31:55");
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 0 + 55)).toBe("00:55");
|
||||
expect(formatSeconds(60 * 60 * 0 + 60 * 31 + 0)).toBe("31:00");
|
||||
expect(formatSeconds(-(60 * 60 * 0 + 60 * 31 + 0))).toBe("-31:00");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRelativeTime", () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
// Tuesday, 2 November 2021 11:18:03 UTC
|
||||
jest.setSystemTime(1635851883000);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.setSystemTime(jest.getRealSystemTime());
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns hour format for events created in the same day", () => {
|
||||
// Tuesday, 2 November 2021 11:01:00 UTC
|
||||
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("11:01");
|
||||
});
|
||||
|
||||
it("returns month and day for events created less than 24h ago but on a different day", () => {
|
||||
// Monday, 1 November 2021 23:01:00 UTC
|
||||
const date = new Date(2021, 10, 1, 23, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("Nov 1");
|
||||
});
|
||||
|
||||
it("honours the hour format setting", () => {
|
||||
const date = new Date(2021, 10, 2, 11, 1, 23, 0);
|
||||
expect(formatRelativeTime(date)).toBe("11:01");
|
||||
expect(formatRelativeTime(date, false)).toBe("11:01");
|
||||
expect(formatRelativeTime(date, true)).toBe("11:01 AM");
|
||||
});
|
||||
|
||||
it("returns month and day for events created in the current year", () => {
|
||||
const date = new Date(1632567741000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Sep 25");
|
||||
});
|
||||
|
||||
it("does not return a leading 0 for single digit days", () => {
|
||||
const date = new Date(1635764541000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Nov 1");
|
||||
});
|
||||
|
||||
it("appends the year for events created in previous years", () => {
|
||||
const date = new Date(1604142141000);
|
||||
expect(formatRelativeTime(date, true)).toBe("Oct 31, 2020");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDuration()", () => {
|
||||
type TestCase = [string, string, number];
|
||||
|
||||
const MINUTE_MS = 60000;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
it.each<TestCase>([
|
||||
["rounds up to nearest day when more than 24h - 40 hours", "2d", 40 * HOUR_MS],
|
||||
["rounds down to nearest day when more than 24h - 26 hours", "1d", 26 * HOUR_MS],
|
||||
["24 hours", "1d", 24 * HOUR_MS],
|
||||
["rounds to nearest hour when less than 24h - 23h", "23h", 23 * HOUR_MS],
|
||||
["rounds to nearest hour when less than 24h - 6h and 10min", "6h", 6 * HOUR_MS + 10 * MINUTE_MS],
|
||||
["rounds to nearest hours when less than 24h", "2h", 2 * HOUR_MS + 124234],
|
||||
["rounds to nearest minute when less than 1h - 59 minutes", "59m", 59 * MINUTE_MS],
|
||||
["rounds to nearest minute when less than 1h - 1 minute", "1m", MINUTE_MS],
|
||||
["rounds to nearest second when less than 1min - 59 seconds", "59s", 59000],
|
||||
["rounds to 0 seconds when less than a second - 123ms", "0s", 123],
|
||||
])("%s formats to %s", (_description, expectedResult, input) => {
|
||||
expect(formatDuration(input)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPreciseDuration", () => {
|
||||
const MINUTE_MS = 1000 * 60;
|
||||
const HOUR_MS = MINUTE_MS * 60;
|
||||
const DAY_MS = HOUR_MS * 24;
|
||||
|
||||
it.each<[string, string, number]>([
|
||||
["3 days, 6 hours, 48 minutes, 59 seconds", "3d 6h 48m 59s", 3 * DAY_MS + 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
|
||||
["6 hours, 48 minutes, 59 seconds", "6h 48m 59s", 6 * HOUR_MS + 48 * MINUTE_MS + 59000],
|
||||
["48 minutes, 59 seconds", "48m 59s", 48 * MINUTE_MS + 59000],
|
||||
["59 seconds", "59s", 59000],
|
||||
["0 seconds", "0s", 0],
|
||||
])("%s formats to %s", (_description, expectedResult, input) => {
|
||||
expect(formatPreciseDuration(input)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoDayISO", () => {
|
||||
it("should return ISO format", () => {
|
||||
expect(formatFullDateNoDayISO(REPEATABLE_DATE)).toEqual("2022-11-17T16:58:32.517Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatFullDateNoDayNoTime", () => {
|
||||
it("should return a date formatted for en-GB locale", () => {
|
||||
expect(formatFullDateNoDayNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"17/11/2022"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDateForInput", () => {
|
||||
it.each([["1993-11-01"], ["1066-10-14"], ["0571-04-22"], ["0062-02-05"]])(
|
||||
"should format %s",
|
||||
(dateString: string) => {
|
||||
expect(formatDateForInput(new Date(dateString))).toBe(dateString);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("formatTimeLeft", () => {
|
||||
it.each([
|
||||
[0, "0s left"],
|
||||
[23, "23s left"],
|
||||
[60 + 23, "1m 23s left"],
|
||||
[60 * 60, "1h 0m 0s left"],
|
||||
[60 * 60 + 23, "1h 0m 23s left"],
|
||||
[5 * 60 * 60 + 7 * 60 + 23, "5h 7m 23s left"],
|
||||
])("should format %s to %s", (seconds: number, expected: string) => {
|
||||
expect(formatTimeLeft(seconds)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatLocalDateShort()", () => {
|
||||
afterAll(() => {
|
||||
unmockIntlDateTimeFormat();
|
||||
});
|
||||
const timestamp = new Date("Fri Dec 17 2021 09:09:00 GMT+0100 (Central European Standard Time)").getTime();
|
||||
it("formats date correctly by locale", () => {
|
||||
const locale = jest.spyOn(languageHandler, "getUserLanguage");
|
||||
mockIntlDateTimeFormat();
|
||||
|
||||
// format is DD/MM/YY
|
||||
locale.mockReturnValue("en-GB");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("17/12/21");
|
||||
|
||||
// US date format is MM/DD/YY
|
||||
locale.mockReturnValue("en-US");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("12/17/21");
|
||||
|
||||
locale.mockReturnValue("de-DE");
|
||||
expect(formatLocalDateShort(timestamp)).toEqual("17.12.21");
|
||||
});
|
||||
});
|
174
test/unit-tests/utils/ErrorUtils-test.ts
Normal file
174
test/unit-tests/utils/ErrorUtils-test.ts
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ReactElement } from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
adminContactStrings,
|
||||
messageForConnectionError,
|
||||
messageForLoginError,
|
||||
messageForResourceLimitError,
|
||||
messageForSyncError,
|
||||
resourceLimitStrings,
|
||||
} from "../../src/utils/ErrorUtils";
|
||||
|
||||
describe("messageForResourceLimitError", () => {
|
||||
it("should match snapshot for monthly_active_user", () => {
|
||||
const { asFragment } = render(
|
||||
messageForResourceLimitError("monthly_active_user", "some@email", resourceLimitStrings) as ReactElement,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for admin contact links", () => {
|
||||
const { asFragment } = render(
|
||||
messageForResourceLimitError("", "some@email", adminContactStrings) as ReactElement,
|
||||
);
|
||||
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForSyncError", () => {
|
||||
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: {
|
||||
limit_type: "monthly_active_user",
|
||||
admin_contact: "some@email",
|
||||
},
|
||||
});
|
||||
const { asFragment } = render(messageForSyncError(err) as ReactElement);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for other errors", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "OTHER_ERROR",
|
||||
});
|
||||
const { asFragment } = render(messageForSyncError(err) as ReactElement);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForLoginError", () => {
|
||||
it("should match snapshot for M_RESOURCE_LIMIT_EXCEEDED", () => {
|
||||
const err = new MatrixError({
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
data: {
|
||||
limit_type: "monthly_active_user",
|
||||
admin_contact: "some@email",
|
||||
},
|
||||
});
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for M_USER_DEACTIVATED", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "M_USER_DEACTIVATED",
|
||||
},
|
||||
403,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for 401", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "UNKNOWN",
|
||||
},
|
||||
401,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for unknown error", () => {
|
||||
const err = new MatrixError({}, 400);
|
||||
const { asFragment } = render(
|
||||
messageForLoginError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("messageForConnectionError", () => {
|
||||
it("should match snapshot for ConnectionError", () => {
|
||||
const err = new ConnectionError("Internal Server Error", new MatrixError({}, 500));
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for MatrixError M_NOT_FOUND", () => {
|
||||
const err = new MatrixError(
|
||||
{
|
||||
errcode: "M_NOT_FOUND",
|
||||
},
|
||||
404,
|
||||
);
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for unknown error", () => {
|
||||
const err = new Error("What even");
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "hsUrl",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should match snapshot for mixed content error", () => {
|
||||
const err = new ConnectionError("Mixed content maybe?");
|
||||
Object.defineProperty(window, "location", { value: { protocol: "https:" } });
|
||||
const { asFragment } = render(
|
||||
messageForConnectionError(err, {
|
||||
hsUrl: "http://server.com",
|
||||
hsName: "hsName",
|
||||
}) as ReactElement,
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
46
test/unit-tests/utils/EventRenderingUtils-test.ts
Normal file
46
test/unit-tests/utils/EventRenderingUtils-test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { getEventDisplayInfo } from "../../src/utils/EventRenderingUtils";
|
||||
import { VoiceBroadcastInfoState } from "../../src/voice-broadcast";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
|
||||
import { createTestClient } from "../test-utils";
|
||||
|
||||
describe("getEventDisplayInfo", () => {
|
||||
const mkBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
|
||||
return mkVoiceBroadcastInfoStateEvent("!room:example.com", state, "@user:example.com", "ASD123");
|
||||
};
|
||||
|
||||
it("should return the expected value for a broadcast started event", () => {
|
||||
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Started), false))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"hasRenderer": true,
|
||||
"isBubbleMessage": false,
|
||||
"isInfoMessage": false,
|
||||
"isLeftAlignedBubbleMessage": false,
|
||||
"isSeeingThroughMessageHiddenForModeration": false,
|
||||
"noBubbleEvent": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it("should return the expected value for a broadcast stopped event", () => {
|
||||
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped), false))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"hasRenderer": true,
|
||||
"isBubbleMessage": false,
|
||||
"isInfoMessage": true,
|
||||
"isLeftAlignedBubbleMessage": false,
|
||||
"isSeeingThroughMessageHiddenForModeration": false,
|
||||
"noBubbleEvent": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
478
test/unit-tests/utils/EventUtils-test.ts
Normal file
478
test/unit-tests/utils/EventUtils-test.ts
Normal file
|
@ -0,0 +1,478 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
M_LOCATION,
|
||||
EventStatus,
|
||||
EventType,
|
||||
IEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
PendingEventOrdering,
|
||||
RelationType,
|
||||
Room,
|
||||
Thread,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import {
|
||||
canCancel,
|
||||
canEditContent,
|
||||
canEditOwnEvent,
|
||||
fetchInitialEvent,
|
||||
findEditableEvent,
|
||||
highlightEvent,
|
||||
isContentActionable,
|
||||
isLocationEvent,
|
||||
isVoiceMessage,
|
||||
} from "../../src/utils/EventUtils";
|
||||
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../test-utils";
|
||||
import dis from "../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../src/dispatcher/actions";
|
||||
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
|
||||
import { VoiceBroadcastInfoState } from "../../src/voice-broadcast/types";
|
||||
|
||||
jest.mock("../../src/dispatcher/dispatcher");
|
||||
|
||||
describe("EventUtils", () => {
|
||||
const userId = "@user:server";
|
||||
const roomId = "!room:server";
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getUserId.mockClear().mockReturnValue(userId);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.spyOn(MatrixClientPeg, "get").mockRestore();
|
||||
});
|
||||
|
||||
// setup events
|
||||
const unsentEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
});
|
||||
unsentEvent.status = EventStatus.ENCRYPTING;
|
||||
|
||||
const redactedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
});
|
||||
redactedEvent.makeRedacted(
|
||||
redactedEvent,
|
||||
new Room(redactedEvent.getRoomId()!, mockClient, mockClient.getUserId()!),
|
||||
);
|
||||
|
||||
const stateEvent = new MatrixEvent({
|
||||
type: EventType.RoomTopic,
|
||||
state_key: "",
|
||||
});
|
||||
const beaconInfoEvent = makeBeaconInfoEvent(userId, roomId);
|
||||
|
||||
const roomMemberEvent = new MatrixEvent({
|
||||
type: EventType.RoomMember,
|
||||
sender: userId,
|
||||
});
|
||||
|
||||
const stickerEvent = new MatrixEvent({
|
||||
type: EventType.Sticker,
|
||||
sender: userId,
|
||||
});
|
||||
|
||||
const pollStartEvent = makePollStartEvent("What?", userId);
|
||||
|
||||
const notDecryptedEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
},
|
||||
});
|
||||
|
||||
const noMsgType = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const noContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Image,
|
||||
},
|
||||
});
|
||||
|
||||
const emptyContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "",
|
||||
},
|
||||
});
|
||||
|
||||
const objectContentBody = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.File,
|
||||
body: {},
|
||||
},
|
||||
});
|
||||
|
||||
const niceTextMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
|
||||
const bobsTextMessage = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@bob:server",
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello from Bob",
|
||||
},
|
||||
});
|
||||
|
||||
const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
|
||||
"!room:example.com",
|
||||
VoiceBroadcastInfoState.Started,
|
||||
"@user:example.com",
|
||||
"ABC123",
|
||||
);
|
||||
|
||||
const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
|
||||
"!room:example.com",
|
||||
VoiceBroadcastInfoState.Stopped,
|
||||
"@user:example.com",
|
||||
"ABC123",
|
||||
);
|
||||
|
||||
describe("isContentActionable()", () => {
|
||||
type TestCase = [string, MatrixEvent];
|
||||
it.each<TestCase>([
|
||||
["unsent event", unsentEvent],
|
||||
["redacted event", redactedEvent],
|
||||
["state event", stateEvent],
|
||||
["undecrypted event", notDecryptedEvent],
|
||||
["room member event", roomMemberEvent],
|
||||
["event without msgtype", noMsgType],
|
||||
["event without content body property", noContentBody],
|
||||
["broadcast stop event", voiceBroadcastStop],
|
||||
])("returns false for %s", (_description, event) => {
|
||||
expect(isContentActionable(event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>([
|
||||
["sticker event", stickerEvent],
|
||||
["poll start event", pollStartEvent],
|
||||
["event with empty content body", emptyContentBody],
|
||||
["event with a content body", niceTextMessage],
|
||||
["beacon_info event", beaconInfoEvent],
|
||||
["broadcast start event", voiceBroadcastStart],
|
||||
])("returns true for %s", (_description, event) => {
|
||||
expect(isContentActionable(event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("editable content helpers", () => {
|
||||
const replaceRelationEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Replace,
|
||||
event_id: "1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const referenceRelationEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Text,
|
||||
body: "Hello",
|
||||
["m.relates_to"]: {
|
||||
rel_type: RelationType.Reference,
|
||||
event_id: "1",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const emoteEvent = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
msgtype: MsgType.Emote,
|
||||
body: "🧪",
|
||||
},
|
||||
});
|
||||
|
||||
type TestCase = [string, MatrixEvent];
|
||||
|
||||
const uneditableCases: TestCase[] = [
|
||||
["redacted event", redactedEvent],
|
||||
["state event", stateEvent],
|
||||
["event that is not room message", roomMemberEvent],
|
||||
["event without msgtype", noMsgType],
|
||||
["event without content body property", noContentBody],
|
||||
["event with empty content body property", emptyContentBody],
|
||||
["event with non-string body", objectContentBody],
|
||||
["event not sent by current user", bobsTextMessage],
|
||||
["event with a replace relation", replaceRelationEvent],
|
||||
];
|
||||
|
||||
const editableCases: TestCase[] = [
|
||||
["event with reference relation", referenceRelationEvent],
|
||||
["emote event", emoteEvent],
|
||||
["poll start event", pollStartEvent],
|
||||
["event with a content body", niceTextMessage],
|
||||
];
|
||||
|
||||
describe("canEditContent()", () => {
|
||||
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
|
||||
expect(canEditContent(mockClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
|
||||
expect(canEditContent(mockClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
describe("canEditOwnContent()", () => {
|
||||
it.each<TestCase>(uneditableCases)("returns false for %s", (_description, event) => {
|
||||
expect(canEditOwnEvent(mockClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
it.each<TestCase>(editableCases)("returns true for %s", (_description, event) => {
|
||||
expect(canEditOwnEvent(mockClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isVoiceMessage()", () => {
|
||||
it("returns true for an event with msc2516.voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
["org.matrix.msc2516.voice"]: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for an event with msc3245.voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
["org.matrix.msc3245.voice"]: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for an event with voice content", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "hello",
|
||||
},
|
||||
});
|
||||
|
||||
expect(isVoiceMessage(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isLocationEvent()", () => {
|
||||
it("returns true for an event with m.location stable type", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: M_LOCATION.altName,
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for an event with m.location unstable prefixed type", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: M_LOCATION.name,
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for a room message with stable m.location msgtype", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: M_LOCATION.altName,
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns true for a room message with unstable m.location msgtype", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
msgtype: M_LOCATION.name,
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(true);
|
||||
});
|
||||
it("returns false for a non location event", () => {
|
||||
const event = new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Hello",
|
||||
},
|
||||
});
|
||||
expect(isLocationEvent(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canCancel()", () => {
|
||||
it.each([[EventStatus.QUEUED], [EventStatus.NOT_SENT], [EventStatus.ENCRYPTING]])(
|
||||
"return true for status %s",
|
||||
(status) => {
|
||||
expect(canCancel(status)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[EventStatus.SENDING],
|
||||
[EventStatus.CANCELLED],
|
||||
[EventStatus.SENT],
|
||||
["invalid-status" as unknown as EventStatus],
|
||||
])("return false for status %s", (status) => {
|
||||
expect(canCancel(status)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchInitialEvent", () => {
|
||||
const ROOM_ID = "!roomId:example.org";
|
||||
let room: Room;
|
||||
let client: MatrixClient;
|
||||
|
||||
const NORMAL_EVENT = "$normalEvent";
|
||||
const THREAD_ROOT = "$threadRoot";
|
||||
const THREAD_REPLY = "$threadReply";
|
||||
|
||||
const events: Record<string, Partial<IEvent>> = {
|
||||
[NORMAL_EVENT]: {
|
||||
event_id: NORMAL_EVENT,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Classic event",
|
||||
msgtype: MsgType.Text,
|
||||
},
|
||||
},
|
||||
[THREAD_ROOT]: {
|
||||
event_id: THREAD_ROOT,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "Thread root",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
unsigned: {
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
latest_event: {
|
||||
event_id: THREAD_REPLY,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
event_id: "$threadRoot",
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
},
|
||||
count: 1,
|
||||
current_user_participated: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[THREAD_REPLY]: {
|
||||
event_id: THREAD_REPLY,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"msgtype": MsgType.Text,
|
||||
"m.relates_to": {
|
||||
event_id: THREAD_ROOT,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
|
||||
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "supportsThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(room);
|
||||
jest.spyOn(client, "fetchRoomEvent").mockImplementation(async (roomId, eventId) => {
|
||||
return events[eventId] ?? Promise.reject();
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for unknown events", async () => {
|
||||
expect(await fetchInitialEvent(client, room.roomId, "$UNKNOWN")).toBeNull();
|
||||
expect(await fetchInitialEvent(client, room.roomId, NORMAL_EVENT)).toBeInstanceOf(MatrixEvent);
|
||||
});
|
||||
|
||||
it("creates a thread when needed", async () => {
|
||||
await fetchInitialEvent(client, room.roomId, THREAD_REPLY);
|
||||
expect(room.getThread(THREAD_ROOT)).toBeInstanceOf(Thread);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findEditableEvent", () => {
|
||||
it("should not explode when given empty events array", () => {
|
||||
expect(
|
||||
findEditableEvent({
|
||||
events: [],
|
||||
isForward: true,
|
||||
matrixClient: mockClient,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("highlightEvent", () => {
|
||||
const eventId = "$zLg9jResFQmMO_UKFeWpgLgOgyWrL8qIgLgZ5VywrCQ";
|
||||
|
||||
it("should dispatch an action to view the event", () => {
|
||||
highlightEvent(roomId, eventId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
event_id: eventId,
|
||||
highlighted: true,
|
||||
room_id: roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
38
test/unit-tests/utils/Feedback-test.ts
Normal file
38
test/unit-tests/utils/Feedback-test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import SdkConfig from "../../src/SdkConfig";
|
||||
import { shouldShowFeedback } from "../../src/utils/Feedback";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
|
||||
jest.mock("../../src/SdkConfig");
|
||||
jest.mock("../../src/settings/SettingsStore");
|
||||
|
||||
describe("shouldShowFeedback", () => {
|
||||
it("should return false if bug_report_endpoint_url is falsey", () => {
|
||||
mocked(SdkConfig).get.mockReturnValue({
|
||||
bug_report_endpoint_url: null,
|
||||
});
|
||||
expect(shouldShowFeedback()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if UIFeature.Feedback is disabled", () => {
|
||||
mocked(SettingsStore).getValue.mockReturnValue(false);
|
||||
expect(shouldShowFeedback()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true if bug_report_endpoint_url is set and UIFeature.Feedback is true", () => {
|
||||
mocked(SdkConfig).get.mockReturnValue({
|
||||
bug_report_endpoint_url: "https://rageshake.server",
|
||||
});
|
||||
mocked(SettingsStore).getValue.mockReturnValue(true);
|
||||
expect(shouldShowFeedback()).toBeTruthy();
|
||||
});
|
||||
});
|
60
test/unit-tests/utils/FileUtils-test.ts
Normal file
60
test/unit-tests/utils/FileUtils-test.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { downloadLabelForFile } from "../../src/utils/FileUtils.ts";
|
||||
|
||||
describe("FileUtils", () => {
|
||||
describe("downloadLabelForFile", () => {
|
||||
it.each([
|
||||
[
|
||||
"File with size",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.file",
|
||||
body: "Test",
|
||||
info: {
|
||||
size: 102434566,
|
||||
},
|
||||
} as MediaEventContent,
|
||||
output: "Download (97.69 MB)",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Image",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.image",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Video",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.video",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
[
|
||||
"Audio",
|
||||
{
|
||||
input: {
|
||||
msgtype: "m.audio",
|
||||
body: "Test",
|
||||
} as MediaEventContent,
|
||||
output: "Download",
|
||||
},
|
||||
],
|
||||
])("should correctly label %s", (_d, { input, output }) => expect(downloadLabelForFile(input)).toBe(output));
|
||||
});
|
||||
});
|
59
test/unit-tests/utils/FixedRollingArray-test.ts
Normal file
59
test/unit-tests/utils/FixedRollingArray-test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { FixedRollingArray } from "../../src/utils/FixedRollingArray";
|
||||
|
||||
describe("FixedRollingArray", () => {
|
||||
it("should seed the array with the given value", () => {
|
||||
const seed = "test";
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
expect(array.value.every((v) => v === seed)).toBe(true);
|
||||
});
|
||||
|
||||
it("should insert at the correct end", () => {
|
||||
const seed = "test";
|
||||
const value = "changed";
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
array.pushValue(value);
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
expect(array.value[0]).toBe(value);
|
||||
});
|
||||
|
||||
it("should roll over", () => {
|
||||
const seed = -1;
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
|
||||
const maxValue = width * 2;
|
||||
const minValue = width; // because we're forcing a rollover
|
||||
for (let i = 0; i <= maxValue; i++) {
|
||||
array.pushValue(i);
|
||||
}
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
|
||||
for (let i = 1; i < width; i++) {
|
||||
const current = array.value[i];
|
||||
const previous = array.value[i - 1];
|
||||
expect(previous - current).toBe(1);
|
||||
|
||||
if (i === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(previous).toBe(maxValue);
|
||||
} else if (i === width) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(current).toBe(minValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
95
test/unit-tests/utils/FormattingUtils-test.tsx
Normal file
95
test/unit-tests/utils/FormattingUtils-test.tsx
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
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
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
225
test/unit-tests/utils/LruCache-test.ts
Normal file
225
test/unit-tests/utils/LruCache-test.ts
Normal file
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { LruCache } from "../../src/utils/LruCache";
|
||||
|
||||
describe("LruCache", () => {
|
||||
it("when creating a cache with negative capacity it should raise an error", () => {
|
||||
expect(() => {
|
||||
new LruCache(-23);
|
||||
}).toThrow("Cache capacity must be at least 1");
|
||||
});
|
||||
|
||||
it("when creating a cache with 0 capacity it should raise an error", () => {
|
||||
expect(() => {
|
||||
new LruCache(0);
|
||||
}).toThrow("Cache capacity must be at least 1");
|
||||
});
|
||||
|
||||
describe("when there is a cache with a capacity of 3", () => {
|
||||
let cache: LruCache<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new LruCache(3);
|
||||
});
|
||||
|
||||
it("has() should return false", () => {
|
||||
expect(cache.has("a")).toBe(false);
|
||||
});
|
||||
|
||||
it("get() should return undefined", () => {
|
||||
expect(cache.get("a")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("values() should return an empty iterator", () => {
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
});
|
||||
|
||||
it("delete() should not raise an error", () => {
|
||||
cache.delete("a");
|
||||
});
|
||||
|
||||
describe("when the cache contains 2 items", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("a", "a value");
|
||||
cache.set("b", "b value");
|
||||
});
|
||||
|
||||
it("has() should return false for an item not in the cache", () => {
|
||||
expect(cache.has("c")).toBe(false);
|
||||
});
|
||||
|
||||
it("get() should return undefined for an item not in the cahce", () => {
|
||||
expect(cache.get("c")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("values() should return the items in the cache", () => {
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value"]);
|
||||
});
|
||||
|
||||
it("clear() should clear the cache", () => {
|
||||
cache.clear();
|
||||
expect(cache.has("a")).toBe(false);
|
||||
expect(cache.has("b")).toBe(false);
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
});
|
||||
|
||||
it("when an error occurs while setting an item the cache should be cleard", () => {
|
||||
jest.spyOn(logger, "warn");
|
||||
const err = new Error("Something weng wrong :(");
|
||||
|
||||
// @ts-ignore
|
||||
cache.safeSet = () => {
|
||||
throw err;
|
||||
};
|
||||
cache.set("c", "c value");
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith("LruCache error", err);
|
||||
});
|
||||
|
||||
describe("and adding another item", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("c", "c value");
|
||||
});
|
||||
|
||||
it("deleting an unkonwn item should not raise an error", () => {
|
||||
cache.delete("unknown");
|
||||
});
|
||||
|
||||
it("deleting the first item should work", () => {
|
||||
cache.delete("a");
|
||||
expect(Array.from(cache.values())).toEqual(["b value", "c value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["b value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting the item in the middle should work", () => {
|
||||
cache.delete("b");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting the last item should work", () => {
|
||||
cache.delete("c");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting all items should work", () => {
|
||||
cache.delete("a");
|
||||
cache.delete("b");
|
||||
cache.delete("c");
|
||||
// should not raise an error
|
||||
cache.delete("a");
|
||||
cache.delete("b");
|
||||
cache.delete("c");
|
||||
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["d value"]);
|
||||
});
|
||||
|
||||
it("deleting and adding some items should work", () => {
|
||||
cache.set("d", "d value");
|
||||
cache.get("b");
|
||||
cache.delete("b");
|
||||
cache.set("e", "e value");
|
||||
expect(Array.from(cache.values())).toEqual(["c value", "d value", "e value"]);
|
||||
});
|
||||
|
||||
describe("and accesing the first added item and adding another item", () => {
|
||||
beforeEach(() => {
|
||||
cache.get("a");
|
||||
cache.set("d", "d value");
|
||||
});
|
||||
|
||||
it("should contain the last recently accessed items", () => {
|
||||
expect(cache.has("a")).toBe(true);
|
||||
expect(cache.get("a")).toEqual("a value");
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.get("c")).toEqual("c value");
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.get("d")).toEqual("d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("should not contain the least recently accessed items", () => {
|
||||
expect(cache.has("b")).toBe(false);
|
||||
expect(cache.get("b")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and adding 2 additional items", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("d", "d value");
|
||||
cache.set("e", "e value");
|
||||
});
|
||||
|
||||
it("has() should return false for expired items", () => {
|
||||
expect(cache.has("a")).toBe(false);
|
||||
expect(cache.has("b")).toBe(false);
|
||||
});
|
||||
|
||||
it("has() should return true for items in the caceh", () => {
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.has("e")).toBe(true);
|
||||
});
|
||||
|
||||
it("get() should return undefined for expired items", () => {
|
||||
expect(cache.get("a")).toBeUndefined();
|
||||
expect(cache.get("b")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("get() should return the items in the cache", () => {
|
||||
expect(cache.get("c")).toBe("c value");
|
||||
expect(cache.get("d")).toBe("d value");
|
||||
expect(cache.get("e")).toBe("e value");
|
||||
});
|
||||
|
||||
it("values() should return the items in the cache", () => {
|
||||
expect(Array.from(cache.values())).toEqual(["c value", "d value", "e value"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the cache contains some items where one of them is a replacement", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("a", "a value");
|
||||
cache.set("b", "b value");
|
||||
cache.set("c", "c value");
|
||||
cache.set("a", "a value 2");
|
||||
cache.set("d", "d value");
|
||||
});
|
||||
|
||||
it("should contain the last recently set items", () => {
|
||||
expect(cache.has("a")).toBe(true);
|
||||
expect(cache.get("a")).toEqual("a value 2");
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.get("c")).toEqual("c value");
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.get("d")).toEqual("d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value 2", "c value", "d value"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
154
test/unit-tests/utils/MegolmExportEncryption-test.ts
Normal file
154
test/unit-tests/utils/MegolmExportEncryption-test.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { TextEncoder } from "util";
|
||||
import nodeCrypto from "crypto";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
|
||||
import type * as MegolmExportEncryptionExport from "../../src/utils/MegolmExportEncryption";
|
||||
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
function getRandomValues<T extends ArrayBufferView | null>(buf: T): T {
|
||||
// @ts-ignore fussy generics
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
}
|
||||
|
||||
const TEST_VECTORS = [
|
||||
[
|
||||
"plain",
|
||||
"password",
|
||||
"-----BEGIN MEGOLM SESSION DATA-----\n" +
|
||||
"AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx\n" +
|
||||
"cissyYBxjsfsAndErh065A8=\n" +
|
||||
"-----END MEGOLM SESSION DATA-----",
|
||||
],
|
||||
[
|
||||
"Hello, World",
|
||||
"betterpassword",
|
||||
"-----BEGIN MEGOLM SESSION DATA-----\n" +
|
||||
"AW1vcmVzYWx0bW9yZXNhbHT//////////wAAAAAAAAAAAAAD6KyBpe1Niv5M5NPm4ZATsJo5nghk\n" +
|
||||
"KYu63a0YQ5DRhUWEKk7CcMkrKnAUiZny\n" +
|
||||
"-----END MEGOLM SESSION DATA-----",
|
||||
],
|
||||
[
|
||||
"alphanumericallyalphanumericallyalphanumericallyalphanumerically",
|
||||
"SWORDFISH",
|
||||
"-----BEGIN MEGOLM SESSION DATA-----\n" +
|
||||
"AXllc3NhbHR5Z29vZG5lc3P//////////wAAAAAAAAAAAAAD6OIW+Je7gwvjd4kYrb+49gKCfExw\n" +
|
||||
"MgJBMD4mrhLkmgAngwR1pHjbWXaoGybtiAYr0moQ93GrBQsCzPbvl82rZhaXO3iH5uHo/RCEpOqp\n" +
|
||||
"Pgg29363BGR+/Ripq/VCLKGNbw==\n" +
|
||||
"-----END MEGOLM SESSION DATA-----",
|
||||
],
|
||||
[
|
||||
"alphanumericallyalphanumericallyalphanumericallyalphanumerically",
|
||||
"passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
|
||||
"passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
|
||||
"passwordpasswordpasswordpasswordpasswordpasswordpasswordpasswordpassword" +
|
||||
"passwordpasswordpasswordpasswordpassword",
|
||||
"-----BEGIN MEGOLM SESSION DATA-----\n" +
|
||||
"Af//////////////////////////////////////////AAAD6IAZJy7IQ7Y0idqSw/bmpngEEVVh\n" +
|
||||
"gsH+8ptgqxw6ZVWQnohr8JsuwH9SwGtiebZuBu5smPCO+RFVWH2cQYslZijXv/BEH/txvhUrrtCd\n" +
|
||||
"bWnSXS9oymiqwUIGs08sXI33ZA==\n" +
|
||||
"-----END MEGOLM SESSION DATA-----",
|
||||
],
|
||||
];
|
||||
|
||||
function stringToArray(s: string): ArrayBufferLike {
|
||||
return new TextEncoder().encode(s).buffer;
|
||||
}
|
||||
|
||||
describe("MegolmExportEncryption", function () {
|
||||
let MegolmExportEncryption: typeof MegolmExportEncryptionExport;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
getRandomValues,
|
||||
randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
MegolmExportEncryption = require("../../src/utils/MegolmExportEncryption");
|
||||
});
|
||||
|
||||
describe("decrypt", function () {
|
||||
it("should handle missing header", function () {
|
||||
const input = stringToArray(`-----`);
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(input, "").then(
|
||||
(res) => {
|
||||
throw new Error("expected to throw");
|
||||
},
|
||||
(error) => {
|
||||
expect(error.message).toEqual("Header line not found");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle missing trailer", function () {
|
||||
const input = stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
|
||||
-----`);
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(input, "").then(
|
||||
(res) => {
|
||||
throw new Error("expected to throw");
|
||||
},
|
||||
(error) => {
|
||||
expect(error.message).toEqual("Trailer line not found");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle a too-short body", function () {
|
||||
const input = stringToArray(`-----BEGIN MEGOLM SESSION DATA-----
|
||||
AXNhbHRzYWx0c2FsdHNhbHSIiIiIiIiIiIiIiIiIiIiIAAAACmIRUW2OjZ3L2l6j9h0lHlV3M2dx
|
||||
cissyYBxjsfsAn
|
||||
-----END MEGOLM SESSION DATA-----
|
||||
`);
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(input, "").then(
|
||||
(res) => {
|
||||
throw new Error("expected to throw");
|
||||
},
|
||||
(error) => {
|
||||
expect(error.message).toEqual("Invalid file: too short");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// TODO find a subtlecrypto shim which doesn't break this test
|
||||
it.skip("should decrypt a range of inputs", function () {
|
||||
function next(i: number): Promise<string | undefined> | undefined {
|
||||
if (i >= TEST_VECTORS.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [plain, password, input] = TEST_VECTORS[i];
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(stringToArray(input), password).then((decrypted) => {
|
||||
expect(decrypted).toEqual(plain);
|
||||
return next(i + 1);
|
||||
});
|
||||
}
|
||||
next(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encrypt", function () {
|
||||
it("should round-trip", function () {
|
||||
const input = "words words many words in plain text here".repeat(100);
|
||||
|
||||
const password = "my super secret passphrase";
|
||||
|
||||
return MegolmExportEncryption.encryptMegolmKeyFile(input, password, { kdf_rounds: 1000 })
|
||||
.then((ciphertext) => {
|
||||
return MegolmExportEncryption.decryptMegolmKeyFile(ciphertext, password);
|
||||
})
|
||||
.then((plaintext) => {
|
||||
expect(plaintext).toEqual(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
83
test/unit-tests/utils/MessageDiffUtils-test.tsx
Normal file
83
test/unit-tests/utils/MessageDiffUtils-test.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
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
|
||||
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();
|
||||
});
|
||||
});
|
235
test/unit-tests/utils/MultiInviter-test.ts
Normal file
235
test/unit-tests/utils/MultiInviter-test.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, MatrixClient, MatrixError, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import Modal, { ComponentType, ComponentProps } from "../../src/Modal";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import MultiInviter, { CompletionStates } from "../../src/utils/MultiInviter";
|
||||
import * as TestUtilsMatrix from "../test-utils";
|
||||
import AskInviteAnywayDialog from "../../src/components/views/dialogs/AskInviteAnywayDialog";
|
||||
import ConfirmUserActionDialog from "../../src/components/views/dialogs/ConfirmUserActionDialog";
|
||||
|
||||
const ROOMID = "!room:server";
|
||||
|
||||
const MXID1 = "@user1:server";
|
||||
const MXID2 = "@user2:server";
|
||||
const MXID3 = "@user3:server";
|
||||
|
||||
const MXID_PROFILE_STATES: Record<string, Promise<any>> = {
|
||||
[MXID1]: Promise.resolve({}),
|
||||
[MXID2]: Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN" })),
|
||||
[MXID3]: Promise.reject(new MatrixError({ errcode: "M_NOT_FOUND" })),
|
||||
};
|
||||
|
||||
jest.mock("../../src/Modal", () => ({
|
||||
createDialog: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/settings/SettingsStore", () => ({
|
||||
getValue: jest.fn(),
|
||||
monitorSetting: jest.fn(),
|
||||
watchSetting: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockPromptBeforeInviteUnknownUsers = (value: boolean) => {
|
||||
mocked(SettingsStore.getValue).mockImplementation(
|
||||
(settingName: string, roomId: string, _excludeDefault = false): any => {
|
||||
if (settingName === "promptBeforeInviteUnknownUsers" && roomId === ROOMID) {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const mockCreateTrackedDialog = (callbackName: "onInviteAnyways" | "onGiveUp") => {
|
||||
mocked(Modal.createDialog).mockImplementation(
|
||||
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
||||
(props as ComponentProps<typeof AskInviteAnywayDialog>)[callbackName]();
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const expectAllInvitedResult = (result: CompletionStates) => {
|
||||
expect(result).toEqual({
|
||||
[MXID1]: "invited",
|
||||
[MXID2]: "invited",
|
||||
[MXID3]: "invited",
|
||||
});
|
||||
};
|
||||
|
||||
describe("MultiInviter", () => {
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let inviter: MultiInviter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
TestUtilsMatrix.stubClient();
|
||||
client = MatrixClientPeg.safeGet() as jest.Mocked<MatrixClient>;
|
||||
|
||||
client.invite = jest.fn();
|
||||
client.invite.mockResolvedValue({});
|
||||
|
||||
client.getProfileInfo = jest.fn();
|
||||
client.getProfileInfo.mockImplementation((userId: string) => {
|
||||
return MXID_PROFILE_STATES[userId] || Promise.reject();
|
||||
});
|
||||
client.unban = jest.fn();
|
||||
|
||||
inviter = new MultiInviter(client, ROOMID);
|
||||
});
|
||||
|
||||
describe("invite", () => {
|
||||
describe("with promptBeforeInviteUnknownUsers = false", () => {
|
||||
beforeEach(() => mockPromptBeforeInviteUnknownUsers(false));
|
||||
|
||||
it("should invite all users", async () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(3);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
||||
|
||||
expectAllInvitedResult(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with promptBeforeInviteUnknownUsers = true and", () => {
|
||||
beforeEach(() => mockPromptBeforeInviteUnknownUsers(true));
|
||||
|
||||
describe("confirming the unknown user dialog", () => {
|
||||
beforeEach(() => mockCreateTrackedDialog("onInviteAnyways"));
|
||||
|
||||
it("should invite all users", async () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(3);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(2, ROOMID, MXID2, undefined);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(3, ROOMID, MXID3, undefined);
|
||||
|
||||
expectAllInvitedResult(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("declining the unknown user dialog", () => {
|
||||
beforeEach(() => mockCreateTrackedDialog("onGiveUp"));
|
||||
|
||||
it("should only invite existing users", async () => {
|
||||
const result = await inviter.invite([MXID1, MXID2, MXID3]);
|
||||
|
||||
expect(client.invite).toHaveBeenCalledTimes(1);
|
||||
expect(client.invite).toHaveBeenNthCalledWith(1, ROOMID, MXID1, undefined);
|
||||
|
||||
// The resolved state is 'invited' for all users.
|
||||
// With the above client expectations, the test ensures that only the first user is invited.
|
||||
expectAllInvitedResult(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should show sensible error when attempting 3pid invite with no identity server", async () => {
|
||||
client.inviteByEmail = jest.fn().mockRejectedValueOnce(
|
||||
new MatrixError({
|
||||
errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
|
||||
}),
|
||||
);
|
||||
await inviter.invite(["foo@bar.com"]);
|
||||
expect(inviter.getErrorText("foo@bar.com")).toMatchInlineSnapshot(
|
||||
`"Cannot invite user by email without an identity server. You can connect to one under "Settings"."`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should ask if user wants to unban user if they have permission", async () => {
|
||||
mocked(Modal.createDialog).mockImplementation(
|
||||
(Element: ComponentType, props?: ComponentProps<ComponentType>): any => {
|
||||
// We stub out the modal with an immediate affirmative (proceed) return
|
||||
return { finished: Promise.resolve([true]) };
|
||||
},
|
||||
);
|
||||
|
||||
const room = new Room(ROOMID, client, client.getSafeUserId());
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
const ourMember = new RoomMember(ROOMID, client.getSafeUserId());
|
||||
ourMember.membership = KnownMembership.Join;
|
||||
ourMember.powerLevel = 100;
|
||||
const member = new RoomMember(ROOMID, MXID1);
|
||||
member.membership = KnownMembership.Ban;
|
||||
member.powerLevel = 0;
|
||||
room.getMember = (userId: string) => {
|
||||
if (userId === client.getSafeUserId()) return ourMember;
|
||||
if (userId === MXID1) return member;
|
||||
return null;
|
||||
};
|
||||
|
||||
await inviter.invite([MXID1]);
|
||||
expect(Modal.createDialog).toHaveBeenCalledWith(ConfirmUserActionDialog, {
|
||||
member,
|
||||
title: "User cannot be invited until they are unbanned",
|
||||
action: "Unban",
|
||||
});
|
||||
expect(client.unban).toHaveBeenCalledWith(ROOMID, MXID1);
|
||||
});
|
||||
|
||||
it("should show sensible error when attempting to invite over federation with m.federate=false", async () => {
|
||||
mocked(client.invite).mockRejectedValueOnce(
|
||||
new MatrixError({
|
||||
errcode: "M_FORBIDDEN",
|
||||
}),
|
||||
);
|
||||
const room = new Room(ROOMID, client, client.getSafeUserId());
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomCreate,
|
||||
state_key: "",
|
||||
content: {
|
||||
"m.federate": false,
|
||||
},
|
||||
room_id: ROOMID,
|
||||
}),
|
||||
]);
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
await inviter.invite(["@user:other_server"]);
|
||||
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
||||
`"This room is unfederated. You cannot invite people from external servers."`,
|
||||
);
|
||||
});
|
||||
|
||||
it("should show sensible error when attempting to invite over federation with m.federate=false to space", async () => {
|
||||
mocked(client.invite).mockRejectedValueOnce(
|
||||
new MatrixError({
|
||||
errcode: "M_FORBIDDEN",
|
||||
}),
|
||||
);
|
||||
const room = new Room(ROOMID, client, client.getSafeUserId());
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomCreate,
|
||||
state_key: "",
|
||||
content: {
|
||||
"m.federate": false,
|
||||
"type": "m.space",
|
||||
},
|
||||
room_id: ROOMID,
|
||||
}),
|
||||
]);
|
||||
mocked(client.getRoom).mockReturnValue(room);
|
||||
|
||||
await inviter.invite(["@user:other_server"]);
|
||||
expect(inviter.getErrorText("@user:other_server")).toMatchInlineSnapshot(
|
||||
`"This space is unfederated. You cannot invite people from external servers."`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
81
test/unit-tests/utils/PhasedRolloutFeature-test.ts
Normal file
81
test/unit-tests/utils/PhasedRolloutFeature-test.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { PhasedRolloutFeature } from "../../src/utils/PhasedRolloutFeature";
|
||||
|
||||
describe("Test PhasedRolloutFeature", () => {
|
||||
function randomUserId() {
|
||||
const characters = "abcdefghijklmnopqrstuvwxyz0123456789.=_-/+";
|
||||
let result = "";
|
||||
const charactersLength = characters.length;
|
||||
const idLength = Math.floor(Math.random() * 15) + 6; // Random number between 6 and 20
|
||||
for (let i = 0; i < idLength; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return "@" + result + ":matrix.org";
|
||||
}
|
||||
|
||||
function randomDeviceId() {
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let result = "";
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it("should only accept valid percentage", () => {
|
||||
expect(() => new PhasedRolloutFeature("test", 0.8)).toThrow();
|
||||
expect(() => new PhasedRolloutFeature("test", -1)).toThrow();
|
||||
expect(() => new PhasedRolloutFeature("test", 123)).toThrow();
|
||||
});
|
||||
|
||||
it("should enable for all if percentage is 100", () => {
|
||||
const phasedRolloutFeature = new PhasedRolloutFeature("test", 100);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
expect(phasedRolloutFeature.isFeatureEnabled(randomUserId())).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
it("should not enable for anyone if percentage is 0", () => {
|
||||
const phasedRolloutFeature = new PhasedRolloutFeature("test", 0);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
expect(phasedRolloutFeature.isFeatureEnabled(randomUserId())).toBeFalsy();
|
||||
}
|
||||
});
|
||||
|
||||
it("should enable for more users if percentage grows", () => {
|
||||
let rolloutPercentage = 0;
|
||||
let previousBatch: string[] = [];
|
||||
const allUsers = new Array(1000).fill(0).map(() => randomDeviceId());
|
||||
|
||||
while (rolloutPercentage <= 90) {
|
||||
rolloutPercentage += 10;
|
||||
const nextRollout = new PhasedRolloutFeature("test", rolloutPercentage);
|
||||
const nextBatch = allUsers.filter((userId) => nextRollout.isFeatureEnabled(userId));
|
||||
expect(previousBatch.length).toBeLessThan(nextBatch.length);
|
||||
expect(previousBatch.every((user) => nextBatch.includes(user))).toBeTruthy();
|
||||
previousBatch = nextBatch;
|
||||
}
|
||||
});
|
||||
|
||||
it("should distribute differently depending on the feature name", () => {
|
||||
const allUsers = new Array(1000).fill(0).map(() => randomUserId());
|
||||
|
||||
const featureARollout = new PhasedRolloutFeature("FeatureA", 50);
|
||||
const featureBRollout = new PhasedRolloutFeature("FeatureB", 50);
|
||||
|
||||
const featureAUsers = allUsers.filter((userId) => featureARollout.isFeatureEnabled(userId));
|
||||
const featureBUsers = allUsers.filter((userId) => featureBRollout.isFeatureEnabled(userId));
|
||||
|
||||
expect(featureAUsers).not.toEqual(featureBUsers);
|
||||
});
|
||||
});
|
295
test/unit-tests/utils/PinningUtils-test.ts
Normal file
295
test/unit-tests/utils/PinningUtils-test.ts
Normal file
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EventTimeline, EventType, IEvent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { createTestClient } from "../test-utils";
|
||||
import PinningUtils from "../../src/utils/PinningUtils";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { isContentActionable } from "../../src/utils/EventUtils";
|
||||
import { ReadPinsEventId } from "../../src/components/views/right_panel/types";
|
||||
|
||||
jest.mock("../../src/utils/EventUtils", () => {
|
||||
return {
|
||||
isContentActionable: jest.fn(),
|
||||
canPinEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe("PinningUtils", () => {
|
||||
const roomId = "!room:example.org";
|
||||
const userId = "@alice:example.org";
|
||||
|
||||
const mockedIsContentActionable = mocked(isContentActionable);
|
||||
|
||||
let matrixClient: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
/**
|
||||
* Create a pinned event with the given content.
|
||||
* @param content
|
||||
*/
|
||||
function makePinEvent(content?: Partial<IEvent>) {
|
||||
return new MatrixEvent({
|
||||
type: EventType.RoomMessage,
|
||||
sender: userId,
|
||||
content: {
|
||||
body: "First pinned message",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
room_id: roomId,
|
||||
origin_server_ts: 0,
|
||||
event_id: "$eventId",
|
||||
...content,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Enable feature pinning
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
|
||||
mockedIsContentActionable.mockImplementation(() => true);
|
||||
|
||||
matrixClient = createTestClient();
|
||||
room = new Room(roomId, matrixClient, userId);
|
||||
matrixClient.getRoom = jest.fn().mockReturnValue(room);
|
||||
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("isUnpinnable", () => {
|
||||
test.each(PinningUtils.PINNABLE_EVENT_TYPES)("should return true for pinnable event types", (eventType) => {
|
||||
const event = makePinEvent({ type: eventType });
|
||||
expect(PinningUtils.isUnpinnable(event)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for a non pinnable event type", () => {
|
||||
const event = makePinEvent({ type: EventType.RoomCreate });
|
||||
expect(PinningUtils.isUnpinnable(event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true for a redacted event", () => {
|
||||
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
|
||||
expect(PinningUtils.isUnpinnable(event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPinnable", () => {
|
||||
test.each(PinningUtils.PINNABLE_EVENT_TYPES)("should return true for pinnable event types", (eventType) => {
|
||||
const event = makePinEvent({ type: eventType });
|
||||
expect(PinningUtils.isPinnable(event)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false for a redacted event", () => {
|
||||
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
|
||||
expect(PinningUtils.isPinnable(event)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPinned", () => {
|
||||
test("should return false if no room", () => {
|
||||
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if no pinned event", () => {
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"getStateEvents",
|
||||
).mockReturnValue(null);
|
||||
|
||||
const event = makePinEvent();
|
||||
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if pinned events do not contain the event id", () => {
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"getStateEvents",
|
||||
).mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: ["$otherEventId"] }),
|
||||
});
|
||||
|
||||
const event = makePinEvent();
|
||||
expect(PinningUtils.isPinned(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if pinned events contains the event id", () => {
|
||||
const event = makePinEvent();
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"getStateEvents",
|
||||
).mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: [event.getId()] }),
|
||||
});
|
||||
|
||||
expect(PinningUtils.isPinned(matrixClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canPin & canUnpin", () => {
|
||||
describe("canPin", () => {
|
||||
test("should return false if event is not actionable", () => {
|
||||
mockedIsContentActionable.mockImplementation(() => false);
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if no room", () => {
|
||||
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if client cannot send state event", () => {
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false if event is not pinnable", () => {
|
||||
const event = makePinEvent({ type: EventType.RoomCreate });
|
||||
|
||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if all conditions are met", () => {
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.canPin(matrixClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canUnpin", () => {
|
||||
test("should return false if event is not unpinnable", () => {
|
||||
const event = makePinEvent({ type: EventType.RoomCreate });
|
||||
|
||||
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(false);
|
||||
});
|
||||
|
||||
test("should return true if all conditions are met", () => {
|
||||
const event = makePinEvent();
|
||||
|
||||
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return true if the event is redacted", () => {
|
||||
const event = makePinEvent({ unsigned: { redacted_because: "because" as unknown as IEvent } });
|
||||
|
||||
expect(PinningUtils.canUnpin(matrixClient, event)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinOrUnpinEvent", () => {
|
||||
test("should do nothing if no room", async () => {
|
||||
matrixClient.getRoom = jest.fn().mockReturnValue(undefined);
|
||||
const event = makePinEvent();
|
||||
|
||||
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should do nothing if no event id", async () => {
|
||||
const event = makePinEvent({ event_id: undefined });
|
||||
|
||||
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||
expect(matrixClient.sendStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should pin the event if not pinned", async () => {
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"getStateEvents",
|
||||
).mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: ["$otherEventId"] }),
|
||||
});
|
||||
|
||||
jest.spyOn(room, "getAccountData").mockReturnValue({
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
event_ids: ["$otherEventId"],
|
||||
}),
|
||||
} as unknown as MatrixEvent);
|
||||
|
||||
const event = makePinEvent();
|
||||
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||
|
||||
expect(matrixClient.setRoomAccountData).toHaveBeenCalledWith(roomId, ReadPinsEventId, {
|
||||
event_ids: ["$otherEventId", event.getId()],
|
||||
});
|
||||
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{ pinned: ["$otherEventId", event.getId()] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
|
||||
test("should unpin the event if already pinned", async () => {
|
||||
const event = makePinEvent();
|
||||
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"getStateEvents",
|
||||
).mockReturnValue({
|
||||
// @ts-ignore
|
||||
getContent: () => ({ pinned: [event.getId(), "$otherEventId"] }),
|
||||
});
|
||||
|
||||
await PinningUtils.pinOrUnpinEvent(matrixClient, event);
|
||||
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{ pinned: ["$otherEventId"] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userHasPinOrUnpinPermission", () => {
|
||||
test("should return true if user can pin or unpin", () => {
|
||||
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false if client cannot send state event", () => {
|
||||
jest.spyOn(
|
||||
matrixClient.getRoom(roomId)!.getLiveTimeline().getState(EventTimeline.FORWARDS)!,
|
||||
"mayClientSendStateEvent",
|
||||
).mockReturnValue(false);
|
||||
|
||||
expect(PinningUtils.userHasPinOrUnpinPermission(matrixClient, room)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unpinAllEvents", () => {
|
||||
it("should unpin all events in the given room", async () => {
|
||||
await PinningUtils.unpinAllEvents(matrixClient, roomId);
|
||||
|
||||
expect(matrixClient.sendStateEvent).toHaveBeenCalledWith(
|
||||
roomId,
|
||||
EventType.RoomPinnedEvents,
|
||||
{ pinned: [] },
|
||||
"",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
52
test/unit-tests/utils/SearchInput-test.ts
Normal file
52
test/unit-tests/utils/SearchInput-test.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Boluwatife Omosowon <boluomosowon@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { parsePermalink } from "../../src/utils/permalinks/Permalinks";
|
||||
import { transformSearchTerm } from "../../src/utils/SearchInput";
|
||||
|
||||
jest.mock("../../src/utils/permalinks/Permalinks");
|
||||
jest.mock("../../src/stores/WidgetStore");
|
||||
jest.mock("../../src/stores/widgets/WidgetLayoutStore");
|
||||
|
||||
describe("transforming search term", () => {
|
||||
it("should return the primaryEntityId if the search term was a permalink", () => {
|
||||
const roomLink = "https://matrix.to/#/#element-dev:matrix.org";
|
||||
const parsedPermalink = "#element-dev:matrix.org";
|
||||
|
||||
mocked(parsePermalink).mockReturnValue({
|
||||
primaryEntityId: parsedPermalink,
|
||||
roomIdOrAlias: parsedPermalink,
|
||||
eventId: "",
|
||||
userId: "",
|
||||
viaServers: [],
|
||||
});
|
||||
|
||||
expect(transformSearchTerm(roomLink)).toBe(parsedPermalink);
|
||||
});
|
||||
|
||||
it("should return the original search term if the search term is a permalink and the primaryEntityId is null", () => {
|
||||
const searchTerm = "https://matrix.to/#/#random-link:matrix.org";
|
||||
|
||||
mocked(parsePermalink).mockReturnValue({
|
||||
primaryEntityId: null,
|
||||
roomIdOrAlias: null,
|
||||
eventId: null,
|
||||
userId: null,
|
||||
viaServers: null,
|
||||
});
|
||||
|
||||
expect(transformSearchTerm(searchTerm)).toBe(searchTerm);
|
||||
});
|
||||
|
||||
it("should return the original search term if the search term was not a permalink", () => {
|
||||
const searchTerm = "search term";
|
||||
mocked(parsePermalink).mockReturnValue(null);
|
||||
expect(transformSearchTerm(searchTerm)).toBe(searchTerm);
|
||||
});
|
||||
});
|
244
test/unit-tests/utils/SessionLock-test.ts
Normal file
244
test/unit-tests/utils/SessionLock-test.ts
Normal file
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { checkSessionLockFree, getSessionLock, SESSION_LOCK_CONSTANTS } from "../../src/utils/SessionLock";
|
||||
import { resetJsDomAfterEach } from "../test-utils";
|
||||
|
||||
describe("SessionLock", () => {
|
||||
const otherWindows: Array<Window> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now: 1000 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// shut down other windows created by `createWindow`
|
||||
otherWindows.forEach((window) => window.close());
|
||||
otherWindows.splice(0);
|
||||
});
|
||||
|
||||
resetJsDomAfterEach();
|
||||
|
||||
it("A single instance starts up normally", async () => {
|
||||
const onNewInstance = jest.fn();
|
||||
const result = await getSessionLock(onNewInstance);
|
||||
expect(result).toBe(true);
|
||||
expect(onNewInstance).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("A second instance starts up normally when the first shut down cleanly", async () => {
|
||||
// first instance starts...
|
||||
const onNewInstance1 = jest.fn();
|
||||
expect(await getSessionLock(onNewInstance1)).toBe(true);
|
||||
expect(onNewInstance1).not.toHaveBeenCalled();
|
||||
|
||||
// ... and navigates away
|
||||
window.dispatchEvent(new Event("pagehide", {}));
|
||||
|
||||
// second instance starts as normal
|
||||
expect(checkSessionLockFree()).toBe(true);
|
||||
const onNewInstance2 = jest.fn();
|
||||
expect(await getSessionLock(onNewInstance2)).toBe(true);
|
||||
|
||||
expect(onNewInstance1).not.toHaveBeenCalled();
|
||||
expect(onNewInstance2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("A second instance starts up *eventually* when the first terminated uncleanly", async () => {
|
||||
// first instance starts...
|
||||
const onNewInstance1 = jest.fn();
|
||||
expect(await getSessionLock(onNewInstance1)).toBe(true);
|
||||
expect(onNewInstance1).not.toHaveBeenCalled();
|
||||
expect(checkSessionLockFree()).toBe(false);
|
||||
|
||||
// and pings the timer after 5 seconds
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(checkSessionLockFree()).toBe(false);
|
||||
|
||||
// oops, now it dies. We simulate this by forcibly clearing the timers.
|
||||
// For some reason `jest.clearAllTimers` also resets the simulated time, so preserve that
|
||||
const time = Date.now();
|
||||
jest.clearAllTimers();
|
||||
jest.setSystemTime(time);
|
||||
expect(checkSessionLockFree()).toBe(false);
|
||||
|
||||
// time advances a bit more
|
||||
jest.advanceTimersByTime(5000);
|
||||
expect(checkSessionLockFree()).toBe(false);
|
||||
|
||||
// second instance tries to start. This should block for 25 more seconds
|
||||
const onNewInstance2 = jest.fn();
|
||||
let session2Result: boolean | undefined;
|
||||
getSessionLock(onNewInstance2).then((res) => {
|
||||
session2Result = res;
|
||||
});
|
||||
|
||||
// after another 24.5 seconds, we are still waiting
|
||||
jest.advanceTimersByTime(24500);
|
||||
expect(session2Result).toBe(undefined);
|
||||
expect(checkSessionLockFree()).toBe(false);
|
||||
|
||||
// another 500ms and we get the lock
|
||||
await jest.advanceTimersByTimeAsync(500);
|
||||
expect(session2Result).toBe(true);
|
||||
expect(checkSessionLockFree()).toBe(false); // still false, because the new session has claimed it
|
||||
|
||||
expect(onNewInstance1).not.toHaveBeenCalled();
|
||||
expect(onNewInstance2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("A second instance waits for the first to shut down", async () => {
|
||||
// first instance starts. Once it gets the shutdown signal, it will wait two seconds and then release the lock.
|
||||
await getSessionLock(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 2000, 0);
|
||||
}),
|
||||
);
|
||||
|
||||
// second instance tries to start, but should block
|
||||
const { window: window2, getSessionLock: getSessionLock2 } = buildNewContext();
|
||||
let session2Result: boolean | undefined;
|
||||
getSessionLock2(async () => {}).then((res) => {
|
||||
session2Result = res;
|
||||
});
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
// should still be blocking
|
||||
expect(session2Result).toBe(undefined);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(2000);
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
|
||||
// session 2 now gets the lock
|
||||
expect(session2Result).toBe(true);
|
||||
window2.close();
|
||||
});
|
||||
|
||||
it("If a third instance starts while we are waiting, we give up immediately", async () => {
|
||||
// first instance starts. It will never release the lock.
|
||||
await getSessionLock(() => new Promise(() => {}));
|
||||
|
||||
// first instance should ping the timer after 5 seconds
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
// second instance starts
|
||||
const { getSessionLock: getSessionLock2 } = buildNewContext();
|
||||
let session2Result: boolean | undefined;
|
||||
const onNewInstance2 = jest.fn();
|
||||
getSessionLock2(onNewInstance2).then((res) => {
|
||||
session2Result = res;
|
||||
});
|
||||
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
// should still be blocking
|
||||
expect(session2Result).toBe(undefined);
|
||||
|
||||
// third instance starts
|
||||
const { getSessionLock: getSessionLock3 } = buildNewContext();
|
||||
getSessionLock3(async () => {});
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
|
||||
// session 2 should have given up
|
||||
expect(session2Result).toBe(false);
|
||||
expect(onNewInstance2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("If two new instances start concurrently, only one wins", async () => {
|
||||
// first instance starts. Once it gets the shutdown signal, it will wait two seconds and then release the lock.
|
||||
await getSessionLock(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 2000, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// first instance should ping the timer after 5 seconds
|
||||
jest.advanceTimersByTime(5000);
|
||||
|
||||
// two new instances start at once
|
||||
const { getSessionLock: getSessionLock2 } = buildNewContext();
|
||||
let session2Result: boolean | undefined;
|
||||
getSessionLock2(async () => {}).then((res) => {
|
||||
session2Result = res;
|
||||
});
|
||||
|
||||
const { getSessionLock: getSessionLock3 } = buildNewContext();
|
||||
let session3Result: boolean | undefined;
|
||||
getSessionLock3(async () => {}).then((res) => {
|
||||
session3Result = res;
|
||||
});
|
||||
|
||||
await jest.advanceTimersByTimeAsync(100);
|
||||
// session 3 still be blocking. Session 2 should have given up.
|
||||
expect(session2Result).toBe(false);
|
||||
expect(session3Result).toBe(undefined);
|
||||
|
||||
await jest.advanceTimersByTimeAsync(2000);
|
||||
await jest.advanceTimersByTimeAsync(0);
|
||||
|
||||
// session 3 now gets the lock
|
||||
expect(session2Result).toBe(false);
|
||||
expect(session3Result).toBe(true);
|
||||
});
|
||||
|
||||
/** build a new Window in the same domain as the current one.
|
||||
*
|
||||
* We do this by constructing an iframe, which gets its own Window object.
|
||||
*/
|
||||
function createWindow() {
|
||||
const iframe = window.document.createElement("iframe");
|
||||
window.document.body.appendChild(iframe);
|
||||
const window2: any = iframe.contentWindow;
|
||||
|
||||
otherWindows.push(window2);
|
||||
|
||||
// make the new Window use the same jest fake timers as us
|
||||
for (const m of ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"]) {
|
||||
// @ts-ignore
|
||||
window2[m] = global[m];
|
||||
}
|
||||
return window2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate `getSessionLock` in a new context (ie, using a different global `window`).
|
||||
*
|
||||
* The new window will share the same fake timer impl as the current context.
|
||||
*
|
||||
* @returns the new window and (a wrapper for) getSessionLock in the new context.
|
||||
*/
|
||||
function buildNewContext(): {
|
||||
window: Window;
|
||||
getSessionLock: (onNewInstance: () => Promise<void>) => Promise<boolean>;
|
||||
} {
|
||||
const window2 = createWindow();
|
||||
|
||||
// import the dependencies of getSessionLock into the new context
|
||||
window2._uuid = require("uuid");
|
||||
window2._logger = require("matrix-js-sdk/src/logger");
|
||||
window2.SESSION_LOCK_CONSTANTS = SESSION_LOCK_CONSTANTS;
|
||||
|
||||
// now, define getSessionLock as a global
|
||||
window2.eval(String(getSessionLock));
|
||||
|
||||
// return a function that will call it
|
||||
function callGetSessionLock(onNewInstance: () => Promise<void>): Promise<boolean> {
|
||||
// import the callback into the context
|
||||
window2._getSessionLockCallback = onNewInstance;
|
||||
|
||||
// start the function
|
||||
try {
|
||||
return window2.eval(`getSessionLock(_getSessionLockCallback)`);
|
||||
} finally {
|
||||
// we can now clear the callback
|
||||
delete window2._getSessionLockCallback;
|
||||
}
|
||||
}
|
||||
|
||||
return { window: window2, getSessionLock: callGetSessionLock };
|
||||
}
|
||||
});
|
222
test/unit-tests/utils/ShieldUtils-test.ts
Normal file
222
test/unit-tests/utils/ShieldUtils-test.ts
Normal file
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { shieldStatusForRoom } from "../../src/utils/ShieldUtils";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
|
||||
function mkClient(selfTrust = false) {
|
||||
return {
|
||||
getUserId: () => "@self:localhost",
|
||||
getCrypto: () => ({
|
||||
getDeviceVerificationStatus: (userId: string, deviceId: string) =>
|
||||
Promise.resolve({
|
||||
isVerified: () => (userId === "@self:localhost" ? selfTrust : userId[2] == "T"),
|
||||
}),
|
||||
getUserDeviceInfo: async (userIds: string[]) => {
|
||||
return new Map(userIds.map((u) => [u, new Map([["DEVICE", {}]])]));
|
||||
},
|
||||
getUserVerificationStatus: async (userId: string): Promise<UserVerificationStatus> =>
|
||||
new UserVerificationStatus(userId[1] == "T", userId[1] == "T" || userId[1] == "W", false),
|
||||
}),
|
||||
} as unknown as MatrixClient;
|
||||
}
|
||||
|
||||
describe("mkClient self-test", function () {
|
||||
test.each([true, false])("behaves well for self-trust=%s", async (v) => {
|
||||
const client = mkClient(v);
|
||||
const status = await client.getCrypto()!.getDeviceVerificationStatus("@self:localhost", "DEVICE");
|
||||
expect(status?.isVerified()).toBe(v);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["@TT:h", true],
|
||||
["@TF:h", true],
|
||||
["@FT:h", false],
|
||||
["@FF:h", false],
|
||||
])("behaves well for user trust %s", async (userId, trust) => {
|
||||
const status = await mkClient().getCrypto()?.getUserVerificationStatus(userId);
|
||||
expect(status!.isCrossSigningVerified()).toBe(trust);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["@TT:h", true],
|
||||
["@TF:h", false],
|
||||
["@FT:h", true],
|
||||
["@FF:h", false],
|
||||
])("behaves well for device trust %s", async (userId, trust) => {
|
||||
const status = await mkClient().getCrypto()!.getDeviceVerificationStatus(userId, "device");
|
||||
expect(status?.isVerified()).toBe(trust);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shieldStatusForMembership self-trust behaviour", function () {
|
||||
beforeAll(() => {
|
||||
const mockInstance = {
|
||||
getUserIdForRoomId: (roomId: string) => (roomId === "DM" ? "@any:h" : null),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(mockInstance);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(DMRoomMap, "shared").mockRestore();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, true],
|
||||
[true, false],
|
||||
[false, true],
|
||||
[false, false],
|
||||
])("2 unverified: returns 'normal', self-trust = %s, DM = %s", async (trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@FF1:h", "@FF2:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual("normal");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["verified", true, true],
|
||||
["verified", true, false],
|
||||
["verified", false, true],
|
||||
["warning", false, false],
|
||||
])("2 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@TT2:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["normal", true, true],
|
||||
["normal", true, false],
|
||||
["normal", false, true],
|
||||
["warning", false, false],
|
||||
])("2 mixed: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@TT1:h", "@FF2:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["verified", true, true],
|
||||
["verified", true, false],
|
||||
["warning", false, true],
|
||||
["warning", false, false],
|
||||
])("0 others: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["verified", true, true],
|
||||
["verified", true, false],
|
||||
["verified", false, true],
|
||||
["verified", false, false],
|
||||
])("1 verified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@TT:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["normal", true, true],
|
||||
["normal", true, false],
|
||||
["normal", false, true],
|
||||
["normal", false, false],
|
||||
])("1 unverified: returns '%s', self-trust = %s, DM = %s", async (result, trusted, dm) => {
|
||||
const client = mkClient(trusted);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shieldStatusForMembership other-trust behaviour", function () {
|
||||
beforeAll(() => {
|
||||
const mockInstance = {
|
||||
getUserIdForRoomId: (roomId: string) => (roomId === "DM" ? "@any:h" : null),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(mockInstance);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["warning", true],
|
||||
["warning", false],
|
||||
])("1 verified/untrusted: returns '%s', DM = %s", async (result, dm) => {
|
||||
const client = mkClient(true);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["warning", true],
|
||||
["warning", false],
|
||||
])("2 verified/untrusted: returns '%s', DM = %s", async (result, dm) => {
|
||||
const client = mkClient(true);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@TF:h", "@TT:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["normal", true],
|
||||
["normal", false],
|
||||
])("2 unverified/untrusted: returns '%s', DM = %s", async (result, dm) => {
|
||||
const client = mkClient(true);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@FF:h", "@FT:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["warning", true],
|
||||
["warning", false],
|
||||
])("2 was verified: returns '%s', DM = %s", async (result, dm) => {
|
||||
const client = mkClient(true);
|
||||
const room = {
|
||||
roomId: dm ? "DM" : "other",
|
||||
getEncryptionTargetMembers: () => ["@self:localhost", "@WF:h", "@FT:h"].map((userId) => ({ userId })),
|
||||
} as unknown as Room;
|
||||
const status = await shieldStatusForRoom(client, room);
|
||||
expect(status).toEqual(result);
|
||||
});
|
||||
});
|
100
test/unit-tests/utils/Singleflight-test.ts
Normal file
100
test/unit-tests/utils/Singleflight-test.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Singleflight } from "../../src/utils/Singleflight";
|
||||
|
||||
describe("Singleflight", () => {
|
||||
afterEach(() => {
|
||||
Singleflight.forgetAll();
|
||||
});
|
||||
|
||||
it("should throw for bad context variables", () => {
|
||||
const permutations: [Object | null, string | null][] = [
|
||||
[null, null],
|
||||
[{}, null],
|
||||
[null, "test"],
|
||||
];
|
||||
for (const p of permutations) {
|
||||
expect(() => Singleflight.for(p[0], p[1])).toThrow("An instance and key must be supplied");
|
||||
}
|
||||
});
|
||||
|
||||
it("should execute the function once", () => {
|
||||
const instance = {};
|
||||
const key = "test";
|
||||
const val = {}; // unique object for reference check
|
||||
const fn = jest.fn().mockReturnValue(val);
|
||||
const sf = Singleflight.for(instance, key);
|
||||
const r1 = sf.do(fn);
|
||||
expect(r1).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
const r2 = sf.do(fn);
|
||||
expect(r2).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should execute the function once, even with new contexts", () => {
|
||||
const instance = {};
|
||||
const key = "test";
|
||||
const val = {}; // unique object for reference check
|
||||
const fn = jest.fn().mockReturnValue(val);
|
||||
let sf = Singleflight.for(instance, key);
|
||||
const r1 = sf.do(fn);
|
||||
expect(r1).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
sf = Singleflight.for(instance, key); // RESET FOR TEST
|
||||
const r2 = sf.do(fn);
|
||||
expect(r2).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should execute the function twice if the result was forgotten", () => {
|
||||
const instance = {};
|
||||
const key = "test";
|
||||
const val = {}; // unique object for reference check
|
||||
const fn = jest.fn().mockReturnValue(val);
|
||||
const sf = Singleflight.for(instance, key);
|
||||
const r1 = sf.do(fn);
|
||||
expect(r1).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
sf.forget();
|
||||
const r2 = sf.do(fn);
|
||||
expect(r2).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should execute the function twice if the instance was forgotten", () => {
|
||||
const instance = {};
|
||||
const key = "test";
|
||||
const val = {}; // unique object for reference check
|
||||
const fn = jest.fn().mockReturnValue(val);
|
||||
const sf = Singleflight.for(instance, key);
|
||||
const r1 = sf.do(fn);
|
||||
expect(r1).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
Singleflight.forgetAllFor(instance);
|
||||
const r2 = sf.do(fn);
|
||||
expect(r2).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it("should execute the function twice if everything was forgotten", () => {
|
||||
const instance = {};
|
||||
const key = "test";
|
||||
const val = {}; // unique object for reference check
|
||||
const fn = jest.fn().mockReturnValue(val);
|
||||
const sf = Singleflight.for(instance, key);
|
||||
const r1 = sf.do(fn);
|
||||
expect(r1).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
Singleflight.forgetAll();
|
||||
const r2 = sf.do(fn);
|
||||
expect(r2).toBe(val);
|
||||
expect(fn.mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
55
test/unit-tests/utils/SnakedObject-test.ts
Normal file
55
test/unit-tests/utils/SnakedObject-test.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { SnakedObject, snakeToCamel } from "../../src/utils/SnakedObject";
|
||||
|
||||
describe("snakeToCamel", () => {
|
||||
it("should convert snake_case to camelCase in simple scenarios", () => {
|
||||
expect(snakeToCamel("snake_case")).toBe("snakeCase");
|
||||
expect(snakeToCamel("snake_case_but_longer")).toBe("snakeCaseButLonger");
|
||||
expect(snakeToCamel("numbered_123")).toBe("numbered123"); // not a thing we would see normally
|
||||
});
|
||||
|
||||
// Not really something we expect to see, but it's defined behaviour of the function
|
||||
it("should not camelCase a trailing or leading underscore", () => {
|
||||
expect(snakeToCamel("_snake")).toBe("_snake");
|
||||
expect(snakeToCamel("snake_")).toBe("snake_");
|
||||
expect(snakeToCamel("_snake_case")).toBe("_snakeCase");
|
||||
expect(snakeToCamel("snake_case_")).toBe("snakeCase_");
|
||||
});
|
||||
|
||||
// Another thing we don't really expect to see, but is "defined behaviour"
|
||||
it("should be predictable with double underscores", () => {
|
||||
expect(snakeToCamel("__snake__")).toBe("_Snake_");
|
||||
expect(snakeToCamel("snake__case")).toBe("snake_case");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SnakedObject", () => {
|
||||
/* eslint-disable camelcase*/
|
||||
const input = {
|
||||
snake_case: "woot",
|
||||
snakeCase: "oh no", // ensure different value from snake_case for tests
|
||||
camelCase: "fallback",
|
||||
};
|
||||
const snake = new SnakedObject(input);
|
||||
/* eslint-enable camelcase*/
|
||||
|
||||
it("should prefer snake_case keys", () => {
|
||||
expect(snake.get("snake_case")).toBe(input.snake_case);
|
||||
expect(snake.get("snake_case", "camelCase")).toBe(input.snake_case);
|
||||
});
|
||||
|
||||
it("should fall back to camelCase keys when needed", () => {
|
||||
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||
expect(snake.get("camel_case")).toBe(input.camelCase);
|
||||
|
||||
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||
expect(snake.get("e_no_exist", "camelCase")).toBe(input.camelCase);
|
||||
});
|
||||
});
|
47
test/unit-tests/utils/StorageAccess-test.ts
Normal file
47
test/unit-tests/utils/StorageAccess-test.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "core-js/stable/structured-clone"; // for idb access
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import { idbDelete, idbLoad, idbSave } from "../../src/utils/StorageAccess";
|
||||
|
||||
const NONEXISTENT_TABLE = "this_is_not_a_table_we_use_ever_and_so_we_can_use_it_in_tests";
|
||||
const KNOWN_TABLES = ["account", "pickleKey"];
|
||||
|
||||
describe("StorageAccess", () => {
|
||||
it.each(KNOWN_TABLES)("should save, load, and delete from known table '%s'", async (tableName: string) => {
|
||||
const key = ["a", "b"];
|
||||
const data = { hello: "world" };
|
||||
|
||||
// Should start undefined
|
||||
let loaded = await idbLoad(tableName, key);
|
||||
expect(loaded).toBeUndefined();
|
||||
|
||||
// ... then define a value
|
||||
await idbSave(tableName, key, data);
|
||||
|
||||
// ... then check that value
|
||||
loaded = await idbLoad(tableName, key);
|
||||
expect(loaded).toEqual(data);
|
||||
|
||||
// ... then set it back to undefined
|
||||
await idbDelete(tableName, key);
|
||||
|
||||
// ... which we then check again
|
||||
loaded = await idbLoad(tableName, key);
|
||||
expect(loaded).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should fail to save, load, and delete from a non-existent table", async () => {
|
||||
// Regardless of validity on the key/data, or write order, these should all fail.
|
||||
await expect(() => idbSave(NONEXISTENT_TABLE, "whatever", "value")).rejects.toThrow();
|
||||
await expect(() => idbLoad(NONEXISTENT_TABLE, "whatever")).rejects.toThrow();
|
||||
await expect(() => idbDelete(NONEXISTENT_TABLE, "whatever")).rejects.toThrow();
|
||||
});
|
||||
});
|
122
test/unit-tests/utils/StorageManager-test.ts
Normal file
122
test/unit-tests/utils/StorageManager-test.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import * as StorageManager from "../../src/utils/StorageManager";
|
||||
|
||||
const LEGACY_CRYPTO_STORE_NAME = "matrix-js-sdk:crypto";
|
||||
const RUST_CRYPTO_STORE_NAME = "matrix-js-sdk::matrix-sdk-crypto";
|
||||
|
||||
describe("StorageManager", () => {
|
||||
async function createDB(name: string, withStores: string[] | undefined = undefined): Promise<IDBDatabase> {
|
||||
const request = indexedDB.open(name);
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = request.result;
|
||||
if (withStores) {
|
||||
withStores.forEach((storeName) => {
|
||||
db.createObjectStore(storeName);
|
||||
});
|
||||
}
|
||||
};
|
||||
request.onsuccess = function (event) {
|
||||
const db = request.result;
|
||||
resolve(db);
|
||||
};
|
||||
request.onerror = function (event) {
|
||||
reject(event);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function populateLegacyStore(migrationState: number | undefined) {
|
||||
const db = await createDB(LEGACY_CRYPTO_STORE_NAME, [IndexedDBCryptoStore.STORE_ACCOUNT]);
|
||||
|
||||
if (migrationState) {
|
||||
const transaction = db.transaction([IndexedDBCryptoStore.STORE_ACCOUNT], "readwrite");
|
||||
const store = transaction.objectStore(IndexedDBCryptoStore.STORE_ACCOUNT);
|
||||
store.put(migrationState, "migrationState");
|
||||
await new Promise((resolve, reject) => {
|
||||
transaction.oncomplete = resolve;
|
||||
transaction.onerror = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
global.structuredClone = (v) => JSON.parse(JSON.stringify(v));
|
||||
});
|
||||
|
||||
describe("Crypto store checks", () => {
|
||||
async function populateHealthySession() {
|
||||
// Storage manager only check for the existence of the `riot-web-sync` store, so just create one.
|
||||
await createDB("riot-web-sync");
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await populateHealthySession();
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
it("should not be ok if sync store but no crypto store", async () => {
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(true);
|
||||
expect(result.dataInCryptoStore).toBe(false);
|
||||
});
|
||||
|
||||
it("should be ok if sync store and a rust crypto store", async () => {
|
||||
await createDB(RUST_CRYPTO_STORE_NAME);
|
||||
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(true);
|
||||
expect(result.dataInCryptoStore).toBe(true);
|
||||
});
|
||||
|
||||
describe("without rust store", () => {
|
||||
it("should be ok if there is non migrated legacy crypto store", async () => {
|
||||
await populateLegacyStore(undefined);
|
||||
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(true);
|
||||
expect(result.dataInCryptoStore).toBe(true);
|
||||
});
|
||||
|
||||
it("should be ok if legacy store in MigrationState `NOT_STARTED`", async () => {
|
||||
await populateLegacyStore(0 /* MigrationState.NOT_STARTED*/);
|
||||
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(true);
|
||||
expect(result.dataInCryptoStore).toBe(true);
|
||||
});
|
||||
|
||||
it("should not be ok if MigrationState greater than `NOT_STARTED`", async () => {
|
||||
await populateLegacyStore(1 /*INITIAL_DATA_MIGRATED*/);
|
||||
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(true);
|
||||
expect(result.dataInCryptoStore).toBe(false);
|
||||
});
|
||||
|
||||
it("should not be healthy if no indexeddb", async () => {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = {} as IDBFactory;
|
||||
|
||||
const result = await StorageManager.checkConsistency();
|
||||
expect(result.healthy).toBe(false);
|
||||
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
43
test/unit-tests/utils/UrlUtils-test.ts
Normal file
43
test/unit-tests/utils/UrlUtils-test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { abbreviateUrl, parseUrl, unabbreviateUrl } from "../../src/utils/UrlUtils";
|
||||
|
||||
describe("abbreviateUrl", () => {
|
||||
it("should return empty string if passed falsey", () => {
|
||||
expect(abbreviateUrl(undefined)).toEqual("");
|
||||
});
|
||||
|
||||
it("should abbreviate to host if empty pathname", () => {
|
||||
expect(abbreviateUrl("https://foo/")).toEqual("foo");
|
||||
});
|
||||
|
||||
it("should not abbreviate if has path parts", () => {
|
||||
expect(abbreviateUrl("https://foo/path/parts")).toEqual("https://foo/path/parts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unabbreviateUrl", () => {
|
||||
it("should return empty string if passed falsey", () => {
|
||||
expect(unabbreviateUrl(undefined)).toEqual("");
|
||||
});
|
||||
|
||||
it("should prepend https to input if it lacks it", () => {
|
||||
expect(unabbreviateUrl("element.io")).toEqual("https://element.io");
|
||||
});
|
||||
|
||||
it("should not prepend https to input if it has it", () => {
|
||||
expect(unabbreviateUrl("https://element.io")).toEqual("https://element.io");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUrl", () => {
|
||||
it("should not throw on no proto", () => {
|
||||
expect(() => parseUrl("test")).not.toThrow();
|
||||
});
|
||||
});
|
44
test/unit-tests/utils/WidgetUtils-test.ts
Normal file
44
test/unit-tests/utils/WidgetUtils-test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 Oliver Sand
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import WidgetUtils from "../../src/utils/WidgetUtils";
|
||||
import { mockPlatformPeg } from "../test-utils";
|
||||
|
||||
describe("getLocalJitsiWrapperUrl", () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
origin: "https://app.element.io",
|
||||
pathname: "",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should generate jitsi URL (for defaults)", () => {
|
||||
mockPlatformPeg();
|
||||
|
||||
expect(WidgetUtils.getLocalJitsiWrapperUrl()).toEqual(
|
||||
"https://app.element.io/jitsi.html" +
|
||||
"#conferenceDomain=$domain" +
|
||||
"&conferenceId=$conferenceId" +
|
||||
"&isAudioOnly=$isAudioOnly" +
|
||||
"&startWithAudioMuted=$startWithAudioMuted" +
|
||||
"&startWithVideoMuted=$startWithVideoMuted" +
|
||||
"&isVideoChannel=$isVideoChannel" +
|
||||
"&displayName=$matrix_display_name" +
|
||||
"&avatarUrl=$matrix_avatar_url" +
|
||||
"&userId=$matrix_user_id" +
|
||||
"&roomId=$matrix_room_id" +
|
||||
"&theme=$theme" +
|
||||
"&roomName=$roomName" +
|
||||
"&supportsScreensharing=true" +
|
||||
"&language=$org.matrix.msc2873.client_language",
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AutoDiscoveryUtils authComponentStateForError should return expected error for the registration page 1`] = `
|
||||
{
|
||||
"serverDeadError": <div>
|
||||
<strong>
|
||||
Your Element is misconfigured
|
||||
</strong>
|
||||
<div>
|
||||
<span>
|
||||
Ask your Element admin to check
|
||||
<a
|
||||
href="https://github.com/vector-im/element-web/blob/master/docs/config.md"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
your config
|
||||
</a>
|
||||
for incorrect or duplicate entries.
|
||||
</span>
|
||||
</div>
|
||||
</div>,
|
||||
"serverErrorIsFatal": true,
|
||||
"serverIsAlive": false,
|
||||
}
|
||||
`;
|
129
test/unit-tests/utils/__snapshots__/ErrorUtils-test.ts.snap
Normal file
129
test/unit-tests/utils/__snapshots__/ErrorUtils-test.ts.snap
Normal file
|
@ -0,0 +1,129 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for ConnectionError 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
<span>
|
||||
Can't connect to homeserver - please check your connectivity, ensure your
|
||||
<a
|
||||
class="mx_ExternalLink"
|
||||
href="hsUrl"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
homeserver's SSL certificate
|
||||
<i
|
||||
class="mx_ExternalLink_icon"
|
||||
/>
|
||||
</a>
|
||||
is trusted, and that a browser extension is not blocking requests.
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for MatrixError M_NOT_FOUND 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later.(M_NOT_FOUND)
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for mixed content error 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
<span>
|
||||
Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or
|
||||
<a
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
enable unsafe scripts
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForConnectionError should match snapshot for unknown error 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for 401 1`] = `
|
||||
<DocumentFragment>
|
||||
Incorrect username and/or password.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div>
|
||||
This homeserver has exceeded one of its resource limits.
|
||||
</div>
|
||||
<div
|
||||
class="mx_Login_smallError"
|
||||
>
|
||||
Please contact your service administrator to continue using this service.
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for M_USER_DEACTIVATED 1`] = `
|
||||
<DocumentFragment>
|
||||
This account has been deactivated.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForLoginError should match snapshot for unknown error 1`] = `
|
||||
<DocumentFragment>
|
||||
There was a problem communicating with the homeserver, please try again later. (HTTP 400)
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForResourceLimitError should match snapshot for admin contact links 1`] = `
|
||||
<DocumentFragment>
|
||||
<span>
|
||||
Please
|
||||
<a
|
||||
href="some@email"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
contact your service administrator
|
||||
</a>
|
||||
to continue using this service.
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForResourceLimitError should match snapshot for monthly_active_user 1`] = `
|
||||
<DocumentFragment>
|
||||
This homeserver has hit its Monthly Active User limit.
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForSyncError should match snapshot for M_RESOURCE_LIMIT_EXCEEDED 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div>
|
||||
This homeserver has exceeded one of its resource limits.
|
||||
</div>
|
||||
<div>
|
||||
Please contact your service administrator to continue using this service.
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`messageForSyncError should match snapshot for other errors 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
Unable to connect to Homeserver. Retrying…
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,48 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattingUtils formatList should return expected sentence in ReactNode when given 2 React children 1`] = `
|
||||
<span>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
and
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`FormattingUtils formatList should return expected sentence in ReactNode when given more React children 1`] = `
|
||||
<span>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
,
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
,
|
||||
<span>
|
||||
c
|
||||
</span>
|
||||
and
|
||||
<span>
|
||||
d
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
exports[`FormattingUtils formatList should return expected sentence in ReactNode when using itemLimit 1`] = `
|
||||
<span>
|
||||
<span>
|
||||
<span>
|
||||
a
|
||||
</span>
|
||||
,
|
||||
<span>
|
||||
b
|
||||
</span>
|
||||
</span>
|
||||
and 2 others
|
||||
</span>
|
||||
`;
|
|
@ -0,0 +1,500 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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 <strong>
|
||||
<span
|
||||
class="mx_EditHistoryMessage_insertion"
|
||||
>
|
||||
t
|
||||
</span>
|
||||
here</strong>
|
||||
</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>
|
||||
`;
|
|
@ -0,0 +1,32 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
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",
|
||||
}
|
||||
`;
|
463
test/unit-tests/utils/arrays-test.ts
Normal file
463
test/unit-tests/utils/arrays-test.ts
Normal file
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
arrayDiff,
|
||||
arrayFastClone,
|
||||
arrayFastResample,
|
||||
arrayHasDiff,
|
||||
arrayHasOrderChange,
|
||||
arrayUnion,
|
||||
arrayRescale,
|
||||
arraySeed,
|
||||
arraySmoothingResample,
|
||||
arrayTrimFill,
|
||||
arrayIntersection,
|
||||
ArrayUtil,
|
||||
GroupedArray,
|
||||
concat,
|
||||
asyncEvery,
|
||||
asyncSome,
|
||||
} from "../../src/utils/arrays";
|
||||
|
||||
type TestParams = { input: number[]; output: number[] };
|
||||
type TestCase = [string, TestParams];
|
||||
|
||||
function expectSample(input: number[], expected: number[], smooth = false) {
|
||||
const result = (smooth ? arraySmoothingResample : arrayFastResample)(input, expected.length);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(expected.length);
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
|
||||
describe("arrays", () => {
|
||||
describe("arrayFastResample", () => {
|
||||
const downsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [1, 2, 3, 4, 5], output: [1, 4] }],
|
||||
["Odd -> Odd", { input: [1, 2, 3, 4, 5], output: [1, 3, 5] }],
|
||||
["Even -> Odd", { input: [1, 2, 3, 4], output: [1, 2, 3] }],
|
||||
["Even -> Even", { input: [1, 2, 3, 4], output: [1, 3] }],
|
||||
];
|
||||
it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output),
|
||||
);
|
||||
|
||||
const upsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3] }],
|
||||
["Odd -> Odd", { input: [1, 2, 3], output: [1, 1, 2, 2, 3] }],
|
||||
["Even -> Odd", { input: [1, 2], output: [1, 1, 1, 2, 2] }],
|
||||
["Even -> Even", { input: [1, 2], output: [1, 1, 1, 2, 2, 2] }],
|
||||
];
|
||||
it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) => expectSample(input, output));
|
||||
|
||||
const maintainSampleCases: TestCase[] = [
|
||||
["Odd", { input: [1, 2, 3], output: [1, 2, 3] }], // Odd
|
||||
["Even", { input: [1, 2], output: [1, 2] }], // Even
|
||||
];
|
||||
|
||||
it.each(maintainSampleCases)("maintains samples for %s", (_d, { input, output }) =>
|
||||
expectSample(input, output),
|
||||
);
|
||||
});
|
||||
|
||||
describe("arraySmoothingResample", () => {
|
||||
// Dev note: these aren't great samples, but they demonstrate the bare minimum. Ideally
|
||||
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
|
||||
// but that's not really feasible to manually verify accuracy.
|
||||
const downsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3] }],
|
||||
["Odd -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3] }],
|
||||
["Even -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3] }],
|
||||
["Even -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3] }],
|
||||
];
|
||||
|
||||
it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output, true),
|
||||
);
|
||||
|
||||
const upsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [2, 0, 2], output: [2, 2, 0, 0, 2, 2] }],
|
||||
["Odd -> Odd", { input: [2, 0, 2], output: [2, 2, 0, 0, 2] }],
|
||||
["Even -> Odd", { input: [2, 0], output: [2, 2, 2, 0, 0] }],
|
||||
["Even -> Even", { input: [2, 0], output: [2, 2, 2, 0, 0, 0] }],
|
||||
];
|
||||
it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output, true),
|
||||
);
|
||||
|
||||
const maintainCases: TestCase[] = [
|
||||
["Odd", { input: [2, 0, 2], output: [2, 0, 2] }],
|
||||
["Even", { input: [2, 0], output: [2, 0] }],
|
||||
];
|
||||
it.each(maintainCases)("maintains samples for %s", (_d, { input, output }) => expectSample(input, output));
|
||||
});
|
||||
|
||||
describe("arrayRescale", () => {
|
||||
it("should rescale", () => {
|
||||
const input = [8, 9, 1, 0, 2, 7, 10];
|
||||
const output = [80, 90, 10, 0, 20, 70, 100];
|
||||
const result = arrayRescale(input, 0, 100);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayTrimFill", () => {
|
||||
it("should shrink arrays", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
|
||||
it("should expand arrays", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2, 3, 4, 5];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
|
||||
it("should keep arrays the same", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2, 3];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arraySeed", () => {
|
||||
it("should create an array of given length", () => {
|
||||
const val = 1;
|
||||
const output = [val, val, val];
|
||||
const result = arraySeed(val, output.length);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
it("should maintain pointers", () => {
|
||||
const val = {}; // this works because `{} !== {}`, which is what toEqual checks
|
||||
const output = [val, val, val];
|
||||
const result = arraySeed(val, output.length);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayFastClone", () => {
|
||||
it("should break pointer reference on source array", () => {
|
||||
const val = {}; // we'll test to make sure the values maintain pointers too
|
||||
const input = [val, val, val];
|
||||
const result = arrayFastClone(input);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(input.length);
|
||||
expect(result).toEqual(input); // we want the array contents to match...
|
||||
expect(result).not.toBe(input); // ... but be a different reference
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayHasOrderChange", () => {
|
||||
it("should flag true on B ordering difference", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [3, 2, 1];
|
||||
const result = arrayHasOrderChange(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag false on no ordering difference", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3];
|
||||
const result = arrayHasOrderChange(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should flag true on A length > B length", () => {
|
||||
const a = [1, 2, 3, 4];
|
||||
const b = [1, 2, 3];
|
||||
const result = arrayHasOrderChange(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag true on A length < B length", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3, 4];
|
||||
const result = arrayHasOrderChange(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayHasDiff", () => {
|
||||
it("should flag true on A length > B length", () => {
|
||||
const a = [1, 2, 3, 4];
|
||||
const b = [1, 2, 3];
|
||||
const result = arrayHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag true on A length < B length", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3, 4];
|
||||
const result = arrayHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag true on element differences", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [4, 5, 6];
|
||||
const result = arrayHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag false if same but order different", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [3, 1, 2];
|
||||
const result = arrayHasDiff(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should flag false if same", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3];
|
||||
const result = arrayHasDiff(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayDiff", () => {
|
||||
it("should see added from A->B", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3, 4];
|
||||
const result = arrayDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.added).toEqual([4]);
|
||||
});
|
||||
|
||||
it("should see removed from A->B", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2];
|
||||
const result = arrayDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.removed).toEqual([3]);
|
||||
});
|
||||
|
||||
it("should see added and removed in the same set", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = arrayDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.added).toEqual([4]);
|
||||
expect(result.removed).toEqual([3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayIntersection", () => {
|
||||
it("should return the intersection", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = arrayIntersection(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("should return an empty array on no matches", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [4, 5, 6];
|
||||
const result = arrayIntersection(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayUnion", () => {
|
||||
it("should union 3 arrays with deduplication", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4, 5]; // note missing 3
|
||||
const c = [6, 7, 8, 9];
|
||||
const result = arrayUnion(a, b, c);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(9);
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it("should deduplicate a single array", () => {
|
||||
// dev note: this is technically an edge case, but it is described behaviour if the
|
||||
// function is only provided one array (it'll merge the array against itself)
|
||||
const a = [1, 1, 2, 2, 3, 3];
|
||||
const result = arrayUnion(a);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ArrayUtil", () => {
|
||||
it("should maintain the pointer to the given array", () => {
|
||||
const input = [1, 2, 3];
|
||||
const result = new ArrayUtil(input);
|
||||
expect(result.value).toBe(input);
|
||||
});
|
||||
|
||||
it("should group appropriately", () => {
|
||||
const input = [
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
["a", 4],
|
||||
["a", 5],
|
||||
["b", 6],
|
||||
];
|
||||
const output = {
|
||||
a: [
|
||||
["a", 1],
|
||||
["a", 4],
|
||||
["a", 5],
|
||||
],
|
||||
b: [
|
||||
["b", 2],
|
||||
["b", 6],
|
||||
],
|
||||
c: [["c", 3]],
|
||||
};
|
||||
const result = new ArrayUtil(input).groupBy((p) => p[0]);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.value).toBeDefined();
|
||||
|
||||
const asObject = Object.fromEntries(result.value.entries());
|
||||
expect(asObject).toMatchObject(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GroupedArray", () => {
|
||||
it("should maintain the pointer to the given map", () => {
|
||||
const input = new Map([
|
||||
["a", [1, 2, 3]],
|
||||
["b", [7, 8, 9]],
|
||||
["c", [4, 5, 6]],
|
||||
]);
|
||||
const result = new GroupedArray(input);
|
||||
expect(result.value).toBe(input);
|
||||
});
|
||||
|
||||
it("should ordering by the provided key order", () => {
|
||||
const input = new Map([
|
||||
["a", [1, 2, 3]],
|
||||
["b", [7, 8, 9]], // note counting diff
|
||||
["c", [4, 5, 6]],
|
||||
]);
|
||||
const output = [4, 5, 6, 1, 2, 3, 7, 8, 9];
|
||||
const keyOrder = ["c", "a", "b"]; // note weird order to cause the `output` to be strange
|
||||
const result = new GroupedArray(input).orderBy(keyOrder);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.value).toBeDefined();
|
||||
expect(result.value).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("concat", () => {
|
||||
const emptyArray = () => new Uint8Array(0);
|
||||
const array1 = () => new Uint8Array([1, 2, 3]);
|
||||
const array2 = () => new Uint8Array([4, 5, 6]);
|
||||
const array3 = () => new Uint8Array([7, 8, 9]);
|
||||
|
||||
it("should work for empty arrays", () => {
|
||||
expect(concat(emptyArray(), emptyArray())).toEqual(emptyArray());
|
||||
});
|
||||
|
||||
it("should concat an empty and non-empty array", () => {
|
||||
expect(concat(emptyArray(), array1())).toEqual(array1());
|
||||
});
|
||||
|
||||
it("should concat an non-empty and empty array", () => {
|
||||
expect(concat(array1(), emptyArray())).toEqual(array1());
|
||||
});
|
||||
|
||||
it("should concat two arrays", () => {
|
||||
expect(concat(array1(), array2())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6]));
|
||||
});
|
||||
|
||||
it("should concat three arrays", () => {
|
||||
expect(concat(array1(), array2(), array3())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncEvery", () => {
|
||||
it("when called with an empty array, it should return true", async () => {
|
||||
expect(await asyncEvery([], jest.fn().mockResolvedValue(true))).toBe(true);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to true for all of them, it should return true", async () => {
|
||||
const predicate = jest.fn().mockResolvedValue(true);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(true);
|
||||
expect(predicate).toHaveBeenCalledTimes(3);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
expect(predicate).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
|
||||
const predicate = jest.fn().mockResolvedValue(false);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(1);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for one of them, it should return false", async () => {
|
||||
const predicate = jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(2);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncSome", () => {
|
||||
it("when called with an empty array, it should return false", async () => {
|
||||
expect(await asyncSome([], jest.fn().mockResolvedValue(true))).toBe(false);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
|
||||
const predicate = jest.fn().mockResolvedValue(false);
|
||||
expect(await asyncSome([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(3);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
expect(predicate).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to true, it should short-circuit and return true", async () => {
|
||||
const predicate = jest.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
expect(await asyncSome([1, 2, 3], predicate)).toBe(true);
|
||||
expect(predicate).toHaveBeenCalledTimes(2);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
});
|
93
test/unit-tests/utils/beacon/bounds-test.ts
Normal file
93
test/unit-tests/utils/beacon/bounds-test.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Beacon } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { Bounds, getBeaconBounds } from "../../../src/utils/beacon/bounds";
|
||||
import { makeBeaconEvent, makeBeaconInfoEvent } from "../../test-utils";
|
||||
|
||||
describe("getBeaconBounds()", () => {
|
||||
const userId = "@user:server";
|
||||
const roomId = "!room:server";
|
||||
const makeBeaconWithLocation = (latLon: { lat: number; lon: number }) => {
|
||||
const geoUri = `geo:${latLon.lat},${latLon.lon}`;
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true }));
|
||||
// @ts-ignore private prop, sets internal live property so addLocations works
|
||||
beacon.checkLiveness();
|
||||
const location = makeBeaconEvent(userId, {
|
||||
beaconInfoId: beacon.beaconInfoId,
|
||||
geoUri,
|
||||
timestamp: Date.now() + 1,
|
||||
});
|
||||
beacon.addLocations([location]);
|
||||
|
||||
return beacon;
|
||||
};
|
||||
|
||||
const geo = {
|
||||
// northern hemi
|
||||
// west of greenwich
|
||||
london: { lat: 51.5, lon: -0.14 },
|
||||
reykjavik: { lat: 64.08, lon: -21.82 },
|
||||
// east of greenwich
|
||||
paris: { lat: 48.85, lon: 2.29 },
|
||||
// southern hemi
|
||||
// east
|
||||
auckland: { lat: -36.85, lon: 174.76 }, // nz
|
||||
// west
|
||||
lima: { lat: -12.013843, lon: -77.008388 }, // peru
|
||||
};
|
||||
|
||||
const london = makeBeaconWithLocation(geo.london);
|
||||
const reykjavik = makeBeaconWithLocation(geo.reykjavik);
|
||||
const paris = makeBeaconWithLocation(geo.paris);
|
||||
const auckland = makeBeaconWithLocation(geo.auckland);
|
||||
const lima = makeBeaconWithLocation(geo.lima);
|
||||
|
||||
it("should return undefined when there are no beacons", () => {
|
||||
expect(getBeaconBounds([])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined when no beacons have locations", () => {
|
||||
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId));
|
||||
expect(getBeaconBounds([beacon])).toBeUndefined();
|
||||
});
|
||||
|
||||
type TestCase = [string, Beacon[], Bounds];
|
||||
it.each<TestCase>([
|
||||
[
|
||||
"one beacon",
|
||||
[london],
|
||||
{ north: geo.london.lat, south: geo.london.lat, east: geo.london.lon, west: geo.london.lon },
|
||||
],
|
||||
[
|
||||
"beacons in the northern hemisphere, west of meridian",
|
||||
[london, reykjavik],
|
||||
{ north: geo.reykjavik.lat, south: geo.london.lat, east: geo.london.lon, west: geo.reykjavik.lon },
|
||||
],
|
||||
[
|
||||
"beacons in the northern hemisphere, both sides of meridian",
|
||||
[london, reykjavik, paris],
|
||||
// reykjavik northmost and westmost, paris southmost and eastmost
|
||||
{ north: geo.reykjavik.lat, south: geo.paris.lat, east: geo.paris.lon, west: geo.reykjavik.lon },
|
||||
],
|
||||
[
|
||||
"beacons in the southern hemisphere",
|
||||
[auckland, lima],
|
||||
// lima northmost and westmost, auckland southmost and eastmost
|
||||
{ north: geo.lima.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon },
|
||||
],
|
||||
[
|
||||
"beacons in both hemispheres",
|
||||
[auckland, lima, paris],
|
||||
{ north: geo.paris.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon },
|
||||
],
|
||||
])("gets correct bounds for %s", (_description, beacons, expectedBounds) => {
|
||||
expect(getBeaconBounds(beacons)).toEqual(expectedBounds);
|
||||
});
|
||||
});
|
106
test/unit-tests/utils/beacon/duration-test.ts
Normal file
106
test/unit-tests/utils/beacon/duration-test.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { M_TIMESTAMP, Beacon } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { msUntilExpiry, sortBeaconsByLatestExpiry, sortBeaconsByLatestCreation } from "../../../src/utils/beacon";
|
||||
import { makeBeaconInfoEvent } from "../../test-utils";
|
||||
|
||||
describe("beacon utils", () => {
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
const HOUR_MS = 3600000;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.spyOn(global.Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
describe("msUntilExpiry", () => {
|
||||
it("returns remaining duration", () => {
|
||||
const start = now - HOUR_MS;
|
||||
const durationMs = HOUR_MS * 3;
|
||||
|
||||
expect(msUntilExpiry(start, durationMs)).toEqual(HOUR_MS * 2);
|
||||
});
|
||||
|
||||
it("returns 0 when expiry has already passed", () => {
|
||||
// created 3h ago
|
||||
const start = now - HOUR_MS * 3;
|
||||
// 1h durations
|
||||
const durationMs = HOUR_MS;
|
||||
|
||||
expect(msUntilExpiry(start, durationMs)).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortBeaconsByLatestExpiry()", () => {
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alive:server";
|
||||
|
||||
// 12h old, 12h left
|
||||
const beacon1 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
|
||||
);
|
||||
// 10h left
|
||||
const beacon2 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 10, timestamp: now }, "$2"),
|
||||
);
|
||||
|
||||
// 1ms left
|
||||
const beacon3 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS + 1, timestamp: now - HOUR_MS }, "$3"),
|
||||
);
|
||||
|
||||
const noTimestampEvent = makeBeaconInfoEvent(
|
||||
aliceId,
|
||||
roomId,
|
||||
{ timeout: HOUR_MS + 1, timestamp: undefined },
|
||||
"$3",
|
||||
);
|
||||
// beacon info helper defaults to date when timestamp is falsy
|
||||
// hard set it to undefined
|
||||
// @ts-ignore
|
||||
noTimestampEvent.event.content[M_TIMESTAMP.name] = undefined;
|
||||
const beaconNoTimestamp = new Beacon(noTimestampEvent);
|
||||
|
||||
it("sorts beacons by descending expiry time", () => {
|
||||
expect([beacon2, beacon3, beacon1].sort(sortBeaconsByLatestExpiry)).toEqual([beacon1, beacon2, beacon3]);
|
||||
});
|
||||
|
||||
it("sorts beacons with timestamps before beacons without", () => {
|
||||
expect([beaconNoTimestamp, beacon3].sort(sortBeaconsByLatestExpiry)).toEqual([beacon3, beaconNoTimestamp]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortBeaconsByLatestCreation()", () => {
|
||||
const roomId = "!room:server";
|
||||
const aliceId = "@alive:server";
|
||||
|
||||
// 12h old, 12h left
|
||||
const beacon1 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
|
||||
);
|
||||
// 10h left
|
||||
const beacon2 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 10, timestamp: now }, "$2"),
|
||||
);
|
||||
|
||||
// 1ms left
|
||||
const beacon3 = new Beacon(
|
||||
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS + 1, timestamp: now - HOUR_MS }, "$3"),
|
||||
);
|
||||
|
||||
it("sorts beacons by descending creation time", () => {
|
||||
expect([beacon1, beacon2, beacon3].sort(sortBeaconsByLatestCreation)).toEqual([beacon2, beacon3, beacon1]);
|
||||
});
|
||||
});
|
||||
});
|
229
test/unit-tests/utils/beacon/geolocation-test.ts
Normal file
229
test/unit-tests/utils/beacon/geolocation-test.ts
Normal file
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
GenericPosition,
|
||||
GeolocationError,
|
||||
getGeoUri,
|
||||
mapGeolocationError,
|
||||
mapGeolocationPositionToTimedGeo,
|
||||
watchPosition,
|
||||
} from "../../../src/utils/beacon";
|
||||
import { getCurrentPosition } from "../../../src/utils/beacon/geolocation";
|
||||
import { makeGeolocationPosition, mockGeolocation, getMockGeolocationPositionError } from "../../test-utils";
|
||||
|
||||
describe("geolocation utilities", () => {
|
||||
let geolocation: Mocked<Geolocation>;
|
||||
const defaultPosition = makeGeolocationPosition({});
|
||||
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
beforeEach(() => {
|
||||
geolocation = mockGeolocation();
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.spyOn(Date, "now").mockRestore();
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
describe("getGeoUri", () => {
|
||||
it("Renders a URI with only lat and lon", () => {
|
||||
const pos: GenericPosition = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: undefined,
|
||||
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||
});
|
||||
|
||||
it("Nulls in location are not shown in URI", () => {
|
||||
const pos: GenericPosition = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
|
||||
});
|
||||
|
||||
it("Renders a URI with 3 coords", () => {
|
||||
const pos: GenericPosition = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 332.54,
|
||||
accuracy: undefined,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,332.54");
|
||||
});
|
||||
|
||||
it("Renders a URI with accuracy", () => {
|
||||
const pos: GenericPosition = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: undefined,
|
||||
accuracy: 21,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4;u=21");
|
||||
});
|
||||
|
||||
it("Renders a URI with accuracy and altitude", () => {
|
||||
const pos = {
|
||||
latitude: 43.2,
|
||||
longitude: 12.4,
|
||||
altitude: 12.3,
|
||||
accuracy: 21,
|
||||
timestamp: 12334,
|
||||
};
|
||||
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,12.3;u=21");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapGeolocationError", () => {
|
||||
beforeEach(() => {
|
||||
// suppress expected errors from test log
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it("returns default for other error", () => {
|
||||
const error = new Error("oh no..");
|
||||
expect(mapGeolocationError(error)).toEqual(GeolocationError.Default);
|
||||
});
|
||||
|
||||
it("returns unavailable for unavailable error", () => {
|
||||
const error = new Error(GeolocationError.Unavailable);
|
||||
expect(mapGeolocationError(error)).toEqual(GeolocationError.Unavailable);
|
||||
});
|
||||
|
||||
it("maps geo error permissiondenied correctly", () => {
|
||||
const error = getMockGeolocationPositionError(1, "message");
|
||||
expect(mapGeolocationError(error)).toEqual(GeolocationError.PermissionDenied);
|
||||
});
|
||||
|
||||
it("maps geo position unavailable error correctly", () => {
|
||||
const error = getMockGeolocationPositionError(2, "message");
|
||||
expect(mapGeolocationError(error)).toEqual(GeolocationError.PositionUnavailable);
|
||||
});
|
||||
|
||||
it("maps geo timeout error correctly", () => {
|
||||
const error = getMockGeolocationPositionError(3, "message");
|
||||
expect(mapGeolocationError(error)).toEqual(GeolocationError.Timeout);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapGeolocationPositionToTimedGeo()", () => {
|
||||
it("maps geolocation position correctly", () => {
|
||||
expect(mapGeolocationPositionToTimedGeo(defaultPosition)).toEqual({
|
||||
timestamp: now,
|
||||
geoUri: "geo:54.001927,-8.253491;u=1",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("watchPosition()", () => {
|
||||
it("throws with unavailable error when geolocation is not available", () => {
|
||||
// suppress expected errors from test log
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
|
||||
// remove the mock we added
|
||||
// @ts-ignore illegal assignment to readonly property
|
||||
navigator.geolocation = undefined;
|
||||
|
||||
const positionHandler = jest.fn();
|
||||
const errorHandler = jest.fn();
|
||||
|
||||
expect(() => watchPosition(positionHandler, errorHandler)).toThrow(GeolocationError.Unavailable);
|
||||
});
|
||||
|
||||
it("sets up position handler with correct options", () => {
|
||||
const positionHandler = jest.fn();
|
||||
const errorHandler = jest.fn();
|
||||
watchPosition(positionHandler, errorHandler);
|
||||
|
||||
const [, , options] = geolocation.watchPosition.mock.calls[0];
|
||||
expect(options).toEqual({
|
||||
maximumAge: 60000,
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns clearWatch function", () => {
|
||||
const watchId = 1;
|
||||
geolocation.watchPosition.mockReturnValue(watchId);
|
||||
const positionHandler = jest.fn();
|
||||
const errorHandler = jest.fn();
|
||||
const clearWatch = watchPosition(positionHandler, errorHandler);
|
||||
|
||||
clearWatch();
|
||||
|
||||
expect(geolocation.clearWatch).toHaveBeenCalledWith(watchId);
|
||||
});
|
||||
|
||||
it("calls position handler with position", () => {
|
||||
const positionHandler = jest.fn();
|
||||
const errorHandler = jest.fn();
|
||||
watchPosition(positionHandler, errorHandler);
|
||||
|
||||
expect(positionHandler).toHaveBeenCalledWith(defaultPosition);
|
||||
});
|
||||
|
||||
it("maps geolocation position error and calls error handler", () => {
|
||||
// suppress expected errors from test log
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
geolocation.watchPosition.mockImplementation((_callback, error) => {
|
||||
error!(getMockGeolocationPositionError(1, "message"));
|
||||
return -1;
|
||||
});
|
||||
const positionHandler = jest.fn();
|
||||
const errorHandler = jest.fn();
|
||||
watchPosition(positionHandler, errorHandler);
|
||||
|
||||
expect(errorHandler).toHaveBeenCalledWith(GeolocationError.PermissionDenied);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCurrentPosition()", () => {
|
||||
it("throws with unavailable error when geolocation is not available", async () => {
|
||||
// suppress expected errors from test log
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
|
||||
// remove the mock we added
|
||||
// @ts-ignore illegal assignment to readonly property
|
||||
navigator.geolocation = undefined;
|
||||
|
||||
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Unavailable);
|
||||
});
|
||||
|
||||
it("throws with geolocation error when geolocation.getCurrentPosition fails", async () => {
|
||||
// suppress expected errors from test log
|
||||
jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
|
||||
const timeoutError = getMockGeolocationPositionError(3, "message");
|
||||
geolocation.getCurrentPosition.mockImplementation((callback, error) => error!(timeoutError));
|
||||
|
||||
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Timeout);
|
||||
});
|
||||
|
||||
it("resolves with current location", async () => {
|
||||
geolocation.getCurrentPosition.mockImplementation((callback, error) => callback(defaultPosition));
|
||||
|
||||
const result = await getCurrentPosition();
|
||||
expect(result).toEqual(defaultPosition);
|
||||
});
|
||||
});
|
||||
});
|
38
test/unit-tests/utils/beacon/timeline-test.ts
Normal file
38
test/unit-tests/utils/beacon/timeline-test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { shouldDisplayAsBeaconTile } from "../../../src/utils/beacon/timeline";
|
||||
import { makeBeaconInfoEvent, stubClient } from "../../test-utils";
|
||||
|
||||
describe("shouldDisplayAsBeaconTile", () => {
|
||||
const userId = "@user:server";
|
||||
const roomId = "!room:server";
|
||||
const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true });
|
||||
const notLiveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false });
|
||||
const memberEvent = new MatrixEvent({ type: EventType.RoomMember });
|
||||
const redactedBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false });
|
||||
redactedBeacon.makeRedacted(redactedBeacon, new Room(roomId, stubClient(), userId));
|
||||
|
||||
it("returns true for a beacon with live property set to true", () => {
|
||||
expect(shouldDisplayAsBeaconTile(liveBeacon)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for a redacted beacon", () => {
|
||||
expect(shouldDisplayAsBeaconTile(redactedBeacon)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for a beacon with live property set to false", () => {
|
||||
expect(shouldDisplayAsBeaconTile(notLiveBeacon)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for a non beacon event", () => {
|
||||
expect(shouldDisplayAsBeaconTile(memberEvent)).toBe(false);
|
||||
});
|
||||
});
|
16
test/unit-tests/utils/colour-test.ts
Normal file
16
test/unit-tests/utils/colour-test.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 Emmanuel Ezeka <eec.studies@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { textToHtmlRainbow } from "../../src/utils/colour";
|
||||
|
||||
describe("textToHtmlRainbow", () => {
|
||||
it("correctly transform text to html without splitting the emoji in two", () => {
|
||||
expect(textToHtmlRainbow("🐻")).toBe('<span data-mx-color="#ff00be">🐻</span>');
|
||||
expect(textToHtmlRainbow("🐕🦺")).toBe('<span data-mx-color="#ff00be">🐕🦺</span>');
|
||||
});
|
||||
});
|
43
test/unit-tests/utils/connection-test.ts
Normal file
43
test/unit-tests/utils/connection-test.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { ClientEvent, ClientEventHandlerMap, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { createReconnectedListener } from "../../src/utils/connection";
|
||||
|
||||
describe("createReconnectedListener", () => {
|
||||
let reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
|
||||
let onReconnect: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onReconnect = jest.fn();
|
||||
reconnectedListener = createReconnectedListener(onReconnect);
|
||||
});
|
||||
|
||||
[
|
||||
[SyncState.Prepared, SyncState.Syncing],
|
||||
[SyncState.Syncing, SyncState.Reconnecting],
|
||||
[SyncState.Reconnecting, SyncState.Syncing],
|
||||
].forEach(([from, to]) => {
|
||||
it(`should invoke the callback on a transition from ${from} to ${to}`, () => {
|
||||
reconnectedListener(to, from);
|
||||
expect(onReconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
[SyncState.Syncing, SyncState.Syncing],
|
||||
[SyncState.Catchup, SyncState.Error],
|
||||
[SyncState.Reconnecting, SyncState.Error],
|
||||
].forEach(([from, to]) => {
|
||||
it(`should not invoke the callback on a transition from ${from} to ${to}`, () => {
|
||||
reconnectedListener(to, from);
|
||||
expect(onReconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
26
test/unit-tests/utils/createVoiceMessageContent-test.ts
Normal file
26
test/unit-tests/utils/createVoiceMessageContent-test.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { 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();
|
||||
});
|
||||
});
|
72
test/unit-tests/utils/crypto/deviceInfo-test.ts
Normal file
72
test/unit-tests/utils/crypto/deviceInfo-test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
import { Device, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getDeviceCryptoInfo, getUserDeviceIds } from "../../../src/utils/crypto/deviceInfo";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsCrypto } from "../../test-utils";
|
||||
|
||||
describe("getDeviceCryptoInfo()", () => {
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto() });
|
||||
});
|
||||
|
||||
it("should return undefined on clients with no crypto", async () => {
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(undefined);
|
||||
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for unknown users", async () => {
|
||||
mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map());
|
||||
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined for unknown devices", async () => {
|
||||
mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map([["@user:id", new Map()]]));
|
||||
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return the right result for known devices", async () => {
|
||||
const mockDevice = { deviceId: "device_id" } as Device;
|
||||
mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||
new Map([["@user:id", new Map([["device_id", mockDevice]])]]),
|
||||
);
|
||||
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBe(mockDevice);
|
||||
expect(mockClient.getCrypto()!.getUserDeviceInfo).toHaveBeenCalledWith(["@user:id"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUserDeviceIds", () => {
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto() });
|
||||
});
|
||||
|
||||
it("should return empty set on clients with no crypto", async () => {
|
||||
jest.spyOn(mockClient, "getCrypto").mockReturnValue(undefined);
|
||||
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set());
|
||||
});
|
||||
|
||||
it("should return empty set for unknown users", async () => {
|
||||
mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map());
|
||||
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set());
|
||||
});
|
||||
|
||||
it("should return the right result for known users", async () => {
|
||||
const mockDevice = { deviceId: "device_id" } as Device;
|
||||
mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
|
||||
new Map([["@user:id", new Map([["device_id", mockDevice]])]]),
|
||||
);
|
||||
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set(["device_id"]));
|
||||
expect(mockClient.getCrypto()!.getUserDeviceInfo).toHaveBeenCalledWith(["@user:id"]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { shouldForceDisableEncryption } from "../../../src/utils/crypto/shouldForceDisableEncryption";
|
||||
import { getMockClientWithEventEmitter } from "../../test-utils";
|
||||
|
||||
describe("shouldForceDisableEncryption()", () => {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getClientWellKnown.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("should return false when there is no e2ee well known", () => {
|
||||
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when there is no force_disable property", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
// empty
|
||||
},
|
||||
});
|
||||
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when force_disable property is falsy", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: false,
|
||||
},
|
||||
});
|
||||
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when force_disable property is not equal to true", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: 1,
|
||||
},
|
||||
});
|
||||
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when force_disable property is true", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: true,
|
||||
},
|
||||
});
|
||||
expect(shouldForceDisableEncryption(mockClient)).toEqual(true);
|
||||
});
|
||||
});
|
121
test/unit-tests/utils/device/clientInformation-test.ts
Normal file
121
test/unit-tests/utils/device/clientInformation-test.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import BasePlatform from "../../../src/BasePlatform";
|
||||
import { IConfigOptions } from "../../../src/IConfigOptions";
|
||||
import { getDeviceClientInformation, recordClientInformation } from "../../../src/utils/device/clientInformation";
|
||||
import { getMockClientWithEventEmitter } from "../../test-utils";
|
||||
import { DEFAULTS } from "../../../src/SdkConfig";
|
||||
import { DeepReadonly } from "../../../src/@types/common";
|
||||
|
||||
describe("recordClientInformation()", () => {
|
||||
const deviceId = "my-device-id";
|
||||
const version = "1.2.3";
|
||||
const isElectron = window.electron;
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getDeviceId: jest.fn().mockReturnValue(deviceId),
|
||||
setAccountData: jest.fn(),
|
||||
});
|
||||
|
||||
const sdkConfig: DeepReadonly<IConfigOptions> = {
|
||||
...DEFAULTS,
|
||||
brand: "Test Brand",
|
||||
element_call: { url: "", use_exclusively: false, brand: "Element Call" },
|
||||
};
|
||||
|
||||
const platform = {
|
||||
getAppVersion: jest.fn().mockResolvedValue(version),
|
||||
} as unknown as BasePlatform;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
window.electron = false;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// restore global
|
||||
window.electron = isElectron;
|
||||
});
|
||||
|
||||
it("saves client information without url for electron clients", async () => {
|
||||
window.electron = true;
|
||||
|
||||
await recordClientInformation(mockClient, sdkConfig, platform);
|
||||
|
||||
expect(mockClient.setAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`, {
|
||||
name: sdkConfig.brand,
|
||||
version,
|
||||
url: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("saves client information with url for non-electron clients", async () => {
|
||||
await recordClientInformation(mockClient, sdkConfig, platform);
|
||||
|
||||
expect(mockClient.setAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`, {
|
||||
name: sdkConfig.brand,
|
||||
version,
|
||||
url: "localhost",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDeviceClientInformation()", () => {
|
||||
const deviceId = "my-device-id";
|
||||
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getAccountData: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("returns an empty object when no event exists for the device", () => {
|
||||
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({});
|
||||
|
||||
expect(mockClient.getAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`);
|
||||
});
|
||||
|
||||
it("returns client information for the device", () => {
|
||||
const eventContent = {
|
||||
name: "Element Web",
|
||||
version: "1.2.3",
|
||||
url: "test.com",
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
type: `io.element.matrix_client_information.${deviceId}`,
|
||||
content: eventContent,
|
||||
});
|
||||
mockClient.getAccountData.mockReturnValue(event);
|
||||
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual(eventContent);
|
||||
});
|
||||
|
||||
it("excludes values with incorrect types", () => {
|
||||
const eventContent = {
|
||||
extraField: "hello",
|
||||
name: "Element Web",
|
||||
// wrong format
|
||||
version: { value: "1.2.3" },
|
||||
url: "test.com",
|
||||
};
|
||||
const event = new MatrixEvent({
|
||||
type: `io.element.matrix_client_information.${deviceId}`,
|
||||
content: eventContent,
|
||||
});
|
||||
mockClient.getAccountData.mockReturnValue(event);
|
||||
// invalid fields excluded
|
||||
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({
|
||||
name: eventContent.name,
|
||||
url: eventContent.url,
|
||||
});
|
||||
});
|
||||
});
|
135
test/unit-tests/utils/device/parseUserAgent-test.ts
Normal file
135
test/unit-tests/utils/device/parseUserAgent-test.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { DeviceType, ExtendedDeviceInformation, parseUserAgent } from "../../../src/utils/device/parseUserAgent";
|
||||
|
||||
const makeDeviceExtendedInfo = (
|
||||
deviceType: DeviceType,
|
||||
deviceModel?: string,
|
||||
deviceOperatingSystem?: string,
|
||||
clientName?: string,
|
||||
clientVersion?: string,
|
||||
): ExtendedDeviceInformation => ({
|
||||
deviceType,
|
||||
deviceModel,
|
||||
deviceOperatingSystem,
|
||||
client: clientName && [clientName, clientVersion].filter(Boolean).join(" "),
|
||||
});
|
||||
|
||||
/* eslint-disable max-len */
|
||||
const ANDROID_UA = [
|
||||
// New User Agent Implementation
|
||||
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
|
||||
// Legacy User Agent Implementation
|
||||
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
|
||||
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
|
||||
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
|
||||
];
|
||||
|
||||
const ANDROID_EXPECTED_RESULT = [
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Xiaomi Mi 9T", "Android 11"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G960F", "Android 6.0.1"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "LG Nexus 5", "Android 7.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) 5", "Android 7.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G973U", "Android 9", "Chrome", "69.0.3497.100"),
|
||||
];
|
||||
|
||||
const IOS_UA = [
|
||||
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
|
||||
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
|
||||
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
|
||||
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
|
||||
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
|
||||
];
|
||||
const IOS_EXPECTED_RESULT = [
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS", "Mobile Safari", "8.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8.0"),
|
||||
];
|
||||
const DESKTOP_UA = [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
|
||||
" Electron/20.1.1 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
|
||||
];
|
||||
const DESKTOP_EXPECTED_RESULT = [
|
||||
makeDeviceExtendedInfo(DeviceType.Desktop, "Apple Macintosh", "Mac OS", "Electron", "20.1.1"),
|
||||
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows", "Electron", "20.1.1"),
|
||||
];
|
||||
|
||||
const WEB_UA = [
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
|
||||
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
|
||||
];
|
||||
|
||||
const WEB_EXPECTED_RESULT = [
|
||||
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Chrome", "104.0.5112.102"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Chrome", "104.0.5112.102"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Firefox", "39.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Safari", "8.0.3"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Firefox", "40.0"),
|
||||
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Edge", "12.246"),
|
||||
];
|
||||
|
||||
const MISC_UA = [
|
||||
"AppleTV11,1/11.1",
|
||||
"Curl Client/1.0",
|
||||
"banana",
|
||||
"",
|
||||
// fluffy chat ios
|
||||
"Dart/2.18 (dart:io)",
|
||||
];
|
||||
|
||||
const MISC_EXPECTED_RESULT = [
|
||||
makeDeviceExtendedInfo(DeviceType.Unknown, "Apple Apple TV", undefined, undefined, undefined),
|
||||
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
|
||||
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
|
||||
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
|
||||
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
|
||||
];
|
||||
/* eslint-disable max-len */
|
||||
|
||||
describe("parseUserAgent()", () => {
|
||||
it("returns deviceType unknown when user agent is falsy", () => {
|
||||
expect(parseUserAgent(undefined)).toEqual({
|
||||
deviceType: DeviceType.Unknown,
|
||||
});
|
||||
});
|
||||
|
||||
type TestCase = [string, ExtendedDeviceInformation];
|
||||
|
||||
const testPlatform = (platform: string, userAgents: string[], results: ExtendedDeviceInformation[]): void => {
|
||||
const testCases: TestCase[] = userAgents.map((userAgent, index) => [userAgent, results[index]]);
|
||||
|
||||
describe(`on platform ${platform}`, () => {
|
||||
it.each(testCases)("should parse the user agent correctly - %s", (userAgent, expectedResult) => {
|
||||
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
testPlatform("Android", ANDROID_UA, ANDROID_EXPECTED_RESULT);
|
||||
testPlatform("iOS", IOS_UA, IOS_EXPECTED_RESULT);
|
||||
testPlatform("Desktop", DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
|
||||
testPlatform("Web", WEB_UA, WEB_EXPECTED_RESULT);
|
||||
testPlatform("Misc", MISC_UA, MISC_EXPECTED_RESULT);
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import {
|
||||
isBulkUnverifiedDeviceReminderSnoozed,
|
||||
snoozeBulkUnverifiedDeviceReminder,
|
||||
} from "../../../src/utils/device/snoozeBulkUnverifiedDeviceReminder";
|
||||
|
||||
const SNOOZE_KEY = "mx_snooze_bulk_unverified_device_nag";
|
||||
|
||||
describe("snooze bulk unverified device nag", () => {
|
||||
const localStorageSetSpy = jest.spyOn(localStorage.__proto__, "setItem");
|
||||
const localStorageGetSpy = jest.spyOn(localStorage.__proto__, "getItem");
|
||||
const localStorageRemoveSpy = jest.spyOn(localStorage.__proto__, "removeItem");
|
||||
|
||||
// 14.03.2022 16:15
|
||||
const now = 1647270879403;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageSetSpy.mockClear().mockImplementation(() => {});
|
||||
localStorageGetSpy.mockClear().mockReturnValue(null);
|
||||
localStorageRemoveSpy.mockClear().mockImplementation(() => {});
|
||||
|
||||
jest.spyOn(Date, "now").mockReturnValue(now);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("snoozeBulkUnverifiedDeviceReminder()", () => {
|
||||
it("sets the current time in local storage", () => {
|
||||
snoozeBulkUnverifiedDeviceReminder();
|
||||
|
||||
expect(localStorageSetSpy).toHaveBeenCalledWith(SNOOZE_KEY, now.toString());
|
||||
});
|
||||
|
||||
it("catches an error from localstorage", () => {
|
||||
const loggerErrorSpy = jest.spyOn(logger, "error");
|
||||
localStorageSetSpy.mockImplementation(() => {
|
||||
throw new Error("oups");
|
||||
});
|
||||
snoozeBulkUnverifiedDeviceReminder();
|
||||
expect(loggerErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isBulkUnverifiedDeviceReminderSnoozed()", () => {
|
||||
it("returns false when there is no snooze in storage", () => {
|
||||
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
expect(localStorageGetSpy).toHaveBeenCalledWith(SNOOZE_KEY);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("catches an error from localstorage and returns false", () => {
|
||||
const loggerErrorSpy = jest.spyOn(logger, "error");
|
||||
localStorageGetSpy.mockImplementation(() => {
|
||||
throw new Error("oups");
|
||||
});
|
||||
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
expect(result).toBe(false);
|
||||
expect(loggerErrorSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns false when snooze timestamp in storage is not a number", () => {
|
||||
localStorageGetSpy.mockReturnValue("test");
|
||||
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when snooze timestamp in storage is over a week ago", () => {
|
||||
const msDay = 1000 * 60 * 60 * 24;
|
||||
// snoozed 8 days ago
|
||||
localStorageGetSpy.mockReturnValue(now - msDay * 8);
|
||||
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when snooze timestamp in storage is less than a week ago", () => {
|
||||
const msDay = 1000 * 60 * 60 * 24;
|
||||
// snoozed 8 days ago
|
||||
localStorageGetSpy.mockReturnValue(now - msDay * 6);
|
||||
const result = isBulkUnverifiedDeviceReminderSnoozed();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
200
test/unit-tests/utils/direct-messages-test.ts
Normal file
200
test/unit-tests/utils/direct-messages-test.ts
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
import { createTestClient } from "../test-utils";
|
||||
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
||||
import * as dmModule from "../../src/utils/direct-messages";
|
||||
import dis from "../../src/dispatcher/dispatcher";
|
||||
import { Action } from "../../src/dispatcher/actions";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { waitForRoomReadyAndApplyAfterCreateCallbacks } from "../../src/utils/local-room";
|
||||
import { findDMRoom } from "../../src/utils/dm/findDMRoom";
|
||||
import { createDmLocalRoom } from "../../src/utils/dm/createDmLocalRoom";
|
||||
import { startDm } from "../../src/utils/dm/startDm";
|
||||
import { Member } from "../../src/utils/direct-messages";
|
||||
import { resolveThreePids } from "../../src/utils/threepids";
|
||||
|
||||
jest.mock("../../src/utils/rooms", () => ({
|
||||
...(jest.requireActual("../../src/utils/rooms") as object),
|
||||
privateShouldBeEncrypted: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/createRoom", () => ({
|
||||
...(jest.requireActual("../../src/createRoom") as object),
|
||||
canEncryptToAllUsers: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/local-room", () => ({
|
||||
waitForRoomReadyAndApplyAfterCreateCallbacks: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/dm/findDMForUser", () => ({
|
||||
findDMForUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/dm/findDMRoom", () => ({
|
||||
findDMRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/dm/createDmLocalRoom", () => ({
|
||||
createDmLocalRoom: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/dm/startDm", () => ({
|
||||
startDm: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../src/utils/threepids", () => ({
|
||||
resolveThreePids: jest.fn().mockImplementation(async (members: Member[]) => {
|
||||
return members;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("direct-messages", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new dmModule.DirectoryMember({ user_id: userId1 });
|
||||
let room1: Room;
|
||||
let localRoom: LocalRoom;
|
||||
let dmRoomMap: DMRoomMap;
|
||||
let mockClient: MatrixClient;
|
||||
let roomEvents: Room[];
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
roomEvents = [];
|
||||
mockClient.on(ClientEvent.Room, (room: Room) => {
|
||||
roomEvents.push(room);
|
||||
});
|
||||
|
||||
room1 = new Room("!room1:example.com", mockClient, userId1);
|
||||
room1.getMyMembership = () => KnownMembership.Join;
|
||||
|
||||
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", mockClient, userId1);
|
||||
|
||||
dmRoomMap = {
|
||||
getDMRoomForIdentifiers: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
jest.spyOn(dis, "dispatch");
|
||||
jest.spyOn(logger, "warn");
|
||||
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date(2022, 7, 4, 11, 12, 30, 42));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("startDmOnFirstMessage", () => {
|
||||
describe("if no room exists", () => {
|
||||
beforeEach(() => {
|
||||
mocked(findDMRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should create a local room and dispatch a view room event", async () => {
|
||||
mocked(createDmLocalRoom).mockResolvedValue(localRoom);
|
||||
const members = [member1];
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(roomId).toBe(localRoom.roomId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: roomId,
|
||||
joining: false,
|
||||
targets: [member1],
|
||||
});
|
||||
|
||||
// assert, that startDmOnFirstMessage tries to resolve 3rd-party IDs
|
||||
expect(resolveThreePids).toHaveBeenCalledWith(members, mockClient);
|
||||
});
|
||||
|
||||
it("should work when resolveThreePids raises an error", async () => {
|
||||
const error = new Error("error 4711");
|
||||
mocked(resolveThreePids).mockRejectedValue(error);
|
||||
|
||||
mocked(createDmLocalRoom).mockResolvedValue(localRoom);
|
||||
const members = [member1];
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, members);
|
||||
expect(roomId).toBe(localRoom.roomId);
|
||||
|
||||
// ensure that startDmOnFirstMessage tries to resolve 3rd-party IDs
|
||||
expect(resolveThreePids).toHaveBeenCalledWith(members, mockClient);
|
||||
|
||||
// ensure that the error is logged
|
||||
expect(logger.warn).toHaveBeenCalledWith("Error resolving 3rd-party members", error);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if a room exists", () => {
|
||||
beforeEach(() => {
|
||||
mocked(findDMRoom).mockReturnValue(room1);
|
||||
});
|
||||
|
||||
it("should return the room and dispatch a view room event", async () => {
|
||||
const roomId = await dmModule.startDmOnFirstMessage(mockClient, [member1]);
|
||||
expect(roomId).toBe(room1.roomId);
|
||||
expect(dis.dispatch).toHaveBeenCalledWith({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room1.roomId,
|
||||
should_peek: false,
|
||||
joining: false,
|
||||
metricsTrigger: "MessageUser",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRoomFromLocalRoom", () => {
|
||||
[LocalRoomState.CREATING, LocalRoomState.CREATED, LocalRoomState.ERROR].forEach((state: LocalRoomState) => {
|
||||
it(`should do nothing for room in state ${state}`, async () => {
|
||||
localRoom.state = state;
|
||||
await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
|
||||
expect(localRoom.state).toBe(state);
|
||||
expect(startDm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("on startDm error", () => {
|
||||
beforeEach(() => {
|
||||
mocked(startDm).mockRejectedValue(true);
|
||||
});
|
||||
|
||||
it("should set the room state to error", async () => {
|
||||
await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
|
||||
expect(localRoom.state).toBe(LocalRoomState.ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("on startDm success", () => {
|
||||
beforeEach(() => {
|
||||
mocked(waitForRoomReadyAndApplyAfterCreateCallbacks).mockResolvedValue(room1.roomId);
|
||||
mocked(startDm).mockResolvedValue(room1.roomId);
|
||||
});
|
||||
|
||||
it("should set the room into creating state and call waitForRoomReadyAndApplyAfterCreateCallbacks", async () => {
|
||||
const result = await dmModule.createRoomFromLocalRoom(mockClient, localRoom);
|
||||
expect(result).toBe(room1.roomId);
|
||||
expect(localRoom.state).toBe(LocalRoomState.CREATING);
|
||||
expect(waitForRoomReadyAndApplyAfterCreateCallbacks).toHaveBeenCalledWith(
|
||||
mockClient,
|
||||
localRoom,
|
||||
room1.roomId,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
107
test/unit-tests/utils/dm/createDmLocalRoom-test.ts
Normal file
107
test/unit-tests/utils/dm/createDmLocalRoom-test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, KNOWN_SAFE_ROOM_VERSION, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { canEncryptToAllUsers } from "../../../src/createRoom";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||
import { DirectoryMember, Member, ThreepidMember } from "../../../src/utils/direct-messages";
|
||||
import { createDmLocalRoom } from "../../../src/utils/dm/createDmLocalRoom";
|
||||
import { privateShouldBeEncrypted } from "../../../src/utils/rooms";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/utils/rooms", () => ({
|
||||
privateShouldBeEncrypted: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../src/createRoom", () => ({
|
||||
canEncryptToAllUsers: jest.fn(),
|
||||
}));
|
||||
|
||||
function assertLocalRoom(room: LocalRoom, targets: Member[], encrypted: boolean) {
|
||||
expect(room.roomId).toBe(LOCAL_ROOM_ID_PREFIX + "t1");
|
||||
expect(room.name).toBe(targets.length ? targets[0].name : "Empty Room");
|
||||
expect(room.encrypted).toBe(encrypted);
|
||||
expect(room.targets).toEqual(targets);
|
||||
expect(room.getMyMembership()).toBe(KnownMembership.Join);
|
||||
|
||||
const roomCreateEvent = room.currentState.getStateEvents(EventType.RoomCreate)[0];
|
||||
expect(roomCreateEvent).toBeDefined();
|
||||
expect(roomCreateEvent.getContent()["room_version"]).toBe(KNOWN_SAFE_ROOM_VERSION);
|
||||
|
||||
// check that the user and all targets are joined
|
||||
expect(room.getMember("@userId:matrix.org")?.membership).toBe(KnownMembership.Join);
|
||||
targets.forEach((target: Member) => {
|
||||
expect(room.getMember(target.userId)?.membership).toBe(KnownMembership.Join);
|
||||
});
|
||||
|
||||
if (encrypted) {
|
||||
const encryptionEvent = room.currentState.getStateEvents(EventType.RoomEncryption)[0];
|
||||
expect(encryptionEvent).toBeDefined();
|
||||
}
|
||||
}
|
||||
|
||||
describe("createDmLocalRoom", () => {
|
||||
let mockClient: MatrixClient;
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new DirectoryMember({ user_id: userId1 });
|
||||
const member2 = new ThreepidMember("user2");
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
});
|
||||
|
||||
describe("when rooms should be encrypted", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should create an encrytped room for 3PID targets", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member2]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member2], true);
|
||||
});
|
||||
|
||||
describe("for MXID targets with encryption available", () => {
|
||||
beforeEach(() => {
|
||||
mocked(canEncryptToAllUsers).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("should create an encrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member1], true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for MXID targets with encryption unavailable", () => {
|
||||
beforeEach(() => {
|
||||
mocked(canEncryptToAllUsers).mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("should create an unencrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
expect(mockClient.store.storeRoom).toHaveBeenCalledWith(room);
|
||||
assertLocalRoom(room, [member1], false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("if rooms should not be encrypted", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should create an unencrypted room", async () => {
|
||||
const room = await createDmLocalRoom(mockClient, [member1]);
|
||||
assertLocalRoom(room, [member1], false);
|
||||
});
|
||||
});
|
||||
});
|
69
test/unit-tests/utils/dm/filterValidMDirect-test.ts
Normal file
69
test/unit-tests/utils/dm/filterValidMDirect-test.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { filterValidMDirect } from "../../../src/utils/dm/filterValidMDirect";
|
||||
|
||||
const roomId1 = "!room1:example.com";
|
||||
const roomId2 = "!room2:example.com";
|
||||
const userId1 = "@user1:example.com";
|
||||
const userId2 = "@user2:example.com";
|
||||
const userId3 = "@user3:example.com";
|
||||
|
||||
describe("filterValidMDirect", () => {
|
||||
it("should return an empty object as valid content", () => {
|
||||
expect(filterValidMDirect({})).toEqual({
|
||||
valid: true,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return valid content", () => {
|
||||
expect(
|
||||
filterValidMDirect({
|
||||
[userId1]: [roomId1, roomId2],
|
||||
[userId2]: [roomId1],
|
||||
}),
|
||||
).toEqual({
|
||||
valid: true,
|
||||
filteredContent: {
|
||||
[userId1]: [roomId1, roomId2],
|
||||
[userId2]: [roomId1],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an empy object for null", () => {
|
||||
expect(filterValidMDirect(null)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an empy object for a non-object", () => {
|
||||
expect(filterValidMDirect(23)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {},
|
||||
});
|
||||
});
|
||||
|
||||
it("should only return valid content", () => {
|
||||
const invalidContent = {
|
||||
[userId1]: [23],
|
||||
[userId2]: [roomId2],
|
||||
[userId3]: "room1",
|
||||
};
|
||||
|
||||
expect(filterValidMDirect(invalidContent)).toEqual({
|
||||
valid: false,
|
||||
filteredContent: {
|
||||
[userId1]: [],
|
||||
[userId2]: [roomId2],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
178
test/unit-tests/utils/dm/findDMForUser-test.ts
Normal file
178
test/unit-tests/utils/dm/findDMForUser-test.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { createTestClient, makeMembershipEvent, mkThirdPartyInviteEvent } from "../../test-utils";
|
||||
import { LocalRoom } from "../../../src/models/LocalRoom";
|
||||
import { findDMForUser } from "../../../src/utils/dm/findDMForUser";
|
||||
import { getFunctionalMembers } from "../../../src/utils/room/getFunctionalMembers";
|
||||
|
||||
jest.mock("../../../src/utils/room/getFunctionalMembers", () => ({
|
||||
getFunctionalMembers: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("findDMForUser", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const userId2 = "@user2:example.com";
|
||||
const userId3 = "@user3:example.com";
|
||||
const botId = "@bot:example.com";
|
||||
const thirdPartyId = "party@example.com";
|
||||
let room1: Room;
|
||||
let room2: LocalRoom;
|
||||
let room3: Room;
|
||||
let room4: Room;
|
||||
let room5: Room;
|
||||
let room6: Room;
|
||||
let room7: Room;
|
||||
const unknownRoomId = "!unknown:example.com";
|
||||
let dmRoomMap: DMRoomMap;
|
||||
let mockClient: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
|
||||
// always return the bot user as functional member
|
||||
mocked(getFunctionalMembers).mockReturnValue([botId]);
|
||||
|
||||
room1 = new Room("!room1:example.com", mockClient, userId1);
|
||||
room1.getMyMembership = () => KnownMembership.Join;
|
||||
room1.currentState.setStateEvents([
|
||||
makeMembershipEvent(room1.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room1.roomId, userId2, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it is a local room
|
||||
room2 = new LocalRoom("!room2:example.com", mockClient, userId1);
|
||||
room2.getMyMembership = () => KnownMembership.Join;
|
||||
room2.getLastActiveTimestamp = () => 100;
|
||||
|
||||
room3 = new Room("!room3:example.com", mockClient, userId1);
|
||||
room3.getMyMembership = () => KnownMembership.Join;
|
||||
room3.currentState.setStateEvents([
|
||||
makeMembershipEvent(room3.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room3.roomId, userId2, KnownMembership.Join),
|
||||
// Adding the bot user here. Should be excluded when determining if the room is a DM.
|
||||
makeMembershipEvent(room3.roomId, botId, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it has only one joined user
|
||||
room4 = new Room("!room4:example.com", mockClient, userId1);
|
||||
room4.getMyMembership = () => KnownMembership.Join;
|
||||
room4.currentState.setStateEvents([
|
||||
makeMembershipEvent(room4.roomId, userId1, KnownMembership.Invite),
|
||||
makeMembershipEvent(room4.roomId, userId2, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// this should not be a DM room because it has no users
|
||||
room5 = new Room("!room5:example.com", mockClient, userId1);
|
||||
room5.getLastActiveTimestamp = () => 100;
|
||||
|
||||
// room not correctly stored in userId → room map; should be found by the "all rooms" fallback
|
||||
room6 = new Room("!room6:example.com", mockClient, userId1);
|
||||
room6.getMyMembership = () => KnownMembership.Join;
|
||||
room6.currentState.setStateEvents([
|
||||
makeMembershipEvent(room6.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room6.roomId, userId3, KnownMembership.Join),
|
||||
]);
|
||||
|
||||
// room with pending third-party invite
|
||||
room7 = new Room("!room7:example.com", mockClient, userId1);
|
||||
room7.getMyMembership = () => KnownMembership.Join;
|
||||
room7.currentState.setStateEvents([
|
||||
makeMembershipEvent(room7.roomId, userId1, KnownMembership.Join),
|
||||
mkThirdPartyInviteEvent(thirdPartyId, "third-party", room7.roomId),
|
||||
]);
|
||||
|
||||
mocked(mockClient.getRoom).mockImplementation((roomId: string) => {
|
||||
return (
|
||||
{
|
||||
[room1.roomId]: room1,
|
||||
[room2.roomId]: room2,
|
||||
[room3.roomId]: room3,
|
||||
[room4.roomId]: room4,
|
||||
[room5.roomId]: room5,
|
||||
[room6.roomId]: room6,
|
||||
[room7.roomId]: room7,
|
||||
}[roomId] || null
|
||||
);
|
||||
});
|
||||
|
||||
dmRoomMap = {
|
||||
getDMRoomForIdentifiers: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
getRoomIds: jest.fn().mockReturnValue(
|
||||
new Set([
|
||||
room1.roomId,
|
||||
room2.roomId,
|
||||
room3.roomId,
|
||||
room4.roomId,
|
||||
room5.roomId,
|
||||
room6.roomId,
|
||||
room7.roomId,
|
||||
unknownRoomId, // this room does not exist in client
|
||||
]),
|
||||
),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
mocked(dmRoomMap.getDMRoomsForUserId).mockImplementation((userId: string) => {
|
||||
if (userId === userId1) {
|
||||
return [room1.roomId, room2.roomId, room3.roomId, room4.roomId, room5.roomId, unknownRoomId];
|
||||
}
|
||||
|
||||
if (userId === thirdPartyId) {
|
||||
return [room7.roomId];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
describe("for an empty DM room list", () => {
|
||||
beforeEach(() => {
|
||||
mocked(dmRoomMap.getDMRoomsForUserId).mockReturnValue([]);
|
||||
mocked(dmRoomMap.getRoomIds).mockReturnValue(new Set());
|
||||
});
|
||||
|
||||
it("should return undefined", () => {
|
||||
expect(findDMForUser(mockClient, userId1)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should find a room ordered by last activity 1", () => {
|
||||
room1.getLastActiveTimestamp = () => 2;
|
||||
room3.getLastActiveTimestamp = () => 1;
|
||||
|
||||
expect(findDMForUser(mockClient, userId1)).toBe(room1);
|
||||
});
|
||||
|
||||
it("should find a room ordered by last activity 2", () => {
|
||||
room1.getLastActiveTimestamp = () => 1;
|
||||
room3.getLastActiveTimestamp = () => 2;
|
||||
|
||||
expect(findDMForUser(mockClient, userId1)).toBe(room3);
|
||||
});
|
||||
|
||||
it("should find a room by the 'all rooms' fallback", () => {
|
||||
room1.getLastActiveTimestamp = () => 1;
|
||||
room6.getLastActiveTimestamp = () => 2;
|
||||
|
||||
expect(findDMForUser(mockClient, userId3)).toBe(room6);
|
||||
});
|
||||
|
||||
it("should find a room with a pending third-party invite", () => {
|
||||
expect(findDMForUser(mockClient, thirdPartyId)).toBe(room7);
|
||||
});
|
||||
|
||||
it("should not find a room for an unknown Id", () => {
|
||||
expect(findDMForUser(mockClient, "@unknown:example.com")).toBe(undefined);
|
||||
});
|
||||
});
|
62
test/unit-tests/utils/dm/findDMRoom-test.ts
Normal file
62
test/unit-tests/utils/dm/findDMRoom-test.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { DirectoryMember, ThreepidMember } from "../../../src/utils/direct-messages";
|
||||
import { findDMForUser } from "../../../src/utils/dm/findDMForUser";
|
||||
import { findDMRoom } from "../../../src/utils/dm/findDMRoom";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/utils/dm/findDMForUser", () => ({
|
||||
findDMForUser: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("findDMRoom", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new DirectoryMember({ user_id: userId1 });
|
||||
const member2 = new ThreepidMember("user2");
|
||||
let mockClient: MatrixClient;
|
||||
let room1: Room;
|
||||
let dmRoomMap: DMRoomMap;
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient = createTestClient();
|
||||
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient);
|
||||
room1 = new Room("!room1:example.com", mockClient, userId1);
|
||||
|
||||
dmRoomMap = {
|
||||
getDMRoomForIdentifiers: jest.fn(),
|
||||
getDMRoomsForUserId: jest.fn(),
|
||||
} as unknown as DMRoomMap;
|
||||
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||
});
|
||||
|
||||
it("should return the room for a single target with a room", () => {
|
||||
mocked(findDMForUser).mockReturnValue(room1);
|
||||
expect(findDMRoom(mockClient, [member1])).toBe(room1);
|
||||
});
|
||||
|
||||
it("should return undefined for a single target without a room", () => {
|
||||
mocked(findDMForUser).mockReturnValue(undefined);
|
||||
expect(findDMRoom(mockClient, [member1])).toBeNull();
|
||||
});
|
||||
|
||||
it("should return the room for 2 targets with a room", () => {
|
||||
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(room1);
|
||||
expect(findDMRoom(mockClient, [member1, member2])).toBe(room1);
|
||||
});
|
||||
|
||||
it("should return null for 2 targets without a room", () => {
|
||||
mocked(dmRoomMap.getDMRoomForIdentifiers).mockReturnValue(null);
|
||||
expect(findDMRoom(mockClient, [member1, member2])).toBeNull();
|
||||
});
|
||||
});
|
59
test/unit-tests/utils/enums-test.ts
Normal file
59
test/unit-tests/utils/enums-test.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { getEnumValues, isEnumValue } from "../../src/utils/enums";
|
||||
|
||||
enum TestStringEnum {
|
||||
First = "__first__",
|
||||
Second = "__second__",
|
||||
}
|
||||
|
||||
enum TestNumberEnum {
|
||||
FirstKey = 10,
|
||||
SecondKey = 20,
|
||||
}
|
||||
|
||||
describe("enums", () => {
|
||||
describe("getEnumValues", () => {
|
||||
it("should work on string enums", () => {
|
||||
const result = getEnumValues(TestStringEnum);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual(["__first__", "__second__"]);
|
||||
});
|
||||
|
||||
it("should work on number enums", () => {
|
||||
const result = getEnumValues(TestNumberEnum);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([10, 20]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isEnumValue", () => {
|
||||
it("should return true on values in a string enum", () => {
|
||||
const result = isEnumValue(TestStringEnum, "__first__");
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false on values not in a string enum", () => {
|
||||
const result = isEnumValue(TestStringEnum, "not a value");
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true on values in a number enum", () => {
|
||||
const result = isEnumValue(TestNumberEnum, 10);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false on values not in a number enum", () => {
|
||||
const result = isEnumValue(TestStringEnum, 99);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
341
test/unit-tests/utils/export-test.tsx
Normal file
341
test/unit-tests/utils/export-test.tsx
Normal file
|
@ -0,0 +1,341 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render } from "jest-matrix-react";
|
||||
import {
|
||||
IContent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
Room,
|
||||
RoomMember,
|
||||
RelationType,
|
||||
EventType,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { IExportOptions, ExportType, ExportFormat } from "../../src/utils/exportUtils/exportUtils";
|
||||
import PlainTextExporter from "../../src/utils/exportUtils/PlainTextExport";
|
||||
import HTMLExporter from "../../src/utils/exportUtils/HtmlExport";
|
||||
import * as TestUtilsMatrix from "../test-utils";
|
||||
import { stubClient } from "../test-utils";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
const MY_USER_ID = "@me:here";
|
||||
|
||||
function generateRoomId() {
|
||||
return "!" + Math.random().toString().slice(2, 10) + ":domain";
|
||||
}
|
||||
|
||||
interface ITestContent extends IContent {
|
||||
expectedText: string;
|
||||
}
|
||||
|
||||
describe("export", function () {
|
||||
const setProgressText = jest.fn();
|
||||
|
||||
let mockExportOptions: IExportOptions;
|
||||
let mockRoom: Room;
|
||||
let ts0: number;
|
||||
let events: MatrixEvent[];
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.safeGet();
|
||||
client.getUserId = () => {
|
||||
return MY_USER_ID;
|
||||
};
|
||||
|
||||
mockExportOptions = {
|
||||
numberOfMessages: 5,
|
||||
maxSize: 100 * 1024 * 1024,
|
||||
attachmentsIncluded: false,
|
||||
};
|
||||
|
||||
function createRoom() {
|
||||
const room = new Room(generateRoomId(), client, client.getUserId()!);
|
||||
return room;
|
||||
}
|
||||
mockRoom = createRoom();
|
||||
ts0 = Date.now();
|
||||
events = mkEvents();
|
||||
jest.spyOn(client, "getRoom").mockReturnValue(mockRoom);
|
||||
});
|
||||
|
||||
function mkRedactedEvent(i = 0) {
|
||||
return new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: MY_USER_ID,
|
||||
content: {},
|
||||
unsigned: {
|
||||
age: 72,
|
||||
transaction_id: "m1212121212.23",
|
||||
redacted_because: {
|
||||
content: {},
|
||||
origin_server_ts: ts0 + i * 1000,
|
||||
redacts: "$9999999999999999999999999999999999999999998",
|
||||
sender: "@me:here",
|
||||
type: EventType.RoomRedaction,
|
||||
unsigned: {
|
||||
age: 94,
|
||||
transaction_id: "m1111111111.1",
|
||||
},
|
||||
event_id: "$9999999999999999999999999999999999999999998",
|
||||
room_id: mockRoom.roomId,
|
||||
},
|
||||
},
|
||||
event_id: "$9999999999999999999999999999999999999999999",
|
||||
room_id: mockRoom.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
function mkFileEvent() {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
body: "index.html",
|
||||
info: {
|
||||
mimetype: "text/html",
|
||||
size: 31613,
|
||||
},
|
||||
msgtype: "m.file",
|
||||
url: "mxc://test.org",
|
||||
},
|
||||
origin_server_ts: 1628872988364,
|
||||
sender: MY_USER_ID,
|
||||
type: "m.room.message",
|
||||
unsigned: {
|
||||
age: 266,
|
||||
transaction_id: "m99999999.2",
|
||||
},
|
||||
event_id: "$99999999999999999999",
|
||||
room_id: mockRoom.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
function mkImageEvent() {
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
body: "image.png",
|
||||
info: {
|
||||
mimetype: "image/png",
|
||||
size: 31613,
|
||||
},
|
||||
msgtype: "m.image",
|
||||
url: "mxc://test.org",
|
||||
},
|
||||
origin_server_ts: 1628872988364,
|
||||
sender: MY_USER_ID,
|
||||
type: "m.room.message",
|
||||
unsigned: {
|
||||
age: 266,
|
||||
transaction_id: "m99999999.2",
|
||||
},
|
||||
event_id: "$99999999999999999999",
|
||||
room_id: mockRoom.roomId,
|
||||
});
|
||||
}
|
||||
|
||||
function mkEvents() {
|
||||
const matrixEvents: MatrixEvent[] = [];
|
||||
let i: number;
|
||||
// plain text
|
||||
for (i = 0; i < 10; i++) {
|
||||
matrixEvents.push(
|
||||
TestUtilsMatrix.mkMessage({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
ts: ts0 + i * 1000,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// reply events
|
||||
for (i = 0; i < 10; i++) {
|
||||
const eventId = "$" + Math.random() + "-" + Math.random();
|
||||
matrixEvents.push(
|
||||
TestUtilsMatrix.mkEvent({
|
||||
content: {
|
||||
"body": "> <@me:here> Hi\n\nTest",
|
||||
"format": "org.matrix.custom.html",
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationType.Reference,
|
||||
"event_id": eventId,
|
||||
"m.in_reply_to": {
|
||||
event_id: eventId,
|
||||
},
|
||||
},
|
||||
"msgtype": "m.text",
|
||||
},
|
||||
user: "@me:here",
|
||||
type: "m.room.message",
|
||||
room: mockRoom.roomId,
|
||||
event: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// membership events
|
||||
for (i = 0; i < 10; i++) {
|
||||
matrixEvents.push(
|
||||
TestUtilsMatrix.mkMembership({
|
||||
event: true,
|
||||
room: "!room:id",
|
||||
user: "@user:id",
|
||||
target: {
|
||||
userId: "@user:id",
|
||||
name: "Bob",
|
||||
getAvatarUrl: () => {
|
||||
return "avatar.jpeg";
|
||||
},
|
||||
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||
} as unknown as RoomMember,
|
||||
ts: ts0 + i * 1000,
|
||||
mship: KnownMembership.Join,
|
||||
prevMship: KnownMembership.Join,
|
||||
name: "A user",
|
||||
}),
|
||||
);
|
||||
}
|
||||
// emote
|
||||
matrixEvents.push(
|
||||
TestUtilsMatrix.mkEvent({
|
||||
content: {
|
||||
body: "waves",
|
||||
msgtype: "m.emote",
|
||||
},
|
||||
user: "@me:here",
|
||||
type: "m.room.message",
|
||||
room: mockRoom.roomId,
|
||||
event: true,
|
||||
}),
|
||||
);
|
||||
// redacted events
|
||||
for (i = 0; i < 10; i++) {
|
||||
matrixEvents.push(mkRedactedEvent(i));
|
||||
}
|
||||
return matrixEvents;
|
||||
}
|
||||
|
||||
function renderToString(elem: JSX.Element): string {
|
||||
return render(elem).container.outerHTML;
|
||||
}
|
||||
|
||||
it("checks if the export format is valid", function () {
|
||||
function isValidFormat(format: string): boolean {
|
||||
const options: string[] = Object.values(ExportFormat);
|
||||
return options.includes(format);
|
||||
}
|
||||
expect(isValidFormat("Html")).toBeTruthy();
|
||||
expect(isValidFormat("Json")).toBeTruthy();
|
||||
expect(isValidFormat("PlainText")).toBeTruthy();
|
||||
expect(isValidFormat("Pdf")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("checks if the icons' html corresponds to export regex", function () {
|
||||
const exporter = new HTMLExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
|
||||
const fileRegex = /<span class="mx_MFileBody_info_icon">.*?<\/span>/;
|
||||
expect(fileRegex.test(renderToString(exporter.getEventTile(mkFileEvent(), true)))).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should export images if attachments are enabled", () => {
|
||||
const exporter = new HTMLExporter(
|
||||
mockRoom,
|
||||
ExportType.Beginning,
|
||||
{
|
||||
numberOfMessages: 5,
|
||||
maxSize: 100 * 1024 * 1024,
|
||||
attachmentsIncluded: true,
|
||||
},
|
||||
setProgressText,
|
||||
);
|
||||
const imageRegex = /<img.+ src="mxc:\/\/test.org" alt="image\.png"\/?>/;
|
||||
expect(imageRegex.test(renderToString(exporter.getEventTile(mkImageEvent(), true)))).toBeTruthy();
|
||||
});
|
||||
|
||||
const invalidExportOptions: [string, IExportOptions][] = [
|
||||
[
|
||||
"numberOfMessages exceeds max",
|
||||
{
|
||||
numberOfMessages: 10 ** 9,
|
||||
maxSize: 1024 * 1024 * 1024,
|
||||
attachmentsIncluded: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"maxSize exceeds 8GB",
|
||||
{
|
||||
numberOfMessages: -1,
|
||||
maxSize: 8001 * 1024 * 1024,
|
||||
attachmentsIncluded: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
"maxSize is less than 1mb",
|
||||
{
|
||||
numberOfMessages: 0,
|
||||
maxSize: 0,
|
||||
attachmentsIncluded: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
it.each(invalidExportOptions)("%s", (_d, options) => {
|
||||
expect(() => new PlainTextExporter(mockRoom, ExportType.Beginning, options, setProgressText)).toThrow(
|
||||
"Invalid export options",
|
||||
);
|
||||
});
|
||||
|
||||
it("tests the file extension splitter", function () {
|
||||
const exporter = new PlainTextExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
|
||||
const fileNameWithExtensions: Record<string, [string, string]> = {
|
||||
"": ["", ""],
|
||||
"name": ["name", ""],
|
||||
"name.txt": ["name", ".txt"],
|
||||
".htpasswd": ["", ".htpasswd"],
|
||||
"name.with.many.dots.myext": ["name.with.many.dots", ".myext"],
|
||||
};
|
||||
for (const fileName in fileNameWithExtensions) {
|
||||
expect(exporter.splitFileName(fileName)).toStrictEqual(fileNameWithExtensions[fileName]);
|
||||
}
|
||||
});
|
||||
|
||||
it("checks if the reply regex executes correctly", function () {
|
||||
const eventContents: ITestContent[] = [
|
||||
{
|
||||
msgtype: "m.text",
|
||||
body: "> <@me:here> Source\n\nReply",
|
||||
expectedText: '<@me:here "Source"> Reply',
|
||||
},
|
||||
{
|
||||
msgtype: "m.text",
|
||||
// if the reply format is invalid, then return the body
|
||||
body: "Invalid reply format",
|
||||
expectedText: "Invalid reply format",
|
||||
},
|
||||
{
|
||||
msgtype: "m.text",
|
||||
body: "> <@me:here> The source is more than 32 characters\n\nReply",
|
||||
expectedText: '<@me:here "The source is more than 32 chara..."> Reply',
|
||||
},
|
||||
{
|
||||
msgtype: "m.text",
|
||||
body: "> <@me:here> This\nsource\nhas\nnew\nlines\n\nReply",
|
||||
expectedText: '<@me:here "This"> Reply',
|
||||
},
|
||||
];
|
||||
const exporter = new PlainTextExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
|
||||
for (const content of eventContents) {
|
||||
expect(exporter.textForReplyEvent(content)).toBe(content.expectedText);
|
||||
}
|
||||
});
|
||||
|
||||
it("checks if the render to string doesn't throw any error for different types of events", function () {
|
||||
const exporter = new HTMLExporter(mockRoom, ExportType.Beginning, mockExportOptions, setProgressText);
|
||||
for (const event of events) {
|
||||
expect(renderToString(exporter.getEventTile(event, false))).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
590
test/unit-tests/utils/exportUtils/HTMLExport-test.ts
Normal file
590
test/unit-tests/utils/exportUtils/HTMLExport-test.ts
Normal file
|
@ -0,0 +1,590 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
EventType,
|
||||
IRoomEvent,
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MsgType,
|
||||
Room,
|
||||
RoomMember,
|
||||
RoomState,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import escapeHtml from "escape-html";
|
||||
|
||||
import { filterConsole, mkStubRoom, REPEATABLE_DATE, stubClient } from "../../test-utils";
|
||||
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
|
||||
import SdkConfig from "../../../src/SdkConfig";
|
||||
import HTMLExporter from "../../../src/utils/exportUtils/HtmlExport";
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { mediaFromMxc } from "../../../src/customisations/Media";
|
||||
|
||||
jest.mock("jszip");
|
||||
|
||||
const EVENT_MESSAGE: IRoomEvent = {
|
||||
event_id: "$1",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@bob:example.com",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Message",
|
||||
avatar_url: "mxc://example.org/avatar.bmp",
|
||||
},
|
||||
};
|
||||
|
||||
const EVENT_ATTACHMENT: IRoomEvent = {
|
||||
event_id: "$2",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
msgtype: MsgType.File,
|
||||
body: "hello.txt",
|
||||
filename: "hello.txt",
|
||||
url: "mxc://example.org/test-id",
|
||||
},
|
||||
};
|
||||
|
||||
const EVENT_ATTACHMENT_MALFORMED: IRoomEvent = {
|
||||
event_id: "$2",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 1,
|
||||
content: {
|
||||
msgtype: MsgType.File,
|
||||
body: "hello.txt",
|
||||
file: {
|
||||
url: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("HTMLExport", () => {
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
|
||||
filterConsole(
|
||||
"Starting export",
|
||||
"events in", // Fetched # events in # seconds
|
||||
"events so far",
|
||||
"Export successful!",
|
||||
"does not have an m.room.create event",
|
||||
"Creating HTML",
|
||||
"Generating a ZIP",
|
||||
"Cleaning up",
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
|
||||
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||
DMRoomMap.makeShared(client);
|
||||
|
||||
room = new Room("!myroom:example.org", client, "@me:example.org");
|
||||
client.getRoom.mockReturnValue(room);
|
||||
});
|
||||
|
||||
function mockMessages(...events: IRoomEvent[]): void {
|
||||
client.createMessagesRequest.mockImplementation((_roomId, fromStr, limit = 30) => {
|
||||
const from = fromStr === null ? 0 : parseInt(fromStr);
|
||||
const chunk = events.slice(from, limit);
|
||||
return Promise.resolve({
|
||||
chunk,
|
||||
from: from.toString(),
|
||||
to: (from + limit).toString(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Retrieve a map of files within the zip. */
|
||||
function getFiles(exporter: HTMLExporter): { [filename: string]: Blob } {
|
||||
//@ts-ignore private access
|
||||
const files = exporter.files;
|
||||
return files.reduce((d, f) => ({ ...d, [f.name]: f.blob }), {});
|
||||
}
|
||||
|
||||
function getMessageFile(exporter: HTMLExporter): Blob {
|
||||
const files = getFiles(exporter);
|
||||
return files["messages.html"]!;
|
||||
}
|
||||
|
||||
/** set a mock fetch response for an MXC */
|
||||
function mockMxc(mxc: string, body: string) {
|
||||
const media = mediaFromMxc(mxc, client);
|
||||
fetchMock.get(media.srcHttp!, body);
|
||||
}
|
||||
|
||||
it("should throw when created with invalid config for LastNMessages", async () => {
|
||||
expect(
|
||||
() =>
|
||||
new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: undefined,
|
||||
},
|
||||
() => {},
|
||||
),
|
||||
).toThrow("Invalid export options");
|
||||
});
|
||||
|
||||
it("should have an SDK-branded destination file name", () => {
|
||||
const roomName = "My / Test / Room: Welcome";
|
||||
const stubOptions: IExportOptions = {
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 50000000,
|
||||
numberOfMessages: 40,
|
||||
};
|
||||
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
|
||||
const exporter = new HTMLExporter(stubRoom, ExportType.LastNMessages, stubOptions, () => {});
|
||||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
|
||||
SdkConfig.put({ brand: "BrandedChat/WithSlashes/ForFun" });
|
||||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should export", async () => {
|
||||
const events = [...Array(50)].map<IRoomEvent>((_, i) => ({
|
||||
event_id: `${i}`,
|
||||
type: EventType.RoomMessage,
|
||||
sender: `@user${i}:example.com`,
|
||||
origin_server_ts: 5_000 + i * 1000,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: `Message #${i}`,
|
||||
},
|
||||
}));
|
||||
mockMessages(...events);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: events.length,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const file = getMessageFile(exporter);
|
||||
expect(await file.text()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should include the room's avatar", async () => {
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
|
||||
const mxc = "mxc://www.example.com/avatars/nice-room.jpeg";
|
||||
const avatar = "011011000110111101101100";
|
||||
jest.spyOn(room, "getMxcAvatarUrl").mockReturnValue(mxc);
|
||||
mockMxc(mxc, avatar);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const files = getFiles(exporter);
|
||||
expect(await files["room.png"]!.text()).toBe(avatar);
|
||||
});
|
||||
|
||||
it("should include the creation event", async () => {
|
||||
const creator = "@bob:example.com";
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomCreate,
|
||||
event_id: "$00001",
|
||||
room_id: room.roomId,
|
||||
sender: creator,
|
||||
origin_server_ts: 0,
|
||||
content: {},
|
||||
state_key: "",
|
||||
}),
|
||||
]);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
expect(await getMessageFile(exporter).text()).toContain(`${creator} created this room.`);
|
||||
});
|
||||
|
||||
it("should include the topic", async () => {
|
||||
const topic = ":^-) (-^:";
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomTopic,
|
||||
event_id: "$00001",
|
||||
room_id: room.roomId,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 0,
|
||||
content: { topic },
|
||||
state_key: "",
|
||||
}),
|
||||
]);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
expect(await getMessageFile(exporter).text()).toContain(`Topic: ${topic}`);
|
||||
});
|
||||
|
||||
it("should include avatars", async () => {
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
|
||||
jest.spyOn(RoomMember.prototype, "getMxcAvatarUrl").mockReturnValue("mxc://example.org/avatar.bmp");
|
||||
|
||||
const avatarContent = "this is a bitmap all the pixels are red :^-)";
|
||||
mockMxc("mxc://example.org/avatar.bmp", avatarContent);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the avatar is present
|
||||
const files = getFiles(exporter);
|
||||
const file = files["users/@bob-example.com.png"];
|
||||
expect(file).not.toBeUndefined();
|
||||
|
||||
// Ensure it has the expected content
|
||||
expect(await file.text()).toBe(avatarContent);
|
||||
});
|
||||
|
||||
it("should handle when an event has no sender", async () => {
|
||||
const EVENT_MESSAGE_NO_SENDER: IRoomEvent = {
|
||||
event_id: "$1",
|
||||
type: EventType.RoomMessage,
|
||||
sender: "",
|
||||
origin_server_ts: 0,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "Message with no sender",
|
||||
},
|
||||
};
|
||||
mockMessages(EVENT_MESSAGE_NO_SENDER);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const file = getMessageFile(exporter);
|
||||
expect(await file.text()).toContain(EVENT_MESSAGE_NO_SENDER.content.body);
|
||||
});
|
||||
|
||||
it("should handle when events sender cannot be found in room state", async () => {
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
|
||||
jest.spyOn(RoomState.prototype, "getSentinelMember").mockReturnValue(null);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const file = getMessageFile(exporter);
|
||||
expect(await file.text()).toContain(EVENT_MESSAGE.content.body);
|
||||
});
|
||||
|
||||
it("should include attachments", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||
const attachmentBody = "Lorem ipsum dolor sit amet";
|
||||
|
||||
mockMxc("mxc://example.org/test-id", attachmentBody);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the attachment is present
|
||||
const files = getFiles(exporter);
|
||||
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
||||
expect(file).not.toBeUndefined();
|
||||
|
||||
// Ensure that the attachment has the expected content
|
||||
const text = await file.text();
|
||||
expect(text).toBe(attachmentBody);
|
||||
});
|
||||
|
||||
it("should handle when attachment cannot be fetched", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT_MALFORMED, EVENT_ATTACHMENT);
|
||||
const attachmentBody = "Lorem ipsum dolor sit amet";
|
||||
|
||||
mockMxc("mxc://example.org/test-id", attachmentBody);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// good attachment present
|
||||
const files = getFiles(exporter);
|
||||
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
||||
expect(file).not.toBeUndefined();
|
||||
|
||||
// Ensure that the attachment has the expected content
|
||||
const text = await file.text();
|
||||
expect(text).toBe(attachmentBody);
|
||||
|
||||
// messages export still successful
|
||||
const messagesFile = getMessageFile(exporter);
|
||||
expect(await messagesFile.text()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should handle when attachment srcHttp is falsy", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||
const attachmentBody = "Lorem ipsum dolor sit amet";
|
||||
|
||||
mockMxc("mxc://example.org/test-id", attachmentBody);
|
||||
|
||||
jest.spyOn(client, "mxcUrlToHttp").mockReturnValue(null);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: true,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// attachment not present
|
||||
const files = getFiles(exporter);
|
||||
const file = files[Object.keys(files).find((k) => k.endsWith(".txt"))!];
|
||||
expect(file).toBeUndefined();
|
||||
|
||||
// messages export still successful
|
||||
const messagesFile = getMessageFile(exporter);
|
||||
expect(await messagesFile.text()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should omit attachments", async () => {
|
||||
mockMessages(EVENT_MESSAGE, EVENT_ATTACHMENT);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
// Ensure that the attachment is present
|
||||
const files = getFiles(exporter);
|
||||
for (const fileName of Object.keys(files)) {
|
||||
expect(fileName).not.toMatch(/^files\/hello/);
|
||||
}
|
||||
});
|
||||
|
||||
it("should add link to next and previous file", async () => {
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 5000,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
// test link to the first page
|
||||
//@ts-ignore private access
|
||||
let result = await exporter.wrapHTML("", 0, 3);
|
||||
expect(result).not.toContain("Previous group of messages");
|
||||
expect(result).toContain(
|
||||
'<div style="text-align:center;margin:10px"><a href="./messages2.html" style="font-weight:bold">Next group of messages</a></div>',
|
||||
);
|
||||
|
||||
// test link for a middle page
|
||||
//@ts-ignore private access
|
||||
result = await exporter.wrapHTML("", 1, 3);
|
||||
expect(result).toContain(
|
||||
'<div style="text-align:center"><a href="./messages.html" style="font-weight:bold">Previous group of messages</a></div>',
|
||||
);
|
||||
expect(result).toContain(
|
||||
'<div style="text-align:center;margin:10px"><a href="./messages3.html" style="font-weight:bold">Next group of messages</a></div>',
|
||||
);
|
||||
|
||||
// test link for last page
|
||||
//@ts-ignore private access
|
||||
result = await exporter.wrapHTML("", 2, 3);
|
||||
expect(result).toContain(
|
||||
'<div style="text-align:center"><a href="./messages2.html" style="font-weight:bold">Previous group of messages</a></div>',
|
||||
);
|
||||
expect(result).not.toContain("Next group of messages");
|
||||
});
|
||||
|
||||
it("should not leak javascript from room names or topics", async () => {
|
||||
const name = "<svg onload=alert(3)>";
|
||||
const topic = "<svg onload=alert(5)>";
|
||||
mockMessages(EVENT_MESSAGE);
|
||||
room.currentState.setStateEvents([
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomName,
|
||||
event_id: "$00001",
|
||||
room_id: room.roomId,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 0,
|
||||
content: { name },
|
||||
state_key: "",
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: EventType.RoomTopic,
|
||||
event_id: "$00002",
|
||||
room_id: room.roomId,
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 1,
|
||||
content: { topic },
|
||||
state_key: "",
|
||||
}),
|
||||
]);
|
||||
room.recalculate();
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.LastNMessages,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
numberOfMessages: 40,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
const html = await getMessageFile(exporter).text();
|
||||
|
||||
expect(html).not.toContain(`${name}`);
|
||||
expect(html).toContain(`${escapeHtml(name)}`);
|
||||
expect(html).not.toContain(`${topic}`);
|
||||
expect(html).toContain(`Topic: ${escapeHtml(topic)}`);
|
||||
});
|
||||
|
||||
it("should not make /messages requests when exporting 'Current Timeline'", async () => {
|
||||
client.createMessagesRequest.mockRejectedValue(new Error("Should never be called"));
|
||||
room.addLiveEvents([
|
||||
new MatrixEvent({
|
||||
event_id: `$eventId`,
|
||||
type: EventType.RoomMessage,
|
||||
sender: client.getSafeUserId(),
|
||||
origin_server_ts: 123456789,
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: `testing testing`,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const exporter = new HTMLExporter(
|
||||
room,
|
||||
ExportType.Timeline,
|
||||
{
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 1_024 * 1_024,
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
await exporter.export();
|
||||
|
||||
const file = getMessageFile(exporter);
|
||||
expect(await file.text()).toContain("testing testing");
|
||||
expect(client.createMessagesRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
31
test/unit-tests/utils/exportUtils/JSONExport-test.ts
Normal file
31
test/unit-tests/utils/exportUtils/JSONExport-test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import JSONExporter from "../../../src/utils/exportUtils/JSONExport";
|
||||
import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
|
||||
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
|
||||
|
||||
describe("JSONExport", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
});
|
||||
|
||||
it("should have a Matrix-branded destination file name", () => {
|
||||
const roomName = "My / Test / Room: Welcome";
|
||||
const client = createTestClient();
|
||||
const stubOptions: IExportOptions = {
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 50000000,
|
||||
};
|
||||
const stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
|
||||
const exporter = new JSONExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
|
||||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
});
|
||||
});
|
64
test/unit-tests/utils/exportUtils/PlainTextExport-test.ts
Normal file
64
test/unit-tests/utils/exportUtils/PlainTextExport-test.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { createTestClient, mkStubRoom, REPEATABLE_DATE } from "../../test-utils";
|
||||
import { ExportType, IExportOptions } from "../../../src/utils/exportUtils/exportUtils";
|
||||
import PlainTextExporter from "../../../src/utils/exportUtils/PlainTextExport";
|
||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||
|
||||
class TestablePlainTextExporter extends PlainTextExporter {
|
||||
public async testCreateOutput(events: MatrixEvent[]): Promise<string> {
|
||||
return this.createOutput(events);
|
||||
}
|
||||
}
|
||||
|
||||
describe("PlainTextExport", () => {
|
||||
let stubOptions: IExportOptions;
|
||||
let stubRoom: Room;
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(REPEATABLE_DATE);
|
||||
const roomName = "My / Test / Room: Welcome";
|
||||
const client = createTestClient();
|
||||
stubOptions = {
|
||||
attachmentsIncluded: false,
|
||||
maxSize: 50000000,
|
||||
};
|
||||
stubRoom = mkStubRoom("!myroom:example.org", roomName, client);
|
||||
});
|
||||
|
||||
it("should have a Matrix-branded destination file name", () => {
|
||||
const exporter = new PlainTextExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
|
||||
|
||||
expect(exporter.destinationFileName).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[24, false, "Fri, Apr 16, 2021, 17:20:00 - @alice:example.com: Hello, world!\n"],
|
||||
[12, true, "Fri, Apr 16, 2021, 5:20:00 PM - @alice:example.com: Hello, world!\n"],
|
||||
])("should return text with %i hr time format", async (hour: number, setting: boolean, expectedMessage: string) => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) =>
|
||||
settingName === "showTwelveHourTimestamps" ? setting : undefined,
|
||||
);
|
||||
const events: MatrixEvent[] = [
|
||||
new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
body: "Hello, world!",
|
||||
},
|
||||
sender: "@alice:example.com",
|
||||
origin_server_ts: 1618593600000,
|
||||
}),
|
||||
];
|
||||
const exporter = new TestablePlainTextExporter(stubRoom, ExportType.Timeline, stubOptions, () => {});
|
||||
const output = await exporter.testCreateOutput(events);
|
||||
expect(output).toBe(expectedMessage);
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JSONExport should have a Matrix-branded destination file name 1`] = `"matrix - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.json"`;
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PlainTextExport should have a Matrix-branded destination file name 1`] = `"matrix - My Test Room Welcome - Chat Export - 2022-11-17T16-58-32.517Z.txt"`;
|
18
test/unit-tests/utils/exportUtils/exportCSS-test.ts
Normal file
18
test/unit-tests/utils/exportUtils/exportCSS-test.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import getExportCSS from "../../../src/utils/exportUtils/exportCSS";
|
||||
|
||||
describe("exportCSS", () => {
|
||||
describe("getExportCSS", () => {
|
||||
it("supports documents missing stylesheets", async () => {
|
||||
const css = await getExportCSS(new Set());
|
||||
expect(css).not.toContain("color-scheme: light");
|
||||
});
|
||||
});
|
||||
});
|
117
test/unit-tests/utils/generate-megolm-test-vectors.py
Executable file
117
test/unit-tests/utils/generate-megolm-test-vectors.py
Executable file
|
@ -0,0 +1,117 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
|
||||
from cryptography.hazmat import backends
|
||||
from cryptography.hazmat.primitives import ciphers, hashes, hmac
|
||||
from cryptography.hazmat.primitives.kdf import pbkdf2
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms, modes
|
||||
|
||||
backend = backends.default_backend()
|
||||
|
||||
def parse_u128(s):
|
||||
a, b = struct.unpack(">QQ", s)
|
||||
return (a << 64) | b
|
||||
|
||||
def encrypt_ctr(key, iv, plaintext, counter_bits=64):
|
||||
alg = algorithms.AES(key)
|
||||
|
||||
# Some AES-CTR implementations treat some parts of the IV as a nonce (which
|
||||
# remains constant throughought encryption), and some as a counter (which
|
||||
# increments every block, ie 16 bytes, and wraps after a while). Different
|
||||
# implmententations use different amounts of the IV for each part.
|
||||
#
|
||||
# The python cryptography library uses the whole IV as a counter; to make
|
||||
# it match other implementations with a given counter size, we manually
|
||||
# implement wrapping the counter.
|
||||
|
||||
# number of AES blocks between each counter wrap
|
||||
limit = 1 << counter_bits
|
||||
|
||||
# parse IV as a 128-bit int
|
||||
parsed_iv = parse_u128(iv)
|
||||
|
||||
# split IV into counter and nonce
|
||||
counter = parsed_iv & (limit - 1)
|
||||
nonce = parsed_iv & ~(limit - 1)
|
||||
|
||||
# encrypt up to the first counter wraparound
|
||||
size = 16 * (limit - counter)
|
||||
encryptor = ciphers.Cipher(
|
||||
alg,
|
||||
modes.CTR(iv),
|
||||
backend=backend
|
||||
).encryptor()
|
||||
input = plaintext[:size]
|
||||
result = encryptor.update(input) + encryptor.finalize()
|
||||
offset = size
|
||||
|
||||
# do remaining data starting with a counter of zero
|
||||
iv = struct.pack(">QQ", nonce >> 64, nonce & ((1 << 64) - 1))
|
||||
size = 16 * limit
|
||||
|
||||
while offset < len(plaintext):
|
||||
encryptor = ciphers.Cipher(
|
||||
alg,
|
||||
modes.CTR(iv),
|
||||
backend=backend
|
||||
).encryptor()
|
||||
input = plaintext[offset:offset+size]
|
||||
result += encryptor.update(input) + encryptor.finalize()
|
||||
offset += size
|
||||
|
||||
return result
|
||||
|
||||
def hmac_sha256(key, message):
|
||||
h = hmac.HMAC(key, hashes.SHA256(), backend=backend)
|
||||
h.update(message)
|
||||
return h.finalize()
|
||||
|
||||
def encrypt(key, iv, salt, plaintext, iterations=1000):
|
||||
"""
|
||||
Returns:
|
||||
(bytes) ciphertext
|
||||
"""
|
||||
if len(salt) != 16:
|
||||
raise Exception("Expected 128 bits of salt - got %i bits" % len((salt) * 8))
|
||||
if len(iv) != 16:
|
||||
raise Exception("Expected 128 bits of IV - got %i bits" % (len(iv) * 8))
|
||||
|
||||
sha = hashes.SHA512()
|
||||
kdf = pbkdf2.PBKDF2HMAC(sha, 64, salt, iterations, backend)
|
||||
k = kdf.derive(key)
|
||||
|
||||
aes_key = k[0:32]
|
||||
sha_key = k[32:]
|
||||
|
||||
packed_file = (
|
||||
b"\x01" # version
|
||||
+ salt
|
||||
+ iv
|
||||
+ struct.pack(">L", iterations)
|
||||
+ encrypt_ctr(aes_key, iv, plaintext)
|
||||
)
|
||||
packed_file += hmac_sha256(sha_key, packed_file)
|
||||
|
||||
return (
|
||||
b"-----BEGIN MEGOLM SESSION DATA-----\n" +
|
||||
base64.encodestring(packed_file) +
|
||||
b"-----END MEGOLM SESSION DATA-----"
|
||||
)
|
||||
|
||||
def gen(password, iv, salt, plaintext, iterations=1000):
|
||||
ciphertext = encrypt(
|
||||
password.encode('utf-8'), iv, salt, plaintext.encode('utf-8'), iterations
|
||||
)
|
||||
return (plaintext, password, ciphertext.decode('utf-8'))
|
||||
|
||||
print (json.dumps([
|
||||
gen("password", b"\x88"*16, b"saltsaltsaltsalt", "plain", 10),
|
||||
gen("betterpassword", b"\xFF"*8 + b"\x00"*8, b"moresaltmoresalt", "Hello, World"),
|
||||
gen("SWORDFISH", b"\xFF"*8 + b"\x00"*8, b"yessaltygoodness", "alphanumerically" * 4),
|
||||
gen("password"*32, b"\xFF"*16, b"\xFF"*16, "alphanumerically" * 4),
|
||||
], indent=4))
|
54
test/unit-tests/utils/i18n-helpers-test.ts
Normal file
54
test/unit-tests/utils/i18n-helpers-test.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import SpaceStore from "../../src/stores/spaces/SpaceStore";
|
||||
import { stubClient } from "../test-utils";
|
||||
import { roomContextDetails } from "../../src/utils/i18n-helpers";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
|
||||
describe("roomContextDetails", () => {
|
||||
const client = stubClient();
|
||||
DMRoomMap.makeShared(client);
|
||||
|
||||
const room = new Room("!room:server", client, client.getSafeUserId());
|
||||
const parent1 = new Room("!parent1:server", client, client.getSafeUserId());
|
||||
parent1.name = "Alpha";
|
||||
const parent2 = new Room("!parent2:server", client, client.getSafeUserId());
|
||||
parent2.name = "Beta";
|
||||
const parent3 = new Room("!parent3:server", client, client.getSafeUserId());
|
||||
parent3.name = "Charlie";
|
||||
mocked(client.getRoom).mockImplementation((roomId) => {
|
||||
return [parent1, parent2, parent3].find((r) => r.roomId === roomId) ?? null;
|
||||
});
|
||||
|
||||
it("should return 1-parent variant", () => {
|
||||
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parent1.roomId]));
|
||||
const res = roomContextDetails(room);
|
||||
expect(res!.details).toMatchInlineSnapshot(`"Alpha"`);
|
||||
expect(res!.ariaLabel).toMatchInlineSnapshot(`"In Alpha."`);
|
||||
});
|
||||
|
||||
it("should return 2-parent variant", () => {
|
||||
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parent2.roomId, parent3.roomId]));
|
||||
const res = roomContextDetails(room);
|
||||
expect(res!.details).toMatchInlineSnapshot(`"Beta and Charlie"`);
|
||||
expect(res!.ariaLabel).toMatchInlineSnapshot(`"In spaces Beta and Charlie."`);
|
||||
});
|
||||
|
||||
it("should return n-parent variant", () => {
|
||||
jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(
|
||||
new Set([parent1.roomId, parent2.roomId, parent3.roomId]),
|
||||
);
|
||||
const res = roomContextDetails(room);
|
||||
expect(res!.details).toMatchInlineSnapshot(`"Alpha and one other"`);
|
||||
expect(res!.ariaLabel).toMatchInlineSnapshot(`"In Alpha and one other space."`);
|
||||
});
|
||||
});
|
69
test/unit-tests/utils/iterables-test.ts
Normal file
69
test/unit-tests/utils/iterables-test.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { iterableDiff, iterableIntersection } from "../../src/utils/iterables";
|
||||
|
||||
describe("iterables", () => {
|
||||
describe("iterableIntersection", () => {
|
||||
it("should return the intersection", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = iterableIntersection(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("should return an empty array on no matches", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [4, 5, 6];
|
||||
const result = iterableIntersection(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("iterableDiff", () => {
|
||||
it("should see added from A->B", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 3, 4];
|
||||
const result = iterableDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.added).toEqual([4]);
|
||||
});
|
||||
|
||||
it("should see removed from A->B", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2];
|
||||
const result = iterableDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.removed).toEqual([3]);
|
||||
});
|
||||
|
||||
it("should see added and removed in the same set", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = iterableDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.added).toEqual([4]);
|
||||
expect(result.removed).toEqual([3]);
|
||||
});
|
||||
});
|
||||
});
|
149
test/unit-tests/utils/leave-behaviour-test.ts
Normal file
149
test/unit-tests/utils/leave-behaviour-test.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { mkRoom, resetAsyncStoreWithClient, setupAsyncStoreWithClient, stubClient } from "../test-utils";
|
||||
import defaultDispatcher from "../../src/dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../../src/dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../src/dispatcher/actions";
|
||||
import { leaveRoomBehaviour } from "../../src/utils/leave-behaviour";
|
||||
import { SdkContextClass } from "../../src/contexts/SDKContext";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
import SpaceStore from "../../src/stores/spaces/SpaceStore";
|
||||
import { MetaSpace } from "../../src/stores/spaces";
|
||||
import { ActionPayload } from "../../src/dispatcher/payloads";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
|
||||
describe("leaveRoomBehaviour", () => {
|
||||
SdkContextClass.instance.constructEagerStores(); // Initialize RoomViewStore
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Mocked<Room>;
|
||||
let space: Mocked<Room>;
|
||||
|
||||
beforeEach(async () => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
DMRoomMap.makeShared(client);
|
||||
|
||||
room = mkRoom(client, "!1:example.org");
|
||||
space = mkRoom(client, "!2:example.org");
|
||||
space.isSpaceRoom.mockReturnValue(true);
|
||||
client.getRoom.mockImplementation((roomId) => {
|
||||
switch (roomId) {
|
||||
case room.roomId:
|
||||
return room;
|
||||
case space.roomId:
|
||||
return space;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
await setupAsyncStoreWithClient(SpaceStore.instance, client);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
SpaceStore.instance.setActiveSpace(MetaSpace.Home);
|
||||
await resetAsyncStoreWithClient(SpaceStore.instance);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const viewRoom = (room: Room) =>
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
metricsTrigger: undefined,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const expectDispatch = async <T extends ActionPayload>(payload: T) => {
|
||||
const dispatcherSpy = jest.fn();
|
||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||
await sleep(0);
|
||||
expect(dispatcherSpy).toHaveBeenCalledWith(payload);
|
||||
defaultDispatcher.unregister(dispatcherRef);
|
||||
};
|
||||
|
||||
it("returns to the home page after leaving a room outside of a space that was being viewed", async () => {
|
||||
viewRoom(room);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => {
|
||||
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? space : null,
|
||||
);
|
||||
viewRoom(room);
|
||||
SpaceStore.instance.setActiveSpace(space.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: space.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns to the home page after leaving a top-level space that was being viewed", async () => {
|
||||
viewRoom(space);
|
||||
SpaceStore.instance.setActiveSpace(space.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, space.roomId);
|
||||
await expectDispatch({ action: Action.ViewHomePage });
|
||||
});
|
||||
|
||||
it("returns to the parent space after leaving a subspace that was being viewed", async () => {
|
||||
room.isSpaceRoom.mockReturnValue(true);
|
||||
jest.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) =>
|
||||
roomId === room.roomId ? space : null,
|
||||
);
|
||||
viewRoom(room);
|
||||
SpaceStore.instance.setActiveSpace(room.roomId, false);
|
||||
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
await expectDispatch({
|
||||
action: Action.ViewRoom,
|
||||
room_id: space.roomId,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is not enabled", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("If the feature_dynamic_room_predecessors is enabled", () => {
|
||||
beforeEach(() => {
|
||||
// Turn on feature_dynamic_room_predecessors setting
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation(
|
||||
(settingName) => settingName === "feature_dynamic_room_predecessors",
|
||||
);
|
||||
});
|
||||
|
||||
it("Passes through the dynamic predecessor setting", async () => {
|
||||
await leaveRoomBehaviour(client, room.roomId);
|
||||
expect(client.getRoomUpgradeHistory).toHaveBeenCalledWith(room.roomId, false, true);
|
||||
});
|
||||
});
|
||||
});
|
156
test/unit-tests/utils/local-room-test.ts
Normal file
156
test/unit-tests/utils/local-room-test.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { LocalRoom, LocalRoomState, LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
||||
import * as localRoomModule from "../../src/utils/local-room";
|
||||
import defaultDispatcher from "../../src/dispatcher/dispatcher";
|
||||
import { createTestClient } from "../test-utils";
|
||||
import { isRoomReady } from "../../src/utils/localRoom/isRoomReady";
|
||||
|
||||
jest.mock("../../src/utils/localRoom/isRoomReady", () => ({
|
||||
isRoomReady: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("local-room", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
let room1: Room;
|
||||
let localRoom: LocalRoom;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
room1 = new Room("!room1:example.com", client, userId1);
|
||||
room1.getMyMembership = () => KnownMembership.Join;
|
||||
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, "@test:example.com");
|
||||
mocked(client.getRoom).mockImplementation((roomId: string) => {
|
||||
if (roomId === localRoom.roomId) {
|
||||
return localRoom;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
describe("doMaybeLocalRoomAction", () => {
|
||||
let callback: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
callback = jest.fn();
|
||||
callback.mockReturnValue(Promise.resolve());
|
||||
localRoom.actualRoomId = "@new:example.com";
|
||||
});
|
||||
|
||||
it("should invoke the callback for a non-local room", () => {
|
||||
localRoomModule.doMaybeLocalRoomAction("!room:example.com", callback, client);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should invoke the callback with the new room ID for a created room", () => {
|
||||
localRoom.state = LocalRoomState.CREATED;
|
||||
localRoomModule.doMaybeLocalRoomAction(localRoom.roomId, callback, client);
|
||||
expect(callback).toHaveBeenCalledWith(localRoom.actualRoomId);
|
||||
});
|
||||
|
||||
describe("for a local room", () => {
|
||||
let prom: Promise<unknown>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(defaultDispatcher, "dispatch");
|
||||
prom = localRoomModule.doMaybeLocalRoomAction(localRoom.roomId, callback, client);
|
||||
});
|
||||
|
||||
it("dispatch a local_room_event", () => {
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
|
||||
action: "local_room_event",
|
||||
roomId: localRoom.roomId,
|
||||
});
|
||||
});
|
||||
|
||||
it("should resolve the promise after invoking the callback", async () => {
|
||||
localRoom.afterCreateCallbacks.forEach((callback) => {
|
||||
callback(localRoom.actualRoomId);
|
||||
});
|
||||
await prom;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("waitForRoomReadyAndApplyAfterCreateCallbacks", () => {
|
||||
let localRoomCallbackRoomId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
localRoom.actualRoomId = room1.roomId;
|
||||
localRoom.afterCreateCallbacks.push((roomId: string) => {
|
||||
localRoomCallbackRoomId = roomId;
|
||||
return Promise.resolve();
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
describe("for an immediate ready room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(isRoomReady).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should invoke the callbacks, set the room state to created and return the actual room id", async () => {
|
||||
const result = await localRoomModule.waitForRoomReadyAndApplyAfterCreateCallbacks(
|
||||
client,
|
||||
localRoom,
|
||||
room1.roomId,
|
||||
);
|
||||
expect(localRoom.state).toBe(LocalRoomState.CREATED);
|
||||
expect(localRoomCallbackRoomId).toBe(room1.roomId);
|
||||
expect(result).toBe(room1.roomId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a room running into the create timeout", () => {
|
||||
beforeEach(() => {
|
||||
mocked(isRoomReady).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should invoke the callbacks, set the room state to created and return the actual room id", async () => {
|
||||
const prom = localRoomModule.waitForRoomReadyAndApplyAfterCreateCallbacks(
|
||||
client,
|
||||
localRoom,
|
||||
room1.roomId,
|
||||
);
|
||||
jest.advanceTimersByTime(5000);
|
||||
const roomId = await prom;
|
||||
expect(localRoom.state).toBe(LocalRoomState.CREATED);
|
||||
expect(localRoomCallbackRoomId).toBe(room1.roomId);
|
||||
expect(roomId).toBe(room1.roomId);
|
||||
expect(jest.getTimerCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for a room that is ready after a while", () => {
|
||||
beforeEach(() => {
|
||||
mocked(isRoomReady).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should invoke the callbacks, set the room state to created and return the actual room id", async () => {
|
||||
const prom = localRoomModule.waitForRoomReadyAndApplyAfterCreateCallbacks(
|
||||
client,
|
||||
localRoom,
|
||||
room1.roomId,
|
||||
);
|
||||
mocked(isRoomReady).mockReturnValue(true);
|
||||
jest.advanceTimersByTime(500);
|
||||
const roomId = await prom;
|
||||
expect(localRoom.state).toBe(LocalRoomState.CREATED);
|
||||
expect(localRoomCallbackRoomId).toBe(room1.roomId);
|
||||
expect(roomId).toBe(room1.roomId);
|
||||
expect(jest.getTimerCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
40
test/unit-tests/utils/localRoom/isLocalRoom-test.ts
Normal file
40
test/unit-tests/utils/localRoom/isLocalRoom-test.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||
import { isLocalRoom } from "../../../src/utils/localRoom/isLocalRoom";
|
||||
import { createTestClient } from "../../test-utils";
|
||||
|
||||
describe("isLocalRoom", () => {
|
||||
let room: Room;
|
||||
let localRoom: LocalRoom;
|
||||
|
||||
beforeEach(() => {
|
||||
const client = createTestClient();
|
||||
room = new Room("!room:example.com", client, client.getUserId()!);
|
||||
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, client.getUserId()!);
|
||||
});
|
||||
|
||||
it("should return false for a Room", () => {
|
||||
expect(isLocalRoom(room)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a non-local room ID", () => {
|
||||
expect(isLocalRoom(room.roomId)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for LocalRoom", () => {
|
||||
expect(isLocalRoom(localRoom)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for local room ID", () => {
|
||||
expect(isLocalRoom(LOCAL_ROOM_ID_PREFIX + "test")).toBe(true);
|
||||
});
|
||||
});
|
123
test/unit-tests/utils/localRoom/isRoomReady-test.ts
Normal file
123
test/unit-tests/utils/localRoom/isRoomReady-test.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { EventType, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||
import { DirectoryMember } from "../../../src/utils/direct-messages";
|
||||
import { isRoomReady } from "../../../src/utils/localRoom/isRoomReady";
|
||||
import { createTestClient, makeMembershipEvent, mkEvent } from "../../test-utils";
|
||||
|
||||
describe("isRoomReady", () => {
|
||||
const userId1 = "@user1:example.com";
|
||||
const member1 = new DirectoryMember({ user_id: userId1 });
|
||||
const userId2 = "@user2:example.com";
|
||||
let room1: Room;
|
||||
let localRoom: LocalRoom;
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
room1 = new Room("!room1:example.com", client, userId1);
|
||||
room1.getMyMembership = () => KnownMembership.Join;
|
||||
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, "@test:example.com");
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
localRoom.targets = [member1];
|
||||
});
|
||||
|
||||
it("should return false if the room has no actual room id", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(false);
|
||||
});
|
||||
|
||||
describe("for a room with an actual room id", () => {
|
||||
beforeEach(() => {
|
||||
localRoom.actualRoomId = room1.roomId;
|
||||
mocked(client.getRoom).mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("should return false", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(false);
|
||||
});
|
||||
|
||||
describe("and the room is known to the client", () => {
|
||||
beforeEach(() => {
|
||||
mocked(client.getRoom).mockImplementation((roomId: string) => {
|
||||
if (roomId === room1.roomId) return room1;
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(false);
|
||||
});
|
||||
|
||||
describe("and all members have been invited or joined", () => {
|
||||
beforeEach(() => {
|
||||
room1.currentState.setStateEvents([
|
||||
makeMembershipEvent(room1.roomId, userId1, KnownMembership.Join),
|
||||
makeMembershipEvent(room1.roomId, userId2, KnownMembership.Invite),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return false", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(false);
|
||||
});
|
||||
|
||||
describe("and a RoomHistoryVisibility event", () => {
|
||||
beforeEach(() => {
|
||||
room1.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
user: userId1,
|
||||
event: true,
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
room: room1.roomId,
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return true", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(true);
|
||||
});
|
||||
|
||||
describe("and an encrypted room", () => {
|
||||
beforeEach(() => {
|
||||
localRoom.encrypted = true;
|
||||
});
|
||||
|
||||
it("should return false", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(false);
|
||||
});
|
||||
|
||||
describe("and a room encryption state event", () => {
|
||||
beforeEach(() => {
|
||||
room1.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
user: userId1,
|
||||
event: true,
|
||||
type: EventType.RoomEncryption,
|
||||
room: room1.roomId,
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return true", () => {
|
||||
expect(isRoomReady(client, localRoom)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
65
test/unit-tests/utils/location/isSelfLocation-test.ts
Normal file
65
test/unit-tests/utils/location/isSelfLocation-test.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
M_TEXT,
|
||||
ILocationContent,
|
||||
LocationAssetType,
|
||||
M_ASSET,
|
||||
M_LOCATION,
|
||||
M_TIMESTAMP,
|
||||
ContentHelpers,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { isSelfLocation } from "../../../src/utils/location";
|
||||
|
||||
describe("isSelfLocation", () => {
|
||||
it("Returns true for a full m.asset event", () => {
|
||||
const content = ContentHelpers.makeLocationContent("", "0", Date.now());
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns true for a missing m.asset", () => {
|
||||
const content = {
|
||||
body: "",
|
||||
msgtype: "m.location",
|
||||
geo_uri: "",
|
||||
[M_LOCATION.name]: { uri: "" },
|
||||
[M_TEXT.name]: "",
|
||||
[M_TIMESTAMP.name]: 0,
|
||||
// Note: no m.asset!
|
||||
} as unknown as ILocationContent;
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns true for a missing m.asset type", () => {
|
||||
const content = {
|
||||
body: "",
|
||||
msgtype: "m.location",
|
||||
geo_uri: "",
|
||||
[M_LOCATION.name]: { uri: "" },
|
||||
[M_TEXT.name]: "",
|
||||
[M_TIMESTAMP.name]: 0,
|
||||
[M_ASSET.name]: {
|
||||
// Note: no type!
|
||||
},
|
||||
} as unknown as ILocationContent;
|
||||
expect(isSelfLocation(content)).toBe(true);
|
||||
});
|
||||
|
||||
it("Returns false for an unknown asset type", () => {
|
||||
const content = ContentHelpers.makeLocationContent(
|
||||
undefined /* text */,
|
||||
"geo:foo",
|
||||
0,
|
||||
undefined /* description */,
|
||||
"org.example.unknown" as unknown as LocationAssetType,
|
||||
);
|
||||
expect(isSelfLocation(content)).toBe(false);
|
||||
});
|
||||
});
|
20
test/unit-tests/utils/location/locationEventGeoUri-test.ts
Normal file
20
test/unit-tests/utils/location/locationEventGeoUri-test.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { locationEventGeoUri } from "../../../src/utils/location";
|
||||
import { makeLegacyLocationEvent, makeLocationEvent } from "../../test-utils/location";
|
||||
|
||||
describe("locationEventGeoUri()", () => {
|
||||
it("returns m.location uri when available", () => {
|
||||
expect(locationEventGeoUri(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276");
|
||||
});
|
||||
|
||||
it("returns legacy uri when m.location content not found", () => {
|
||||
expect(locationEventGeoUri(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual("geo:51.5076,-0.1276");
|
||||
});
|
||||
});
|
45
test/unit-tests/utils/location/map-test.ts
Normal file
45
test/unit-tests/utils/location/map-test.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { createMapSiteLinkFromEvent } from "../../../src/utils/location";
|
||||
import { mkMessage } from "../../test-utils";
|
||||
import { makeLegacyLocationEvent, makeLocationEvent } from "../../test-utils/location";
|
||||
|
||||
describe("createMapSiteLinkFromEvent", () => {
|
||||
it("returns null if event does not contain geouri", () => {
|
||||
expect(
|
||||
createMapSiteLinkFromEvent(
|
||||
mkMessage({
|
||||
room: "1",
|
||||
user: "@sender:server",
|
||||
event: true,
|
||||
}),
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("returns OpenStreetMap link if event contains m.location with valid uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLocationEvent("geo:51.5076,-0.1276"))).toEqual(
|
||||
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null if event contains m.location with invalid uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLocationEvent("123 Sesame St"))).toBeNull();
|
||||
});
|
||||
|
||||
it("returns OpenStreetMap link if event contains geo_uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("geo:51.5076,-0.1276"))).toEqual(
|
||||
"https://www.openstreetmap.org/" + "?mlat=51.5076&mlon=-0.1276" + "#map=16/51.5076/-0.1276",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null if event contains an invalid geo_uri", () => {
|
||||
expect(createMapSiteLinkFromEvent(makeLegacyLocationEvent("123 Sesame St"))).toBeNull();
|
||||
});
|
||||
});
|
148
test/unit-tests/utils/location/parseGeoUri-test.ts
Normal file
148
test/unit-tests/utils/location/parseGeoUri-test.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { parseGeoUri } from "../../../src/utils/location/parseGeoUri";
|
||||
|
||||
describe("parseGeoUri", () => {
|
||||
it("fails if the supplied URI is empty", () => {
|
||||
expect(parseGeoUri("")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns undefined if latitude is not a number", () => {
|
||||
expect(parseGeoUri("geo:ABCD,16.3695,183")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined if longitude is not a number", () => {
|
||||
expect(parseGeoUri("geo:48.2010,EFGH,183")).toBeUndefined();
|
||||
});
|
||||
|
||||
// We use some examples from the spec, but don't check semantics
|
||||
// like two textually-different URIs being equal, since we are
|
||||
// just a humble parser.
|
||||
|
||||
// Note: we do not understand geo URIs with percent-encoded coords
|
||||
// or accuracy. It is RECOMMENDED in the spec never to percent-encode
|
||||
// these, but it is permitted, and we will fail to parse in that case.
|
||||
|
||||
it("rfc5870 6.1 Simple 3-dimensional", () => {
|
||||
expect(parseGeoUri("geo:48.2010,16.3695,183")).toEqual({
|
||||
latitude: 48.201,
|
||||
longitude: 16.3695,
|
||||
altitude: 183,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.2 Explicit CRS and accuracy", () => {
|
||||
expect(parseGeoUri("geo:48.198634,16.371648;crs=wgs84;u=40")).toEqual({
|
||||
latitude: 48.198634,
|
||||
longitude: 16.371648,
|
||||
altitude: null,
|
||||
accuracy: 40,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Negative longitude and explicit CRS", () => {
|
||||
expect(parseGeoUri("geo:90,-22.43;crs=WGS84")).toEqual({
|
||||
latitude: 90,
|
||||
longitude: -22.43,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Integer lat and lon", () => {
|
||||
expect(parseGeoUri("geo:90,46")).toEqual({
|
||||
latitude: 90,
|
||||
longitude: 46,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Percent-encoded param value", () => {
|
||||
expect(parseGeoUri("geo:66,30;u=6.500;FOo=this%2dthat")).toEqual({
|
||||
latitude: 66,
|
||||
longitude: 30,
|
||||
altitude: null,
|
||||
accuracy: 6.5,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Unknown param", () => {
|
||||
expect(parseGeoUri("geo:66.0,30;u=6.5;foo=this-that>")).toEqual({
|
||||
latitude: 66.0,
|
||||
longitude: 30,
|
||||
altitude: null,
|
||||
accuracy: 6.5,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("rfc5870 6.4 Multiple unknown params", () => {
|
||||
expect(parseGeoUri("geo:70,20;foo=1.00;bar=white")).toEqual({
|
||||
latitude: 70,
|
||||
longitude: 20,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("Negative latitude", () => {
|
||||
expect(parseGeoUri("geo:-7.5,20")).toEqual({
|
||||
latitude: -7.5,
|
||||
longitude: 20,
|
||||
altitude: null,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("Zero altitude is not unknown", () => {
|
||||
expect(parseGeoUri("geo:-7.5,-20,0")).toEqual({
|
||||
latitude: -7.5,
|
||||
longitude: -20,
|
||||
altitude: 0,
|
||||
accuracy: undefined,
|
||||
altitudeAccuracy: null,
|
||||
heading: null,
|
||||
speed: null,
|
||||
toJSON: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { positionFailureMessage } from "../../../src/utils/location/positionFailureMessage";
|
||||
|
||||
describe("positionFailureMessage()", () => {
|
||||
// error codes from GeolocationPositionError
|
||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
|
||||
// 1: PERMISSION_DENIED
|
||||
// 2: POSITION_UNAVAILABLE
|
||||
// 3: TIMEOUT
|
||||
type TestCase = [number, string | undefined];
|
||||
it.each<TestCase>([
|
||||
[
|
||||
1,
|
||||
"Element was denied permission to fetch your location. Please allow location access in your browser settings.",
|
||||
],
|
||||
[2, "Failed to fetch your location. Please try again later."],
|
||||
[3, "Timed out trying to fetch your location. Please try again later."],
|
||||
[4, "Unknown error fetching location. Please try again later."],
|
||||
[5, undefined],
|
||||
])("returns correct message for error code %s", (code, expectedMessage) => {
|
||||
expect(positionFailureMessage(code)).toEqual(expectedMessage);
|
||||
});
|
||||
});
|
219
test/unit-tests/utils/maps-test.ts
Normal file
219
test/unit-tests/utils/maps-test.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { EnhancedMap, mapDiff } from "../../src/utils/maps";
|
||||
|
||||
describe("maps", () => {
|
||||
describe("mapDiff", () => {
|
||||
it("should indicate no differences when the pointers are the same", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const result = mapDiff(a, a);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should indicate no differences when there are none", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const b = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should indicate added properties", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const b = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
]);
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.added).toEqual([4]);
|
||||
});
|
||||
|
||||
it("should indicate removed properties", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const b = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
]);
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.removed).toEqual([3]);
|
||||
});
|
||||
|
||||
it("should indicate changed properties", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const b = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 4],
|
||||
]); // note change
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toHaveLength(1);
|
||||
expect(result.changed).toEqual([3]);
|
||||
});
|
||||
|
||||
it("should indicate changed, added, and removed properties", () => {
|
||||
const a = new Map([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
]);
|
||||
const b = new Map([
|
||||
[1, 1],
|
||||
[2, 8],
|
||||
[4, 4],
|
||||
]); // note change
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.changed).toHaveLength(1);
|
||||
expect(result.added).toEqual([4]);
|
||||
expect(result.removed).toEqual([3]);
|
||||
expect(result.changed).toEqual([2]);
|
||||
});
|
||||
|
||||
it("should indicate changes for difference in pointers", () => {
|
||||
const a = new Map([[1, {}]]); // {} always creates a new object
|
||||
const b = new Map([[1, {}]]);
|
||||
const result = mapDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toHaveLength(1);
|
||||
expect(result.changed).toEqual([1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("EnhancedMap", () => {
|
||||
// Most of these tests will make sure it implements the Map<K, V> class
|
||||
|
||||
it("should be empty by default", () => {
|
||||
const result = new EnhancedMap();
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should use the provided entries", () => {
|
||||
const obj = { a: 1, b: 2 };
|
||||
const result = new EnhancedMap(Object.entries(obj));
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get("a")).toBe(1);
|
||||
expect(result.get("b")).toBe(2);
|
||||
});
|
||||
|
||||
it("should create keys if they do not exist", () => {
|
||||
const key = "a";
|
||||
const val = {}; // we'll check pointers
|
||||
|
||||
const result = new EnhancedMap<string, any>();
|
||||
expect(result.size).toBe(0);
|
||||
|
||||
let get = result.getOrCreate(key, val);
|
||||
expect(get).toBeDefined();
|
||||
expect(get).toBe(val);
|
||||
expect(result.size).toBe(1);
|
||||
|
||||
get = result.getOrCreate(key, 44); // specifically change `val`
|
||||
expect(get).toBeDefined();
|
||||
expect(get).toBe(val);
|
||||
expect(result.size).toBe(1);
|
||||
|
||||
get = result.get(key); // use the base class function
|
||||
expect(get).toBeDefined();
|
||||
expect(get).toBe(val);
|
||||
expect(result.size).toBe(1);
|
||||
});
|
||||
|
||||
it("should proxy remove to delete and return it", () => {
|
||||
const val = {};
|
||||
const result = new EnhancedMap<string, any>();
|
||||
result.set("a", val);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
|
||||
const removed = result.remove("a");
|
||||
expect(result.size).toBe(0);
|
||||
expect(removed).toBeDefined();
|
||||
expect(removed).toBe(val);
|
||||
});
|
||||
|
||||
it("should support removing unknown keys", () => {
|
||||
const val = {};
|
||||
const result = new EnhancedMap<string, any>();
|
||||
result.set("a", val);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
|
||||
const removed = result.remove("not-a");
|
||||
expect(result.size).toBe(1);
|
||||
expect(removed).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
112
test/unit-tests/utils/media/requestMediaPermissions-test.tsx
Normal file
112
test/unit-tests/utils/media/requestMediaPermissions-test.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { screen } from "jest-matrix-react";
|
||||
|
||||
import { requestMediaPermissions } from "../../../src/utils/media/requestMediaPermissions";
|
||||
import { flushPromises, useMockMediaDevices } from "../../test-utils";
|
||||
|
||||
describe("requestMediaPermissions", () => {
|
||||
let error: Error;
|
||||
const audioVideoStream = {} as MediaStream;
|
||||
const audioStream = {} as MediaStream;
|
||||
|
||||
const itShouldLogTheErrorAndShowTheNoMediaPermissionsModal = () => {
|
||||
it("should log the error and show the »No media permissions« modal", () => {
|
||||
expect(logger.log).toHaveBeenCalledWith("Failed to list userMedia devices", error);
|
||||
screen.getByText("No media permissions");
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useMockMediaDevices();
|
||||
error = new Error();
|
||||
jest.spyOn(logger, "log");
|
||||
});
|
||||
|
||||
describe("when an audio and video device is available", () => {
|
||||
beforeEach(() => {
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
|
||||
async ({ audio, video }: MediaStreamConstraints): Promise<MediaStream> => {
|
||||
if (audio && video) return audioVideoStream;
|
||||
return audioStream;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the audio/video stream", async () => {
|
||||
expect(await requestMediaPermissions()).toBe(audioVideoStream);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when calling with video = false and an audio device is available", () => {
|
||||
beforeEach(() => {
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
|
||||
async ({ audio, video }: MediaStreamConstraints): Promise<MediaStream> => {
|
||||
if (audio && !video) return audioStream;
|
||||
return audioVideoStream;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the audio stream", async () => {
|
||||
expect(await requestMediaPermissions(false)).toBe(audioStream);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when only an audio stream is available", () => {
|
||||
beforeEach(() => {
|
||||
error.name = "NotFoundError";
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
|
||||
async ({ audio, video }: MediaStreamConstraints): Promise<MediaStream> => {
|
||||
if (audio && video) throw error;
|
||||
if (audio) return audioStream;
|
||||
return audioVideoStream;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the audio stream", async () => {
|
||||
expect(await requestMediaPermissions()).toBe(audioStream);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when no device is available", () => {
|
||||
beforeEach(async () => {
|
||||
error.name = "NotFoundError";
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(async (): Promise<MediaStream> => {
|
||||
throw error;
|
||||
});
|
||||
await requestMediaPermissions();
|
||||
// required for the modal to settle
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
itShouldLogTheErrorAndShowTheNoMediaPermissionsModal();
|
||||
});
|
||||
|
||||
describe("when an Error is raised", () => {
|
||||
beforeEach(async () => {
|
||||
mocked(navigator.mediaDevices.getUserMedia).mockImplementation(
|
||||
async ({ audio, video }: MediaStreamConstraints): Promise<MediaStream> => {
|
||||
if (audio && video) throw error;
|
||||
return audioVideoStream;
|
||||
},
|
||||
);
|
||||
await requestMediaPermissions();
|
||||
// required for the modal to settle
|
||||
await flushPromises();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
itShouldLogTheErrorAndShowTheNoMediaPermissionsModal();
|
||||
});
|
||||
});
|
120
test/unit-tests/utils/membership-test.ts
Normal file
120
test/unit-tests/utils/membership-test.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixClient, MatrixEvent, Room, RoomMember, RoomState, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { isKnockDenied, waitForMember } from "../../src/utils/membership";
|
||||
import { createTestClient, mkRoomMember, stubClient } from "../test-utils";
|
||||
|
||||
describe("isKnockDenied", () => {
|
||||
const userId = "alice";
|
||||
let client: jest.Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
|
||||
beforeEach(() => {
|
||||
client = stubClient() as jest.Mocked<MatrixClient>;
|
||||
room = new Room("!room-id:example.com", client, "@user:example.com");
|
||||
});
|
||||
|
||||
it("checks that the user knock has been denied", () => {
|
||||
const roomMember = mkRoomMember(room.roomId, userId, KnownMembership.Leave, true, {
|
||||
membership: KnownMembership.Knock,
|
||||
});
|
||||
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||
expect(isKnockDenied(room)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ membership: KnownMembership.Leave, isKicked: false, prevMembership: KnownMembership.Invite },
|
||||
{ membership: KnownMembership.Leave, isKicked: true, prevMembership: KnownMembership.Invite },
|
||||
{ membership: KnownMembership.Leave, isKicked: false, prevMembership: KnownMembership.Join },
|
||||
{ membership: KnownMembership.Leave, isKicked: true, prevMembership: KnownMembership.Join },
|
||||
])("checks that the user knock has been not denied", ({ membership, isKicked, prevMembership }) => {
|
||||
const roomMember = mkRoomMember(room.roomId, userId, membership, isKicked, { membership: prevMembership });
|
||||
jest.spyOn(room, "getMember").mockReturnValue(roomMember);
|
||||
expect(isKnockDenied(room)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/* Shorter timeout, we've got tests to run */
|
||||
const timeout = 30;
|
||||
|
||||
describe("waitForMember", () => {
|
||||
const STUB_ROOM_ID = "!stub_room:domain";
|
||||
const STUB_MEMBER_ID = "!stub_member:domain";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
client = createTestClient();
|
||||
|
||||
// getRoom() only knows about !stub_room, which has only one member
|
||||
const stubRoom = {
|
||||
getMember: jest.fn().mockImplementation((userId) => {
|
||||
return userId === STUB_MEMBER_ID ? ({} as RoomMember) : null;
|
||||
}),
|
||||
};
|
||||
mocked(client.getRoom).mockImplementation((roomId) => {
|
||||
return roomId === STUB_ROOM_ID ? (stubRoom as unknown as Room) : null;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("resolves with false if the timeout is reached", async () => {
|
||||
const result = await waitForMember(client, "", "", { timeout: 0 });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", async () => {
|
||||
jest.useFakeTimers();
|
||||
const roomId = "!roomId:domain";
|
||||
const userId = "@clientId:domain";
|
||||
const resultProm = waitForMember(client, roomId, userId, { timeout });
|
||||
jest.advanceTimersByTime(50);
|
||||
expect(await resultProm).toBe(false);
|
||||
client.emit(
|
||||
RoomStateEvent.NewMember,
|
||||
undefined as unknown as MatrixEvent,
|
||||
undefined as unknown as RoomState,
|
||||
{
|
||||
roomId,
|
||||
userId: "@anotherClient:domain",
|
||||
} as RoomMember,
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("resolves with true if RoomState.newMember fires", async () => {
|
||||
const roomId = "!roomId:domain";
|
||||
const userId = "@clientId:domain";
|
||||
const resultProm = waitForMember(client, roomId, userId, { timeout });
|
||||
client.emit(
|
||||
RoomStateEvent.NewMember,
|
||||
undefined as unknown as MatrixEvent,
|
||||
undefined as unknown as RoomState,
|
||||
{ roomId, userId } as RoomMember,
|
||||
);
|
||||
expect(await resultProm).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves immediately if the user is already a member", async () => {
|
||||
jest.useFakeTimers();
|
||||
const resultProm = waitForMember(client, STUB_ROOM_ID, STUB_MEMBER_ID, { timeout });
|
||||
expect(await resultProm).toBe(true);
|
||||
});
|
||||
|
||||
it("waits for the timeout if the room is known but the user is not", async () => {
|
||||
const result = await waitForMember(client, STUB_ROOM_ID, "@other_user", { timeout: 0 });
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
365
test/unit-tests/utils/notifications-test.ts
Normal file
365
test/unit-tests/utils/notifications-test.ts
Normal file
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixEvent, NotificationCountType, Room, MatrixClient, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import { Mocked, mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
localNotificationsAreSilenced,
|
||||
getLocalNotificationAccountDataEventType,
|
||||
createLocalNotificationSettingsIfNeeded,
|
||||
deviceNotificationSettingsKeys,
|
||||
clearAllNotifications,
|
||||
clearRoomNotification,
|
||||
notificationLevelToIndicator,
|
||||
getThreadNotificationLevel,
|
||||
getMarkedUnreadState,
|
||||
setMarkedUnreadState,
|
||||
} from "../../src/utils/notifications";
|
||||
import SettingsStore from "../../src/settings/SettingsStore";
|
||||
import { getMockClientWithEventEmitter } from "../test-utils/client";
|
||||
import { mkMessage, stubClient } from "../test-utils/test-utils";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { NotificationLevel } from "../../src/stores/notifications/NotificationLevel";
|
||||
|
||||
jest.mock("../../src/settings/SettingsStore");
|
||||
|
||||
describe("notifications", () => {
|
||||
let accountDataStore: Record<string, MatrixEvent> = {};
|
||||
let mockClient: Mocked<MatrixClient>;
|
||||
let accountDataEventKey: string;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockClient = getMockClientWithEventEmitter({
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
getAccountData: jest.fn().mockImplementation((eventType) => accountDataStore[eventType]),
|
||||
setAccountData: jest.fn().mockImplementation((eventType, content) => {
|
||||
accountDataStore[eventType] = new MatrixEvent({
|
||||
type: eventType,
|
||||
content,
|
||||
});
|
||||
}),
|
||||
});
|
||||
accountDataStore = {};
|
||||
accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId!);
|
||||
mocked(SettingsStore).getValue.mockReturnValue(false);
|
||||
});
|
||||
|
||||
describe("createLocalNotification", () => {
|
||||
it("creates account data event", async () => {
|
||||
await createLocalNotificationSettingsIfNeeded(mockClient);
|
||||
const event = mockClient.getAccountData(accountDataEventKey);
|
||||
expect(event?.getContent().is_silenced).toBe(true);
|
||||
});
|
||||
|
||||
it("does not do anything for guests", async () => {
|
||||
mockClient.isGuest.mockReset().mockReturnValue(true);
|
||||
await createLocalNotificationSettingsIfNeeded(mockClient);
|
||||
const event = mockClient.getAccountData(accountDataEventKey);
|
||||
expect(event).toBeFalsy();
|
||||
});
|
||||
|
||||
it.each(deviceNotificationSettingsKeys)(
|
||||
"unsilenced for existing sessions when %s setting is truthy",
|
||||
async (settingKey) => {
|
||||
mocked(SettingsStore).getValue.mockImplementation((key): any => {
|
||||
return key === settingKey;
|
||||
});
|
||||
|
||||
await createLocalNotificationSettingsIfNeeded(mockClient);
|
||||
const event = mockClient.getAccountData(accountDataEventKey);
|
||||
expect(event?.getContent().is_silenced).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not override an existing account event data", async () => {
|
||||
mockClient.setAccountData(accountDataEventKey, {
|
||||
is_silenced: false,
|
||||
});
|
||||
|
||||
await createLocalNotificationSettingsIfNeeded(mockClient);
|
||||
const event = mockClient.getAccountData(accountDataEventKey);
|
||||
expect(event?.getContent().is_silenced).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("localNotificationsAreSilenced", () => {
|
||||
it("defaults to false when no setting exists", () => {
|
||||
expect(localNotificationsAreSilenced(mockClient)).toBeFalsy();
|
||||
});
|
||||
it("checks the persisted value", () => {
|
||||
mockClient.setAccountData(accountDataEventKey, { is_silenced: true });
|
||||
expect(localNotificationsAreSilenced(mockClient)).toBeTruthy();
|
||||
|
||||
mockClient.setAccountData(accountDataEventKey, { is_silenced: false });
|
||||
expect(localNotificationsAreSilenced(mockClient)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearRoomNotification", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let sendReadReceiptSpy: jest.SpyInstance;
|
||||
const ROOM_ID = "123";
|
||||
const USER_ID = "@bob:example.org";
|
||||
let message: MatrixEvent;
|
||||
let sendReceiptsSetting = true;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, client, USER_ID);
|
||||
message = mkMessage({
|
||||
event: true,
|
||||
room: ROOM_ID,
|
||||
user: USER_ID,
|
||||
msg: "Hello",
|
||||
});
|
||||
room.addLiveEvents([message]);
|
||||
sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({});
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room]);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
|
||||
return name === "sendReadReceipts" && sendReceiptsSetting;
|
||||
});
|
||||
});
|
||||
|
||||
it("sends a request even if everything has been read", async () => {
|
||||
await clearRoomNotification(room, client);
|
||||
expect(sendReadReceiptSpy).toHaveBeenCalledWith(message, ReceiptType.Read, true);
|
||||
});
|
||||
|
||||
it("marks the room as read even if the receipt failed", async () => {
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 5);
|
||||
sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockReset().mockRejectedValue({ error: 42 });
|
||||
|
||||
await expect(async () => {
|
||||
await clearRoomNotification(room, client);
|
||||
}).rejects.toEqual({ error: 42 });
|
||||
expect(room.getUnreadNotificationCount(NotificationCountType.Total)).toBe(0);
|
||||
});
|
||||
|
||||
describe("when sendReadReceipts setting is disabled", () => {
|
||||
beforeEach(() => {
|
||||
sendReceiptsSetting = false;
|
||||
});
|
||||
|
||||
it("should send a private read receipt", async () => {
|
||||
await clearRoomNotification(room, client);
|
||||
expect(sendReadReceiptSpy).toHaveBeenCalledWith(message, ReceiptType.ReadPrivate, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearAllNotifications", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let sendReadReceiptSpy: jest.SpyInstance;
|
||||
|
||||
const ROOM_ID = "123";
|
||||
const USER_ID = "@bob:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, client, USER_ID);
|
||||
sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({});
|
||||
jest.spyOn(client, "getRooms").mockReturnValue([room]);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
|
||||
return name === "sendReadReceipts";
|
||||
});
|
||||
});
|
||||
|
||||
it("does not send any requests if everything has been read", () => {
|
||||
clearAllNotifications(client);
|
||||
expect(sendReadReceiptSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends unthreaded receipt requests", async () => {
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: ROOM_ID,
|
||||
user: USER_ID,
|
||||
ts: 1,
|
||||
});
|
||||
room.addLiveEvents([message]);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
await clearAllNotifications(client);
|
||||
|
||||
expect(sendReadReceiptSpy).toHaveBeenCalledWith(message, ReceiptType.Read, true);
|
||||
});
|
||||
|
||||
it("sends private read receipts", async () => {
|
||||
const message = mkMessage({
|
||||
event: true,
|
||||
room: ROOM_ID,
|
||||
user: USER_ID,
|
||||
ts: 1,
|
||||
});
|
||||
room.addLiveEvents([message]);
|
||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||
|
||||
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);
|
||||
|
||||
await clearAllNotifications(client);
|
||||
|
||||
expect(sendReadReceiptSpy).toHaveBeenCalledWith(message, ReceiptType.ReadPrivate, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMarkedUnreadState", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const ROOM_ID = "123";
|
||||
const USER_ID = "@bob:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, client, USER_ID);
|
||||
});
|
||||
|
||||
it("reads from stable prefix", async () => {
|
||||
room.getAccountData = jest.fn().mockImplementation((eventType: string) => {
|
||||
if (eventType === "m.marked_unread") {
|
||||
return { getContent: jest.fn().mockReturnValue({ unread: true }) };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
expect(getMarkedUnreadState(room)).toBe(true);
|
||||
});
|
||||
|
||||
it("reads from unstable prefix", async () => {
|
||||
room.getAccountData = jest.fn().mockImplementation((eventType: string) => {
|
||||
if (eventType === "com.famedly.marked_unread") {
|
||||
return { getContent: jest.fn().mockReturnValue({ unread: true }) };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
expect(getMarkedUnreadState(room)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns undefined if neither prefix is present", async () => {
|
||||
room.getAccountData = jest.fn().mockImplementation((eventType: string) => {
|
||||
return null;
|
||||
});
|
||||
expect(getMarkedUnreadState(room)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setUnreadMarker", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
|
||||
const ROOM_ID = "123";
|
||||
const USER_ID = "@bob:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.safeGet());
|
||||
room = new Room(ROOM_ID, client, USER_ID);
|
||||
});
|
||||
|
||||
// set true, no existing event
|
||||
it("sets unread flag if event doesn't exist", async () => {
|
||||
await setMarkedUnreadState(room, client, true);
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: true,
|
||||
});
|
||||
});
|
||||
|
||||
// set false, no existing event
|
||||
it("does nothing when clearing if flag is false", async () => {
|
||||
await setMarkedUnreadState(room, client, false);
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// set true, existing event = false
|
||||
it("sets unread flag to if existing event is false", async () => {
|
||||
room.getAccountData = jest
|
||||
.fn()
|
||||
.mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: false }) });
|
||||
await setMarkedUnreadState(room, client, true);
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: true,
|
||||
});
|
||||
});
|
||||
|
||||
// set false, existing event = false
|
||||
it("does nothing if set false and existing event is false", async () => {
|
||||
room.getAccountData = jest
|
||||
.fn()
|
||||
.mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: false }) });
|
||||
await setMarkedUnreadState(room, client, false);
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// set true, existing event = true
|
||||
it("does nothing if setting true and existing event is true", async () => {
|
||||
room.getAccountData = jest
|
||||
.fn()
|
||||
.mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: true }) });
|
||||
await setMarkedUnreadState(room, client, true);
|
||||
expect(client.setRoomAccountData).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// set false, existing event = true
|
||||
it("sets flag if setting false and existing event is true", async () => {
|
||||
room.getAccountData = jest
|
||||
.fn()
|
||||
.mockReturnValue({ getContent: jest.fn().mockReturnValue({ unread: true }) });
|
||||
await setMarkedUnreadState(room, client, false);
|
||||
expect(client.setRoomAccountData).toHaveBeenCalledWith(ROOM_ID, "com.famedly.marked_unread", {
|
||||
unread: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("notificationLevelToIndicator", () => {
|
||||
it("returns undefined if notification level is None", () => {
|
||||
expect(notificationLevelToIndicator(NotificationLevel.None)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns default if notification level is Activity", () => {
|
||||
expect(notificationLevelToIndicator(NotificationLevel.Activity)).toEqual("default");
|
||||
});
|
||||
|
||||
it("returns success if notification level is Notification", () => {
|
||||
expect(notificationLevelToIndicator(NotificationLevel.Notification)).toEqual("success");
|
||||
});
|
||||
|
||||
it("returns critical if notification level is Highlight", () => {
|
||||
expect(notificationLevelToIndicator(NotificationLevel.Highlight)).toEqual("critical");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getThreadNotificationLevel", () => {
|
||||
let room: Room;
|
||||
|
||||
const ROOM_ID = "123";
|
||||
const USER_ID = "@bob:example.org";
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room(ROOM_ID, MatrixClientPeg.safeGet(), USER_ID);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ notificationCountType: NotificationCountType.Highlight, expected: NotificationLevel.Highlight },
|
||||
{ notificationCountType: NotificationCountType.Total, expected: NotificationLevel.Notification },
|
||||
{ notificationCountType: null, expected: NotificationLevel.Activity },
|
||||
])(
|
||||
"returns NotificationLevel $expected when notificationCountType is $expected",
|
||||
({ notificationCountType, expected }) => {
|
||||
jest.spyOn(room, "threadsAggregateNotificationType", "get").mockReturnValue(notificationCountType);
|
||||
expect(getThreadNotificationLevel(room)).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
160
test/unit-tests/utils/numbers-test.ts
Normal file
160
test/unit-tests/utils/numbers-test.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { clamp, defaultNumber, percentageOf, percentageWithin, sum } from "../../src/utils/numbers";
|
||||
|
||||
describe("numbers", () => {
|
||||
describe("defaultNumber", () => {
|
||||
it("should use the default when the input is not a number", () => {
|
||||
const def = 42;
|
||||
|
||||
let result = defaultNumber(null, def);
|
||||
expect(result).toBe(def);
|
||||
|
||||
result = defaultNumber(undefined, def);
|
||||
expect(result).toBe(def);
|
||||
|
||||
result = defaultNumber(Number.NaN, def);
|
||||
expect(result).toBe(def);
|
||||
});
|
||||
|
||||
it("should use the number when it is a number", () => {
|
||||
const input = 24;
|
||||
const def = 42;
|
||||
const result = defaultNumber(input, def);
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clamp", () => {
|
||||
it("should clamp high numbers", () => {
|
||||
const input = 101;
|
||||
const min = 0;
|
||||
const max = 100;
|
||||
const result = clamp(input, min, max);
|
||||
expect(result).toBe(max);
|
||||
});
|
||||
|
||||
it("should clamp low numbers", () => {
|
||||
const input = -1;
|
||||
const min = 0;
|
||||
const max = 100;
|
||||
const result = clamp(input, min, max);
|
||||
expect(result).toBe(min);
|
||||
});
|
||||
|
||||
it("should not clamp numbers in range", () => {
|
||||
const input = 50;
|
||||
const min = 0;
|
||||
const max = 100;
|
||||
const result = clamp(input, min, max);
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
|
||||
it("should clamp floats", () => {
|
||||
const min = -0.1;
|
||||
const max = +0.1;
|
||||
|
||||
let result = clamp(-1.2, min, max);
|
||||
expect(result).toBe(min);
|
||||
|
||||
result = clamp(1.2, min, max);
|
||||
expect(result).toBe(max);
|
||||
|
||||
result = clamp(0.02, min, max);
|
||||
expect(result).toBe(0.02);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sum", () => {
|
||||
it("should sum", () => {
|
||||
// duh
|
||||
const result = sum(1, 2, 1, 4);
|
||||
expect(result).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("percentageWithin", () => {
|
||||
it("should work within 0-100", () => {
|
||||
const result = percentageWithin(0.4, 0, 100);
|
||||
expect(result).toBe(40);
|
||||
});
|
||||
|
||||
it("should work within 0-100 when pct > 1", () => {
|
||||
const result = percentageWithin(1.4, 0, 100);
|
||||
expect(result).toBe(140);
|
||||
});
|
||||
|
||||
it("should work within 0-100 when pct < 0", () => {
|
||||
const result = percentageWithin(-1.4, 0, 100);
|
||||
expect(result).toBe(-140);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100", () => {
|
||||
const result = percentageWithin(0.4, 10, 20);
|
||||
expect(result).toBe(14);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100 when pct > 1", () => {
|
||||
const result = percentageWithin(1.4, 10, 20);
|
||||
expect(result).toBe(24);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100 when pct < 0", () => {
|
||||
const result = percentageWithin(-1.4, 10, 20);
|
||||
expect(result).toBe(-4);
|
||||
});
|
||||
|
||||
it("should work with floats", () => {
|
||||
const result = percentageWithin(0.4, 10.2, 20.4);
|
||||
expect(result).toBe(14.28);
|
||||
});
|
||||
});
|
||||
|
||||
// These are the inverse of percentageWithin
|
||||
describe("percentageOf", () => {
|
||||
it("should work within 0-100", () => {
|
||||
const result = percentageOf(40, 0, 100);
|
||||
expect(result).toBe(0.4);
|
||||
});
|
||||
|
||||
it("should work within 0-100 when val > 100", () => {
|
||||
const result = percentageOf(140, 0, 100);
|
||||
expect(result).toBe(1.4);
|
||||
});
|
||||
|
||||
it("should work within 0-100 when val < 0", () => {
|
||||
const result = percentageOf(-140, 0, 100);
|
||||
expect(result).toBe(-1.4);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100", () => {
|
||||
const result = percentageOf(14, 10, 20);
|
||||
expect(result).toBe(0.4);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100 when val > 100", () => {
|
||||
const result = percentageOf(24, 10, 20);
|
||||
expect(result).toBe(1.4);
|
||||
});
|
||||
|
||||
it("should work with ranges other than 0-100 when val < 0", () => {
|
||||
const result = percentageOf(-4, 10, 20);
|
||||
expect(result).toBe(-1.4);
|
||||
});
|
||||
|
||||
it("should work with floats", () => {
|
||||
const result = percentageOf(14.28, 10.2, 20.4);
|
||||
expect(result).toBe(0.4);
|
||||
});
|
||||
|
||||
it("should return 0 for values that cause a division by zero", () => {
|
||||
expect(percentageOf(0, 0, 0)).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
236
test/unit-tests/utils/objects-test.ts
Normal file
236
test/unit-tests/utils/objects-test.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
objectClone,
|
||||
objectDiff,
|
||||
objectExcluding,
|
||||
objectHasDiff,
|
||||
objectKeyChanges,
|
||||
objectShallowClone,
|
||||
objectWithOnly,
|
||||
} from "../../src/utils/objects";
|
||||
|
||||
describe("objects", () => {
|
||||
describe("objectExcluding", () => {
|
||||
it("should exclude the given properties", () => {
|
||||
const input = { hello: "world", test: true };
|
||||
const output = { hello: "world" };
|
||||
const props = ["test", "doesnotexist"]; // we also make sure it doesn't explode on missing props
|
||||
const result = objectExcluding(input, <any>props); // any is to test the missing prop
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toMatchObject(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectWithOnly", () => {
|
||||
it("should exclusively use the given properties", () => {
|
||||
const input = { hello: "world", test: true };
|
||||
const output = { hello: "world" };
|
||||
const props = ["hello", "doesnotexist"]; // we also make sure it doesn't explode on missing props
|
||||
const result = objectWithOnly(input, <any>props); // any is to test the missing prop
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toMatchObject(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectShallowClone", () => {
|
||||
it("should create a new object", () => {
|
||||
const input = { test: 1 };
|
||||
const result = objectShallowClone(input);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBe(input);
|
||||
expect(result).toMatchObject(input);
|
||||
});
|
||||
|
||||
it("should only clone the top level properties", () => {
|
||||
const input = { a: 1, b: { c: 2 } };
|
||||
const result = objectShallowClone(input);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toMatchObject(input);
|
||||
expect(result.b).toBe(input.b);
|
||||
});
|
||||
|
||||
it("should support custom clone functions", () => {
|
||||
const input = { a: 1, b: 2 };
|
||||
const output = { a: 4, b: 8 };
|
||||
const result = objectShallowClone(input, (k, v) => {
|
||||
// XXX: inverted expectation for ease of assertion
|
||||
expect(Object.keys(input)).toContain(k);
|
||||
|
||||
return v * 4;
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toMatchObject(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectHasDiff", () => {
|
||||
it("should return false for the same pointer", () => {
|
||||
const a = {};
|
||||
const result = objectHasDiff(a, a);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true if keys for A > keys for B", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1 };
|
||||
const result = objectHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true if keys for A < keys for B", () => {
|
||||
const a = { a: 1 };
|
||||
const b = { a: 1, b: 2 };
|
||||
const result = objectHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false if the objects are the same but different pointers", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1, b: 2 };
|
||||
const result = objectHasDiff(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should consider pointers when testing values", () => {
|
||||
const a = { a: {}, b: 2 }; // `{}` is shorthand for `new Object()`
|
||||
const b = { a: {}, b: 2 };
|
||||
const result = objectHasDiff(a, b);
|
||||
expect(result).toBe(true); // even though the keys are the same, the value pointers vary
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectDiff", () => {
|
||||
it("should return empty sets for the same object", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1, b: 2 };
|
||||
const result = objectDiff(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return empty sets for the same object pointer", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const result = objectDiff(a, a);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should indicate when property changes are made", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 11, b: 2 };
|
||||
const result = objectDiff(a, b);
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(1);
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.changed).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("should indicate when properties are added", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1, b: 2, c: 3 };
|
||||
const result = objectDiff(a, b);
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(0);
|
||||
expect(result.added).toEqual(["c"]);
|
||||
});
|
||||
|
||||
it("should indicate when properties are removed", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1 };
|
||||
const result = objectDiff(a, b);
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(0);
|
||||
expect(result.added).toHaveLength(0);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.removed).toEqual(["b"]);
|
||||
});
|
||||
|
||||
it("should indicate when multiple aspects change", () => {
|
||||
const a = { a: 1, b: 2, c: 3 };
|
||||
const b: typeof a | { d: number } = { a: 1, b: 22, d: 4 };
|
||||
const result = objectDiff(a, b);
|
||||
expect(result.changed).toBeDefined();
|
||||
expect(result.added).toBeDefined();
|
||||
expect(result.removed).toBeDefined();
|
||||
expect(result.changed).toHaveLength(1);
|
||||
expect(result.added).toHaveLength(1);
|
||||
expect(result.removed).toHaveLength(1);
|
||||
expect(result.changed).toEqual(["b"]);
|
||||
expect(result.removed).toEqual(["c"]);
|
||||
expect(result.added).toEqual(["d"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectKeyChanges", () => {
|
||||
it("should return an empty set if no properties changed", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const b = { a: 1, b: 2 };
|
||||
const result = objectKeyChanges(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return an empty set if no properties changed for the same pointer", () => {
|
||||
const a = { a: 1, b: 2 };
|
||||
const result = objectKeyChanges(a, a);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return properties which were changed, added, or removed", () => {
|
||||
const a = { a: 1, b: 2, c: 3 };
|
||||
const b: typeof a | { d: number } = { a: 1, b: 22, d: 4 };
|
||||
const result = objectKeyChanges(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(["c", "d", "b"]); // order isn't important, but the test cares
|
||||
});
|
||||
});
|
||||
|
||||
describe("objectClone", () => {
|
||||
it("should deep clone an object", () => {
|
||||
const a = {
|
||||
hello: "world",
|
||||
test: {
|
||||
another: "property",
|
||||
test: 42,
|
||||
third: {
|
||||
prop: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = objectClone(a);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).not.toBe(a);
|
||||
expect(result).toMatchObject(a);
|
||||
expect(result.test).not.toBe(a.test);
|
||||
expect(result.test.third).not.toBe(a.test.third);
|
||||
});
|
||||
});
|
||||
});
|
88
test/unit-tests/utils/oidc/TokenRefresher-test.ts
Normal file
88
test/unit-tests/utils/oidc/TokenRefresher-test.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { TokenRefresher } from "../../../src/utils/oidc/TokenRefresher";
|
||||
import { persistAccessTokenInStorage, persistRefreshTokenInStorage } from "../../../src/utils/tokens/tokens";
|
||||
import { mockPlatformPeg } from "../../test-utils";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
jest.mock("../../../src/utils/tokens/tokens", () => ({
|
||||
persistAccessTokenInStorage: jest.fn(),
|
||||
persistRefreshTokenInStorage: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("TokenRefresher", () => {
|
||||
const clientId = "test-client-id";
|
||||
const issuer = "https://auth.com/";
|
||||
const redirectUri = "https://test.com";
|
||||
const deviceId = "test-device-id";
|
||||
const userId = "@alice:server.org";
|
||||
const accessToken = "test-access-token";
|
||||
const refreshToken = "test-refresh-token";
|
||||
|
||||
const authConfig = makeDelegatedAuthConfig(issuer);
|
||||
const idTokenClaims = {
|
||||
aud: "123",
|
||||
iss: issuer,
|
||||
sub: "123",
|
||||
exp: 123,
|
||||
iat: 456,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(`${issuer}.well-known/openid-configuration`, authConfig.metadata);
|
||||
fetchMock.get(`${issuer}jwks`, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
keys: [],
|
||||
});
|
||||
|
||||
mocked(persistAccessTokenInStorage).mockResolvedValue(undefined);
|
||||
mocked(persistRefreshTokenInStorage).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should persist tokens with a pickle key", async () => {
|
||||
const pickleKey = "test-pickle-key";
|
||||
const getPickleKey = jest.fn().mockResolvedValue(pickleKey);
|
||||
mockPlatformPeg({ getPickleKey });
|
||||
|
||||
const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
|
||||
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
await refresher.persistTokens({ accessToken, refreshToken });
|
||||
|
||||
expect(getPickleKey).toHaveBeenCalledWith(userId, deviceId);
|
||||
expect(persistAccessTokenInStorage).toHaveBeenCalledWith(accessToken, pickleKey);
|
||||
expect(persistRefreshTokenInStorage).toHaveBeenCalledWith(refreshToken, pickleKey);
|
||||
});
|
||||
|
||||
it("should persist tokens without a pickle key", async () => {
|
||||
const getPickleKey = jest.fn().mockResolvedValue(null);
|
||||
mockPlatformPeg({ getPickleKey });
|
||||
|
||||
const refresher = new TokenRefresher(issuer, clientId, redirectUri, deviceId, idTokenClaims, userId);
|
||||
|
||||
await refresher.oidcClientReady;
|
||||
|
||||
await refresher.persistTokens({ accessToken, refreshToken });
|
||||
|
||||
expect(getPickleKey).toHaveBeenCalledWith(userId, deviceId);
|
||||
expect(persistAccessTokenInStorage).toHaveBeenCalledWith(accessToken, undefined);
|
||||
expect(persistRefreshTokenInStorage).toHaveBeenCalledWith(refreshToken, undefined);
|
||||
});
|
||||
});
|
164
test/unit-tests/utils/oidc/authorize-test.ts
Normal file
164
test/unit-tests/utils/oidc/authorize-test.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { completeAuthorizationCodeGrant } from "matrix-js-sdk/src/oidc/authorize";
|
||||
import * as randomStringUtils from "matrix-js-sdk/src/randomstring";
|
||||
import { BearerTokenResponse } from "matrix-js-sdk/src/oidc/validate";
|
||||
import { mocked } from "jest-mock";
|
||||
import { Crypto } from "@peculiar/webcrypto";
|
||||
import { getRandomValues } from "node:crypto";
|
||||
|
||||
import { completeOidcLogin, startOidcLogin } from "../../../src/utils/oidc/authorize";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
import { OidcClientError } from "../../../src/utils/oidc/error";
|
||||
import { mockPlatformPeg } from "../../test-utils";
|
||||
|
||||
jest.unmock("matrix-js-sdk/src/randomstring");
|
||||
|
||||
jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
|
||||
...jest.requireActual("matrix-js-sdk/src/oidc/authorize"),
|
||||
completeAuthorizationCodeGrant: jest.fn(),
|
||||
}));
|
||||
|
||||
const webCrypto = new Crypto();
|
||||
|
||||
describe("OIDC authorization", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const homeserverUrl = "https://matrix.org";
|
||||
const identityServerUrl = "https://is.org";
|
||||
const clientId = "xyz789";
|
||||
const baseUrl = "https://test.com";
|
||||
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||
|
||||
// to restore later
|
||||
const realWindowLocation = window.location;
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore allow delete of non-optional prop
|
||||
delete window.location;
|
||||
// @ts-ignore ugly mocking
|
||||
window.location = {
|
||||
href: baseUrl,
|
||||
origin: baseUrl,
|
||||
};
|
||||
|
||||
jest.spyOn(randomStringUtils, "randomString").mockRestore();
|
||||
mockPlatformPeg();
|
||||
Object.defineProperty(window, "crypto", {
|
||||
value: {
|
||||
getRandomValues,
|
||||
randomUUID: jest.fn().mockReturnValue("not-random-uuid"),
|
||||
subtle: webCrypto.subtle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
fetchMock.get(
|
||||
`${delegatedAuthConfig.metadata.issuer}.well-known/openid-configuration`,
|
||||
delegatedAuthConfig.metadata,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = realWindowLocation;
|
||||
});
|
||||
|
||||
describe("startOidcLogin()", () => {
|
||||
it("navigates to authorization endpoint with correct parameters", async () => {
|
||||
await startOidcLogin(delegatedAuthConfig, clientId, homeserverUrl);
|
||||
|
||||
const expectedScopeWithoutDeviceId = `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:`;
|
||||
|
||||
const authUrl = new URL(window.location.href);
|
||||
|
||||
expect(authUrl.searchParams.get("response_mode")).toEqual("query");
|
||||
expect(authUrl.searchParams.get("response_type")).toEqual("code");
|
||||
expect(authUrl.searchParams.get("client_id")).toEqual(clientId);
|
||||
expect(authUrl.searchParams.get("code_challenge_method")).toEqual("S256");
|
||||
|
||||
// scope ends with a 10char randomstring deviceId
|
||||
const scope = authUrl.searchParams.get("scope")!;
|
||||
expect(scope.substring(0, scope.length - 10)).toEqual(expectedScopeWithoutDeviceId);
|
||||
expect(scope.substring(scope.length - 10)).toBeTruthy();
|
||||
|
||||
// random string, just check they are set
|
||||
expect(authUrl.searchParams.has("state")).toBeTruthy();
|
||||
expect(authUrl.searchParams.has("nonce")).toBeTruthy();
|
||||
expect(authUrl.searchParams.has("code_challenge")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("completeOidcLogin()", () => {
|
||||
const state = "test-state-444";
|
||||
const code = "test-code-777";
|
||||
const queryDict = {
|
||||
code,
|
||||
state: state,
|
||||
};
|
||||
|
||||
const tokenResponse: BearerTokenResponse = {
|
||||
access_token: "abc123",
|
||||
refresh_token: "def456",
|
||||
id_token: "ghi789",
|
||||
scope: "test",
|
||||
token_type: "Bearer",
|
||||
expires_at: 12345,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(completeAuthorizationCodeGrant)
|
||||
.mockClear()
|
||||
.mockResolvedValue({
|
||||
oidcClientSettings: {
|
||||
clientId,
|
||||
issuer,
|
||||
},
|
||||
tokenResponse,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
idTokenClaims: {
|
||||
aud: "123",
|
||||
iss: issuer,
|
||||
sub: "123",
|
||||
exp: 123,
|
||||
iat: 456,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw when query params do not include state and code", async () => {
|
||||
await expect(async () => await completeOidcLogin({})).rejects.toThrow(
|
||||
OidcClientError.InvalidQueryParameters,
|
||||
);
|
||||
});
|
||||
|
||||
it("should make request complete authorization code grant", async () => {
|
||||
await completeOidcLogin(queryDict);
|
||||
|
||||
expect(completeAuthorizationCodeGrant).toHaveBeenCalledWith(code, state);
|
||||
});
|
||||
|
||||
it("should return accessToken, configured homeserver and identityServer", async () => {
|
||||
const result = await completeOidcLogin(queryDict);
|
||||
|
||||
expect(result).toEqual({
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
homeserverUrl,
|
||||
identityServerUrl,
|
||||
issuer,
|
||||
clientId,
|
||||
idToken: "ghi789",
|
||||
idTokenClaims: result.idTokenClaims,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
107
test/unit-tests/utils/oidc/persistOidcSettings-test.ts
Normal file
107
test/unit-tests/utils/oidc/persistOidcSettings-test.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { IdTokenClaims } from "oidc-client-ts";
|
||||
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import {
|
||||
getStoredOidcClientId,
|
||||
getStoredOidcIdToken,
|
||||
getStoredOidcIdTokenClaims,
|
||||
getStoredOidcTokenIssuer,
|
||||
persistOidcAuthenticatedSettings,
|
||||
} from "../../../src/utils/oidc/persistOidcSettings";
|
||||
|
||||
jest.mock("matrix-js-sdk/src/matrix");
|
||||
|
||||
describe("persist OIDC settings", () => {
|
||||
jest.spyOn(Storage.prototype, "getItem");
|
||||
jest.spyOn(Storage.prototype, "setItem");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const clientId = "test-client-id";
|
||||
const issuer = "https://auth.org/";
|
||||
const idToken = "test-id-token";
|
||||
const idTokenClaims: IdTokenClaims = {
|
||||
// audience is this client
|
||||
aud: "123",
|
||||
// issuer matches
|
||||
iss: issuer,
|
||||
sub: "123",
|
||||
exp: 123,
|
||||
iat: 456,
|
||||
};
|
||||
|
||||
describe("persistOidcAuthenticatedSettings", () => {
|
||||
it("should set clientId and issuer in localStorage", () => {
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token", idToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcTokenIssuer()", () => {
|
||||
it("should return issuer from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_token_issuer", issuer);
|
||||
expect(getStoredOidcTokenIssuer()).toEqual(issuer);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_token_issuer");
|
||||
});
|
||||
|
||||
it("should return undefined when no issuer in localStorage", () => {
|
||||
expect(getStoredOidcTokenIssuer()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcClientId()", () => {
|
||||
it("should return clientId from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||
expect(getStoredOidcClientId()).toEqual(clientId);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_client_id");
|
||||
});
|
||||
it("should throw when no clientId in localStorage", () => {
|
||||
expect(() => getStoredOidcClientId()).toThrow("Oidc client id not found in storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdToken()", () => {
|
||||
it("should return token from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
expect(getStoredOidcIdToken()).toEqual(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token");
|
||||
});
|
||||
|
||||
it("should return undefined when no token in localStorage", () => {
|
||||
expect(getStoredOidcIdToken()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdTokenClaims()", () => {
|
||||
it("should return claims from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token_claims", JSON.stringify(idTokenClaims));
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return claims extracted from id_token in localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
mocked(decodeIdToken).mockReturnValue(idTokenClaims);
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(decodeIdToken).toHaveBeenCalledWith(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return undefined when no claims in localStorage", () => {
|
||||
expect(getStoredOidcIdTokenClaims()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
128
test/unit-tests/utils/oidc/registerClient-test.ts
Normal file
128
test/unit-tests/utils/oidc/registerClient-test.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import fetchMockJest from "fetch-mock-jest";
|
||||
import { OidcError } from "matrix-js-sdk/src/oidc/error";
|
||||
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getOidcClientId } from "../../../src/utils/oidc/registerClient";
|
||||
import { mockPlatformPeg } from "../../test-utils";
|
||||
import PlatformPeg from "../../../src/PlatformPeg";
|
||||
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
|
||||
|
||||
describe("getOidcClientId()", () => {
|
||||
const issuer = "https://auth.com/";
|
||||
const clientName = "Element";
|
||||
const baseUrl = "https://just.testing";
|
||||
const dynamicClientId = "xyz789";
|
||||
const staticOidcClients = {
|
||||
[issuer]: {
|
||||
client_id: "abc123",
|
||||
},
|
||||
};
|
||||
const delegatedAuthConfig = makeDelegatedAuthConfig(issuer);
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMockJest.mockClear();
|
||||
fetchMockJest.resetBehavior();
|
||||
mockPlatformPeg();
|
||||
Object.defineProperty(PlatformPeg.get(), "baseUrl", {
|
||||
get(): string {
|
||||
return baseUrl;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(PlatformPeg.get(), "defaultOidcClientUri", {
|
||||
get(): string {
|
||||
return baseUrl;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(PlatformPeg.get(), "getOidcCallbackUrl", {
|
||||
value: () => ({
|
||||
href: baseUrl,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("should return static clientId when configured", async () => {
|
||||
expect(await getOidcClientId(delegatedAuthConfig, staticOidcClients)).toEqual("abc123");
|
||||
// didn't try to register
|
||||
expect(fetchMockJest).toHaveFetchedTimes(0);
|
||||
});
|
||||
|
||||
it("should throw when no static clientId is configured and no registration endpoint", async () => {
|
||||
const authConfigWithoutRegistration: OidcClientConfig = makeDelegatedAuthConfig(
|
||||
"https://issuerWithoutStaticClientId.org/",
|
||||
);
|
||||
authConfigWithoutRegistration.registrationEndpoint = undefined;
|
||||
await expect(getOidcClientId(authConfigWithoutRegistration, staticOidcClients)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationNotSupported,
|
||||
);
|
||||
// didn't try to register
|
||||
expect(fetchMockJest).toHaveFetchedTimes(0);
|
||||
});
|
||||
|
||||
it("should handle when staticOidcClients object is falsy", async () => {
|
||||
const authConfigWithoutRegistration: OidcClientConfig = {
|
||||
...delegatedAuthConfig,
|
||||
registrationEndpoint: undefined,
|
||||
};
|
||||
await expect(getOidcClientId(authConfigWithoutRegistration)).rejects.toThrow(
|
||||
OidcError.DynamicRegistrationNotSupported,
|
||||
);
|
||||
// didn't try to register
|
||||
expect(fetchMockJest).toHaveFetchedTimes(0);
|
||||
});
|
||||
|
||||
it("should make correct request to register client", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
body: JSON.stringify({ client_id: dynamicClientId }),
|
||||
});
|
||||
expect(await getOidcClientId(delegatedAuthConfig)).toEqual(dynamicClientId);
|
||||
// didn't try to register
|
||||
expect(fetchMockJest).toHaveBeenCalledWith(
|
||||
delegatedAuthConfig.registrationEndpoint!,
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
}),
|
||||
);
|
||||
expect(JSON.parse(fetchMockJest.mock.calls[0][1]!.body as string)).toEqual(
|
||||
expect.objectContaining({
|
||||
client_name: clientName,
|
||||
client_uri: baseUrl,
|
||||
response_types: ["code"],
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
redirect_uris: [baseUrl],
|
||||
id_token_signed_response_alg: "RS256",
|
||||
token_endpoint_auth_method: "none",
|
||||
application_type: "web",
|
||||
logo_uri: `${baseUrl}/vector-icons/1024.png`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when registration request fails", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 500,
|
||||
});
|
||||
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationFailed);
|
||||
});
|
||||
|
||||
it("should throw when registration response is invalid", async () => {
|
||||
fetchMockJest.post(delegatedAuthConfig.registrationEndpoint!, {
|
||||
status: 200,
|
||||
// no clientId in response
|
||||
body: "{}",
|
||||
});
|
||||
await expect(getOidcClientId(delegatedAuthConfig)).rejects.toThrow(OidcError.DynamicRegistrationInvalid);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
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
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
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
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
435
test/unit-tests/utils/permalinks/Permalinks-test.ts
Normal file
435
test/unit-tests/utils/permalinks/Permalinks-test.ts
Normal file
|
@ -0,0 +1,435 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { 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 { 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 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: Room["roomId"]) => {
|
||||
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: Room["roomId"]) => {
|
||||
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),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
134
test/unit-tests/utils/pillify-test.tsx
Normal file
134
test/unit-tests/utils/pillify-test.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { render } from "jest-matrix-react";
|
||||
import { MatrixEvent, ConditionKind, EventType, PushRuleActionName, Room, TweakName } from "matrix-js-sdk/src/matrix";
|
||||
import { mocked } from "jest-mock";
|
||||
|
||||
import { pillifyLinks } from "../../src/utils/pillify";
|
||||
import { stubClient } from "../test-utils";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../src/utils/DMRoomMap";
|
||||
|
||||
describe("pillify", () => {
|
||||
const roomId = "!room:id";
|
||||
const event = new MatrixEvent({
|
||||
room_id: roomId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
body: "@room",
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
const cli = MatrixClientPeg.safeGet();
|
||||
const room = new Room(roomId, cli, cli.getUserId()!);
|
||||
room.currentState.mayTriggerNotifOfType = jest.fn().mockReturnValue(true);
|
||||
(cli.getRoom as jest.Mock).mockReturnValue(room);
|
||||
cli.pushRules!.global = {
|
||||
override: [
|
||||
{
|
||||
rule_id: ".m.rule.roomnotif",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventMatch,
|
||||
key: "content.body",
|
||||
pattern: "@room",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
rule_id: ".m.rule.is_room_mention",
|
||||
default: true,
|
||||
enabled: true,
|
||||
conditions: [
|
||||
{
|
||||
kind: ConditionKind.EventPropertyIs,
|
||||
key: "content.m\\.mentions.room",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
kind: ConditionKind.SenderNotificationPermission,
|
||||
key: "room",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
PushRuleActionName.Notify,
|
||||
{
|
||||
set_tweak: TweakName.Highlight,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
DMRoomMap.makeShared(cli);
|
||||
});
|
||||
|
||||
it("should do nothing for empty element", () => {
|
||||
const { container } = render(<div />);
|
||||
const originalHtml = container.outerHTML;
|
||||
const containers: Element[] = [];
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
expect(containers).toHaveLength(0);
|
||||
expect(container.outerHTML).toEqual(originalHtml);
|
||||
});
|
||||
|
||||
it("should pillify @room", () => {
|
||||
const { container } = render(<div>@room</div>);
|
||||
const containers: Element[] = [];
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
expect(containers).toHaveLength(1);
|
||||
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
|
||||
});
|
||||
|
||||
it("should pillify @room in an intentional mentions world", () => {
|
||||
mocked(MatrixClientPeg.safeGet().supportsIntentionalMentions).mockReturnValue(true);
|
||||
const { container } = render(<div>@room</div>);
|
||||
const containers: Element[] = [];
|
||||
pillifyLinks(
|
||||
MatrixClientPeg.safeGet(),
|
||||
[container],
|
||||
new MatrixEvent({
|
||||
room_id: roomId,
|
||||
type: EventType.RoomMessage,
|
||||
content: {
|
||||
"body": "@room",
|
||||
"m.mentions": {
|
||||
room: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
containers,
|
||||
);
|
||||
expect(containers).toHaveLength(1);
|
||||
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
|
||||
});
|
||||
|
||||
it("should not double up pillification on repeated calls", () => {
|
||||
const { container } = render(<div>@room</div>);
|
||||
const containers: Element[] = [];
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
pillifyLinks(MatrixClientPeg.safeGet(), [container], event, containers);
|
||||
expect(containers).toHaveLength(1);
|
||||
expect(container.querySelector(".mx_Pill.mx_AtRoomPill")?.textContent).toBe("!@room");
|
||||
});
|
||||
});
|
57
test/unit-tests/utils/promise-test.ts
Normal file
57
test/unit-tests/utils/promise-test.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*
|
||||
*/
|
||||
|
||||
import { batch } from "../../src/utils/promise.ts";
|
||||
|
||||
describe("promise.ts", () => {
|
||||
describe("batch", () => {
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
it("should batch promises into groups of a given size", async () => {
|
||||
const promises = [() => Promise.resolve(1), () => Promise.resolve(2), () => Promise.resolve(3)];
|
||||
const batchSize = 2;
|
||||
const result = await batch(promises, batchSize);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("should wait for the current batch to finish to request the next one", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
let promise1Called = false;
|
||||
const promise1 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise1Called = true;
|
||||
resolve(1);
|
||||
});
|
||||
let promise2Called = false;
|
||||
const promise2 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise2Called = true;
|
||||
setTimeout(() => {
|
||||
resolve(2);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
let promise3Called = false;
|
||||
const promise3 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise3Called = true;
|
||||
resolve(3);
|
||||
});
|
||||
const batchSize = 2;
|
||||
const batchPromise = batch([promise1, promise2, promise3], batchSize);
|
||||
|
||||
expect(promise1Called).toBe(true);
|
||||
expect(promise2Called).toBe(true);
|
||||
expect(promise3Called).toBe(false);
|
||||
|
||||
jest.advanceTimersByTime(11);
|
||||
expect(await batchPromise).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
98
test/unit-tests/utils/room/canInviteTo-test.ts
Normal file
98
test/unit-tests/utils/room/canInviteTo-test.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../src/settings/UIFeature";
|
||||
import { canInviteTo } from "../../../src/utils/room/canInviteTo";
|
||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("canInviteTo()", () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const makeRoom = (): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
...mockClientMethodsUser(userId),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(true);
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(shouldShowComponent).mockReturnValue(true);
|
||||
});
|
||||
|
||||
describe("when user has permissions to issue an invite for this room", () => {
|
||||
// aka when Room.canInvite is true
|
||||
|
||||
it("should return false when current user membership is not joined", () => {
|
||||
const room = makeRoom();
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Invite);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when UIComponent.InviteUsers customisation hides invite", () => {
|
||||
const room = makeRoom();
|
||||
mocked(shouldShowComponent).mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
|
||||
});
|
||||
|
||||
it("should return true when user can invite and is a room member", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
expect(canInviteTo(room)).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when user does not have permissions to issue an invite for this room", () => {
|
||||
// aka when Room.canInvite is false
|
||||
|
||||
it("should return false when room is a private space", () => {
|
||||
const room = makeRoom();
|
||||
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when room is just a room", () => {
|
||||
const room = makeRoom();
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when room is a public space", () => {
|
||||
const room = makeRoom();
|
||||
// default join rule is public
|
||||
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
|
||||
jest.spyOn(room, "canInvite").mockReturnValue(false);
|
||||
|
||||
expect(canInviteTo(room)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getFunctionalMembers } from "../../../src/utils/room/getFunctionalMembers";
|
||||
import { getJoinedNonFunctionalMembers } from "../../../src/utils/room/getJoinedNonFunctionalMembers";
|
||||
|
||||
jest.mock("../../../src/utils/room/getFunctionalMembers", () => ({
|
||||
getFunctionalMembers: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("getJoinedNonFunctionalMembers", () => {
|
||||
let room: Room;
|
||||
let roomMember1: RoomMember;
|
||||
let roomMember2: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
room = new Room("!room:example.com", {} as unknown as MatrixClient, "@user:example.com");
|
||||
room.getJoinedMembers = jest.fn();
|
||||
|
||||
roomMember1 = new RoomMember(room.roomId, "@user1:example.com");
|
||||
roomMember2 = new RoomMember(room.roomId, "@user2:example.com");
|
||||
});
|
||||
|
||||
describe("if there are no members", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.getJoinedMembers).mockReturnValue([]);
|
||||
mocked(getFunctionalMembers).mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(getJoinedNonFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are only regular room members", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.getJoinedMembers).mockReturnValue([roomMember1, roomMember2]);
|
||||
mocked(getFunctionalMembers).mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("should return the room members", () => {
|
||||
const members = getJoinedNonFunctionalMembers(room);
|
||||
expect(members).toContain(roomMember1);
|
||||
expect(members).toContain(roomMember2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are only functional room members", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.getJoinedMembers).mockReturnValue([]);
|
||||
mocked(getFunctionalMembers).mockReturnValue(["@functional:example.com"]);
|
||||
});
|
||||
|
||||
it("should return an empty list", () => {
|
||||
expect(getJoinedNonFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("if there are some functional room members", () => {
|
||||
beforeEach(() => {
|
||||
mocked(room.getJoinedMembers).mockReturnValue([roomMember1, roomMember2]);
|
||||
mocked(getFunctionalMembers).mockReturnValue([roomMember1.userId]);
|
||||
});
|
||||
|
||||
it("should only return the non-functional members", () => {
|
||||
const members = getJoinedNonFunctionalMembers(room);
|
||||
expect(members).not.toContain(roomMember1);
|
||||
expect(members).toContain(roomMember2);
|
||||
});
|
||||
});
|
||||
});
|
49
test/unit-tests/utils/room/getRoomFunctionalMembers-test.ts
Normal file
49
test/unit-tests/utils/room/getRoomFunctionalMembers-test.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { getFunctionalMembers } from "../../../src/utils/room/getFunctionalMembers";
|
||||
import { createTestClient, mkEvent } from "../../test-utils";
|
||||
|
||||
describe("getRoomFunctionalMembers", () => {
|
||||
const client = createTestClient();
|
||||
const room = new Room("!room:example.com", client, client.getUserId()!);
|
||||
|
||||
it("should return an empty array if no functional members state event exists", () => {
|
||||
expect(getFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return an empty array if functional members state event does not have a service_members field", () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
||||
user: "@user:example.com)",
|
||||
room: room.roomId,
|
||||
skey: "",
|
||||
content: {},
|
||||
}),
|
||||
]);
|
||||
expect(getFunctionalMembers(room)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should return service_members field of the functional users state event", () => {
|
||||
room.currentState.setStateEvents([
|
||||
mkEvent({
|
||||
event: true,
|
||||
type: UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name,
|
||||
user: "@user:example.com)",
|
||||
room: room.roomId,
|
||||
skey: "",
|
||||
content: { service_members: ["@user:example.com"] },
|
||||
}),
|
||||
]);
|
||||
expect(getFunctionalMembers(room)).toEqual(["@user:example.com"]);
|
||||
});
|
||||
});
|
57
test/unit-tests/utils/room/inviteToRoom-test.ts
Normal file
57
test/unit-tests/utils/room/inviteToRoom-test.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { inviteToRoom } from "../../../src/utils/room/inviteToRoom";
|
||||
import { getMockClientWithEventEmitter } from "../../test-utils";
|
||||
|
||||
describe("inviteToRoom()", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const makeRoom = (): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
isGuest: jest.fn(),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// stub
|
||||
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("requires registration when a guest tries to invite to a room", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
jest.spyOn(room.client, "isGuest").mockReturnValue(true);
|
||||
|
||||
inviteToRoom(room);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
|
||||
});
|
||||
|
||||
it("opens the room inviter", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
jest.spyOn(room.client, "isGuest").mockReturnValue(false);
|
||||
|
||||
inviteToRoom(room);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "view_invite", roomId });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import DMRoomMap from "../../../src/utils/DMRoomMap";
|
||||
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../src/utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
|
||||
import { privateShouldBeEncrypted } from "../../../src/utils/rooms";
|
||||
import { mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../test-utils";
|
||||
|
||||
jest.mock("../../../src/utils/rooms", () => ({
|
||||
privateShouldBeEncrypted: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("shouldEncryptRoomWithSingle3rdPartyInvite", () => {
|
||||
let client: MatrixClient;
|
||||
let thirdPartyInviteEvent: MatrixEvent;
|
||||
let roomWithOneThirdPartyInvite: Room;
|
||||
|
||||
beforeAll(() => {
|
||||
client = stubClient();
|
||||
DMRoomMap.makeShared(client);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
roomWithOneThirdPartyInvite = new Room("!room1:example.com", client, client.getSafeUserId());
|
||||
thirdPartyInviteEvent = mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
);
|
||||
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent(client.getSafeUserId(), roomWithOneThirdPartyInvite.roomId),
|
||||
thirdPartyInviteEvent,
|
||||
]);
|
||||
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([roomWithOneThirdPartyInvite.roomId]));
|
||||
});
|
||||
|
||||
describe("when well-known promotes encryption", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return true + invite event for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: true,
|
||||
inviteEvent: thirdPartyInviteEvent,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a non-DM room with one third-party invite", () => {
|
||||
mocked(DMRoomMap.shared().getRoomIds).mockReturnValue(new Set());
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two members", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkRoomMemberJoinEvent("@user2:example.com", roomWithOneThirdPartyInvite.roomId),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return false for a DM room with two third-party invites", () => {
|
||||
roomWithOneThirdPartyInvite.currentState.setStateEvents([
|
||||
mkThirdPartyInviteEvent(
|
||||
client.getSafeUserId(),
|
||||
"user2@example.com",
|
||||
roomWithOneThirdPartyInvite.roomId,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when well-known does not promote encryption", () => {
|
||||
beforeEach(() => {
|
||||
mocked(privateShouldBeEncrypted).mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return false for a DM room with one third-party invite", () => {
|
||||
expect(shouldEncryptRoomWithSingle3rdPartyInvite(roomWithOneThirdPartyInvite)).toEqual({
|
||||
shouldEncrypt: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
146
test/unit-tests/utils/room/tagRoom-test.ts
Normal file
146
test/unit-tests/utils/room/tagRoom-test.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import RoomListActions from "../../../src/actions/RoomListActions";
|
||||
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
|
||||
import { DefaultTagID, TagID } from "../../../src/stores/room-list/models";
|
||||
import RoomListStore from "../../../src/stores/room-list/RoomListStore";
|
||||
import { tagRoom } from "../../../src/utils/room/tagRoom";
|
||||
import { getMockClientWithEventEmitter } from "../../test-utils";
|
||||
|
||||
describe("tagRoom()", () => {
|
||||
const userId = "@alice:server.org";
|
||||
const roomId = "!room:server.org";
|
||||
|
||||
const makeRoom = (tags: TagID[] = []): Room => {
|
||||
const client = getMockClientWithEventEmitter({
|
||||
isGuest: jest.fn(),
|
||||
});
|
||||
const room = new Room(roomId, client, userId);
|
||||
|
||||
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValue(tags);
|
||||
|
||||
return room;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// stub
|
||||
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
|
||||
jest.spyOn(RoomListActions, "tagRoom").mockReturnValue({ action: "mocked_tag_room_action", fn: () => {} });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("does nothing when room tag is not allowed", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.ServerNotice);
|
||||
|
||||
expect(defaultDispatcher.dispatch).not.toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when a room has no tags", () => {
|
||||
it("should tag a room as favourite", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.LowPriority, // remove
|
||||
DefaultTagID.Favourite, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("should tag a room low priority", () => {
|
||||
const room = makeRoom();
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
DefaultTagID.LowPriority, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a room is tagged as favourite", () => {
|
||||
it("should unfavourite a room", () => {
|
||||
const room = makeRoom([DefaultTagID.Favourite]);
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
null, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("should tag a room low priority", () => {
|
||||
const room = makeRoom([DefaultTagID.Favourite]);
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.Favourite, // remove
|
||||
DefaultTagID.LowPriority, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("when a room is tagged as low priority", () => {
|
||||
it("should favourite a room", () => {
|
||||
const room = makeRoom([DefaultTagID.LowPriority]);
|
||||
|
||||
tagRoom(room, DefaultTagID.Favourite);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.LowPriority, // remove
|
||||
DefaultTagID.Favourite, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it("should untag a room low priority", () => {
|
||||
const room = makeRoom([DefaultTagID.LowPriority]);
|
||||
|
||||
tagRoom(room, DefaultTagID.LowPriority);
|
||||
|
||||
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
|
||||
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
|
||||
room.client,
|
||||
room,
|
||||
DefaultTagID.LowPriority, // remove
|
||||
null, // add
|
||||
0,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
71
test/unit-tests/utils/rooms-test.ts
Normal file
71
test/unit-tests/utils/rooms-test.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { privateShouldBeEncrypted } from "../../src/utils/rooms";
|
||||
import { getMockClientWithEventEmitter } from "../test-utils";
|
||||
|
||||
describe("privateShouldBeEncrypted()", () => {
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
getClientWellKnown: jest.fn(),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockClient.getClientWellKnown.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("should return true when there is no e2ee well known", () => {
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return true when default is not set to false", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return true when there is no default property", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
// no default
|
||||
},
|
||||
});
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false when encryption is force disabled", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: true,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false when default encryption setting is false", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
force_disable: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return true when default encryption setting is set to something other than false", () => {
|
||||
mockClient.getClientWellKnown.mockReturnValue({
|
||||
"io.element.e2ee": {
|
||||
default: "test",
|
||||
},
|
||||
});
|
||||
expect(privateShouldBeEncrypted(mockClient)).toEqual(true);
|
||||
});
|
||||
});
|
48
test/unit-tests/utils/sets-test.ts
Normal file
48
test/unit-tests/utils/sets-test.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { setHasDiff } from "../../src/utils/sets";
|
||||
|
||||
describe("sets", () => {
|
||||
describe("setHasDiff", () => {
|
||||
it("should flag true on A length > B length", () => {
|
||||
const a = new Set([1, 2, 3, 4]);
|
||||
const b = new Set([1, 2, 3]);
|
||||
const result = setHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag true on A length < B length", () => {
|
||||
const a = new Set([1, 2, 3]);
|
||||
const b = new Set([1, 2, 3, 4]);
|
||||
const result = setHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag true on element differences", () => {
|
||||
const a = new Set([1, 2, 3]);
|
||||
const b = new Set([4, 5, 6]);
|
||||
const result = setHasDiff(a, b);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should flag false if same but order different", () => {
|
||||
const a = new Set([1, 2, 3]);
|
||||
const b = new Set([3, 1, 2]);
|
||||
const result = setHasDiff(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should flag false if same", () => {
|
||||
const a = new Set([1, 2, 3]);
|
||||
const b = new Set([1, 2, 3]);
|
||||
const result = setHasDiff(a, b);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
188
test/unit-tests/utils/stringOrderField-test.ts
Normal file
188
test/unit-tests/utils/stringOrderField-test.ts
Normal file
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { sortBy } from "lodash";
|
||||
import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField";
|
||||
|
||||
const moveLexicographicallyTest = (
|
||||
orders: Array<string | undefined>,
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
expectedChanges: number,
|
||||
maxLength?: number,
|
||||
): void => {
|
||||
const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
|
||||
|
||||
const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
|
||||
ops.forEach(({ index, order }) => {
|
||||
zipped[index][1] = order;
|
||||
});
|
||||
|
||||
const newOrders = sortBy(zipped, (i) => i[1]);
|
||||
expect(newOrders[toIndex][0]).toBe(fromIndex);
|
||||
expect(ops).toHaveLength(expectedChanges);
|
||||
};
|
||||
|
||||
describe("stringOrderField", () => {
|
||||
describe("midPointsBetweenStrings", () => {
|
||||
it("should work", () => {
|
||||
expect(averageBetweenStrings("!!", "##")).toBe('""');
|
||||
const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
|
||||
expect(midpoints[0]).toBe("a");
|
||||
expect(midpoints[4]).toBe("e");
|
||||
expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
|
||||
});
|
||||
|
||||
it("should return empty array when the request is not possible", () => {
|
||||
expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
|
||||
expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reorderLexicographically", () => {
|
||||
it("should work when moving left", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
|
||||
});
|
||||
|
||||
it("should work when moving right", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
|
||||
});
|
||||
|
||||
it("should work when all orders are undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 4, 1, 2);
|
||||
});
|
||||
|
||||
it("should work when moving to end and all orders are undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 1, 4, 5);
|
||||
});
|
||||
|
||||
it("should work when moving left and some orders are undefined", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", undefined, undefined, undefined], 5, 2, 1);
|
||||
|
||||
moveLexicographicallyTest(["a", "a", "e", undefined, undefined, undefined], 5, 1, 2);
|
||||
});
|
||||
|
||||
it("should work moving to the start when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 2, 0, 1);
|
||||
});
|
||||
|
||||
it("should work moving to the end when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 1, 3, 4);
|
||||
});
|
||||
|
||||
it("should work moving left when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 4, 1, 2);
|
||||
});
|
||||
|
||||
it("should work moving right when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 1, 2, 3);
|
||||
});
|
||||
|
||||
it("should work moving more right when all is undefined", () => {
|
||||
moveLexicographicallyTest(
|
||||
[undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||
1,
|
||||
4,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving left when right is undefined", () => {
|
||||
moveLexicographicallyTest(["20", undefined, undefined, undefined, undefined, undefined], 4, 2, 2);
|
||||
});
|
||||
|
||||
it("should work moving right when right is undefined", () => {
|
||||
moveLexicographicallyTest(
|
||||
["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||
1,
|
||||
4,
|
||||
4,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving left when right is defined", () => {
|
||||
moveLexicographicallyTest(["10", "20", "30", "40", undefined, undefined], 3, 1, 1);
|
||||
});
|
||||
|
||||
it("should work moving right when right is defined", () => {
|
||||
moveLexicographicallyTest(["10", "20", "30", "40", "50", undefined], 1, 3, 1);
|
||||
});
|
||||
|
||||
it("should work moving left when all is defined", () => {
|
||||
moveLexicographicallyTest(["11", "13", "15", "17", "19"], 2, 1, 1);
|
||||
});
|
||||
|
||||
it("should work moving right when all is defined", () => {
|
||||
moveLexicographicallyTest(["11", "13", "15", "17", "19"], 1, 2, 1);
|
||||
});
|
||||
|
||||
it("should work moving left into no left space", () => {
|
||||
moveLexicographicallyTest(["11", "12", "13", "14", "19"], 3, 1, 2, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(0),
|
||||
// Target
|
||||
DEFAULT_ALPHABET.charAt(1),
|
||||
DEFAULT_ALPHABET.charAt(2),
|
||||
DEFAULT_ALPHABET.charAt(3),
|
||||
DEFAULT_ALPHABET.charAt(4),
|
||||
DEFAULT_ALPHABET.charAt(5),
|
||||
],
|
||||
5,
|
||||
1,
|
||||
5,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving right into no right space", () => {
|
||||
moveLexicographicallyTest(["15", "16", "17", "18", "19"], 1, 3, 3, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||
],
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving right into no left space", () => {
|
||||
moveLexicographicallyTest(["11", "12", "13", "14", "15", "16", undefined], 1, 3, 3);
|
||||
|
||||
moveLexicographicallyTest(["0", "1", "2", "3", "4", "5"], 1, 3, 3, 1);
|
||||
});
|
||||
|
||||
it("should work moving left into no right space", () => {
|
||||
moveLexicographicallyTest(["15", "16", "17", "18", "19"], 4, 3, 4, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||
],
|
||||
4,
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
148
test/unit-tests/utils/threepids-test.ts
Normal file
148
test/unit-tests/utils/threepids-test.ts
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Mocked } from "jest-mock";
|
||||
import { IIdentityServerProvider, 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([]);
|
||||
});
|
||||
});
|
||||
});
|
76
test/unit-tests/utils/tooltipify-test.tsx
Normal file
76
test/unit-tests/utils/tooltipify-test.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { act, render } from "jest-matrix-react";
|
||||
|
||||
import { tooltipifyLinks } from "../../src/utils/tooltipify";
|
||||
import PlatformPeg from "../../src/PlatformPeg";
|
||||
import BasePlatform from "../../src/BasePlatform";
|
||||
|
||||
describe("tooltipify", () => {
|
||||
jest.spyOn(PlatformPeg, "get").mockReturnValue({ needsUrlTooltips: () => true } as unknown as BasePlatform);
|
||||
|
||||
it("does nothing for empty element", () => {
|
||||
const { container: root } = render(<div />);
|
||||
const originalHtml = root.outerHTML;
|
||||
const containers: Element[] = [];
|
||||
tooltipifyLinks([root], [], containers);
|
||||
expect(containers).toHaveLength(0);
|
||||
expect(root.outerHTML).toEqual(originalHtml);
|
||||
});
|
||||
|
||||
it("wraps single anchor", () => {
|
||||
const { container: root } = render(
|
||||
<div>
|
||||
<a href="/foo">click</a>
|
||||
</div>,
|
||||
);
|
||||
const containers: Element[] = [];
|
||||
tooltipifyLinks([root], [], containers);
|
||||
expect(containers).toHaveLength(1);
|
||||
const anchor = root.querySelector("a");
|
||||
expect(anchor?.getAttribute("href")).toEqual("/foo");
|
||||
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");
|
||||
expect(tooltip).toBeDefined();
|
||||
});
|
||||
|
||||
it("ignores node", () => {
|
||||
const { container: root } = render(
|
||||
<div>
|
||||
<a href="/foo">click</a>
|
||||
</div>,
|
||||
);
|
||||
const originalHtml = root.outerHTML;
|
||||
const containers: Element[] = [];
|
||||
tooltipifyLinks([root], [root.children[0]], containers);
|
||||
expect(containers).toHaveLength(0);
|
||||
expect(root.outerHTML).toEqual(originalHtml);
|
||||
});
|
||||
|
||||
it("does not re-wrap if called multiple times", async () => {
|
||||
const { container: root, unmount } = render(
|
||||
<div>
|
||||
<a href="/foo">click</a>
|
||||
</div>,
|
||||
);
|
||||
const containers: Element[] = [];
|
||||
tooltipifyLinks([root], [], containers);
|
||||
tooltipifyLinks([root], [], containers);
|
||||
tooltipifyLinks([root], [], containers);
|
||||
tooltipifyLinks([root], [], containers);
|
||||
expect(containers).toHaveLength(1);
|
||||
const anchor = root.querySelector("a");
|
||||
expect(anchor?.getAttribute("href")).toEqual("/foo");
|
||||
const tooltip = anchor!.querySelector(".mx_TextWithTooltip_target");
|
||||
expect(tooltip).toBeDefined();
|
||||
await act(async () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
35
test/unit-tests/utils/validate/numberInRange-test.ts
Normal file
35
test/unit-tests/utils/validate/numberInRange-test.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { validateNumberInRange } from "../../../src/utils/validate";
|
||||
|
||||
describe("validateNumberInRange", () => {
|
||||
const min = 1;
|
||||
const max = 10;
|
||||
it("returns false when value is a not a number", () => {
|
||||
expect(validateNumberInRange(min, max)("test" as unknown as number)).toEqual(false);
|
||||
});
|
||||
it("returns false when value is undefined", () => {
|
||||
expect(validateNumberInRange(min, max)(undefined)).toEqual(false);
|
||||
});
|
||||
it("returns false when value is NaN", () => {
|
||||
expect(validateNumberInRange(min, max)(NaN)).toEqual(false);
|
||||
});
|
||||
it("returns true when value is equal to min", () => {
|
||||
expect(validateNumberInRange(min, max)(min)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is equal to max", () => {
|
||||
expect(validateNumberInRange(min, max)(max)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is an int in range", () => {
|
||||
expect(validateNumberInRange(min, max)(2)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is a float in range", () => {
|
||||
expect(validateNumberInRange(min, max)(2.2)).toEqual(true);
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue