Update e2e tests and header
Don't add normalisation of widget headers either
This commit is contained in:
@@ -19,7 +19,6 @@ This is useful when widgets have multiple routes or capabilities include variabl
|
||||
|
||||
## Matching and precedence
|
||||
|
||||
- Widget URLs are normalized by stripping query parameters and hash fragments before matching.
|
||||
- Patterns ending in `*` are matched by prefix; other patterns must match exactly.
|
||||
- If multiple rules match, the most specific match wins per field.
|
||||
- The capabilities allow-list is not merged across rules; the most specific rule that defines it wins.
|
||||
|
||||
@@ -47,6 +47,8 @@ test.use({
|
||||
|
||||
test.describe("Widget Lifecycle", () => {
|
||||
test.describe("trusted widgets", () => {
|
||||
// Configure the module to pre-approve the widget URL for preloading, identity tokens,
|
||||
// and the m.room.topic state event capability.
|
||||
test.use({
|
||||
config: {
|
||||
"io.element.element-web-modules.widget-lifecycle": {
|
||||
@@ -62,6 +64,9 @@ test.describe("Widget Lifecycle", () => {
|
||||
});
|
||||
|
||||
test("auto-approves preload and identity", async ({ page, user, homeserver }, testInfo) => {
|
||||
// A bot creates a room with the widget pinned to the top panel, then invites the test user.
|
||||
// Because the widget was added by a different user (the bot), Element would normally show a
|
||||
// preload consent dialog before loading it — this test verifies that dialog is skipped.
|
||||
const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot");
|
||||
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
|
||||
"POST",
|
||||
@@ -102,6 +107,11 @@ test.describe("Widget Lifecycle", () => {
|
||||
await page.getByText("Trusted Widget").click();
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// No preload dialog should appear — the widget loads immediately.
|
||||
await expect(page.getByRole("button", { name: "Continue" })).not.toBeVisible();
|
||||
|
||||
// The widget greets the user by ID, proving the identity token was also auto-approved
|
||||
// and passed to the widget without any consent prompts.
|
||||
await expect(
|
||||
page
|
||||
.frameLocator('iframe[title="Trusted Widget"]')
|
||||
@@ -119,6 +129,7 @@ test.describe("Widget Lifecycle", () => {
|
||||
name: "Capabilities Widget",
|
||||
},
|
||||
);
|
||||
// The widget requests two capabilities: m.room.topic (in the allowlist) and m.room.name (not in the allowlist).
|
||||
await homeserver.csApi.request<{ event_id: string }>(
|
||||
"PUT",
|
||||
`/v3/rooms/${encodeURIComponent(roomId)}/state/im.vector.modular.widgets/1`,
|
||||
@@ -150,11 +161,13 @@ test.describe("Widget Lifecycle", () => {
|
||||
await page.getByText("Capabilities Widget").click();
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// A capabilities approval dialog should appear since m.room.name was not pre-approved.
|
||||
await expect(page.getByRole("button", { name: "Approve" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("untrusted widgets", () => {
|
||||
// No widget URLs are pre-approved, so all lifecycle prompts should be shown to the user.
|
||||
test.use({
|
||||
config: {
|
||||
"io.element.element-web-modules.widget-lifecycle": {
|
||||
@@ -163,7 +176,11 @@ test.describe("Widget Lifecycle", () => {
|
||||
},
|
||||
});
|
||||
|
||||
test("shows dialogs for untrusted widgets", async ({ page, user, homeserver }, testInfo) => {
|
||||
test("shows preload, capabilities, and OpenID dialogs for untrusted widgets", async ({
|
||||
page,
|
||||
user,
|
||||
homeserver,
|
||||
}, testInfo) => {
|
||||
const bot = await homeserver.registerUser(`bot_${testInfo.testId}`, "password", "Bot");
|
||||
const { room_id: roomId } = await homeserver.csApi.request<{ room_id: string }>(
|
||||
"POST",
|
||||
@@ -204,6 +221,15 @@ test.describe("Widget Lifecycle", () => {
|
||||
await page.getByText("Untrusted Widget").click();
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
// 1. Preload consent dialog — shown because the widget was added by another user (the bot).
|
||||
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// 2. Capabilities dialog — the widget requests m.room.topic once it loads.
|
||||
await expect(page.getByRole("button", { name: "Approve" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Approve" }).click();
|
||||
|
||||
// 3. OpenID identity dialog — the widget requests an identity token after capabilities are granted.
|
||||
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,11 +6,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { Api, Module, WidgetDescriptor, WidgetLifecycleApi } from "@element-hq/element-web-module-api";
|
||||
|
||||
import { CONFIG_KEY, parseWidgetLifecycleConfig, type WidgetLifecycleModuleConfig } from "./config";
|
||||
import { constructWidgetPermissions } from "./utils/constructWidgetPermissions";
|
||||
import { matchPattern } from "./utils/matchPattern";
|
||||
import { normalizeWidgetUrl } from "./utils/normalizeWidgetUrl";
|
||||
|
||||
/** Subset of {@link WidgetLifecycleApi} used by the module for registration only. */
|
||||
export type WidgetLifecycleApiAdapter = Pick<
|
||||
@@ -53,14 +51,12 @@ export default class WidgetLifecycleModule implements Module {
|
||||
}
|
||||
|
||||
private preapprovePreload(widget: WidgetDescriptor): boolean {
|
||||
const normalizedUrl = normalizeWidgetUrl(widget.templateUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, normalizedUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
return configuration.preload_approved === true;
|
||||
}
|
||||
|
||||
private preapproveIdentity(widget: WidgetDescriptor): boolean {
|
||||
const normalizedUrl = normalizeWidgetUrl(widget.templateUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, normalizedUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
return configuration.identity_approved === true;
|
||||
}
|
||||
|
||||
@@ -68,8 +64,7 @@ export default class WidgetLifecycleModule implements Module {
|
||||
widget: WidgetDescriptor,
|
||||
requestedCapabilities: Set<string>,
|
||||
): Set<string> | undefined {
|
||||
const normalizedUrl = normalizeWidgetUrl(widget.templateUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, normalizedUrl);
|
||||
const configuration = constructWidgetPermissions(this.config, widget.templateUrl);
|
||||
const capabilitiesApproved = configuration.capabilities_approved;
|
||||
|
||||
if (!capabilitiesApproved) return undefined;
|
||||
|
||||
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { ModuleFactory } from "@element-hq/element-web-module-api";
|
||||
|
||||
import WidgetLifecycleModule from "./WidgetLifecycleModule";
|
||||
|
||||
export default WidgetLifecycleModule satisfies ModuleFactory;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Strips query parameters and fragment from a widget URL for matching against config patterns.
|
||||
*/
|
||||
export function normalizeWidgetUrl(widgetUrl: string): string {
|
||||
const url = new URL(widgetUrl);
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.toString();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ const createApi = (config: unknown = {}) => {
|
||||
|
||||
const widget: WidgetDescriptor = {
|
||||
id: "widget-id",
|
||||
templateUrl: "https://example.com",
|
||||
templateUrl: "https://example.com/",
|
||||
creatorUserId: "@user-id",
|
||||
kind: "room",
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { matchPattern } from "../src/utils/matchPattern";
|
||||
|
||||
describe("matchPattern", () => {
|
||||
it.each([
|
||||
"*",
|
||||
"org.matrix.msc2762.receive.*",
|
||||
"org.matrix.msc2762.receive.state_event:*",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom*",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom#*",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom#state_key",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom#state_key*",
|
||||
])("matches %s", (pattern) => {
|
||||
expect(matchPattern("org.matrix.msc2762.receive.state_event:m.custom#state_key", pattern)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"org.matrix.msc2762.receive.state_event:",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom",
|
||||
"org.matrix.msc2762.receive.state_event:m.custom#other_key",
|
||||
"org.matrix.msc2762.receive.*:m.custom#state_key",
|
||||
"org.matrix.msc2762.receive.room_event:m.custom",
|
||||
])("does not match %s", (pattern) => {
|
||||
expect(matchPattern("org.matrix.msc2762.receive.state_event:m.custom#state_key", pattern)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user