diff --git a/playwright/services.ts b/playwright/services.ts index b480cbc405..1e2501dc2d 100644 --- a/playwright/services.ts +++ b/playwright/services.ts @@ -136,5 +136,6 @@ export const test = base.extend<{}, Services>({ await logger.testStarted(testInfo); await use(context); await logger.testFinished(testInfo); + await homeserver.onTestFinished(testInfo); }, }); diff --git a/playwright/testcontainers/HomeserverContainer.ts b/playwright/testcontainers/HomeserverContainer.ts index 09eea7da77..73c9882418 100644 --- a/playwright/testcontainers/HomeserverContainer.ts +++ b/playwright/testcontainers/HomeserverContainer.ts @@ -6,17 +6,17 @@ Please see LICENSE files in the repository root for full details. */ import { AbstractStartedContainer, GenericContainer } from "testcontainers"; -import { APIRequestContext } from "@playwright/test"; +import { APIRequestContext, TestInfo } from "@playwright/test"; -import { StartedSynapseContainer } from "./synapse.ts"; import { HomeserverInstance } from "../plugins/homeserver"; export interface HomeserverContainer extends GenericContainer { withConfigField(key: string, value: any): this; withConfig(config: Partial): this; - start(): Promise; + start(): Promise; } export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { setRequest(request: APIRequestContext): void; + onTestFinished(testInfo: TestInfo): Promise; } diff --git a/playwright/testcontainers/dendrite.ts b/playwright/testcontainers/dendrite.ts index 629ea70c65..517b0f7a64 100644 --- a/playwright/testcontainers/dendrite.ts +++ b/playwright/testcontainers/dendrite.ts @@ -235,7 +235,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon return this; } - public override async start(): Promise { + public override async start(): Promise { this.withCopyContentToContainer([ { target: "/etc/dendrite/dendrite.yaml", @@ -244,8 +244,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon ]); const container = await super.start(); - // Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it - return new StartedSynapseContainer( + return new StartedDendriteContainer( container, `http://${container.getHost()}:${container.getMappedPort(8008)}`, this.config.client_api.registration_shared_secret, @@ -258,3 +257,6 @@ export class PineconeContainer extends DendriteContainer { super("matrixdotorg/dendrite-demo-pinecone:main", "/usr/bin/dendrite-demo-pinecone"); } } + +// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it +export class StartedDendriteContainer extends StartedSynapseContainer {} diff --git a/playwright/testcontainers/synapse.ts b/playwright/testcontainers/synapse.ts index 5111a6f0a6..27249b7595 100644 --- a/playwright/testcontainers/synapse.ts +++ b/playwright/testcontainers/synapse.ts @@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details. */ import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; -import { APIRequestContext } from "@playwright/test"; +import { APIRequestContext, TestInfo } from "@playwright/test"; import crypto from "node:crypto"; import * as YAML from "yaml"; import { set } from "lodash"; @@ -138,6 +138,8 @@ const DEFAULT_CONFIG = { }, }; +type Verb = "GET" | "POST" | "PUT" | "DELETE"; + export type SynapseConfigOptions = Partial; export class SynapseContainer extends GenericContainer implements HomeserverContainer { @@ -228,8 +230,8 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont } export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { - private adminToken?: string; - private request?: APIRequestContext; + private adminTokenPromise?: Promise; + protected _request?: APIRequestContext; constructor( container: StartedTestContainer, @@ -240,7 +242,24 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public setRequest(request: APIRequestContext): void { - this.request = request; + this._request = request; + } + + public async onTestFinished(testInfo: TestInfo): Promise { + // Clean up the server to prevent rooms leaking between tests + await this.deletePublicRooms(); + } + + protected async deletePublicRooms(): Promise { + // We hide the rooms from the room directory to save time between tests and for portability between homeservers + const { chunk: rooms } = await this.request<{ + chunk: { room_id: string }[]; + }>("GET", "v3/publicRooms", {}); + await Promise.all( + rooms.map((room) => + this.request("PUT", `v3/directory/list/room/${room.room_id}`, { visibility: "private" }), + ), + ); } private async registerUserInternal( @@ -250,12 +269,12 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements admin = false, ): Promise { const url = `${this.baseUrl}/_synapse/admin/v1/register`; - const { nonce } = await this.request.get(url).then((r) => r.json()); + const { nonce } = await this._request.get(url).then((r) => r.json()); const mac = crypto .createHmac("sha1", this.registrationSharedSecret) .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .digest("hex"); - const res = await this.request.post(url, { + const res = await this._request.post(url, { data: { nonce, username, @@ -282,23 +301,76 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements }; } + protected async getAdminToken(): Promise { + if (this.adminTokenPromise === undefined) { + this.adminTokenPromise = this.registerUserInternal( + "admin", + "totalyinsecureadminpassword", + undefined, + true, + ).then((res) => res.accessToken); + } + return this.adminTokenPromise; + } + + private async adminRequest(verb: "GET", path: string, data?: never): Promise; + private async adminRequest(verb: Verb, path: string, data?: object): Promise; + private async adminRequest(verb: Verb, path: string, data?: object): Promise { + const adminToken = await this.getAdminToken(); + const url = `${this.baseUrl}/_synapse/admin/${path}`; + const res = await this._request.fetch(url, { + data, + method: verb, + headers: { + Authorization: `Bearer ${adminToken}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + + return res.json(); + } + + public async request(verb: "GET", path: string, data?: never): Promise; + public async request(verb: Verb, path: string, data?: object): Promise; + public async request(verb: Verb, path: string, data?: object): Promise { + const token = await this.getAdminToken(); + const url = `${this.baseUrl}/_matrix/client/${path}`; + const res = await this._request.fetch(url, { + data, + method: verb, + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!res.ok()) { + throw await res.json(); + } + + return res.json(); + } + public registerUser(username: string, password: string, displayName?: string): Promise { return this.registerUserInternal(username, password, displayName, false); } public async loginUser(userId: string, password: string): Promise { - const url = `${this.baseUrl}/_matrix/client/v3/login`; - const res = await this.request.post(url, { - data: { - type: "m.login.password", - identifier: { - type: "m.id.user", - user: userId, - }, - password: password, + const json = await this.request<{ + access_token: string; + user_id: string; + device_id: string; + home_server: string; + }>("POST", "v3/login", { + type: "m.login.password", + identifier: { + type: "m.id.user", + user: userId, }, + password: password, }); - const json = await res.json(); return { password, @@ -311,28 +383,13 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements } public async setThreepid(userId: string, medium: string, address: string): Promise { - if (this.adminToken === undefined) { - const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); - this.adminToken = result.accessToken; - } - - const url = `${this.baseUrl}/_synapse/admin/v2/users/${userId}`; - const res = await this.request.put(url, { - data: { - threepids: [ - { - medium, - address, - }, - ], - }, - headers: { - Authorization: `Bearer ${this.adminToken}`, - }, + await this.adminRequest("PUT", `v2/users/${userId}`, { + threepids: [ + { + medium, + address, + }, + ], }); - - if (!res.ok()) { - throw await res.json(); - } } }