Skip to content

Commit d556466

Browse files
committed
Some tests for EventIndexer
1 parent 21cbc85 commit d556466

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
Copyright 2025 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { type Mocked } from "jest-mock";
18+
import {
19+
Direction,
20+
type MatrixClient,
21+
type IEvent,
22+
MatrixEvent,
23+
type Room,
24+
ClientEvent,
25+
SyncState,
26+
} from "matrix-js-sdk/src/matrix";
27+
28+
import EventIndex from "../../../src/indexing/EventIndex.ts";
29+
import { emitPromise, getMockClientWithEventEmitter, mockClientMethodsRooms, mockPlatformPeg } from "../../test-utils";
30+
import type BaseEventIndexManager from "../../../src/indexing/BaseEventIndexManager.ts";
31+
import { type ICrawlerCheckpoint } from "../../../src/indexing/BaseEventIndexManager.ts";
32+
import SettingsStore from "../../../src/settings/SettingsStore.ts";
33+
34+
afterEach(() => {
35+
jest.restoreAllMocks();
36+
});
37+
38+
describe("EventIndex", () => {
39+
it("crawls through the loaded checkpoints", async () => {
40+
const mockIndexingManager = {
41+
loadCheckpoints: jest.fn(),
42+
removeCrawlerCheckpoint: jest.fn(),
43+
isEventIndexEmpty: jest.fn().mockResolvedValue(false),
44+
} as any as Mocked<BaseEventIndexManager>;
45+
mockPlatformPeg({ getEventIndexingManager: () => mockIndexingManager });
46+
47+
const room1 = { roomId: "!room1:id" } as any as Room;
48+
const room2 = { roomId: "!room2:id" } as any as Room;
49+
const mockClient = getMockClientWithEventEmitter({
50+
getEventMapper: () => (obj: Partial<IEvent>) => new MatrixEvent(obj),
51+
createMessagesRequest: jest.fn(),
52+
...mockClientMethodsRooms([room1, room2]),
53+
});
54+
55+
jest.spyOn(SettingsStore, "getValueAt").mockImplementation((_level, settingName): any => {
56+
if (settingName === "crawlerSleepTime") return 0;
57+
return undefined;
58+
});
59+
60+
mockIndexingManager.loadCheckpoints.mockResolvedValue([
61+
{ roomId: "!room1:id", token: "token1", direction: Direction.Backward } as ICrawlerCheckpoint,
62+
{ roomId: "!room2:id", token: "token2", direction: Direction.Forward } as ICrawlerCheckpoint,
63+
]);
64+
65+
const indexer = new EventIndex();
66+
await indexer.init();
67+
let changedCheckpointPromise = emitPromise(indexer, "changedCheckpoint") as Promise<Room>;
68+
69+
indexer.startCrawler();
70+
71+
// Mock out the /messags request, and wait for the crawler to hit the first room
72+
const mock1 = mockCreateMessagesRequest(mockClient);
73+
let changedCheckpoint = await changedCheckpointPromise;
74+
expect(changedCheckpoint.roomId).toEqual("!room1:id");
75+
76+
await mock1.called;
77+
expect(mockClient.createMessagesRequest).toHaveBeenCalledWith("!room1:id", "token1", 100, "b");
78+
79+
// Continue, and wait for the crawler to hit the second room
80+
changedCheckpointPromise = emitPromise(indexer, "changedCheckpoint") as Promise<Room>;
81+
mock1.resolve({ chunk: [] });
82+
changedCheckpoint = await changedCheckpointPromise;
83+
expect(changedCheckpoint.roomId).toEqual("!room2:id");
84+
85+
// Mock out the /messages request again, and wait for it to be called
86+
const mock2 = mockCreateMessagesRequest(mockClient);
87+
await mock2.called;
88+
expect(mockClient.createMessagesRequest).toHaveBeenCalledWith("!room2:id", "token2", 100, "f");
89+
});
90+
91+
it("adds checkpoints for the encrypted rooms after the first sync", async () => {
92+
const mockIndexingManager = {
93+
loadCheckpoints: jest.fn().mockResolvedValue([]),
94+
isEventIndexEmpty: jest.fn().mockResolvedValue(true),
95+
addCrawlerCheckpoint: jest.fn(),
96+
removeCrawlerCheckpoint: jest.fn(),
97+
commitLiveEvents: jest.fn(),
98+
} as any as Mocked<BaseEventIndexManager>;
99+
mockPlatformPeg({ getEventIndexingManager: () => mockIndexingManager });
100+
101+
const room1 = {
102+
roomId: "!room1:id",
103+
getLiveTimeline: () => ({
104+
getPaginationToken: () => "token1",
105+
}),
106+
} as any as Room;
107+
const room2 = {
108+
roomId: "!room2:id",
109+
getLiveTimeline: () => ({
110+
getPaginationToken: () => "token2",
111+
}),
112+
} as any as Room;
113+
const mockCrypto = {
114+
isEncryptionEnabledInRoom: jest.fn().mockResolvedValue(true),
115+
};
116+
const mockClient = getMockClientWithEventEmitter({
117+
getEventMapper: () => (obj: Partial<IEvent>) => new MatrixEvent(obj),
118+
createMessagesRequest: jest.fn(),
119+
getCrypto: () => mockCrypto as any,
120+
...mockClientMethodsRooms([room1, room2]),
121+
});
122+
123+
const commitLiveEventsCalled = Promise.withResolvers<void>();
124+
mockIndexingManager.commitLiveEvents.mockImplementation(async () => {
125+
commitLiveEventsCalled.resolve();
126+
});
127+
128+
const indexer = new EventIndex();
129+
await indexer.init();
130+
131+
// During the first sync, some events are added to the index, meaning that `isEventIndexEmpty` will now be false.
132+
mockIndexingManager.isEventIndexEmpty.mockResolvedValue(false);
133+
134+
// The first sync completes:
135+
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, null, {});
136+
137+
// Wait for `commitLiveEvents` to be called, by which time the checkpoints should have been added.
138+
await commitLiveEventsCalled.promise;
139+
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledTimes(4);
140+
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
141+
roomId: "!room1:id",
142+
token: "token1",
143+
direction: Direction.Backward,
144+
fullCrawl: true,
145+
});
146+
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
147+
roomId: "!room1:id",
148+
token: "token1",
149+
direction: Direction.Forward,
150+
});
151+
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
152+
roomId: "!room2:id",
153+
token: "token2",
154+
direction: Direction.Backward,
155+
fullCrawl: true,
156+
});
157+
expect(mockIndexingManager.addCrawlerCheckpoint).toHaveBeenCalledWith({
158+
roomId: "!room2:id",
159+
token: "token2",
160+
direction: Direction.Forward,
161+
});
162+
});
163+
});
164+
165+
/**
166+
* Mock out the `createMessagesRequest` method on the client, with an implementation that will block until a resolver is called.
167+
*
168+
* @returns An object with the following properties:
169+
* * `called`: A promise that resolves when `createMessagesRequest` is called.
170+
* * `resolve`: A function that can be called to allow `createMessagesRequest` to complete.
171+
*/
172+
function mockCreateMessagesRequest(mockClient: Mocked<MatrixClient>): {
173+
called: Promise<void>;
174+
resolve: (result: any) => void;
175+
} {
176+
const messagesCalledPromise = Promise.withResolvers<void>();
177+
const messagesResultPromise = Promise.withResolvers();
178+
mockClient.createMessagesRequest.mockImplementationOnce(() => {
179+
messagesCalledPromise.resolve();
180+
return messagesResultPromise.promise as any;
181+
});
182+
return {
183+
called: messagesCalledPromise.promise,
184+
resolve: messagesResultPromise.resolve,
185+
};
186+
}

0 commit comments

Comments
 (0)