Clean up public rooms between tests on reused homeserver
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user