Add a login test against Synapse to Playwright (#11913)

* Install playwright

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

* Add foundations for writing tests under Playwright

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

* .gitignore juggling

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

* Add tsconfig and fix eslint rules

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

* Add docker & synapse plugins

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

* Add login.spec.ts

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

* Wire up fixture which sets up ElementAppPage & bakes config.json

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

* Remove launch test, it has served its purpose

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

* Remove test which has been ported to Playwright

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

* Fix test not cleaning up after itself

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

* Move registerUser to the Homeserver interface

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

* Remove unused fixture param

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

* Remove redundant launch test

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

* Add newline

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
Michael Telatynski 2023-11-22 11:49:51 +00:00 committed by GitHub
parent ecc46aeb8c
commit 52e3e0de1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1138 additions and 89 deletions

View file

@ -26,65 +26,6 @@ describe("Login", () => {
cy.stopHomeserver(homeserver);
});
describe("m.login.password", () => {
const username = "user1234";
const password = "p4s5W0rD";
beforeEach(() => {
cy.startHomeserver("consent").then((data) => {
homeserver = data;
cy.registerUser(homeserver, username, password);
cy.visit("/#/login");
});
});
it("logs in with an existing account and lands on the home screen", () => {
cy.injectAxe();
// first pick the homeserver, as otherwise the user picker won't be visible
cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get(".mx_Spinner").should("not.exist");
cy.get(".mx_ServerPicker_server").should("have.text", homeserver.baseUrl);
cy.findByRole("button", { name: "Edit" }).click();
// select the default server again
cy.get(".mx_StyledRadioButton").first().click();
cy.findByRole("button", { name: "Continue" }).click();
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get(".mx_Spinner").should("not.exist");
// name of default server
cy.get(".mx_ServerPicker_server").should("have.text", "server.invalid");
// switch back to the custom homeserver
cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl);
cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get(".mx_Spinner").should("not.exist");
cy.get(".mx_ServerPicker_server").should("have.text", homeserver.baseUrl);
cy.findByRole("textbox", { name: "Username", timeout: 15000 }).should("be.visible");
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
//cy.percySnapshot("Login");
cy.checkA11y();
cy.findByRole("textbox", { name: "Username" }).type(username);
cy.findByPlaceholderText("Password").type(password);
cy.findByRole("button", { name: "Sign in" }).click();
cy.url().should("contain", "/#/home", { timeout: 30000 });
});
});
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
describe("SSO login", () => {
beforeEach(() => {

View file

@ -33,6 +33,7 @@ describe("Soft logout", () => {
afterEach(() => {
cy.stopHomeserver(homeserver);
cy.task("stopOAuthServer");
});
describe("with password user", () => {

View file

@ -184,6 +184,7 @@
"@typescript-eslint/parser": "^5.6.0",
"allchange": "^1.1.0",
"axe-core": "4.8.2",
"axe-playwright": "^1.2.3",
"babel-jest": "^29.0.0",
"blob-polyfill": "^7.0.0",
"cypress": "^12.0.0",

View file

@ -1,2 +1,3 @@
/test-results/
/html-report/
/synapselogs/

View file

@ -1,29 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test, expect } from "@playwright/test";
test.describe("App launch", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
});
test("should launch and render the welcome view successfully", async ({ page }) => {
await page.locator("#matrixchat").waitFor();
await page.locator(".mx_Welcome").waitFor();
await expect(page).toHaveURL("http://localhost:8080/#/welcome");
});
});

View file

@ -0,0 +1,78 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { checkA11y, injectAxe } from "axe-playwright";
import { test, expect } from "../../element-web-test";
test.describe("Consent", () => {
test.describe("m.login.password", () => {
test.use({ startHomeserverOpts: "consent" });
const username = "user1234";
const password = "p4s5W0rD";
test.beforeEach(async ({ page, homeserver }) => {
await homeserver.registerUser(username, password);
await page.goto("/#/login");
});
test("logs in with an existing account and lands on the home screen", async ({ page, homeserver }) => {
await injectAxe(page);
// first pick the homeserver, as otherwise the user picker won't be visible
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click();
// wait for the dialog to go away
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Edit" }).click();
// select the default server again
await page.locator(".mx_StyledRadioButton").first().click();
await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
// name of default server
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
// switch back to the custom homeserver
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click();
// wait for the dialog to go away
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
await expect(page.locator(".mx_ServerPicker_server")).toHaveText(homeserver.config.baseUrl);
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
// cy.percySnapshot("Login");
await checkA11y(page);
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/\/#\/home$/);
});
});
});

View file

@ -0,0 +1,64 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { test as base } from "@playwright/test";
import { HomeserverInstance, StartHomeserverOpts } from "./plugins/utils/homeserver";
import { Synapse } from "./plugins/synapse";
const CONFIG_JSON = {
// This is deliberately quite a minimal config.json, so that we can test that the default settings
// actually work.
//
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
// the location tests want a map style url.
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
};
export const test = base.extend<{
startHomeserverOpts: StartHomeserverOpts | string;
homeserver: HomeserverInstance;
}>({
page: async ({ context, page }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => {
await route.fulfill({ json: CONFIG_JSON });
});
await use(page);
},
startHomeserverOpts: "default",
homeserver: async ({ request, startHomeserverOpts: opts }, use) => {
if (typeof opts === "string") {
opts = { template: opts };
}
const server = new Synapse(request);
await use(await server.start(opts));
await server.stop();
},
});
test.use({});
export { expect } from "@playwright/test";

View file

@ -0,0 +1,153 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as os from "os";
import * as crypto from "crypto";
import * as childProcess from "child_process";
import * as fse from "fs-extra";
export class Docker {
public id: string;
async run(opts: { image: string; containerName: string; params?: string[]; cmd?: string[] }): Promise<string> {
const userInfo = os.userInfo();
const params = opts.params ?? [];
if (params?.includes("-v") && userInfo.uid >= 0) {
// Run the docker container as our uid:gid to prevent problems with permissions.
if (await Docker.isPodman()) {
// Note: this setup is for podman rootless containers.
// In podman, run as root in the container, which maps to the current
// user on the host. This is probably the default since Synapse's
// Dockerfile doesn't specify, but we're being explicit here
// because it's important for the permissions to work.
params.push("-u", "0:0");
// Tell Synapse not to switch UID
params.push("-e", "UID=0");
params.push("-e", "GID=0");
} else {
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
}
}
const args = [
"run",
"--name",
`${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"-d",
"--rm",
...params,
opts.image,
];
if (opts.cmd) args.push(...opts.cmd);
this.id = await new Promise<string>((resolve, reject) => {
childProcess.execFile("docker", args, (err, stdout) => {
if (err) reject(err);
resolve(stdout.trim());
});
});
return this.id;
}
stop(): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile("docker", ["stop", this.id], (err) => {
if (err) reject(err);
resolve();
});
});
}
exec(params: string[]): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile(
"docker",
["exec", this.id, ...params],
{ encoding: "utf8" },
(err, stdout, stderr) => {
if (err) {
console.log(stdout);
console.log(stderr);
reject(err);
return;
}
resolve();
},
);
});
}
rm(): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile("docker", ["rm", this.id], (err) => {
if (err) reject(err);
resolve();
});
});
}
getContainerIp(): Promise<string> {
return new Promise<string>((resolve, reject) => {
childProcess.execFile(
"docker",
["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", this.id],
(err, stdout) => {
if (err) reject(err);
else resolve(stdout.trim());
},
);
});
}
async persistLogsToFile(args: { stdoutFile?: string; stderrFile?: string }): Promise<void> {
const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore";
const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore";
await new Promise<void>((resolve) => {
childProcess
.spawn("docker", ["logs", this.id], {
stdio: ["ignore", stdoutFile, stderrFile],
})
.once("close", resolve);
});
if (args.stdoutFile) await fse.close(<number>stdoutFile);
if (args.stderrFile) await fse.close(<number>stderrFile);
}
/**
* Detects whether the docker command is actually podman.
* To do this, it looks for "podman" in the output of "docker --help".
*/
static isPodman(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
childProcess.execFile("docker", ["--help"], (err, stdout) => {
if (err) reject(err);
else resolve(stdout.toLowerCase().includes("podman"));
});
});
}
/**
* Supply the right hostname to use to talk to the host machine. On Docker this
* is "host.docker.internal" and on Podman this is "host.containers.internal".
*/
static async hostnameOfHost(): Promise<"host.containers.internal" | "host.docker.internal"> {
return (await Docker.isPodman()) ? "host.containers.internal" : "host.docker.internal";
}
}

View file

@ -0,0 +1,205 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as path from "path";
import * as os from "os";
import * as crypto from "crypto";
import * as fse from "fs-extra";
import { APIRequestContext } from "@playwright/test";
import { getFreePort } from "../utils/port";
import { Docker } from "../docker";
import {
HomeserverConfig,
HomeserverInstance,
Homeserver,
StartHomeserverOpts,
Credentials,
} from "../utils/homeserver";
function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
}
async function cfgDirFromTemplate(
opts: StartHomeserverOpts,
): Promise<HomeserverConfig & { registrationSecret: string }> {
const templateDir = path.join(__dirname, "templates", opts.template);
const stats = await fse.stat(templateDir);
if (!stats?.isDirectory) {
throw new Error(`No such template: ${opts.template}`);
}
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-synapsedocker-"));
// 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) !== "homeserver.yaml" });
const registrationSecret = randB64Bytes(16);
const macaroonSecret = randB64Bytes(16);
const formSecret = randB64Bytes(16);
const port = await getFreePort();
const baseUrl = `http://localhost:${port}`;
// now copy homeserver.yaml, applying substitutions
const templateHomeserver = path.join(templateDir, "homeserver.yaml");
const outputHomeserver = path.join(tempDir, "homeserver.yaml");
console.log(`Gen ${templateHomeserver} -> ${outputHomeserver}`);
let hsYaml = await fse.readFile(templateHomeserver, "utf8");
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
if (opts.oAuthServerPort) {
hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort.toString());
}
hsYaml = hsYaml.replace(/{{HOST_DOCKER_INTERNAL}}/g, await Docker.hostnameOfHost());
if (opts.variables) {
let fetchedHostContainer: Awaited<ReturnType<typeof Docker.hostnameOfHost>> | null = null;
for (const key in opts.variables) {
let value = String(opts.variables[key]);
if (value === "{{HOST_DOCKER_INTERNAL}}") {
if (!fetchedHostContainer) {
fetchedHostContainer = await Docker.hostnameOfHost();
}
value = fetchedHostContainer;
}
hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value);
}
}
await fse.writeFile(outputHomeserver, hsYaml);
// now generate a signing key (we could use synapse's config generation for
// this, or we could just do this...)
// NB. This assumes the homeserver.yaml specifies the key in this location
const signingKey = randB64Bytes(32);
const outputSigningKey = path.join(tempDir, "localhost.signing.key");
console.log(`Gen -> ${outputSigningKey}`);
await fse.writeFile(outputSigningKey, `ed25519 x ${signingKey}`);
return {
port,
baseUrl,
configDir: tempDir,
registrationSecret,
};
}
export class Synapse implements Homeserver, HomeserverInstance {
private docker: Docker = new Docker();
public config: HomeserverConfig & { serverId: string; registrationSecret: string };
public constructor(private readonly request: APIRequestContext) {}
/**
* Start a synapse instance: the template must be the name of
* one of the templates in the playwright/plugins/synapsedocker/templates
* directory.
*
* Any value in opts.variables that is set to `{{HOST_DOCKER_INTERNAL}}'
* will be replaced with 'host.docker.internal' (if we are on Docker) or
* 'host.containers.internal' if we are on Podman.
*/
public async start(opts: StartHomeserverOpts): Promise<HomeserverInstance> {
if (this.config) await this.stop();
const synCfg = await cfgDirFromTemplate(opts);
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`];
if (await Docker.isPodman()) {
// Make host.containers.internal work to allow Synapse to talk to the test OIDC server.
dockerSynapseParams.push("--network");
dockerSynapseParams.push("slirp4netns:allow_host_loopback=true");
} else {
// Make host.docker.internal work to allow Synapse to talk to the test OIDC server.
dockerSynapseParams.push("--add-host");
dockerSynapseParams.push("host.docker.internal:host-gateway");
}
const synapseId = await this.docker.run({
image: "matrixdotorg/synapse:develop",
containerName: `react-sdk-playwright-synapse`,
params: dockerSynapseParams,
cmd: ["run"],
});
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
// Await Synapse healthcheck
await this.docker.exec([
"curl",
"--connect-timeout",
"30",
"--retry",
"30",
"--retry-delay",
"1",
"--retry-all-errors",
"--silent",
"http://localhost:8008/health",
]);
this.config = {
...synCfg,
serverId: synapseId,
};
return this;
}
public async stop(): Promise<void> {
if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?");
const id = this.config.serverId;
const synapseLogsPath = path.join("playwright", "synapselogs", id);
await fse.ensureDir(synapseLogsPath);
await this.docker.persistLogsToFile({
stdoutFile: path.join(synapseLogsPath, "stdout.log"),
stderrFile: path.join(synapseLogsPath, "stderr.log"),
});
await this.docker.stop();
await fse.remove(this.config.configDir);
console.log(`Stopped synapse id ${id}.`);
}
public async registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
const url = `${this.config.baseUrl}/_synapse/admin/v1/register`;
const { nonce } = await this.request.get(url).then((r) => r.json());
const mac = crypto
.createHmac("sha1", this.config.registrationSecret)
.update(`${nonce}\0${username}\0${password}\0notadmin`)
.digest("hex");
const res = await this.request.post(url, {
data: {
nonce,
username,
password,
mac,
admin: false,
displayname: displayName,
},
});
const data = await res.json();
return {
homeServer: data.home_server,
accessToken: data.access_token,
userId: data.user_id,
deviceId: data.device_id,
password,
};
}
}

View file

@ -0,0 +1,3 @@
# Meta-template for synapse templates
To make another template, you can copy this directory

View file

@ -0,0 +1,72 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
# XXX: This won't actually be right: it lets docker allocate an ephemeral port,
# so we have a chicken-and-egg problem
public_baseurl: http://localhost:8008/
# Listener is always port 8008 (configured in the container)
listeners:
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client, federation, consent]
compress: false
# An sqlite in-memory database is fast & automatically wipes each time
database:
name: "sqlite3"
args:
database: ":memory:"
# Needs to be configured to log to the console like a good docker process
log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
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: "/data/media_store"
uploads_path: "/data/uploads"
enable_registration: true
enable_registration_without_verification: true
disable_msisdn_registration: false
# These placeholders will be be replaced with values generated at start
registration_shared_secret: "{{REGISTRATION_SECRET}}"
report_stats: false
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
# Signing key must be here: it will be generated to this file
signing_key_path: "/data/localhost.signing.key"
email:
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true

View file

@ -0,0 +1,50 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [console]
propagate: false
root:
level: INFO
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [console]
disable_existing_loggers: false

View file

@ -0,0 +1 @@
A synapse configured with user privacy consent enabled

View file

@ -0,0 +1,84 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client, federation, consent]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
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: "/data/media_store"
uploads_path: "/data/uploads"
enable_registration: true
enable_registration_without_verification: true
disable_msisdn_registration: false
registration_shared_secret: "{{REGISTRATION_SECRET}}"
report_stats: false
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
email:
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
user_consent:
template_dir: /data/res/templates/privacy
version: 1.0
server_notice_content:
msgtype: m.text
body: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
send_server_notice_to_guests: True
block_events_error: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
require_at_registration: true
server_notices:
system_mxid_localpart: notices
system_mxid_display_name: "Server Notices"
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
room_name: "Server Notices"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true

View file

@ -0,0 +1,50 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: DEBUG
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [console]
propagate: false
root:
level: DEBUG
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [console]
disable_existing_loggers: false

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
{% if has_consented %}
<p>Thank you, you've already accepted the license.</p>
{% else %}
<p>Please accept the license!</p>
<form method="post" action="consent">
<input type="hidden" name="v" value="{{version}}" />
<input type="hidden" name="u" value="{{user}}" />
<input type="hidden" name="h" value="{{userhmac}}" />
<input type="submit" value="Sure thing!" />
</form>
{% endif %}
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
<p>Danke schoen</p>
</body>
</html>

View file

@ -0,0 +1 @@
A synapse configured with user privacy consent disabled

View file

@ -0,0 +1,94 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
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: "/data/media_store"
uploads_path: "/data/uploads"
enable_registration: true
enable_registration_without_verification: true
disable_msisdn_registration: false
registration_shared_secret: "{{REGISTRATION_SECRET}}"
report_stats: false
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
oidc_providers:
- idp_id: test
idp_name: "OAuth test"
issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth"
authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html"
# the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container.
# Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to
# host.docker.internal on Docker and host.containers.internal on Podman.
token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token"
userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo"
client_id: "synapse"
discover: false
scopes: ["profile"]
skip_verification: true
user_mapping_provider:
config:
display_name_template: "{{ user.name }}"

View file

@ -0,0 +1,50 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: DEBUG
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [console]
propagate: false
root:
level: DEBUG
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [console]
disable_existing_loggers: false

View file

@ -0,0 +1 @@
A synapse configured to require an email for registration

View file

@ -0,0 +1,44 @@
server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
media_store_path: "/data/media_store"
uploads_path: "/data/uploads"
enable_registration: true
registrations_require_3pid:
- email
registration_shared_secret: "{{REGISTRATION_SECRET}}"
report_stats: false
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
email:
smtp_host: "%SMTP_HOST%"
smtp_port: %SMTP_PORT%
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: my_branded_matrix_server

View file

@ -0,0 +1,50 @@
# Log configuration for Synapse.
#
# This is a YAML file containing a standard Python logging configuration
# dictionary. See [1] for details on the valid settings.
#
# Synapse also supports structured logging for machine readable logs which can
# be ingested by ELK stacks. See [2] for details.
#
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
version: 1
formatters:
precise:
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
handlers:
# A handler that writes logs to stderr. Unused by default, but can be used
# instead of "buffer" and "file" in the logger handlers.
console:
class: logging.StreamHandler
formatter: precise
loggers:
synapse.storage.SQL:
# beware: increasing this to DEBUG will make synapse log sensitive
# information such as access tokens.
level: INFO
twisted:
# We send the twisted logging directly to the file handler,
# to work around https://github.com/matrix-org/synapse/issues/3471
# when using "buffer" logger. Use "console" to log to stderr instead.
handlers: [console]
propagate: false
root:
level: INFO
# Write logs to the `buffer` handler, which will buffer them together in memory,
# then write them to a file.
#
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
# also need to update the configuration for the `twisted` logger above, in
# this case.)
#
handlers: [console]
disable_existing_loggers: false

View file

@ -0,0 +1,57 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface HomeserverConfig {
readonly configDir: string;
readonly baseUrl: string;
readonly port: number;
}
export interface HomeserverInstance {
readonly config: HomeserverConfig;
/**
* Register a user on the given Homeserver using the shared registration secret.
* @param username the username of the user to register
* @param password the password of the user to register
* @param displayName optional display name to set on the newly registered user
*/
registerUser(username: string, password: string, displayName?: string): Promise<Credentials>;
}
export interface StartHomeserverOpts {
/** path to template within playwright/plugins/{homeserver}docker/template/ directory. */
template: string;
/** Port of an OAuth server to configure the homeserver to use */
oAuthServerPort?: number;
/** Additional variables to inject into the configuration template **/
variables?: Record<string, string | number>;
}
export interface Homeserver {
start(opts: StartHomeserverOpts): Promise<HomeserverInstance>;
stop(): Promise<void>;
}
export interface Credentials {
accessToken: string;
userId: string;
deviceId: string;
homeServer: string;
password: string;
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as net from "net";
export async function getFreePort(): Promise<number> {
return new Promise<number>((resolve) => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port;
srv.close(() => resolve(port));
});
});
}

View file

@ -3434,7 +3434,7 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
axe-core@4.8.2:
axe-core@4.8.2, axe-core@^4.5.1:
version "4.8.2"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae"
integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g==
@ -3444,6 +3444,23 @@ axe-core@=4.7.0:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
axe-html-reporter@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/axe-html-reporter/-/axe-html-reporter-2.2.3.tgz#2d56e239fe9bd1f09ba0735d94596bf79dd389a7"
integrity sha512-io8aCEt4fJvv43W+33n3zEa8rdplH5Ti2v5fOnth3GBKLhLHarNs7jj46xGfpnGnpaNrz23/tXPHC3HbwTzwwA==
dependencies:
mustache "^4.0.1"
rimraf "^3.0.2"
axe-playwright@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/axe-playwright/-/axe-playwright-1.2.3.tgz#b590b4edf3898ed5784c4932cbad2937115b31f2"
integrity sha512-bTxCTNp3kx6sQRMjmuLv8pG3+v+kOCvFXATim1+XUXzW6ykulbbuJdQfgB+vQPNAF9uvYbW2qrv9pg81ZSFV/A==
dependencies:
axe-core "^4.5.1"
axe-html-reporter "2.2.3"
picocolors "^1.0.0"
axios-retry@^3.7.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.9.1.tgz#c8924a8781c8e0a2c5244abf773deb7566b3830d"
@ -7748,6 +7765,11 @@ murmurhash-js@^1.0.0:
resolved "https://registry.yarnpkg.com/murmurhash-js/-/murmurhash-js-1.0.0.tgz#b06278e21fc6c37fa5313732b0412bcb6ae15f51"
integrity sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==
mustache@^4.0.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"