1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

MatrixClient.setAccountData: await remote echo. (#4695)

* Rewrite `deleteAccountData` test

use fetch-mock rather than whatever this was

* `MatrixClient.setAccountData`: await remote echo

Wait for the echo to come back from the server before we assume the account
data has been successfully set

* Update integration tests

Fix up the integ tests which call `setAccountData` and now need a sync
response.

* Address review comment
This commit is contained in:
Richard van der Hoff
2025-02-10 17:35:08 +01:00
committed by GitHub
parent 30d9b0518f
commit c537a361fb
6 changed files with 303 additions and 80 deletions

View File

@ -47,6 +47,7 @@ import {
ClientPrefix,
ConditionKind,
ContentHelpers,
createClient,
Direction,
EventTimeline,
EventTimelineSet,
@ -81,6 +82,8 @@ import { KnownMembership } from "../../src/@types/membership";
import { type RoomMessageEventContent } from "../../src/@types/events";
import { mockOpenIdConfiguration } from "../test-utils/oidc.ts";
import { type CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { SyncResponder } from "../test-utils/SyncResponder.ts";
import { mockInitialApiRequests } from "../test-utils/mockEndpoints.ts";
jest.useFakeTimers();
@ -125,6 +128,18 @@ type WrappedRoom = Room & {
_state: Map<string, any>;
};
/** A list of methods to run after the current test */
const afterTestHooks: (() => Promise<void> | void)[] = [];
afterEach(async () => {
fetchMock.reset();
jest.restoreAllMocks();
for (const hook of afterTestHooks) {
await hook();
}
afterTestHooks.length = 0;
});
describe("convertQueryDictToMap", () => {
it("returns an empty map when dict is undefined", () => {
expect(convertQueryDictToMap(undefined)).toEqual(new Map());
@ -165,6 +180,11 @@ describe("MatrixClient", function () {
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server";
/**
* @deprecated this is hard to use correctly; better to create a regular client with {@link createClient}
* and use fetch-mock.
*/
let client: MatrixClient;
let store: Store;
let scheduler: MatrixScheduler;
@ -2259,7 +2279,7 @@ describe("MatrixClient", function () {
});
describe("checkTurnServers", () => {
beforeAll(() => {
beforeEach(() => {
mocked(supportsMatrixCall).mockReturnValue(true);
});
@ -2659,10 +2679,164 @@ describe("MatrixClient", function () {
});
});
describe("delete account data", () => {
afterEach(() => {
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockRestore();
describe("setAccountData", () => {
const TEST_HOMESERVER_URL = "https://alice-server.com";
/** Create and start a MatrixClient, connected to the `TEST_HOMESERVER_URL` */
async function setUpClient(): Promise<MatrixClient> {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
mockInitialApiRequests(TEST_HOMESERVER_URL, userId);
const client = createClient({ baseUrl: TEST_HOMESERVER_URL, userId });
await client.startClient();
// Remember to stop the client again, to stop it spamming logs and HTTP requests
afterTestHooks.push(() => client.stopClient());
return client;
}
it("falls back to raw request if called before the client is started", async () => {
// GIVEN a bunch of setup
const client = createClient({ baseUrl: TEST_HOMESERVER_URL, userId });
const eventType = "im.vector.test";
const content = { a: 1 };
const testresponse = { test: 1 };
// ... including an expected REST request
const url = new URL(
`/_matrix/client/v3/user/${encodeURIComponent(client.getSafeUserId())}/account_data/${eventType}`,
TEST_HOMESERVER_URL,
).toString();
fetchMock.put({ url, name: "put-account-data" }, testresponse);
// suppress the expected warning on the console
jest.spyOn(console, "warn").mockImplementation();
// WHEN we call `setAccountData` ...
const result = await client.setAccountData(eventType, content);
// THEN, method should have returned the right thing
expect(result).toEqual(testresponse);
// and the REST call should have happened, and had the correct content
const lastCall = fetchMock.lastCall("put-account-data");
expect(lastCall).toBeDefined();
expect(lastCall?.[1]?.body).toEqual(JSON.stringify(content));
// and a warning should have been logged
// eslint-disable-next-line no-console
expect(console.warn).toHaveBeenCalledWith(
expect.stringContaining("Calling `setAccountData` before the client is started"),
);
});
it("makes a request to the server, and waits for the /sync response", async () => {
// GIVEN a bunch of setup
const syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
const client = await setUpClient();
const eventType = "im.vector.test";
const content = { a: 1 };
const testresponse = { test: 1 };
// ... including an expected REST request
const url = new URL(
`/_matrix/client/v3/user/${encodeURIComponent(client.getSafeUserId())}/account_data/${eventType}`,
TEST_HOMESERVER_URL,
).toString();
fetchMock.put({ url, name: "put-account-data" }, testresponse);
// WHEN we call `setAccountData` ...
const setProm = client.setAccountData(eventType, content);
// THEN, the REST call should have happened, and had the correct content
const lastCall = fetchMock.lastCall("put-account-data");
expect(lastCall).toBeDefined();
expect(lastCall?.[1]?.body).toEqual(JSON.stringify(content));
// Even after waiting a bit, the method should not yet have returned
await jest.advanceTimersByTimeAsync(10);
let finished = false;
setProm.finally(() => (finished = true));
expect(finished).toBeFalsy();
// ... and `getAccountData` still returns the wrong thing
expect(client.getAccountData(eventType)).not.toBeDefined();
// WHEN the update arrives over /sync
const content2 = { a: 2 };
syncResponder.sendOrQueueSyncResponse({
account_data: { events: [{ type: eventType, content: content2 }] },
});
// THEN the method should complete, and `getAccountData` returns the new data
expect(await setProm).toEqual(testresponse);
expect(client.getAccountData(eventType)?.event).toEqual({ type: eventType, content: content2 });
});
it("does nothing if the data matches what is there", async () => {
// GIVEN a running matrix client ...
const syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
const client = await setUpClient();
// ... which has previously received the account data over /sync
const eventType = "im.vector.test";
const content = { a: 1, b: 2 };
// Keys in a different order, to check that doesn't matter.
const syncedContent = { b: 2, a: 1 };
syncResponder.sendOrQueueSyncResponse({
account_data: { events: [{ type: eventType, content: syncedContent }] },
});
await jest.advanceTimersByTimeAsync(1);
// Check that getAccountData is ready
expect(client.getAccountData(eventType)?.event).toEqual({ type: eventType, content: syncedContent });
// WHEN we call `setAccountData` ...
await client.setAccountData(eventType, content);
// THEN there should be no REST call
expect(fetchMock.calls(/account_data/).length).toEqual(0);
});
});
describe("delete account data", () => {
const TEST_HOMESERVER_URL = "https://alice-server.com";
/** Create and start a MatrixClient, connected to the `TEST_HOMESERVER_URL` */
async function setUpClient(versionsResponse: object = { versions: ["1"] }): Promise<MatrixClient> {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
fetchMock.getOnce(new URL("/_matrix/client/versions", TEST_HOMESERVER_URL).toString(), versionsResponse, {
overwriteRoutes: true,
});
fetchMock.getOnce(
new URL("/_matrix/client/v3/pushrules/", TEST_HOMESERVER_URL).toString(),
{},
{ overwriteRoutes: true },
);
fetchMock.postOnce(
new URL(`/_matrix/client/v3/user/${encodeURIComponent(userId)}/filter`, TEST_HOMESERVER_URL).toString(),
{ filter_id: "fid" },
{ overwriteRoutes: true },
);
const client = createClient({ baseUrl: TEST_HOMESERVER_URL, userId });
await client.startClient();
// Remember to stop the client again, to stop it spamming logs and HTTP requests
afterTestHooks.push(() => client.stopClient());
return client;
}
it("makes correct request when deletion is supported by server in unstable versions", async () => {
const eventType = "im.vector.test";
const versionsResponse = {
@ -2671,17 +2845,17 @@ describe("MatrixClient", function () {
"org.matrix.msc3391": true,
},
};
const requestSpy = jest.spyOn(client.http, "authedRequest").mockResolvedValue(versionsResponse);
const unstablePrefix = "/_matrix/client/unstable/org.matrix.msc3391";
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
const client = await setUpClient(versionsResponse);
const url = new URL(
`/_matrix/client/unstable/org.matrix.msc3391/user/${encodeURIComponent(userId)}/account_data/${eventType}`,
TEST_HOMESERVER_URL,
).toString();
fetchMock.delete({ url, name: "delete-data" }, {});
// populate version support
await client.getVersions();
await client.deleteAccountData(eventType);
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, {
prefix: unstablePrefix,
});
expect(fetchMock.calls("delete-data").length).toEqual(1);
});
it("makes correct request when deletion is supported by server based on matrix version", async () => {
@ -2690,34 +2864,43 @@ describe("MatrixClient", function () {
// so mock the support map to fake stable support
const stableSupportedDeletionMap = new Map();
stableSupportedDeletionMap.set(featureUtils.Feature.AccountDataDeletion, featureUtils.ServerSupport.Stable);
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockResolvedValue(new Map());
const requestSpy = jest.spyOn(client.http, "authedRequest").mockImplementation(() => Promise.resolve());
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
jest.spyOn(featureUtils, "buildFeatureSupportMap").mockResolvedValue(stableSupportedDeletionMap);
const client = await setUpClient();
const url = new URL(
`/_matrix/client/v3/user/${encodeURIComponent(userId)}/account_data/${eventType}`,
TEST_HOMESERVER_URL,
).toString();
fetchMock.delete({ url, name: "delete-data" }, {});
// populate version support
await client.getVersions();
await client.deleteAccountData(eventType);
expect(requestSpy).toHaveBeenCalledWith(Method.Delete, path, undefined, undefined, undefined);
expect(fetchMock.calls("delete-data").length).toEqual(1);
});
it("makes correct request when deletion is not supported by server", async () => {
const eventType = "im.vector.test";
const versionsResponse = {
versions: ["1"],
unstable_features: {
"org.matrix.msc3391": false,
},
};
const requestSpy = jest.spyOn(client.http, "authedRequest").mockResolvedValue(versionsResponse);
const path = `/user/${encodeURIComponent(userId)}/account_data/${eventType}`;
// populate version support
await client.getVersions();
await client.deleteAccountData(eventType);
const syncResponder = new SyncResponder(TEST_HOMESERVER_URL);
const client = await setUpClient();
const url = new URL(
`/_matrix/client/v3/user/${encodeURIComponent(userId)}/account_data/${eventType}`,
TEST_HOMESERVER_URL,
).toString();
fetchMock.put({ url, name: "put-account-data" }, {});
const setProm = client.deleteAccountData(eventType);
syncResponder.sendOrQueueSyncResponse({
account_data: { events: [{ type: eventType, content: {} }] },
});
await setProm;
// account data updated with empty content
expect(requestSpy).toHaveBeenCalledWith(Method.Put, path, undefined, {});
const lastCall = fetchMock.lastCall("put-account-data");
expect(lastCall).toBeDefined();
expect(lastCall?.[1]?.body).toEqual("{}");
});
});