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).
This commit is contained in:
Andrew Morgan
2026-02-05 14:04:33 +00:00
parent 306e21331b
commit 7c62578252
2 changed files with 251 additions and 80 deletions
@@ -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<MasConfig["http"]>["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<MasConfig> = {
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<SynapseConfig>)
.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)");
@@ -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<string, unknown> {
return {};
}
public override async start(): Promise<StartedSynapseContainer> {
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<string, unknown> {
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,
},
};
}
}