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

@@ -0,0 +1,19 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { AbstractStartedContainer, GenericContainer } from "testcontainers";
import { StartedSynapseContainer } from "./synapse.ts";
import { HomeserverInstance } from "../plugins/homeserver";
export interface HomeserverContainer<Config> extends GenericContainer {
withConfigField(key: string, value: any): this;
withConfig(config: Partial<Config>): this;
start(): Promise<StartedSynapseContainer>;
}
export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {}

View File

@@ -0,0 +1,266 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { GenericContainer, Wait } from "testcontainers";
import { APIRequestContext } from "@playwright/test";
import * as YAML from "yaml";
import { set } from "lodash";
import { randB64Bytes } from "../plugins/utils/rand.ts";
import { StartedSynapseContainer } from "./synapse.ts";
import { deepCopy } from "../plugins/utils/object.ts";
import { HomeserverContainer } from "./HomeserverContainer.ts";
const DEFAULT_CONFIG = {
version: 2,
global: {
server_name: "localhost",
private_key: "matrix_key.pem",
old_private_keys: null,
key_validity_period: "168h0m0s",
cache: {
max_size_estimated: "1gb",
max_age: "1h",
},
well_known_server_name: "",
well_known_client_name: "",
trusted_third_party_id_servers: ["matrix.org", "vector.im"],
disable_federation: false,
presence: {
enable_inbound: false,
enable_outbound: false,
},
report_stats: {
enabled: false,
endpoint: "https://matrix.org/report-usage-stats/push",
},
server_notices: {
enabled: false,
local_part: "_server",
display_name: "Server Alerts",
avatar_url: "",
room_name: "Server Alerts",
},
jetstream: {
addresses: null,
disable_tls_validation: false,
storage_path: "./",
topic_prefix: "Dendrite",
},
metrics: {
enabled: false,
basic_auth: {
username: "metrics",
password: "metrics",
},
},
dns_cache: {
enabled: false,
cache_size: 256,
cache_lifetime: "5m",
},
},
app_service_api: {
disable_tls_validation: false,
config_files: null,
},
client_api: {
registration_disabled: false,
guests_disabled: true,
registration_shared_secret: "secret",
enable_registration_captcha: false,
recaptcha_public_key: "",
recaptcha_private_key: "",
recaptcha_bypass_secret: "",
turn: {
turn_user_lifetime: "5m",
turn_uris: null,
turn_shared_secret: "",
},
rate_limiting: {
enabled: true,
threshold: 20,
cooloff_ms: 500,
exempt_user_ids: null,
},
},
federation_api: {
send_max_retries: 16,
disable_tls_validation: false,
disable_http_keepalives: false,
key_perspectives: [
{
server_name: "matrix.org",
keys: [
{
key_id: "ed25519:auto",
public_key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw",
},
{
key_id: "ed25519:a_RXGa",
public_key: "l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ",
},
],
},
],
prefer_direct_fetch: false,
database: {
connection_string: "file:dendrite-federationapi.db",
},
},
media_api: {
base_path: "./media_store",
max_file_size_bytes: 10485760,
dynamic_thumbnails: false,
max_thumbnail_generators: 10,
thumbnail_sizes: [
{
width: 32,
height: 32,
method: "crop",
},
{
width: 96,
height: 96,
method: "crop",
},
{
width: 640,
height: 480,
method: "scale",
},
],
database: {
connection_string: "file:dendrite-mediaapi.db",
},
},
mscs: {
mscs: null,
database: {
connection_string: "file:dendrite-msc.db",
},
},
sync_api: {
search: {
enabled: false,
index_path: "./searchindex",
language: "en",
},
database: {
connection_string: "file:dendrite-syncapi.db",
},
},
user_api: {
bcrypt_cost: 10,
auto_join_rooms: null,
account_database: {
connection_string: "file:dendrite-userapi.db",
},
},
room_server: {
database: {
connection_string: "file:dendrite-roomserverapi.db",
},
},
key_server: {
database: {
connection_string: "file:dendrite-keyserverapi.db",
},
},
relay_api: {
database: {
connection_string: "file:dendrite-relayapi.db",
},
},
tracing: {
enabled: false,
jaeger: {
serviceName: "",
disabled: false,
rpc_metrics: false,
tags: [],
sampler: null,
reporter: null,
headers: null,
baggage_restrictions: null,
throttler: null,
},
},
logging: [
{
type: "std",
level: "debug",
},
{
type: "file",
level: "debug",
params: {
path: "./logs",
},
},
],
};
export class DendriteContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
private config: typeof DEFAULT_CONFIG;
constructor(
private request: APIRequestContext,
image = "matrixdotorg/dendrite-monolith:main",
binary = "/usr/bin/dendrite",
) {
super(image);
this.config = deepCopy(DEFAULT_CONFIG);
this.config.client_api.registration_shared_secret = randB64Bytes(16);
this.withEntrypoint(["/bin/sh"])
.withCommand([
"-c",
`/usr/bin/generate-keys -private-key /etc/dendrite/matrix_key.pem && ${binary} --config /etc/dendrite/dendrite.yaml --really-enable-open-registration true run`,
])
.withExposedPorts(8008)
.withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008));
}
public withConfigField(key: string, value: any): this {
set(this.config, key, value);
return this;
}
public withConfig(config: Partial<typeof DEFAULT_CONFIG>): this {
this.config = {
...this.config,
...config,
};
return this;
}
public override async start(): Promise<StartedSynapseContainer> {
this.withCopyContentToContainer([
{
target: "/etc/dendrite/dendrite.yaml",
content: YAML.stringify(this.config),
},
]);
const container = await super.start();
// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it
return new StartedSynapseContainer(
container,
`http://${container.getHost()}:${container.getMappedPort(8008)}`,
this.config.client_api.registration_shared_secret,
this.request,
);
}
}
export class PineconeContainer extends DendriteContainer {
constructor(request: APIRequestContext) {
super(request, "matrixdotorg/dendrite-demo-pinecone:main", "/usr/bin/dendrite-demo-pinecone");
}
}

