Clean up public rooms between tests on reused homeserver

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-01-10 11:37:49 +00:00
parent b203e4b2a5
commit 3eeb2216f9
4 changed files with 104 additions and 44 deletions

View File

@@ -136,5 +136,6 @@ export const test = base.extend<{}, Services>({
await logger.testStarted(testInfo); await logger.testStarted(testInfo);
await use(context); await use(context);
await logger.testFinished(testInfo); await logger.testFinished(testInfo);
await homeserver.onTestFinished(testInfo);
}, },
}); });

View File

@@ -6,17 +6,17 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { AbstractStartedContainer, GenericContainer } from "testcontainers"; 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"; import { HomeserverInstance } from "../plugins/homeserver";
export interface HomeserverContainer<Config> extends GenericContainer { export interface HomeserverContainer<Config> extends GenericContainer {
withConfigField(key: string, value: any): this; withConfigField(key: string, value: any): this;
withConfig(config: Partial<Config>): this; withConfig(config: Partial<Config>): this;
start(): Promise<StartedSynapseContainer>; start(): Promise<StartedHomeserverContainer>;
} }
export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance { export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
setRequest(request: APIRequestContext): void; setRequest(request: APIRequestContext): void;
onTestFinished(testInfo: TestInfo): Promise<void>;
} }

View File

@@ -235,7 +235,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
return this; return this;
} }
public override async start(): Promise<StartedSynapseContainer> { public override async start(): Promise<StartedDendriteContainer> {
this.withCopyContentToContainer([ this.withCopyContentToContainer([
{ {
target: "/etc/dendrite/dendrite.yaml", target: "/etc/dendrite/dendrite.yaml",
@@ -244,8 +244,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
]); ]);
const container = await super.start(); const container = await super.start();
// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it return new StartedDendriteContainer(
return new StartedSynapseContainer(
container, container,
`http://${container.getHost()}:${container.getMappedPort(8008)}`, `http://${container.getHost()}:${container.getMappedPort(8008)}`,
this.config.client_api.registration_shared_secret, 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"); 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 {}

View File

@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { APIRequestContext } from "@playwright/test"; import { APIRequestContext, TestInfo } from "@playwright/test";
import crypto from "node:crypto"; import crypto from "node:crypto";
import * as YAML from "yaml"; import * as YAML from "yaml";
import { set } from "lodash"; import { set } from "lodash";
@@ -138,6 +138,8 @@ const DEFAULT_CONFIG = {
}, },
}; };
type Verb = "GET" | "POST" | "PUT" | "DELETE";
export type SynapseConfigOptions = Partial<typeof DEFAULT_CONFIG>; export type SynapseConfigOptions = Partial<typeof DEFAULT_CONFIG>;
export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> { export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
@@ -228,8 +230,8 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont
} }
export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer { export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer {
private adminToken?: string; private adminTokenPromise?: Promise<string>;
private request?: APIRequestContext; protected _request?: APIRequestContext;
constructor( constructor(
container: StartedTestContainer, container: StartedTestContainer,
@@ -240,7 +242,24 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
} }
public setRequest(request: APIRequestContext): void { public setRequest(request: APIRequestContext): void {
this.request = request; this._request = request;
}
public async onTestFinished(testInfo: TestInfo): Promise<void> {
// Clean up the server to prevent rooms leaking between tests
await this.deletePublicRooms();
}
protected async deletePublicRooms(): Promise<void> {
// 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( private async registerUserInternal(
@@ -250,12 +269,12 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
admin = false, admin = false,
): Promise<Credentials> { ): Promise<Credentials> {
const url = `${this.baseUrl}/_synapse/admin/v1/register`; 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 const mac = crypto
.createHmac("sha1", this.registrationSharedSecret) .createHmac("sha1", this.registrationSharedSecret)
.update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`) .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
.digest("hex"); .digest("hex");
const res = await this.request.post(url, { const res = await this._request.post(url, {
data: { data: {
nonce, nonce,
username, username,
@@ -282,23 +301,76 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
}; };
} }
protected async getAdminToken(): Promise<string> {
if (this.adminTokenPromise === undefined) {
this.adminTokenPromise = this.registerUserInternal(
"admin",
"totalyinsecureadminpassword",
undefined,
true,
).then((res) => res.accessToken);
}
return this.adminTokenPromise;
}
private async adminRequest<R extends {}>(verb: "GET", path: string, data?: never): Promise<R>;
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
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<R extends {}>(verb: "GET", path: string, data?: never): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
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<Credentials> { public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
return this.registerUserInternal(username, password, displayName, false); return this.registerUserInternal(username, password, displayName, false);
} }
public async loginUser(userId: string, password: string): Promise<Credentials> { public async loginUser(userId: string, password: string): Promise<Credentials> {
const url = `${this.baseUrl}/_matrix/client/v3/login`; const json = await this.request<{
const res = await this.request.post(url, { access_token: string;
data: { user_id: string;
type: "m.login.password", device_id: string;
identifier: { home_server: string;
type: "m.id.user", }>("POST", "v3/login", {
user: userId, type: "m.login.password",
}, identifier: {
password: password, type: "m.id.user",
user: userId,
}, },
password: password,
}); });
const json = await res.json();
return { return {
password, password,
@@ -311,28 +383,13 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
} }
public async setThreepid(userId: string, medium: string, address: string): Promise<void> { public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
if (this.adminToken === undefined) { await this.adminRequest("PUT", `v2/users/${userId}`, {
const result = await this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true); threepids: [
this.adminToken = result.accessToken; {
} medium,
address,
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}`,
},
}); });
if (!res.ok()) {
throw await res.json();
}
} }
} }