Skip to content
This repository was archived by the owner on Dec 23, 2024. It is now read-only.

Commit 36a8d50

Browse files
authored
Add Playwright tests for OIDC-aware & OIDC-native (matrix-org#12252)
* Resolve race condition between opening settings & well-known check in OIDC mode Signed-off-by: Michael Telatynski <[email protected]> * Add OIDC-aware and OIDC-native tests using MAS Signed-off-by: Michael Telatynski <[email protected]> --------- Signed-off-by: Michael Telatynski <[email protected]>
1 parent 6e73d65 commit 36a8d50

File tree

14 files changed

+798
-9
lines changed

14 files changed

+798
-9
lines changed

docs/playwright.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ to be left with some stray containers if, for example, you terminate a test such
8585
that the `after()` does not run and also exit Playwright uncleanly. All the containers
8686
it starts are prefixed, so they are easy to recognise. They can be removed safely.
8787

88-
After each test run, logs from the Synapse instances are saved in `playwright/synapselogs`
88+
After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse`
8989
with each instance in a separate directory named after its ID. These logs are removed
9090
at the start of each test run.
9191

playwright/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/test-results/
22
/html-report/
3-
/synapselogs/
3+
/logs/
44
# Only commit snapshots from Linux
55
/snapshots/**/*.png
66
!/snapshots/**/*-linux.png

playwright/e2e/oidc/index.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { API, Messages } from "mailhog";
18+
import { Page } from "@playwright/test";
19+
20+
import { test as base, expect } from "../../element-web-test";
21+
import { MatrixAuthenticationService } from "../../plugins/matrix-authentication-service";
22+
import { StartHomeserverOpts } from "../../plugins/homeserver";
23+
24+
export const test = base.extend<{
25+
masPrepare: MatrixAuthenticationService;
26+
mas: MatrixAuthenticationService;
27+
}>({
28+
// There's a bit of a chicken and egg problem between MAS & Synapse where they each need to know how to reach each other
29+
// so spinning up a MAS is split into the prepare & start stage: prepare mas -> homeserver -> start mas to disentangle this.
30+
masPrepare: async ({ context }, use) => {
31+
const mas = new MatrixAuthenticationService(context);
32+
await mas.prepare();
33+
await use(mas);
34+
},
35+
mas: [
36+
async ({ masPrepare: mas, homeserver, mailhog }, use, testInfo) => {
37+
await mas.start(homeserver, mailhog.instance);
38+
await use(mas);
39+
await mas.stop(testInfo);
40+
},
41+
{ auto: true },
42+
],
43+
startHomeserverOpts: async ({ masPrepare }, use) => {
44+
await use({
45+
template: "mas-oidc",
46+
variables: {
47+
MAS_PORT: masPrepare.port,
48+
},
49+
});
50+
},
51+
config: async ({ homeserver, startHomeserverOpts, context }, use) => {
52+
const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`;
53+
const wellKnown = {
54+
"m.homeserver": {
55+
base_url: homeserver.config.baseUrl,
56+
},
57+
"org.matrix.msc2965.authentication": {
58+
issuer,
59+
account: `${issuer}account`,
60+
},
61+
};
62+
63+
// Ensure org.matrix.msc2965.authentication is in well-known
64+
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
65+
await route.fulfill({ json: wellKnown });
66+
});
67+
68+
await use({
69+
default_server_config: wellKnown,
70+
});
71+
},
72+
});
73+
74+
export { expect };
75+
76+
export async function registerAccountMas(
77+
page: Page,
78+
mailhog: API,
79+
username: string,
80+
email: string,
81+
password: string,
82+
): Promise<void> {
83+
await expect(page.getByText("Please sign in to continue:")).toBeVisible();
84+
85+
await page.getByRole("link", { name: "Create Account" }).click();
86+
await page.getByRole("textbox", { name: "Username" }).fill(username);
87+
await page.getByRole("textbox", { name: "Email address" }).fill(email);
88+
await page.getByRole("textbox", { name: "Password", exact: true }).fill(password);
89+
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
90+
await page.getByRole("button", { name: "Continue" }).click();
91+
92+
let messages: Messages;
93+
await expect(async () => {
94+
messages = await mailhog.messages();
95+
expect(messages.items).toHaveLength(1);
96+
}).toPass();
97+
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
98+
const [code] = messages.items[0].text.match(/(\d{6})/);
99+
100+
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
101+
await page.getByRole("button", { name: "Continue" }).click();
102+
await expect(page.getByText("Allow access to your account?")).toBeVisible();
103+
await page.getByRole("button", { name: "Continue" }).click();
104+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test, expect, registerAccountMas } from ".";
18+
import { isDendrite } from "../../plugins/homeserver/dendrite";
19+
20+
test.describe("OIDC Aware", () => {
21+
test.skip(isDendrite, "does not yet support MAS");
22+
test.slow(); // trace recording takes a while here
23+
24+
test("can register an account and manage it", async ({ context, page, homeserver, mailhog, app }) => {
25+
await page.goto("/#/login");
26+
await page.getByRole("button", { name: "Continue" }).click();
27+
await registerAccountMas(page, mailhog.api, "alice", "[email protected]", "Pa$sW0rD!");
28+
29+
// Eventually, we should end up at the home screen.
30+
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
31+
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
32+
33+
// Open settings and navigate to account management
34+
await app.settings.openUserSettings("General");
35+
const newPagePromise = context.waitForEvent("page");
36+
await page.getByRole("button", { name: "Manage account" }).click();
37+
38+
// Assert new tab opened
39+
const newPage = await newPagePromise;
40+
await expect(newPage.getByText("Primary email")).toBeVisible();
41+
});
42+
});
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { test, expect, registerAccountMas } from ".";
18+
import { isDendrite } from "../../plugins/homeserver/dendrite";
19+
20+
test.describe("OIDC Native", () => {
21+
test.skip(isDendrite, "does not yet support MAS");
22+
test.slow(); // trace recording takes a while here
23+
24+
test.use({
25+
labsFlags: ["feature_oidc_native_flow"],
26+
});
27+
28+
test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, app, mas }) => {
29+
const tokenUri = `http://localhost:${mas.port}/oauth2/token`;
30+
const tokenApiPromise = page.waitForRequest(
31+
(request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code",
32+
);
33+
34+
await page.goto("/#/login");
35+
await page.getByRole("button", { name: "Continue" }).click();
36+
await registerAccountMas(page, mailhog.api, "alice", "[email protected]", "Pa$sW0rD!");
37+
38+
// Eventually, we should end up at the home screen.
39+
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
40+
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
41+
42+
const tokenApiRequest = await tokenApiPromise;
43+
expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code");
44+
45+
const deviceId = await page.evaluate<string>(() => window.localStorage.mx_device_id);
46+
47+
await app.settings.openUserSettings("General");
48+
const newPagePromise = context.waitForEvent("page");
49+
await page.getByRole("button", { name: "Manage account" }).click();
50+
await app.settings.closeDialog();
51+
52+
// Assert MAS sees the session as OIDC Native
53+
const newPage = await newPagePromise;
54+
await newPage.getByText("Sessions").click();
55+
await newPage.getByText(deviceId).click();
56+
await expect(newPage.getByText("Element")).toBeVisible();
57+
await expect(newPage.getByText("oauth2_session:")).toBeVisible();
58+
await expect(newPage.getByText("http://localhost:8080/")).toBeVisible();
59+
await newPage.close();
60+
61+
// Assert logging out revokes both tokens
62+
const revokeUri = `http://localhost:${mas.port}/oauth2/revoke`;
63+
const revokeAccessTokenPromise = page.waitForRequest(
64+
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token",
65+
);
66+
const revokeRefreshTokenPromise = page.waitForRequest(
67+
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "refresh_token",
68+
);
69+
const locator = await app.settings.openUserMenu();
70+
await locator.getByRole("menuitem", { name: "Sign out", exact: true }).click();
71+
await revokeAccessTokenPromise;
72+
await revokeRefreshTokenPromise;
73+
});
74+
});

playwright/plugins/homeserver/synapse/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export class Synapse implements Homeserver, HomeserverInstance {
134134
public async stop(): Promise<string[]> {
135135
if (!this.config) throw new Error("Missing existing synapse instance, did you call stop() before start()?");
136136
const id = this.config.serverId;
137-
const synapseLogsPath = path.join("playwright", "synapselogs", id);
137+
const synapseLogsPath = path.join("playwright", "logs", "synapse", id);
138138
await fse.ensureDir(synapseLogsPath);
139139
await this.docker.persistLogsToFile({
140140
stdoutFile: path.join(synapseLogsPath, "stdout.log"),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A synapse configured with auth delegated to via matrix authentication service

0 commit comments

Comments
 (0)