Switch to TestContainers for manging services in Playwright (#28860)

* Switch to TestContainers for manging services in Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Flip fixture dependency order

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove mas dep

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update matrix-authentication-service in Playwright tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix SMTP port

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Comments

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Strip ansi from playwright logs to make them more readable

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Actually do the update

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove access to homeserver.config.baseUrl field in favour of homeserver.baseUrl

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Use sane default_server_config and specify server.invalid in the specific tests which demand it

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix mas run

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* break cycle

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* typo

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Wire up basics of dendriteHomeserver

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-01-07 18:11:44 +00:00
committed by GitHub
parent 66bbb84e56
commit f75d1f5a5e
61 changed files with 1922 additions and 2703 deletions

View File

@@ -6,142 +6,32 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as path from "node:path";
import * as os from "node:os";
import * as fse from "fs-extra";
import { Fixtures, PlaywrightTestArgs } from "@playwright/test";
import { getFreePort } from "../../utils/port";
import { Homeserver, HomeserverConfig, HomeserverInstance, StartHomeserverOpts } from "../";
import { randB64Bytes } from "../../utils/rand";
import { Synapse } from "../synapse";
import { Docker } from "../../docker";
import { Fixtures as BaseFixtures } from "../../../element-web-test.ts";
import { DendriteContainer, PineconeContainer } from "../../../testcontainers/dendrite.ts";
import { Services } from "../../../services.ts";
const dockerConfigDir = "/etc/dendrite/";
const dendriteConfigFile = "dendrite.yaml";
type Fixture = PlaywrightTestArgs & Services & BaseFixtures;
export const dendriteHomeserver: Fixtures<Fixture, {}, Fixture> = {
_homeserver: async ({ request }, use) => {
const container =
process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite"
? new DendriteContainer(request)
: new PineconeContainer(request);
await use(container);
},
homeserver: async ({ logger, network, _homeserver: homeserver }, use) => {
const container = await homeserver
.withNetwork(network)
.withNetworkAliases("homeserver")
.withLogConsumer(logger.getConsumer("dendrite"))
.start();
// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it
export class Dendrite extends Synapse implements Homeserver, HomeserverInstance {
protected image = "matrixdotorg/dendrite-monolith:main";
protected entrypoint = "/usr/bin/dendrite";
/**
* Start a dendrite instance: the template must be the name of one of the templates
* in the playwright/plugins/dendritedocker/templates directory
* @param opts
*/
public async start(opts: StartHomeserverOpts): Promise<HomeserverInstance> {
const denCfg = await cfgDirFromTemplate(this.image, opts);
console.log(`Starting dendrite with config dir ${denCfg.configDir}...`);
const dendriteId = await this.docker.run({
image: this.image,
params: [
"-v",
`${denCfg.configDir}:` + dockerConfigDir,
"-p",
`${denCfg.port}:8008/tcp`,
"--entrypoint",
this.entrypoint,
],
containerName: `react-sdk-playwright-dendrite`,
cmd: ["--config", dockerConfigDir + dendriteConfigFile, "--really-enable-open-registration", "true", "run"],
});
console.log(`Started dendrite with id ${dendriteId} on port ${denCfg.port}.`);
// Await Dendrite healthcheck
await this.docker.exec([
"curl",
"--connect-timeout",
"30",
"--retry",
"30",
"--retry-delay",
"1",
"--retry-all-errors",
"--silent",
"http://localhost:8008/_matrix/client/versions",
]);
const dockerUrl = `http://${await this.docker.getContainerIp()}:8008`;
this.config = {
...denCfg,
serverId: dendriteId,
dockerUrl,
};
return this;
}
public async stop(): Promise<string[]> {
if (!this.config) throw new Error("Missing existing dendrite instance, did you call stop() before start()?");
const dendriteLogsPath = path.join("playwright", "dendritelogs", this.config.serverId);
await fse.ensureDir(dendriteLogsPath);
await this.docker.persistLogsToFile({
stdoutFile: path.join(dendriteLogsPath, "stdout.log"),
stderrFile: path.join(dendriteLogsPath, "stderr.log"),
});
await this.docker.stop();
await fse.remove(this.config.configDir);
console.log(`Stopped dendrite id ${this.config.serverId}.`);
return [path.join(dendriteLogsPath, "stdout.log"), path.join(dendriteLogsPath, "stderr.log")];
}
}
export class Pinecone extends Dendrite {
protected image = "matrixdotorg/dendrite-demo-pinecone:main";
protected entrypoint = "/usr/bin/dendrite-demo-pinecone";
}
async function cfgDirFromTemplate(
dendriteImage: string,
opts: StartHomeserverOpts,
): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const template = "default"; // XXX: for now we only have one template
const templateDir = path.join(__dirname, "templates", template);
const stats = await fse.stat(templateDir);
if (!stats?.isDirectory) {
throw new Error(`No such template: ${template}`);
}
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-dendritedocker-"));
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
console.log(`Copy ${templateDir} -> ${tempDir}`);
await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== dendriteConfigFile });
const registrationSecret = randB64Bytes(16);
const port = await getFreePort();
const baseUrl = `http://localhost:${port}`;
// now copy homeserver.yaml, applying substitutions
console.log(`Gen ${path.join(templateDir, dendriteConfigFile)}`);
let hsYaml = await fse.readFile(path.join(templateDir, dendriteConfigFile), "utf8");
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
await fse.writeFile(path.join(tempDir, dendriteConfigFile), hsYaml);
const docker = new Docker();
await docker.run({
image: dendriteImage,
params: ["--entrypoint=", "-v", `${tempDir}:/mnt`],
containerName: `react-sdk-playwright-dendrite-keygen`,
cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"],
});
return {
port,
baseUrl,
configDir: tempDir,
registrationSecret,
};
}
await use(container);
await container.stop();
},
};
export function isDendrite(): boolean {
return process.env["PLAYWRIGHT_HOMESERVER"] === "dendrite" || process.env["PLAYWRIGHT_HOMESERVER"] === "pinecone";