Skip to content

Commit

Permalink
Fetch capabilities in the background (#4246)
Browse files Browse the repository at this point in the history
* Fetch capabilities in the background

& keep them up to date

* Add missed await

* Replace some more runAllTimers

and round down the wait time for sanity

* Remove double comment

* Typo

* Add a method back that will fetch capabilities if they're not already there

* Add tests

* Catch exception here too

* Add test for room version code
  • Loading branch information
dbkr authored Jun 19, 2024
1 parent c70aa33 commit 819fc75
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 99 deletions.
4 changes: 2 additions & 2 deletions spec/integ/crypto/megolm-backup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,7 +796,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe

const result = await aliceCrypto.checkKeyBackupAndEnable();
expect(result).toBeTruthy();
jest.runAllTimers();
jest.advanceTimersByTime(10 * 60 * 1000);
await failurePromise;

// Fix the endpoint to do successful uploads
Expand Down Expand Up @@ -829,7 +829,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});

// run the timers, which will make the backup loop redo the request
await jest.runAllTimersAsync();
await jest.advanceTimersByTimeAsync(10 * 60 * 1000);
await successPromise;
await allKeysUploadedPromise;
});
Expand Down
107 changes: 99 additions & 8 deletions spec/integ/matrix-client-methods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1293,18 +1293,109 @@ describe("MatrixClient", function () {
});

describe("getCapabilities", () => {
it("should cache by default", async () => {
it("should return cached capabilities if present", async () => {
const capsObject = {
"m.change_password": false,
};

httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: {
"m.change_password": false,
},
capabilities: capsObject,
});

client.startClient();
await httpBackend!.flushAllExpected();

expect(await client.getCapabilities()).toEqual(capsObject);
});

it("should fetch capabilities if cache not present", async () => {
const capsObject = {
"m.change_password": false,
};

httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});

const capsPromise = client.getCapabilities();
await httpBackend!.flushAllExpected();

expect(await capsPromise).toEqual(capsObject);
});
});

describe("getCachedCapabilities", () => {
it("should return cached capabilities or undefined", async () => {
const capsObject = {
"m.change_password": false,
};

httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});

expect(client.getCachedCapabilities()).toBeUndefined();

client.startClient();

await httpBackend!.flushAllExpected();

expect(client.getCachedCapabilities()).toEqual(capsObject);
});
});

describe("fetchCapabilities", () => {
const capsObject = {
"m.change_password": false,
};

beforeEach(() => {
httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: capsObject,
});
const prom = httpBackend.flushAllExpected();
const capabilities1 = await client.getCapabilities();
const capabilities2 = await client.getCapabilities();
});

afterEach(() => {
jest.useRealTimers();
});

it("should always fetch capabilities and then cache", async () => {
const prom = client.fetchCapabilities();
await httpBackend.flushAllExpected();
const caps = await prom;

expect(caps).toEqual(capsObject);
});

it("should write-through the cache", async () => {
httpBackend!.when("GET", "/versions").respond(200, {});
httpBackend!.when("GET", "/pushrules").respond(200, {});
httpBackend!.when("POST", "/filter").respond(200, { filter_id: "a filter id" });

client.startClient();
await httpBackend!.flushAllExpected();

expect(client.getCachedCapabilities()).toEqual(capsObject);

const newCapsObject = {
"m.change_password": true,
};

httpBackend.when("GET", "/capabilities").respond(200, {
capabilities: newCapsObject,
});

const prom = client.fetchCapabilities();
await httpBackend.flushAllExpected();
await prom;

expect(capabilities1).toStrictEqual(capabilities2);
expect(client.getCachedCapabilities()).toEqual(newCapsObject);
});
});

Expand Down
9 changes: 5 additions & 4 deletions spec/integ/matrix-client-syncing-errors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,21 @@ describe("MatrixClient syncing errors", () => {

await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
jest.advanceTimersByTime(60 * 1000); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});

it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.get("end:capabilities", {})
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
Expand Down
2 changes: 1 addition & 1 deletion spec/test-utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ export const mockClientMethodsEvents = () => ({
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockReturnValue({}),
getCachedCapabilities: jest.fn().mockReturnValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
});
2 changes: 1 addition & 1 deletion spec/unit/rendezvous/rendezvous.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function makeMockClient(opts: {
},
};
},
getCapabilities() {
getCachedCapabilities() {
return opts.msc3882r0Only
? {}
: {
Expand Down
43 changes: 43 additions & 0 deletions spec/unit/room.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4034,4 +4034,47 @@ describe("Room", function () {
expect(room.getLastThread()).toBe(thread2);
});
});

describe("getRecommendedVersion", () => {
it("returns the server's recommended version from capabilities", async () => {
const client = new TestClient(userA).client;
client.getCapabilities = jest.fn().mockReturnValue({
["m.room_versions"]: {
default: "1",
available: ["1", "2"],
},
});
const room = new Room(roomId, client, userA);
expect(await room.getRecommendedVersion()).toEqual({
version: "1",
needsUpgrade: false,
urgent: false,
});
});

it("force-refreshes versions to make sure an upgrade is necessary", async () => {
const client = new TestClient(userA).client;
client.getCapabilities = jest.fn().mockReturnValue({
["m.room_versions"]: {
default: "5",
available: ["5"],
},
});

client.fetchCapabilities = jest.fn().mockResolvedValue({
["m.room_versions"]: {
default: "1",
available: ["1"],
},
});

const room = new Room(roomId, client, userA);
expect(await room.getRecommendedVersion()).toEqual({
version: "1",
needsUpgrade: false,
urgent: false,
});
expect(client.fetchCapabilities).toHaveBeenCalled();
});
});
});
Loading

0 comments on commit 819fc75

Please sign in to comment.