View File

@@ -0,0 +1,224 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import * as YAML from "yaml";
import { getFreePort } from "../plugins/utils/port.ts";
import { deepCopy } from "../plugins/utils/object.ts";
const DEFAULT_CONFIG = {
http: {
listeners: [
{
name: "web",
resources: [
{
name: "discovery",
},
{
name: "human",
},
{
name: "oauth",
},
{
name: "compat",
},
{
name: "graphql",
playground: true,
},
{
name: "assets",
path: "/usr/local/share/mas-cli/assets/",
},
],
binds: [
{
address: "[::]:8080",
},
],
proxy_protocol: false,
},
{
name: "internal",
resources: [
{
name: "health",
},
],
binds: [
{
address: "[::]:8081",
},
],
proxy_protocol: false,
},
],
trusted_proxies: ["192.128.0.0/16", "172.16.0.0/12", "10.0.0.0/10", "127.0.0.1/8", "fd00::/8", "::1/128"],
public_base: "", // Needs to be set
issuer: "", // Needs to be set
},
database: {
host: "postgres",
port: 5432,
database: "postgres",
username: "postgres",
password: "p4S5w0rD",
max_connections: 10,
min_connections: 0,
connect_timeout: 30,
idle_timeout: 600,
max_lifetime: 1800,
},
telemetry: {
tracing: {
exporter: "none",
propagators: [],
},
metrics: {
exporter: "none",
},
sentry: {
dsn: null,
},
},
templates: {
path: "/usr/local/share/mas-cli/templates/",
assets_manifest: "/usr/local/share/mas-cli/manifest.json",
translations_path: "/usr/local/share/mas-cli/translations/",
},
email: {
from: '"Authentication Service" <root@localhost>',
reply_to: '"Authentication Service" <root@localhost>',
transport: "smtp",
mode: "plain",
hostname: "mailhog",
port: 1025,
username: "username",
password: "password",
},
secrets: {
encryption: "984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5",
keys: [
{
kid: "YEAhzrKipJ",
key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B\nS79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/\n+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki\nOXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW\nR+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA\nuiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83\nCdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8\nz8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv\nx2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w\nVkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK\nUdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F\nvYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7\nXnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4\ncgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V\n4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT\nhr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V\n5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN\nyO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ\nNghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw\nb4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/\n/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH\nfjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt\n+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ\n1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m\nMC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq\n-----END RSA PRIVATE KEY-----\n",
},
{
kid: "8J1AxrlNZT",
key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49\nAwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW\ndE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==\n-----END EC PRIVATE KEY-----\n",
},
{
kid: "3BW6un1EBi",
key: "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2\nq3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK\nmZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P\n9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=\n-----END EC PRIVATE KEY-----\n",
},
{
kid: "pkZ0pTKK0X",
key: "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK\noUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl\nAer+6PMZpPc8ycyeH9N+U9NAyliBhQ==\n-----END EC PRIVATE KEY-----\n",
},
],
},
passwords: {
enabled: true,
schemes: [
{
version: 1,
algorithm: "argon2id",
},
],
minimum_complexity: 0,
},
policy: {
wasm_module: "/usr/local/share/mas-cli/policy.wasm",
client_registration_entrypoint: "client_registration/violation",
register_entrypoint: "register/violation",
authorization_grant_entrypoint: "authorization_grant/violation",
password_entrypoint: "password/violation",
email_entrypoint: "email/violation",
data: {
client_registration: {
// allow non-SSL and localhost URIs
allow_insecure_uris: true,
// EW doesn't have contacts at this time
allow_missing_contacts: true,
},
},
},
upstream_oauth2: {
providers: [],
},
branding: {
service_name: null,
policy_uri: null,
tos_uri: null,
imprint: null,
logo_uri: null,
},
account: {
password_registration_enabled: true,
},
experimental: {
access_token_ttl: 300,
compat_token_ttl: 300,
},
};
export class MatrixAuthenticationServiceContainer extends GenericContainer {
private config: typeof DEFAULT_CONFIG;
constructor(db: StartedPostgreSqlContainer) {
super("ghcr.io/element-hq/matrix-authentication-service:0.12.0");
this.config = deepCopy(DEFAULT_CONFIG);
this.config.database.username = db.getUsername();
this.config.database.password = db.getPassword();
this.withExposedPorts(8080, 8081)
.withWaitStrategy(Wait.forHttp("/health", 8081))
.withCommand(["server", "--config", "/config/config.yaml"]);
}
public withConfig(config: object): this {
this.config = {
...this.config,
...config,
};
return this;
}
public override async start(): Promise<StartedMatrixAuthenticationServiceContainer> {
// MAS config issuer needs to know what URL it'll be accessed from, so we have to map the port manually
const port = await getFreePort();
this.config.http.public_base = `http://localhost:${port}/`;
this.config.http.issuer = `http://localhost:${port}/`;
this.withExposedPorts({
container: 8080,
host: port,
}).withCopyContentToContainer([
{
target: "/config/config.yaml",
content: YAML.stringify(this.config),
},
]);
return new StartedMatrixAuthenticationServiceContainer(await super.start(), `http://localhost:${port}`);
}
}
export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer {
constructor(
container: StartedTestContainer,
public readonly baseUrl: string,
) {
super(container);
}
}

