diff --git a/apps/web/playwright/e2e/retention/retention-global.spec.ts b/apps/web/playwright/e2e/retention/retention-global.spec.ts new file mode 100644 index 0000000000..3ff37ce49a --- /dev/null +++ b/apps/web/playwright/e2e/retention/retention-global.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Element Creations 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 { rejectToastIfExists } from "@element-hq/element-web-playwright-common"; + +import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { checkRetentionInRoom } from "./utils"; +import { test } from "../../element-web-test"; + +const ONE_MINUTE_STR = "60s"; + +test.use({ + synapseConfig: { + retention: { + enabled: true, + default_policy: { + max_lifetime: ONE_MINUTE_STR, + }, + allowed_lifetime_min: ONE_MINUTE_STR, + allowed_lifetime_max: ONE_MINUTE_STR, + }, + experimental_features: { + msc1763_enabled: true, + }, + }, +}); +test.describe("global retention rules", () => { + test.skip(isDendrite, "dendrite does not support retention"); + test.use({ + displayName: "Tom", + botCreateOpts: { + displayName: "Bob", + }, + labsFlags: ["feature_retention"], + }); + test.beforeEach(async ({ app, homeserver, page, user }) => { + await rejectToastIfExists(page, "Verify this device"); + await rejectToastIfExists(page, "Notifications"); + await page.clock.install(); + }); + + test("should apply", async ({ app, bot, page }) => { + const roomId = await app.client.createRoom({ + name: "Test", + invite: [bot.credentials.userId], + }); + await checkRetentionInRoom({ app, bot, page }, roomId); + }); +}); diff --git a/apps/web/playwright/e2e/retention/retention.spec.ts b/apps/web/playwright/e2e/retention/retention.spec.ts index f1440e4c4e..9988e57412 100644 --- a/apps/web/playwright/e2e/retention/retention.spec.ts +++ b/apps/web/playwright/e2e/retention/retention.spec.ts @@ -7,30 +7,28 @@ import { rejectToastIfExists } from "@element-hq/element-web-playwright-common"; -import { test, expect, type TestFixtures } from "../../element-web-test"; -import type { Page } from "@playwright/test"; +import { isDendrite } from "../../plugins/homeserver/dendrite"; +import { test, expect } from "../../element-web-test"; +import { checkRetentionInRoom, ONE_MINUTE } from "./utils"; -const ONE_MINUTE = 60 * 1000; +const ONE_MINUTE_STR = "60s"; -async function checkRetentionInRoom( - { bot, app, page }: Pick & { page: Page }, - roomId: string, -) { - await bot.joinRoom(roomId); - await app.viewRoomById(roomId); - const tiles = ( - await Promise.all(Array.from({ length: 5 }).map((_o, index) => bot.sendMessage(roomId, `Message ${index}`))) - ).map(({ event_id: evtId }) => page.locator(`.mx_RoomView_MessageList .mx_EventTile[data-event-id='${evtId}']`)); - for (const tile of tiles) { - await expect(tile).toBeVisible(); - } - await page.clock.fastForward(ONE_MINUTE + 1); - for (const tile of tiles) { - await expect(tile).toBeHidden(); - } -} +test.use({ + synapseConfig: { + retention: { + enabled: true, + // deliberately no default policy. + allowed_lifetime_min: ONE_MINUTE_STR, + allowed_lifetime_max: ONE_MINUTE_STR, + }, + experimental_features: { + msc1763_enabled: true, + }, + }, +}); test.describe("Retention", () => { + test.skip(isDendrite, "dendrite does not support retention"); test.use({ displayName: "Tom", botCreateOpts: { @@ -65,32 +63,6 @@ test.describe("Retention", () => { await checkRetentionInRoom({ app, bot, page }, roomId); }); - test.describe("global retention rules", () => { - test.use({ - page: async ({ page }, runFixture) => { - await page.route("**/_matrix/client/unstable/org.matrix.msc1763/retention/configuration", (route) => { - return route.fulfill({ - json: { - policies: { - "*": { - max_lifetime: ONE_MINUTE, - }, - }, - }, - }); - }); - await runFixture(page); - }, - }); - test("should apply", async ({ app, bot, page }) => { - const roomId = await app.client.createRoom({ - name: "Test", - invite: [bot.credentials.userId], - }); - await checkRetentionInRoom({ app, bot, page }, roomId); - }); - }); - test("retention rules should apply after restart", async ({ app, bot, page }) => { const roomId = await app.client.createRoom({ name: "Test", diff --git a/apps/web/playwright/e2e/retention/utils.ts b/apps/web/playwright/e2e/retention/utils.ts new file mode 100644 index 0000000000..d94167dd8c --- /dev/null +++ b/apps/web/playwright/e2e/retention/utils.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Element Creations 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 { expect, type TestFixtures } from "../../element-web-test"; +import type { Page } from "@playwright/test"; + +export const ONE_MINUTE = 60 * 1000; + +export async function checkRetentionInRoom( + { bot, app, page }: Pick & { page: Page }, + roomId: string, +) { + await bot.joinRoom(roomId); + await app.viewRoomById(roomId); + const tiles = ( + await Promise.all(Array.from({ length: 5 }).map((_o, index) => bot.sendMessage(roomId, `Message ${index}`))) + ).map(({ event_id: evtId }) => page.locator(`.mx_RoomView_MessageList .mx_EventTile[data-event-id='${evtId}']`)); + for (const tile of tiles) { + await expect(tile).toBeVisible(); + } + await page.clock.fastForward(ONE_MINUTE + 1); + for (const tile of tiles) { + await expect(tile).toBeHidden(); + } +} diff --git a/apps/web/playwright/testcontainers/synapse.ts b/apps/web/playwright/testcontainers/synapse.ts index a8e7bd761b..2f110d2d25 100644 --- a/apps/web/playwright/testcontainers/synapse.ts +++ b/apps/web/playwright/testcontainers/synapse.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js"; const DOCKER_IMAGE = - "ghcr.io/element-hq/synapse:develop@sha256:9ec4c58ea1384d3a854cd4b25a56b5a9e98b7b0c36844c8e3a442f2a9aa0c5bc"; + "ghcr.io/element-hq/synapse:develop@sha256:34656f82f110460b44c4ec7b77830b04ee325f0f7dc423ec2f95810737ba357e"; /** * SynapseContainer which freezes the docker digest to stabilise tests, diff --git a/packages/playwright-common/src/testcontainers/synapse.ts b/packages/playwright-common/src/testcontainers/synapse.ts index 65d92c0ab9..eecb1bbbea 100644 --- a/packages/playwright-common/src/testcontainers/synapse.ts +++ b/packages/playwright-common/src/testcontainers/synapse.ts @@ -25,7 +25,174 @@ import { type StartedMatrixAuthenticationServiceContainer } from "./mas.js"; import { Api, ClientServerApi, type Verb, type Credentials } from "../utils/api.js"; import { type StartedMailpitContainer } from "./mailpit.js"; -const DEFAULT_CONFIG = { +/** + * Incomplete type describing the configuration for a Synapse homeserver + */ +export interface SynapseConfig { + server_name: string; + public_baseurl: string; + pid_file: string; + web_client: boolean; + soft_file_limit: number; + log_config: string; + listeners: Array<{ + port: number; + tls: boolean; + bind_addresses: string[]; + type: string; + x_forwarded: boolean; + resources: Array<{ + names: string[]; + compress: boolean; + }>; + }>; + database: { + name: string; + args: { + database: string; + }; + }; + rc_messages_per_second: number; + rc_message_burst_count: number; + rc_registration: { + per_second: number; + burst_count: number; + }; + rc_joins: { + local: { + per_second: number; + burst_count: number; + }; + remote: { + per_second: number; + burst_count: number; + }; + }; + rc_joins_per_room: { + per_second: number; + burst_count: number; + }; + rc_3pid_validation: { + per_second: number; + burst_count: number; + }; + rc_invites: { + per_room: { + per_second: number; + burst_count: number; + }; + per_user: { + per_second: number; + burst_count: number; + }; + }; + rc_login: { + address: { + per_second: number; + burst_count: number; + }; + account: { + per_second: number; + burst_count: number; + }; + failed_attempts: { + per_second: number; + burst_count: number; + }; + }; + rc_room_creation: { + per_second: number; + burst_count: number; + }; + media_store_path: string; + max_upload_size: string; + max_image_pixels: string; + dynamic_thumbnails: boolean; + enable_registration: boolean; + enable_registration_without_verification: boolean; + disable_msisdn_registration: boolean; + registrations_require_3pid: string[]; + enable_metrics: boolean; + report_stats: boolean; + registration_shared_secret: string; + macaroon_secret_key: string; + form_secret: string; + signing_key_path: string; + trusted_key_servers: never[]; + password_config: { + enabled: boolean; + }; + ui_auth?: { + session_timeout: `${number}s`; + }; + background_updates: { + min_batch_size: number; + sleep_duration_ms: number; + }; + enable_authenticated_media: boolean; + email?: { + enable_notifs: boolean; + smtp_host: string; + smtp_port: number; + smtp_user: string; + smtp_pass: string; + require_transport_security: false; + notif_from: string; + app_name: string; + notif_template_html: string; + notif_template_text: string; + notif_for_new_users: boolean; + client_base_url: string; + }; + user_consent?: { + template_dir: string; + version: string; + server_notice_content: Record; + send_server_notice_to_guests: boolean; + block_events_error: string; + require_at_registration: boolean; + }; + server_notices?: { + system_mxid_localpart: string; + system_mxid_display_name: string; + system_mxid_avatar_url: string; + room_name: string; + }; + allow_guest_access: boolean; + experimental_features: Record; + matrix_rtc?: { + transports: Array<{ type: string; [field: string]: string }>; + }; + oidc_providers: unknown[]; + serve_server_wellknown: boolean; + presence: { + enabled: boolean; + include_offline_users_on_sync: boolean; + }; + room_list_publication_rules: Array<{ action: string }>; + modules: Array<{ module: string; config?: Record }>; + matrix_authentication_service?: { + enabled?: boolean; + endpoint?: string; + secret?: string | null; + secret_path?: string | null; + }; + /** + * Server-wide retention rules. + * @see https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html?highlight=retent#retention + */ + retention?: { + enabled: boolean; + default_policy?: { + min_lifetime?: string; + max_lifetime?: string; + }; + allowed_lifetime_min?: string; + allowed_lifetime_max?: string; + }; +} + +const DEFAULT_CONFIG: SynapseConfig = { server_name: "localhost", public_baseurl: "", // set by start method pid_file: "/homeserver.pid", @@ -128,54 +295,15 @@ const DEFAULT_CONFIG = { password_config: { enabled: true, }, - ui_auth: {}, 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 as - | undefined - | { - enable_notifs: boolean; - smtp_host: string; - smtp_port: number; - smtp_user: string; - smtp_pass: string; - require_transport_security: false; - notif_from: string; - app_name: string; - notif_template_html: string; - notif_template_text: string; - notif_for_new_users: boolean; - client_base_url: string; - }, - user_consent: undefined as - | undefined - | { - template_dir: string; - version: string; - server_notice_content: Record; - send_server_notice_to_guests: boolean; - block_events_error: string; - require_at_registration: boolean; - }, - server_notices: undefined as - | undefined - | { - system_mxid_localpart: string; - system_mxid_display_name: string; - system_mxid_avatar_url: string; - room_name: string; - }, allow_guest_access: false, - experimental_features: {} as Record, - matrix_rtc: undefined as - | undefined - | { - transports: Array<{ type: string; [field: string]: string }>; - }, + experimental_features: {}, + matrix_rtc: undefined, oidc_providers: [], serve_server_wellknown: true, presence: { @@ -183,22 +311,9 @@ const DEFAULT_CONFIG = { include_offline_users_on_sync: true, }, room_list_publication_rules: [{ action: "allow" }], - modules: [] as Array<{ module: string; config?: Record }>, - matrix_authentication_service: undefined as - | undefined - | { - enabled?: boolean; - endpoint?: string; - secret?: string | null; - secret_path?: string | null; - }, + modules: [], }; -/** - * Incomplete type describing the configuration for a Synapse homeserver - */ -export type SynapseConfig = typeof DEFAULT_CONFIG; - /** * A Synapse testcontainer *