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:
committed by
GitHub
parent
66bbb84e56
commit
f75d1f5a5e
19
playwright/testcontainers/HomeserverContainer.ts
Normal file
19
playwright/testcontainers/HomeserverContainer.ts
Normal 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 {}
|
||||
266
playwright/testcontainers/dendrite.ts
Normal file
266
playwright/testcontainers/dendrite.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
224
playwright/testcontainers/mas.ts
Normal file
224
playwright/testcontainers/mas.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
331
playwright/testcontainers/synapse.ts
Normal file
331
playwright/testcontainers/synapse.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
37
playwright/testcontainers/utils.ts
Normal file
37
playwright/testcontainers/utils.ts
Normal 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user