View File

@@ -0,0 +1,331 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { APIRequestContext } from "@playwright/test";
import crypto from "node:crypto";
import * as YAML from "yaml";
import { set } from "lodash";
import { getFreePort } from "../plugins/utils/port.ts";
import { randB64Bytes } from "../plugins/utils/rand.ts";
import { Credentials } from "../plugins/homeserver";
import { deepCopy } from "../plugins/utils/object.ts";
import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts";
const TAG = "develop@sha256:17cc0a301447430624afb860276e5c13270ddeb99a3f6d1c6d519a20b1a8f650";
const DEFAULT_CONFIG = {
server_name: "localhost",
public_baseurl: "", // set by start method
pid_file: "/homeserver.pid",
web_client: false,
soft_file_limit: 0,
// Needs to be configured to log to the console like a good docker process
log_config: "/data/log.config",
listeners: [
{
// Listener is always port 8008 (configured in the container)
port: 8008,
tls: false,
bind_addresses: ["::"],
type: "http",
x_forwarded: true,
resources: [
{
names: ["client"],
compress: false,
},
],
},
],
database: {
// An sqlite in-memory database is fast & automatically wipes each time
name: "sqlite3",
args: {
database: ":memory:",
},
},
rc_messages_per_second: 10000,
rc_message_burst_count: 10000,
rc_registration: {
per_second: 10000,
burst_count: 10000,
},
rc_joins: {
local: {
per_second: 9999,
burst_count: 9999,
},
remote: {
per_second: 9999,
burst_count: 9999,
},
},
rc_joins_per_room: {
per_second: 9999,
burst_count: 9999,
},
rc_3pid_validation: {
per_second: 1000,
burst_count: 1000,
},
rc_invites: {
per_room: {
per_second: 1000,
burst_count: 1000,
},
per_user: {
per_second: 1000,
burst_count: 1000,
},
},
rc_login: {
address: {
per_second: 10000,
burst_count: 10000,
},
account: {
per_second: 10000,
burst_count: 10000,
},
failed_attempts: {
per_second: 10000,
burst_count: 10000,
},
},
media_store_path: "/tmp/media_store",
max_upload_size: "50M",
max_image_pixels: "32M",
dynamic_thumbnails: false,
enable_registration: true,
enable_registration_without_verification: true,
disable_msisdn_registration: false,
registrations_require_3pid: [],
enable_metrics: false,
report_stats: false,
// These placeholders will be replaced with values generated at start
registration_shared_secret: "secret",
macaroon_secret_key: "secret",
form_secret: "secret",
// Signing key must be here: it will be generated to this file
signing_key_path: "/data/localhost.signing.key",
trusted_key_servers: [],
password_config: {
enabled: true,
},
ui_auth: {
session_timeout: "300s",
},
background_updates: {
// Inhibit background updates as this Synapse isn't long-lived
min_batch_size: 100000,
sleep_duration_ms: 100000,
},
enable_authenticated_media: true,
email: undefined,
user_consent: undefined,
server_notices: undefined,
allow_guest_access: false,
experimental_features: {},
oidc_providers: [],
serve_server_wellknown: true,
};
export type SynapseConfigOptions = Partial<typeof DEFAULT_CONFIG>;
export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
private config: typeof DEFAULT_CONFIG;
constructor(private readonly request: APIRequestContext) {
super(`ghcr.io/element-hq/synapse:${TAG}`);
this.config = deepCopy(DEFAULT_CONFIG);
this.config.registration_shared_secret = randB64Bytes(16);
this.config.macaroon_secret_key = randB64Bytes(16);
this.config.form_secret = randB64Bytes(16);
const signingKey = randB64Bytes(32);
this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([
{ target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` },
{
target: this.config.log_config,
content: YAML.stringify({
version: 1,
formatters: {
precise: {
format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s",
},
},
handlers: {
console: {
class: "logging.StreamHandler",
formatter: "precise",
},
},
loggers: {
"synapse.storage.SQL": {
level: "DEBUG",
},
"twisted": {
handlers: ["console"],
propagate: false,
},
},
root: {
level: "DEBUG",
handlers: ["console"],
},
disable_existing_loggers: false,
}),
},
]);
}
public withConfigField(key: string, value: any): this {
set(this.config, key, value);
return this;
}
public withConfig(config: Partial<typeof DEFAULT_CONFIG>): this {
this.config = {
...this.config,
...config,
};
return this;
}
public override async start(): Promise<StartedSynapseContainer> {
// Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually
const port = await getFreePort();
this.withExposedPorts({
container: 8008,
host: port,
})
.withConfig({
public_baseurl: `http://localhost:${port}`,
})
.withCopyContentToContainer([
{
target: "/data/homeserver.yaml",
content: YAML.stringify(this.config),
},
]);
return new StartedSynapseContainer(
await super.start(),
`http://localhost:${port}`,
this.config.registration_shared_secret,
this.request,
);
}
}
export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer {
private adminToken?: string;
constructor(
container: StartedTestContainer,
public readonly baseUrl: string,
private readonly registrationSharedSecret: string,
private readonly request: APIRequestContext,
) {
super(container);
}
private async registerUserInternal(
username: string,
password: string,
displayName?: string,
admin = false,
): Promise<Credentials> {
const url = `${this.baseUrl}/_synapse/admin/v1/register`;
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, {
data: {
nonce,
username,
password,
mac,
admin,
displayname: displayName,
},
});
if (!res.ok()) {
throw await res.json();
}
const data = await res.json();
return {
homeServer: data.home_server,
accessToken: data.access_token,
userId: data.user_id,
deviceId: data.device_id,
password,
displayName,
};
}
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
return this.registerUserInternal(username, password, displayName, false);
}
public async loginUser(userId: string, password: string): Promise<Credentials> {
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 res.json();
return {
password,
accessToken: json.access_token,
userId: json.user_id,
deviceId: json.device_id,
homeServer: json.home_server,
};
}
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
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}`,
},
});
if (!res.ok()) {
throw await res.json();
}
}
}

View File

@@ -0,0 +1,37 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { TestInfo } from "@playwright/test";
import { Readable } from "stream";
import stripAnsi from "strip-ansi";
export class ContainerLogger {
private logs: Record<string, string> = {};
public getConsumer(container: string) {
this.logs[container] = "";
return (stream: Readable) => {
stream.on("data", (chunk) => {
this.logs[container] += chunk.toString();
});
stream.on("err", (chunk) => {
this.logs[container] += "ERR " + chunk.toString();
});
};
}
public async testFinished(testInfo: TestInfo) {
if (testInfo.status !== "passed") {
for (const container in this.logs) {
await testInfo.attach(container, {
body: stripAnsi(this.logs[container]),
contentType: "text/plain",
});
}
}
}
}