diff --git a/backend/dev_homeserver-othersite.yaml b/backend/dev_homeserver-othersite.yaml index 81e775ca..7eb8f294 100644 --- a/backend/dev_homeserver-othersite.yaml +++ b/backend/dev_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index dc7b42c8..0aea2ece 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index d3ddbc53..fdefa3bd 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -28,11 +28,18 @@ server { # Reason: the lk-jwt-service uses the federation API for the openid token # verification, which requires TLS location ~ ^(/_matrix|/_synapse/client) { - proxy_pass "http://homeserver:8008"; + proxy_pass "http://homeserver:8008"; proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; + } + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver:8008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; @@ -73,7 +80,15 @@ server { proxy_http_version 1.1; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header Host $host; + proxy_set_header Host $host; + } + + location ~ ^(/_matrix|/_synapse/admin) { + proxy_pass "http://homeserver-1:18008"; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; } error_page 500 502 503 504 /50x.html; diff --git a/backend/playwright_homeserver-othersite.yaml b/backend/playwright_homeserver-othersite.yaml index 35640ae9..86c77b35 100644 --- a/backend/playwright_homeserver-othersite.yaml +++ b/backend/playwright_homeserver-othersite.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index a83247cd..8f437524 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -50,6 +50,9 @@ max_event_delay_duration: 24h enable_registration: true enable_registration_without_verification: true +# Shared secret for admin user registration via API (for testing only!) +registration_shared_secret: "test_shared_secret_for_local_dev_only" + report_stats: false serve_server_wellknown: true diff --git a/playwright.config.ts b/playwright.config.ts index 4fb86b95..84afed64 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,11 +7,22 @@ Please see LICENSE in the repository root for full details. */ import { defineConfig, devices } from "@playwright/test"; +import { join } from "path"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; const baseURL = process.env.USE_DOCKER ? "http://localhost:8080" : "https://localhost:3000"; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Needed by the synapse admin API called in fixtures +process.env.NODE_EXTRA_CA_CERTS = join( + __dirname, + "backend/dev_tls_local-ca.crt", +); + /** * See https://playwright.dev/docs/test-configuration. */ diff --git a/playwright/utils/synapse-admin.ts b/playwright/utils/synapse-admin.ts new file mode 100644 index 00000000..b1d0039c --- /dev/null +++ b/playwright/utils/synapse-admin.ts @@ -0,0 +1,142 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { createHmac } from "crypto"; + +/** + * Response from Synapse registration API + */ +export interface SynapseRegistrationResponse { + access_token: string; + user_id: string; + home_server: string; + device_id: string; +} + +/** + * Utility class for interacting with Synapse Admin API + * This provides fast user registration without going through the UI + * + * @see https://matrix-org.github.io/synapse/latest/admin_api/register_api.html + */ +export class SynapseAdmin { + public constructor( + private baseUrl: string = "https://synapse.m.localhost", + private sharedSecret: string = "test_shared_secret_for_local_dev_only", + ) {} + + /** + * Register a user using the Synapse Admin API + * This is much faster than going through the UI registration flow + * + * @param username - The username (localpart) for the new user + * @param password - The password for the new user + * @param displayName - Optional display name (defaults to username) + * @param admin - Whether the user should be an admin (defaults to false) + * @returns Registration response containing access token and user ID + */ + public async registerUser( + username: string, + password: string, + displayName?: string, + admin: boolean = false, + ): Promise { + // Get a nonce first + const nonce = await this.getNonce(); + + // Generate the HMAC + const mac = this.generateMac(username, password, admin, nonce); + + // Make the registration request + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + nonce, + username, + password, + displayname: displayName || username, + admin, + mac, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error( + `Failed to register user ${username}: ${response.status} ${error}`, + ); + } + + return response.json(); + } + + /** + * Get a nonce for registration + * The nonce is required for the HMAC calculation + * + * @returns A nonce string + */ + private async getNonce(): Promise { + const response = await fetch(`${this.baseUrl}/_synapse/admin/v1/register`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error( + `Failed to get nonce: ${response.status} ${await response.text()}`, + ); + } + + const data = await response.json(); + return data.nonce; + } + + /** + * Generate HMAC for shared secret registration + * This is the authentication mechanism for the admin API + * + * @param username - The username + * @param password - The password + * @param admin - Whether the user is an admin + * @param nonce - The nonce from the server + * @returns The HMAC hex string + */ + private generateMac( + username: string, + password: string, + admin: boolean, + nonce: string, + ): string { + const mac = createHmac("sha1", this.sharedSecret); + mac.update(nonce); + mac.update("\x00"); + mac.update(username); + mac.update("\x00"); + mac.update(password); + mac.update("\x00"); + mac.update(admin ? "admin" : "notadmin"); + + return mac.digest("hex"); + } + + /** + * Create a new SynapseAdmin instance for a different homeserver + * + * @param baseUrl - The base URL of the homeserver + * @param sharedSecret - The shared secret (defaults to test secret) + * @returns A new SynapseAdmin instance + */ + public static forHomeserver( + baseUrl: string, + sharedSecret: string = "test_shared_secret_for_local_dev_only", + ): SynapseAdmin { + return new SynapseAdmin(baseUrl, sharedSecret); + } +} diff --git a/playwright/widget/test-helpers.ts b/playwright/widget/test-helpers.ts index ff5fa7e2..d0674f8a 100644 --- a/playwright/widget/test-helpers.ts +++ b/playwright/widget/test-helpers.ts @@ -13,6 +13,8 @@ import { } from "@playwright/test"; import { type MatrixClient } from "matrix-js-sdk"; +import { SynapseAdmin } from "../utils/synapse-admin.ts"; + const PASSWORD = "foobarbaz1!"; export const HOST1 = "https://app.m.localhost/#/welcome"; @@ -74,29 +76,41 @@ export class TestHelpers { clientHandle: JSHandle; mxId: string; }> { + // Determine which homeserver to use based on the host + const synapseBaseUrl = + host === HOST2 + ? "https://synapse.othersite.m.localhost" + : "https://synapse.m.localhost"; + + // Register user via Synapse Admin API to speed things up + const synapseAdmin = SynapseAdmin.forHomeserver(synapseBaseUrl); + const credentials = await synapseAdmin.registerUser( + username, + PASSWORD, + username, + ); + + // STEP 2: Open browser and login const userContext = await browser.newContext({ reducedMotion: "reduce", }); const page = await userContext.newPage(); - await page.goto(host); - await page.getByRole("link", { name: "Create Account" }).click(); + await page.goto(host + "/#/login"); + + await page.getByRole("link", { name: "Sign in" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill(username); - await page.getByRole("textbox", { name: "Password", exact: true }).click(); - await page - .getByRole("textbox", { name: "Password", exact: true }) - .fill(PASSWORD); - await page.getByRole("textbox", { name: "Confirm password" }).click(); - await page - .getByRole("textbox", { name: "Confirm password" }) - .fill(PASSWORD); - await page.getByRole("button", { name: "Register" }).click(); + await page.getByRole("textbox", { name: "Password" }).fill(PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + + // 😤For reasons web is staying on an infinite loading page after login, so we reload the page + // Super annoying to have to wait... + await page.waitForTimeout(2000); + await page.reload(); await expect( page.getByRole("heading", { name: `Welcome ${username}` }), - ).toBeVisible({ - // Increase timeout as registration can be slow :/ - timeout: 15_000, - }); + ).toBeVisible(); await this.maybeDismissBrowserNotSupportedToast(page); await this.maybeDismissServiceWorkerWarningToast(page); @@ -106,11 +120,7 @@ export class TestHelpers { const clientHandle = await page.evaluateHandle(() => window.mxMatrixClientPeg.get(), ); - const mxId = (await clientHandle.evaluate( - (cli: MatrixClient) => cli.getUserId(), - clientHandle, - ))!; - + const mxId = credentials.user_id; return { page, clientHandle, mxId }; }