From 7c62578252e900c56f8d1110589e12814a8d9ddd Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Mon, 19 Jan 2026 17:53:01 +0000 Subject: [PATCH] Create MAS container and tests for restricted guests testing Add a new container that both: - Spins up a MAS instance. - Loads the restricted guests module into Synapse, and configures it accordingly. Then extend restricted guests spec to include MAS tests. This requires setting up MAS' config to recognise the module as an automated client (so that it can request Admin perms to create/deactivate users). --- .../tests/restricted-guests.spec.ts | 301 +++++++++++++----- .../element-web/tests/services.ts | 30 +- 2 files changed, 251 insertions(+), 80 deletions(-) diff --git a/modules/restricted-guests/element-web/tests/restricted-guests.spec.ts b/modules/restricted-guests/element-web/tests/restricted-guests.spec.ts index f8f180a0cb..964cbdbd46 100644 --- a/modules/restricted-guests/element-web/tests/restricted-guests.spec.ts +++ b/modules/restricted-guests/element-web/tests/restricted-guests.spec.ts @@ -5,41 +5,115 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE files in the repository root for full details. */ -import { StartedSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; -import { Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api.ts"; +import { + MatrixAuthenticationServiceContainer, + type MasConfig, + type StartedMatrixAuthenticationServiceContainer, + type StartedSynapseContainer, + type SynapseConfig, +} from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; +import type { Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api"; +import type { Fixtures } from "@playwright/test"; -import { test as base, expect } from "../../../../playwright/element-web-test.ts"; -import { RestrictedGuestsSynapseContainer } from "./services.ts"; +import { test as base, expect } from "../../../../playwright/element-web-test"; +import { RestrictedGuestsSynapseContainer, RestrictedGuestsSynapseWithMasContainer } from "./services"; -const test = base.extend< +const MAS_CLIENT_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"; +const MAS_CLIENT_SECRET = "restricted-guests-secret"; +const MAS_SHARED_SECRET = "restricted-guests-shared-secret"; +const MAS_INTERNAL_URL = "http://mas:8080"; +const GUEST_HOMESERVER_NAME = "guest-homeserver"; +const GUEST_HOMESERVER_INTERNAL_URL = "http://guest-homeserver:8008"; + +const MAS_HTTP_LISTENERS: NonNullable["listeners"] = [ { - testRoomId: string; + name: "web", + resources: [ + { name: "discovery" }, + { name: "human" }, + { name: "oauth" }, + { name: "compat" }, + { name: "graphql" }, + { name: "assets" }, + { name: "adminapi" }, + ], + binds: [ + { + address: "[::]:8080", + }, + ], + proxy_protocol: false, }, { - guestHomeserver: StartedSynapseContainer; - bot: Credentials; - } ->({ + name: "internal", + resources: [ + { + name: "health", + }, + ], + binds: [ + { + address: "[::]:8081", + }, + ], + proxy_protocol: false, + }, +]; + +const MAS_CONFIG: Partial = { + http: { + listeners: MAS_HTTP_LISTENERS, + public_base: "", + }, + matrix: { + kind: "synapse", + homeserver: GUEST_HOMESERVER_NAME, + endpoint: GUEST_HOMESERVER_INTERNAL_URL, + secret: MAS_SHARED_SECRET, + }, + policy: { + data: { + admin_clients: [MAS_CLIENT_ID], + client_registration: { + allow_insecure_uris: true, + }, + }, + }, + clients: [ + { + client_id: MAS_CLIENT_ID, + client_auth_method: "client_secret_basic", + client_secret: MAS_CLIENT_SECRET, + }, + ], +}; + +const applySharedTestConfig = (testInstance: typeof base) => { + testInstance.use({ + displayName: "Tommy", + synapseConfig: { + allow_guest_access: true, + }, + labsFlags: ["feature_ask_to_join"], + }); +}; + +const sharedFixtures: Fixtures<{ testRoomId: string }, { bot: Credentials }, any, any> = { testRoomId: [ async ({ homeserver, bot }, use) => { - const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>( - "POST", - "/v3/createRoom", - bot.accessToken, - { - name: "Test room", - preset: "public_chat", - topic: "All about happy hour", - initial_state: [ - { - // This is required to allow guests to join the room with this Synapse module - type: "m.room.join_rule", - state_key: "", - content: { join_rule: "knock" }, - }, - ], - }, - ); + const { room_id: roomId } = (await homeserver.csApi.request("POST", "/v3/createRoom", bot.accessToken, { + name: "Test room", + preset: "public_chat", + topic: "All about happy hour", + initial_state: [ + { + // This is required to allow guests to join the room with this Synapse module + type: "m.room.join_rule", + state_key: "", + content: { join_rule: "knock" }, + }, + ], + })) as { room_id: string }; await use(roomId); }, { scope: "test" }, @@ -51,13 +125,25 @@ const test = base.extend< }, { scope: "worker" }, ], +}; + +const test = base.extend< + { + testRoomId: string; + }, + { + guestHomeserver: StartedSynapseContainer; + bot: Credentials; + } +>({ + ...sharedFixtures, guestHomeserver: [ async ({ logger, synapseConfig, network }, use) => { const container = await new RestrictedGuestsSynapseContainer() .withConfig(synapseConfig) - .withConfig({ server_name: "guest-homeserver" }) + .withConfig({ server_name: GUEST_HOMESERVER_NAME }) .withNetwork(network) - .withNetworkAliases("guest-homeserver") + .withNetworkAliases(GUEST_HOMESERVER_NAME) .withLogConsumer(logger.getConsumer("guest_homeserver")) .start(); @@ -68,66 +154,123 @@ const test = base.extend< ], }); -test.use({ - displayName: "Tommy", - synapseConfig: { - allow_guest_access: true, +const masTest = base.extend< + { + testRoomId: string; }, - labsFlags: ["feature_ask_to_join"], + { + guestHomeserver: StartedSynapseContainer; + guestMas: StartedMatrixAuthenticationServiceContainer; + bot: Credentials; + } +>({ + ...sharedFixtures, + guestMas: [ + async ({ logger, network, postgres }, use) => { + const container = await new MatrixAuthenticationServiceContainer(postgres) + .withNetwork(network) + .withNetworkAliases("mas") + .withLogConsumer(logger.getConsumer("guest_mas")) + .withConfig(MAS_CONFIG) + .start(); + + await use(container); + await container.stop(); + }, + { scope: "worker" }, + ], + guestHomeserver: [ + async ({ logger, synapseConfig, network, guestMas }, use) => { + const container = await new RestrictedGuestsSynapseWithMasContainer({ + adminApiBaseUrl: MAS_INTERNAL_URL, + oauthBaseUrl: MAS_INTERNAL_URL, + clientId: MAS_CLIENT_ID, + clientSecret: MAS_CLIENT_SECRET, + }) + .withConfig(synapseConfig) + .withConfig({ + server_name: GUEST_HOMESERVER_NAME, + matrix_authentication_service: { + enabled: true, + endpoint: `${MAS_INTERNAL_URL}/`, + secret: MAS_SHARED_SECRET, + }, + } as Partial) + .withMatrixAuthenticationService(guestMas) + .withNetwork(network) + .withNetworkAliases(GUEST_HOMESERVER_NAME) + .withLogConsumer(logger.getConsumer("guest_homeserver")) + .start(); + + await use(container); + await container.stop(); + }, + { scope: "worker" }, + ], }); -test.describe("Restricted Guests", () => { - test.use({ - page: async ({ page, homeserver, guestHomeserver }, use) => { - await page.goto("/"); - await use(page); - }, - }); +type RestrictedGuestsTestInstance = typeof test; - test("should error if config is missing", async ({ page }) => { - await expect(page.getByText("Your Element is misconfigured")).toBeVisible(); - await expect(page.getByText("Errors in module configuration")).toBeVisible(); - }); +const defineRestrictedGuestsTests = (testInstance: RestrictedGuestsTestInstance, suiteName: string) => { + applySharedTestConfig(testInstance); - test.describe("with config", () => { - test.beforeEach(({ config, guestHomeserver }) => { - config["io.element.element-web-modules.restricted-guests"] = { - guest_user_homeserver_url: guestHomeserver.baseUrl, - }; + testInstance.describe(suiteName, () => { + testInstance.use({ + page: async ({ page }, use) => { + await page.goto("/"); + await use(page); + }, }); - test( - "should show the default room preview bar for logged in users", - { tag: ["@screenshot"] }, - async ({ page, user, testRoomId }) => { - // Go to a room we are not a member of - await page.goto(`/#/room/${testRoomId}`); + testInstance("should error if config is missing", async ({ page }) => { + await expect(page.getByText("Your Element is misconfigured")).toBeVisible(); + await expect(page.getByText("Errors in module configuration")).toBeVisible(); + }); - const button = page.getByRole("button", { name: "Join the discussion" }); - await expect(button).toBeVisible(); - }, - ); + testInstance.describe("with config", () => { + testInstance.beforeEach(({ config, guestHomeserver }) => { + config["io.element.element-web-modules.restricted-guests"] = { + guest_user_homeserver_url: guestHomeserver.baseUrl, + }; + }); - test( - "should show the module's room preview bar for guests", - { tag: ["@screenshot"] }, - async ({ page, testRoomId }) => { - // Go to a room we are not a member of - await page.goto(`/#/room/${testRoomId}`); + testInstance( + "should show the default room preview bar for logged in users", + { tag: ["@screenshot"] }, + async ({ page, user, testRoomId }) => { + // Go to a room we are not a member of + await page.goto(`/#/room/${testRoomId}`); - const button = page.getByRole("button", { name: "Join", exact: true }); - await expect(button).toBeVisible(); - await expect(page.locator(".mx_RoomPreviewBar")).toMatchScreenshot("preview-bar.png"); + const button = page.getByRole("button", { name: "Join the discussion" }); + await expect(button).toBeVisible(); + }, + ); - await button.click(); - const dialog = page.getByRole("dialog"); - await expect(dialog).toMatchScreenshot("dialog.png"); + testInstance( + "should show the module's room preview bar for guests", + { tag: ["@screenshot"] }, + async ({ page, testRoomId }) => { + // Go to a room we are not a member of + await page.goto(`/#/room/${testRoomId}`); - await dialog.getByPlaceholder("Name").fill("Jim"); - await dialog.getByRole("button", { name: "Continue as guest" }).click(); + const button = page.getByRole("button", { name: "Join", exact: true }); + await expect(button).toBeVisible(); + await expect(page.locator(".mx_RoomPreviewBar")).toMatchScreenshot(`preview-bar.png`); - await expect(page.getByText("Ask to join?")).toBeVisible(); - }, - ); + await button.click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toMatchScreenshot(`dialog.png`); + + await dialog.getByPlaceholder("Name").fill("Jim"); + await dialog.getByRole("button", { name: "Continue as guest" }).click(); + + await expect(page.getByText("Ask to join?")).toBeVisible(); + }, + ); + }); }); -}); +}; + +// The screenshots between the two tests should be identical. +defineRestrictedGuestsTests(test, "Restricted Guests"); +defineRestrictedGuestsTests(masTest as RestrictedGuestsTestInstance, "Restricted Guests (MAS)"); diff --git a/modules/restricted-guests/element-web/tests/services.ts b/modules/restricted-guests/element-web/tests/services.ts index 6b56108b06..c0c9f4c33d 100644 --- a/modules/restricted-guests/element-web/tests/services.ts +++ b/modules/restricted-guests/element-web/tests/services.ts @@ -16,6 +16,10 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // We use the SynapseContainer as a base to have all of its utilities for config setting export class RestrictedGuestsSynapseContainer extends SynapseContainer { + protected getModuleConfig(): Record { + return {}; + } + public override async start(): Promise { this.withCopyDirectoriesToContainer([ { @@ -27,9 +31,33 @@ export class RestrictedGuestsSynapseContainer extends SynapseContainer { }); this.config.modules.push({ module: "synapse_guest_module.GuestModule", - config: {}, + config: this.getModuleConfig(), }); return super.start(); } } + +export interface RestrictedGuestsMasModuleConfig { + adminApiBaseUrl: string; + oauthBaseUrl?: string; + clientId: string; + clientSecret: string; +} + +export class RestrictedGuestsSynapseWithMasContainer extends RestrictedGuestsSynapseContainer { + public constructor(private readonly masConfig: RestrictedGuestsMasModuleConfig) { + super(); + } + + protected override getModuleConfig(): Record { + return { + mas: { + admin_api_base_url: this.masConfig.adminApiBaseUrl, + oauth_base_url: this.masConfig.oauthBaseUrl ?? this.masConfig.adminApiBaseUrl, + client_id: this.masConfig.clientId, + client_secret: this.masConfig.clientSecret, + }, + }; + } +}