From 1b00b5d6d687dd38648613a1845a967095267bdf Mon Sep 17 00:00:00 2001
From: Richard van der Hoff <richard@matrix.org>
Date: Thu, 3 Jul 2025 17:26:33 +0100
Subject: [PATCH] test: Implement `E2EOTKClaimResponder` class

... allowing clients under test to claim OTKs uploaded by other devices.
---
 spec/test-utils/E2EKeyReceiver.ts       | 14 +++++
 spec/test-utils/E2EOTKClaimResponder.ts | 73 +++++++++++++++++++++++++
 2 files changed, 87 insertions(+)
 create mode 100644 spec/test-utils/E2EOTKClaimResponder.ts

diff --git a/spec/test-utils/E2EKeyReceiver.ts b/spec/test-utils/E2EKeyReceiver.ts
index b01ae7ece..1df130a6a 100644
--- a/spec/test-utils/E2EKeyReceiver.ts
+++ b/spec/test-utils/E2EKeyReceiver.ts
@@ -234,4 +234,18 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
         await this.oneTimeKeysPromise;
         return this.oneTimeKeys;
     }
+
+    /**
+     * If no one-time keys have yet been uploaded, return `null`.
+     * Otherwise, pop a key from the uploaded list.
+     */
+    public getOneTimeKey(): [string, IOneTimeKey] | null {
+        const keys = Object.entries(this.oneTimeKeys);
+        if (keys.length == 0) {
+            return null;
+        }
+        const [otkId, otk] = keys[0];
+        delete this.oneTimeKeys[otkId];
+        return [otkId, otk];
+    }
 }
diff --git a/spec/test-utils/E2EOTKClaimResponder.ts b/spec/test-utils/E2EOTKClaimResponder.ts
new file mode 100644
index 000000000..4471be585
--- /dev/null
+++ b/spec/test-utils/E2EOTKClaimResponder.ts
@@ -0,0 +1,73 @@
+/*
+Copyright 2025 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 fetchMock from "fetch-mock-jest";
+
+import { MapWithDefault } from "../../src/utils";
+import { type E2EKeyReceiver } from "./E2EKeyReceiver";
+import { type IClaimKeysRequest } from "../../src";
+
+/**
+ * An object which intercepts `/keys/claim` fetches via fetch-mock.
+ */
+export class E2EOTKClaimResponder {
+    private e2eKeyReceiversByUserByDevice = new MapWithDefault<string, Map<string, E2EKeyReceiver>>(() => new Map());
+
+    /**
+     * Construct a new E2EOTKClaimResponder.
+     *
+     * It will immediately register an intercept of `/keys/claim` requests for the given homeserverUrl.
+     * Only /claim requests made to this server will be intercepted: this allows a single test to use more than one
+     * client and have the keys collected separately.
+     *
+     * @param homeserverUrl - the Homeserver Url of the client under test.
+     */
+    public constructor(homeserverUrl: string) {
+        const listener = (url: string, options: RequestInit) => this.onKeyClaimRequest(options);
+        fetchMock.post(new URL("/_matrix/client/v3/keys/claim", homeserverUrl).toString(), listener);
+    }
+
+    private onKeyClaimRequest(options: RequestInit) {
+        const content = JSON.parse(options.body as string) as IClaimKeysRequest;
+        const response = {
+            one_time_keys: {} as { [userId: string]: any },
+        };
+        for (const [userId, devices] of Object.entries(content["one_time_keys"])) {
+            for (const deviceId of Object.keys(devices)) {
+                const e2eKeyReceiver = this.e2eKeyReceiversByUserByDevice.get(userId)?.get(deviceId);
+                const otk = e2eKeyReceiver?.getOneTimeKey();
+                if (otk) {
+                    const [keyId, key] = otk;
+                    response.one_time_keys[userId] ??= {};
+                    response.one_time_keys[userId][deviceId] = {
+                        [keyId]: key,
+                    };
+                }
+            }
+        }
+        return response;
+    }
+
+    /**
+     * Add an E2EKeyReceiver to poll for uploaded keys
+     *
+     * When the `/keys/claim` request is received, a OTK will be removed from the `E2EKeyReceiver` and
+     * added to the response.
+     */
+    public addKeyReceiver(userId: string, deviceId: string, e2eKeyReceiver: E2EKeyReceiver) {
+        this.e2eKeyReceiversByUserByDevice.getOrCreate(userId).set(deviceId, e2eKeyReceiver);
+    }
+}
-- 
GitLab