mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Bump eslint-plugin-matrix-org to enable @typescript-eslint/consistent-type-imports rule * Re-lint after merge
1083 lines
42 KiB
TypeScript
1083 lines
42 KiB
TypeScript
/*
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import { type IContent, type MatrixClient } from "../../../src";
|
|
import { type Room } from "../../../src/models/room";
|
|
import { MatrixEvent } from "../../../src/models/event";
|
|
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event";
|
|
import {
|
|
DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
MSC3089TreeSpace,
|
|
TreePermissions,
|
|
} from "../../../src/models/MSC3089TreeSpace";
|
|
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
|
import { MatrixError } from "../../../src/http-api";
|
|
import { KnownMembership } from "../../../src/@types/membership";
|
|
import { type EncryptedFile } from "../../../src/@types/media";
|
|
|
|
describe("MSC3089TreeSpace", () => {
|
|
let client: MatrixClient;
|
|
let room: any;
|
|
let tree: MSC3089TreeSpace;
|
|
const roomId = "!tree:localhost";
|
|
const targetUser = "@target:example.org";
|
|
|
|
let powerLevels: MatrixEvent;
|
|
|
|
beforeEach(() => {
|
|
// TODO: Use utility functions to create test rooms and clients
|
|
client = <MatrixClient>{
|
|
getRoom: (fetchRoomId: string) => {
|
|
if (fetchRoomId === roomId) {
|
|
return room;
|
|
} else {
|
|
throw new Error("Unexpected fetch for unknown room");
|
|
}
|
|
},
|
|
};
|
|
room = <Room>{
|
|
currentState: {
|
|
getStateEvents: (evType: EventType, stateKey: string) => {
|
|
if (evType === EventType.RoomPowerLevels && stateKey === "") {
|
|
return powerLevels;
|
|
} else {
|
|
throw new Error("Accessed unexpected state event type or key");
|
|
}
|
|
},
|
|
},
|
|
};
|
|
tree = new MSC3089TreeSpace(client, roomId);
|
|
makePowerLevels(DEFAULT_TREE_POWER_LEVELS_TEMPLATE);
|
|
});
|
|
|
|
function makePowerLevels(content: any) {
|
|
powerLevels = new MatrixEvent({
|
|
type: EventType.RoomPowerLevels,
|
|
state_key: "",
|
|
sender: "@creator:localhost",
|
|
event_id: "$powerlevels",
|
|
room_id: roomId,
|
|
content: content,
|
|
});
|
|
}
|
|
|
|
it("should populate the room reference", () => {
|
|
expect(tree.room).toBe(room);
|
|
});
|
|
|
|
it("should proxy the ID member to room ID", () => {
|
|
expect(tree.id).toEqual(tree.roomId);
|
|
expect(tree.id).toEqual(roomId);
|
|
});
|
|
|
|
it("should support setting the name of the space", async () => {
|
|
const newName = "NEW NAME";
|
|
const fn = jest
|
|
.fn()
|
|
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
|
expect(stateRoomId).toEqual(roomId);
|
|
expect(eventType).toEqual(EventType.RoomName);
|
|
expect(stateKey).toEqual("");
|
|
expect(content).toMatchObject({ name: newName });
|
|
return Promise.resolve();
|
|
});
|
|
client.sendStateEvent = fn;
|
|
await tree.setName(newName);
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should support inviting users to the space", async () => {
|
|
const target = targetUser;
|
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
|
expect(inviteRoomId).toEqual(roomId);
|
|
expect(userId).toEqual(target);
|
|
return Promise.resolve();
|
|
});
|
|
client.invite = fn;
|
|
await tree.invite(target, false);
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should retry invites to the space", async () => {
|
|
const target = targetUser;
|
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
|
expect(inviteRoomId).toEqual(roomId);
|
|
expect(userId).toEqual(target);
|
|
if (fn.mock.calls.length === 1) return Promise.reject(new Error("Sample Failure"));
|
|
return Promise.resolve();
|
|
});
|
|
client.invite = fn;
|
|
await tree.invite(target, false);
|
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("should not retry invite permission errors", async () => {
|
|
const target = targetUser;
|
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
|
expect(inviteRoomId).toEqual(roomId);
|
|
expect(userId).toEqual(target);
|
|
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
|
});
|
|
client.invite = fn;
|
|
|
|
await expect(tree.invite(target, false)).rejects.toThrow("MatrixError: Sample Failure");
|
|
|
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should invite to subspaces", async () => {
|
|
const target = targetUser;
|
|
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
|
expect(inviteRoomId).toEqual(roomId);
|
|
expect(userId).toEqual(target);
|
|
return Promise.resolve();
|
|
});
|
|
client.invite = fn;
|
|
tree.getDirectories = () => [
|
|
// Bare minimum overrides. We proxy to our mock function manually so we can
|
|
// count the calls, not to ensure accuracy. The invite function behaving correctly
|
|
// is covered by another test.
|
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
|
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
|
];
|
|
|
|
await tree.invite(target, true);
|
|
expect(fn).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
|
makePowerLevels(pls);
|
|
const fn = jest
|
|
.fn()
|
|
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
|
expect(stateRoomId).toEqual(roomId);
|
|
expect(eventType).toEqual(EventType.RoomPowerLevels);
|
|
expect(stateKey).toEqual("");
|
|
expect(content).toMatchObject({
|
|
...pls,
|
|
users: {
|
|
[targetUser]: expectedPl,
|
|
},
|
|
});
|
|
|
|
// Store new power levels so the `getPermissions()` test passes
|
|
makePowerLevels(content);
|
|
|
|
return Promise.resolve();
|
|
});
|
|
client.sendStateEvent = fn;
|
|
await tree.setPermissions(targetUser, role);
|
|
expect(fn.mock.calls.length).toBe(1);
|
|
|
|
const finalPermissions = tree.getPermissions(targetUser);
|
|
expect(finalPermissions).toEqual(role);
|
|
}
|
|
|
|
it("should support setting Viewer permissions", () => {
|
|
return evaluatePowerLevels(
|
|
{
|
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
users_default: 1024,
|
|
events_default: 1025,
|
|
events: {
|
|
[EventType.RoomPowerLevels]: 1026,
|
|
},
|
|
},
|
|
TreePermissions.Viewer,
|
|
1024,
|
|
);
|
|
});
|
|
|
|
it("should support setting Editor permissions", () => {
|
|
return evaluatePowerLevels(
|
|
{
|
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
users_default: 1024,
|
|
events_default: 1025,
|
|
events: {
|
|
[EventType.RoomPowerLevels]: 1026,
|
|
},
|
|
},
|
|
TreePermissions.Editor,
|
|
1025,
|
|
);
|
|
});
|
|
|
|
it("should support setting Owner permissions", () => {
|
|
return evaluatePowerLevels(
|
|
{
|
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
users_default: 1024,
|
|
events_default: 1025,
|
|
events: {
|
|
[EventType.RoomPowerLevels]: 1026,
|
|
},
|
|
},
|
|
TreePermissions.Owner,
|
|
1026,
|
|
);
|
|
});
|
|
|
|
it("should support demoting permissions", () => {
|
|
return evaluatePowerLevels(
|
|
{
|
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
users_default: 1024,
|
|
events_default: 1025,
|
|
events: {
|
|
[EventType.RoomPowerLevels]: 1026,
|
|
},
|
|
users: {
|
|
[targetUser]: 2222,
|
|
},
|
|
},
|
|
TreePermissions.Viewer,
|
|
1024,
|
|
);
|
|
});
|
|
|
|
it("should support promoting permissions", () => {
|
|
return evaluatePowerLevels(
|
|
{
|
|
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
|
users_default: 1024,
|
|
events_default: 1025,
|
|
events: {
|
|
[EventType.RoomPowerLevels]: 1026,
|
|
},
|
|
users: {
|
|
[targetUser]: 5,
|
|
},
|
|
},
|
|
TreePermissions.Editor,
|
|
1025,
|
|
);
|
|
});
|
|
|
|
it("should support defaults: Viewer", () => {
|
|
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
|
|
});
|
|
|
|
it("should support defaults: Editor", () => {
|
|
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
|
|
});
|
|
|
|
it("should support defaults: Owner", () => {
|
|
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
|
});
|
|
|
|
it("should create subdirectories", async () => {
|
|
const subspaceName = "subdirectory";
|
|
const subspaceId = "!subspace:localhost";
|
|
const domain = "domain.example.com";
|
|
client.getRoom = (roomId: string) => {
|
|
if (roomId === tree.roomId) {
|
|
return tree.room;
|
|
} else if (roomId === subspaceId) {
|
|
return {} as Room; // we don't need anything important off of this
|
|
} else {
|
|
throw new Error("Unexpected getRoom call");
|
|
}
|
|
};
|
|
client.getDomain = () => domain;
|
|
const createFn = jest.fn().mockImplementation(async (name: string) => {
|
|
expect(name).toEqual(subspaceName);
|
|
return new MSC3089TreeSpace(client, subspaceId);
|
|
});
|
|
const sendStateFn = jest
|
|
.fn()
|
|
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
|
expect([tree.roomId, subspaceId]).toContain(roomId);
|
|
|
|
let expectedType: string;
|
|
let expectedStateKey: string;
|
|
if (roomId === subspaceId) {
|
|
expectedType = EventType.SpaceParent;
|
|
expectedStateKey = tree.roomId;
|
|
} else {
|
|
expectedType = EventType.SpaceChild;
|
|
expectedStateKey = subspaceId;
|
|
}
|
|
expect(eventType).toEqual(expectedType);
|
|
expect(stateKey).toEqual(expectedStateKey);
|
|
expect(content).toMatchObject({ via: [domain] });
|
|
|
|
// return value not used
|
|
});
|
|
client.unstableCreateFileTree = createFn;
|
|
client.sendStateEvent = sendStateFn;
|
|
|
|
const directory = await tree.createDirectory(subspaceName);
|
|
expect(directory).toBeDefined();
|
|
expect(directory).not.toBeNull();
|
|
expect(directory).not.toBe(tree);
|
|
expect(directory.roomId).toEqual(subspaceId);
|
|
expect(createFn).toHaveBeenCalledTimes(1);
|
|
expect(sendStateFn).toHaveBeenCalledTimes(2);
|
|
|
|
const content = expect.objectContaining({ via: [domain] });
|
|
expect(sendStateFn).toHaveBeenCalledWith(subspaceId, EventType.SpaceParent, content, tree.roomId);
|
|
expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId);
|
|
});
|
|
|
|
it("should find subdirectories", () => {
|
|
const firstChildRoom = "!one:example.org";
|
|
const secondChildRoom = "!two:example.org";
|
|
const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories
|
|
room.currentState = {
|
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
|
expect(eventType).toEqual(EventType.SpaceChild);
|
|
expect(stateKey).toBeUndefined();
|
|
return [
|
|
// Partial implementations of Room
|
|
{ getStateKey: () => firstChildRoom },
|
|
{ getStateKey: () => secondChildRoom },
|
|
{ getStateKey: () => thirdChildRoom },
|
|
];
|
|
},
|
|
};
|
|
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
|
|
|
|
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
|
if (roomId === thirdChildRoom) {
|
|
throw new Error("Mock not-a-space room case called (expected)");
|
|
}
|
|
expect([firstChildRoom, secondChildRoom]).toContain(roomId);
|
|
return new MSC3089TreeSpace(client, roomId);
|
|
});
|
|
client.unstableGetFileTreeSpace = getFn;
|
|
|
|
const subdirectories = tree.getDirectories();
|
|
expect(subdirectories).toBeDefined();
|
|
expect(subdirectories.length).toBe(2);
|
|
expect(subdirectories[0].roomId).toBe(firstChildRoom);
|
|
expect(subdirectories[1].roomId).toBe(secondChildRoom);
|
|
expect(getFn).toHaveBeenCalledTimes(3);
|
|
expect(getFn).toHaveBeenCalledWith(firstChildRoom);
|
|
expect(getFn).toHaveBeenCalledWith(secondChildRoom);
|
|
expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried
|
|
});
|
|
|
|
it("should find specific directories", () => {
|
|
client.getRoom = () => ({}) as Room; // to appease the TreeSpace constructor
|
|
|
|
// Only mocking used API
|
|
const firstSubdirectory = { roomId: "!first:example.org" } as any as MSC3089TreeSpace;
|
|
const searchedSubdirectory = { roomId: "!find_me:example.org" } as any as MSC3089TreeSpace;
|
|
const thirdSubdirectory = { roomId: "!third:example.org" } as any as MSC3089TreeSpace;
|
|
tree.getDirectories = () => [firstSubdirectory, searchedSubdirectory, thirdSubdirectory];
|
|
|
|
let result = tree.getDirectory(searchedSubdirectory.roomId);
|
|
expect(result).toBe(searchedSubdirectory);
|
|
|
|
result = tree.getDirectory("not a subdirectory");
|
|
expect(result).toBeFalsy();
|
|
});
|
|
|
|
it("should be able to delete itself", async () => {
|
|
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
|
|
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
|
|
|
|
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
|
|
const subdir2 = { delete: delete2 } as any as MSC3089TreeSpace; // mock tested bits
|
|
|
|
const joinMemberId = "@join:example.org";
|
|
const knockMemberId = "@knock:example.org";
|
|
const inviteMemberId = "@invite:example.org";
|
|
const leaveMemberId = "@leave:example.org";
|
|
const banMemberId = "@ban:example.org";
|
|
const selfUserId = "@self:example.org";
|
|
|
|
tree.getDirectories = () => [subdir1, subdir2];
|
|
room.currentState = {
|
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
|
expect(eventType).toEqual(EventType.RoomMember);
|
|
expect(stateKey).toBeUndefined();
|
|
return [
|
|
// Partial implementations
|
|
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => joinMemberId },
|
|
{ getContent: () => ({ membership: KnownMembership.Knock }), getStateKey: () => knockMemberId },
|
|
{ getContent: () => ({ membership: KnownMembership.Invite }), getStateKey: () => inviteMemberId },
|
|
{ getContent: () => ({ membership: KnownMembership.Leave }), getStateKey: () => leaveMemberId },
|
|
{ getContent: () => ({ membership: KnownMembership.Ban }), getStateKey: () => banMemberId },
|
|
|
|
// ensure we don't kick ourselves
|
|
{ getContent: () => ({ membership: KnownMembership.Join }), getStateKey: () => selfUserId },
|
|
];
|
|
},
|
|
};
|
|
|
|
// These two functions are tested by input expectations, so no expectations in the function bodies
|
|
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
|
|
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
|
|
client.kick = kickFn;
|
|
client.leave = leaveFn;
|
|
client.getUserId = () => selfUserId;
|
|
|
|
await tree.delete();
|
|
|
|
expect(delete1).toHaveBeenCalledTimes(1);
|
|
expect(delete2).toHaveBeenCalledTimes(1);
|
|
expect(kickFn).toHaveBeenCalledTimes(3);
|
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, joinMemberId, expect.any(String));
|
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, knockMemberId, expect.any(String));
|
|
expect(kickFn).toHaveBeenCalledWith(tree.roomId, inviteMemberId, expect.any(String));
|
|
expect(leaveFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
describe("get and set order", () => {
|
|
// Danger: these are partial implementations for testing purposes only
|
|
|
|
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
|
let childState: { [roomId: string]: any[] } = {};
|
|
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
|
let parentState: any[] = [];
|
|
let parentRoom: Room;
|
|
let childTrees: MSC3089TreeSpace[];
|
|
let rooms: { [roomId: string]: Room };
|
|
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
|
|
const staticDomain = "static.example.org";
|
|
|
|
function addSubspace(roomId: string, createTs?: number, order?: string) {
|
|
const content: IContent = {
|
|
via: [staticDomain],
|
|
};
|
|
if (order) content["order"] = order;
|
|
parentState.push({
|
|
getType: () => EventType.SpaceChild,
|
|
getStateKey: () => roomId,
|
|
getContent: () => content,
|
|
});
|
|
childState[roomId] = [
|
|
{
|
|
getType: () => EventType.SpaceParent,
|
|
getStateKey: () => tree.roomId,
|
|
getContent: () => ({
|
|
via: [staticDomain],
|
|
}),
|
|
},
|
|
];
|
|
if (createTs) {
|
|
childState[roomId].push({
|
|
getType: () => EventType.RoomCreate,
|
|
getStateKey: () => "",
|
|
getContent: () => ({}),
|
|
getTs: () => createTs,
|
|
});
|
|
}
|
|
rooms[roomId] = makeMockChildRoom(roomId);
|
|
childTrees.push(new MSC3089TreeSpace(client, roomId));
|
|
}
|
|
|
|
function expectOrder(childRoomId: string, order: number) {
|
|
const child = childTrees.find((c) => c.roomId === childRoomId);
|
|
expect(child).toBeDefined();
|
|
expect(child!.getOrder()).toEqual(order);
|
|
}
|
|
|
|
function makeMockChildRoom(roomId: string): Room {
|
|
return {
|
|
currentState: {
|
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
|
expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType);
|
|
if (eventType === EventType.RoomCreate) {
|
|
expect(stateKey).toEqual("");
|
|
return childState[roomId].find((e) => e.getType() === EventType.RoomCreate);
|
|
} else {
|
|
expect(stateKey).toBeUndefined();
|
|
return childState[roomId].filter((e) => e.getType() === eventType);
|
|
}
|
|
},
|
|
},
|
|
} as Room; // partial
|
|
}
|
|
|
|
beforeEach(() => {
|
|
childState = {};
|
|
parentState = [];
|
|
parentRoom = {
|
|
...tree.room,
|
|
roomId: tree.roomId,
|
|
currentState: {
|
|
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
|
expect([EventType.SpaceChild, EventType.RoomCreate, EventType.SpaceParent]).toContain(
|
|
eventType,
|
|
);
|
|
|
|
if (eventType === EventType.RoomCreate) {
|
|
expect(stateKey).toEqual("");
|
|
return parentState.filter((e) => e.getType() === EventType.RoomCreate)[0];
|
|
} else {
|
|
if (stateKey !== undefined) {
|
|
expect(Object.keys(rooms)).toContain(stateKey);
|
|
expect(stateKey).not.toEqual(tree.roomId);
|
|
return parentState.find(
|
|
(e) => e.getType() === eventType && e.getStateKey() === stateKey,
|
|
);
|
|
} // else fine
|
|
return parentState.filter((e) => e.getType() === eventType);
|
|
}
|
|
},
|
|
},
|
|
} as Room;
|
|
childTrees = [];
|
|
rooms = {};
|
|
rooms[tree.roomId] = parentRoom;
|
|
(<any>tree).room = parentRoom; // override readonly
|
|
client.getRoom = (r) => rooms[r ?? ""];
|
|
|
|
clientSendStateFn = jest
|
|
.fn()
|
|
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
|
expect(roomId).toEqual(tree.roomId);
|
|
expect(eventType).toEqual(EventType.SpaceChild);
|
|
expect(content).toMatchObject(
|
|
expect.objectContaining({
|
|
via: expect.any(Array),
|
|
order: expect.any(String),
|
|
}),
|
|
);
|
|
expect(Object.keys(rooms)).toContain(stateKey);
|
|
expect(stateKey).not.toEqual(tree.roomId);
|
|
|
|
const stateEvent = parentState.find(
|
|
(e) => e.getType() === eventType && e.getStateKey() === stateKey,
|
|
);
|
|
expect(stateEvent).toBeDefined();
|
|
stateEvent.getContent = () => content;
|
|
|
|
return Promise.resolve(); // return value not used
|
|
});
|
|
client.sendStateEvent = clientSendStateFn;
|
|
});
|
|
|
|
it("should know when something is top level", () => {
|
|
const a = "!a:example.org";
|
|
addSubspace(a);
|
|
|
|
expect(tree.isTopLevel).toBe(true);
|
|
expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine
|
|
});
|
|
|
|
it("should return -1 for top level spaces", () => {
|
|
// The tree is what we've defined as top level, so it should work
|
|
expect(tree.getOrder()).toEqual(-1);
|
|
});
|
|
|
|
it("should throw when setting an order at the top level space", async () => {
|
|
// The tree is what we've defined as top level, so it should work
|
|
await expect(tree.setOrder(2)).rejects.toThrow("Cannot set order of top level spaces currently");
|
|
});
|
|
|
|
it("should return a stable order for unordered children", () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(c, 3);
|
|
addSubspace(b, 2);
|
|
addSubspace(a, 1);
|
|
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 1);
|
|
expectOrder(c, 2);
|
|
});
|
|
|
|
it("should return a stable order for ordered children", () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(a, 1, "Z");
|
|
addSubspace(b, 2, "Y");
|
|
addSubspace(c, 3, "X");
|
|
|
|
expectOrder(c, 0);
|
|
expectOrder(b, 1);
|
|
expectOrder(a, 2);
|
|
});
|
|
|
|
it("should return a stable order for partially ordered children", () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
const d = "!d:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(a, 1);
|
|
addSubspace(b, 2);
|
|
addSubspace(c, 3, "Y");
|
|
addSubspace(d, 4, "X");
|
|
|
|
expectOrder(d, 0);
|
|
expectOrder(c, 1);
|
|
expectOrder(b, 3); // note order diff due to room ID comparison expectation
|
|
expectOrder(a, 2);
|
|
});
|
|
|
|
it("should return a stable order if the create event timestamps are the same", () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(c, 3);
|
|
addSubspace(b, 3); // same as C
|
|
addSubspace(a, 3); // same as C
|
|
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 1);
|
|
expectOrder(c, 2);
|
|
});
|
|
|
|
it("should return a stable order if there are no known create events", () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(c);
|
|
addSubspace(b);
|
|
addSubspace(a);
|
|
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 1);
|
|
expectOrder(c, 2);
|
|
});
|
|
|
|
// XXX: These tests rely on `getOrder()` re-calculating and not caching values.
|
|
|
|
it("should allow reordering within unordered children", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(c, 3);
|
|
addSubspace(b, 2);
|
|
addSubspace(a, 1);
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeA = childTrees.find((c) => c.roomId === a);
|
|
expect(treeA).toBeDefined();
|
|
await treeA!.setOrder(1);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
|
|
// Because of how the reordering works (maintain stable ordering before moving), we end up calling this
|
|
// function twice for the same room.
|
|
order: DEFAULT_ALPHABET[0],
|
|
}),
|
|
a,
|
|
);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: DEFAULT_ALPHABET[1],
|
|
}),
|
|
b,
|
|
);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: DEFAULT_ALPHABET[2],
|
|
}),
|
|
a,
|
|
);
|
|
expectOrder(a, 1);
|
|
expectOrder(b, 0);
|
|
expectOrder(c, 2);
|
|
});
|
|
|
|
it("should allow reordering within ordered children", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(c, 3, "Z");
|
|
addSubspace(b, 2, "X");
|
|
addSubspace(a, 1, "V");
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeA = childTrees.find((c) => c.roomId === a);
|
|
expect(treeA).toBeDefined();
|
|
await treeA!.setOrder(1);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "Y",
|
|
}),
|
|
a,
|
|
);
|
|
expectOrder(a, 1);
|
|
expectOrder(b, 0);
|
|
expectOrder(c, 2);
|
|
});
|
|
|
|
it("should allow reordering within partially ordered children", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
const d = "!d:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(a, 1);
|
|
addSubspace(b, 2);
|
|
addSubspace(c, 3, "Y");
|
|
addSubspace(d, 4, "W");
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeA = childTrees.find((c) => c.roomId === a);
|
|
expect(treeA).toBeDefined();
|
|
await treeA!.setOrder(2);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "Z",
|
|
}),
|
|
a,
|
|
);
|
|
expectOrder(a, 2);
|
|
expectOrder(b, 3);
|
|
expectOrder(c, 1);
|
|
expectOrder(d, 0);
|
|
});
|
|
|
|
it("should support moving upwards", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
const d = "!d:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(d, 4, "Z");
|
|
addSubspace(c, 3, "X");
|
|
addSubspace(b, 2, "V");
|
|
addSubspace(a, 1, "T");
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeB = childTrees.find((c) => c.roomId === b);
|
|
expect(treeB).toBeDefined();
|
|
await treeB!.setOrder(2);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "Y",
|
|
}),
|
|
b,
|
|
);
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 2);
|
|
expectOrder(c, 1);
|
|
expectOrder(d, 3);
|
|
});
|
|
|
|
it("should support moving downwards", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
const d = "!d:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(d, 4, "Z");
|
|
addSubspace(c, 3, "X");
|
|
addSubspace(b, 2, "V");
|
|
addSubspace(a, 1, "T");
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeC = childTrees.find((ch) => ch.roomId === c);
|
|
expect(treeC).toBeDefined();
|
|
await treeC!.setOrder(1);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "U",
|
|
}),
|
|
c,
|
|
);
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 2);
|
|
expectOrder(c, 1);
|
|
expectOrder(d, 3);
|
|
});
|
|
|
|
it("should support moving over the partial ordering boundary", async () => {
|
|
const a = "!a:example.org";
|
|
const b = "!b:example.org";
|
|
const c = "!c:example.org";
|
|
const d = "!d:example.org";
|
|
|
|
// Add in reverse order to make sure it gets ordered correctly
|
|
addSubspace(d, 4);
|
|
addSubspace(c, 3);
|
|
addSubspace(b, 2, "V");
|
|
addSubspace(a, 1, "T");
|
|
|
|
// Order of this state is validated by other tests.
|
|
|
|
const treeB = childTrees.find((ch) => ch.roomId === b);
|
|
expect(treeB).toBeDefined();
|
|
await treeB!.setOrder(2);
|
|
|
|
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "W",
|
|
}),
|
|
c,
|
|
);
|
|
expect(clientSendStateFn).toHaveBeenCalledWith(
|
|
tree.roomId,
|
|
EventType.SpaceChild,
|
|
expect.objectContaining({
|
|
via: [staticDomain], // should retain domain independent of client.getDomain()
|
|
order: "X",
|
|
}),
|
|
b,
|
|
);
|
|
expectOrder(a, 0);
|
|
expectOrder(b, 2);
|
|
expectOrder(c, 1);
|
|
expectOrder(d, 3);
|
|
});
|
|
});
|
|
|
|
it("should upload files", async () => {
|
|
const mxc = "mxc://example.org/file";
|
|
const fileInfo = {
|
|
mimetype: "text/plain",
|
|
// other fields as required by encryption, but ignored here
|
|
} as unknown as EncryptedFile;
|
|
const fileEventId = "$file";
|
|
const fileName = "My File.txt";
|
|
const fileContents = "This is a test file";
|
|
|
|
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
|
expect(contents.length).toEqual(fileContents.length);
|
|
expect(opts).toMatchObject({
|
|
includeFilename: false,
|
|
});
|
|
return Promise.resolve({ content_uri: mxc });
|
|
});
|
|
client.uploadContent = uploadFn;
|
|
|
|
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
|
expect(roomId).toEqual(tree.roomId);
|
|
expect(contents).toMatchObject({
|
|
msgtype: MsgType.File,
|
|
body: fileName,
|
|
url: mxc,
|
|
file: fileInfo,
|
|
metadata: true, // additional content from test
|
|
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
|
});
|
|
|
|
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
|
});
|
|
client.sendMessage = sendMsgFn;
|
|
|
|
const sendStateFn = jest
|
|
.fn()
|
|
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
|
expect(roomId).toEqual(tree.roomId);
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toEqual(fileEventId);
|
|
expect(content).toMatchObject({
|
|
active: true,
|
|
name: fileName,
|
|
});
|
|
|
|
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
|
});
|
|
client.sendStateEvent = sendStateFn;
|
|
|
|
const buf = Buffer.from(fileContents);
|
|
|
|
// We clone the file info just to make sure it doesn't get mutated for the test.
|
|
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { metadata: true });
|
|
expect(result).toMatchObject({ event_id: fileEventId });
|
|
|
|
expect(uploadFn).toHaveBeenCalledTimes(1);
|
|
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
|
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should upload file versions", async () => {
|
|
const mxc = "mxc://example.org/file";
|
|
const fileInfo = {
|
|
mimetype: "text/plain",
|
|
// other fields as required by encryption, but ignored here
|
|
} as unknown as EncryptedFile;
|
|
const fileEventId = "$file";
|
|
const fileName = "My File.txt";
|
|
const fileContents = "This is a test file";
|
|
|
|
const uploadFn = jest.fn().mockImplementation((contents: Buffer, opts: any) => {
|
|
expect(contents.length).toEqual(fileContents.length);
|
|
expect(opts).toMatchObject({
|
|
includeFilename: false,
|
|
});
|
|
return Promise.resolve({ content_uri: mxc });
|
|
});
|
|
client.uploadContent = uploadFn;
|
|
|
|
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
|
expect(roomId).toEqual(tree.roomId);
|
|
const content = {
|
|
msgtype: MsgType.File,
|
|
body: fileName,
|
|
url: mxc,
|
|
file: fileInfo,
|
|
};
|
|
expect(contents).toMatchObject({
|
|
...content,
|
|
"m.new_content": content,
|
|
[UNSTABLE_MSC3089_LEAF.unstable!]: {}, // test to ensure we're definitely using unstable
|
|
});
|
|
|
|
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
|
});
|
|
client.sendMessage = sendMsgFn;
|
|
|
|
const sendStateFn = jest
|
|
.fn()
|
|
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
|
expect(roomId).toEqual(tree.roomId);
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toEqual(fileEventId);
|
|
expect(content).toMatchObject({
|
|
active: true,
|
|
name: fileName,
|
|
});
|
|
|
|
return Promise.resolve({ event_id: "wrong" }); // return value shouldn't be used
|
|
});
|
|
client.sendStateEvent = sendStateFn;
|
|
|
|
const buf = Buffer.from(fileContents);
|
|
|
|
// We clone the file info just to make sure it doesn't get mutated for the test.
|
|
const result = await tree.createFile(fileName, buf, Object.assign({}, fileInfo), { "m.new_content": true });
|
|
expect(result).toMatchObject({ event_id: fileEventId });
|
|
|
|
expect(uploadFn).toHaveBeenCalledTimes(1);
|
|
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
|
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("should support getting files", () => {
|
|
const fileEventId = "$file";
|
|
const fileEvent = { forTest: true }; // MatrixEvent mock
|
|
room.currentState = {
|
|
getStateEvents: (eventType: string, stateKey?: string) => {
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toEqual(fileEventId);
|
|
return fileEvent;
|
|
},
|
|
};
|
|
|
|
const file = tree.getFile(fileEventId);
|
|
expect(file).toBeDefined();
|
|
expect(file!.indexEvent).toBe(fileEvent);
|
|
});
|
|
|
|
it("should return falsy for unknown files", () => {
|
|
const fileEventId = "$file";
|
|
room.currentState = {
|
|
getStateEvents: (eventType: string, stateKey?: string): MatrixEvent[] | MatrixEvent | null => {
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toEqual(fileEventId);
|
|
return null;
|
|
},
|
|
};
|
|
|
|
const file = tree.getFile(fileEventId);
|
|
expect(file).toBeFalsy();
|
|
});
|
|
|
|
it("should list files", () => {
|
|
const firstFile = { getContent: () => ({ active: true }) };
|
|
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
|
room.currentState = {
|
|
getStateEvents: (eventType: string, stateKey?: string) => {
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toBeUndefined();
|
|
return [firstFile, secondFile];
|
|
},
|
|
};
|
|
|
|
const files = tree.listFiles();
|
|
expect(files).toBeDefined();
|
|
expect(files.length).toEqual(1);
|
|
expect(files[0].indexEvent).toBe(firstFile);
|
|
});
|
|
|
|
it("should list all files", () => {
|
|
const firstFile = { getContent: () => ({ active: true }) };
|
|
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
|
room.currentState = {
|
|
getStateEvents: (eventType: string, stateKey?: string) => {
|
|
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
|
expect(stateKey).toBeUndefined();
|
|
return [firstFile, secondFile];
|
|
},
|
|
};
|
|
|
|
const files = tree.listAllFiles();
|
|
expect(files).toBeDefined();
|
|
expect(files.length).toEqual(2);
|
|
expect(files[0].indexEvent).toBe(firstFile);
|
|
expect(files[1].indexEvent).toBe(secondFile);
|
|
});
|
|
});
|