Port remaining login.spec.ts & soft_logout.spec.ts tests from Cypress to Playwright (#11917)

Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
Michael Telatynski 2023-11-23 10:27:11 +00:00 committed by GitHub
parent 8dcd13eb6d
commit a6705304aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 465 additions and 386 deletions

View file

@ -1,103 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { doTokenRegistration } from "./utils";
describe("Login", () => {
let homeserver: HomeserverInstance;
afterEach(() => {
cy.stopHomeserver(homeserver);
});
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
describe("SSO login", () => {
beforeEach(() => {
cy.task("startOAuthServer")
.then((oAuthServerPort: number) => {
return cy.startHomeserver({ template: "default", oAuthServerPort });
})
.then((data) => {
homeserver = data;
});
});
afterEach(() => {
cy.task("stopOAuthServer");
});
it("logs in with SSO and lands on the home screen", () => {
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
// your firewall settings: Synapse is unable to reach the OIDC server.
//
// If you are using ufw, try something like:
// sudo ufw allow in on docker0
//
doTokenRegistration(homeserver.baseUrl);
// Eventually, we should end up at the home screen.
cy.url().should("contain", "/#/home", { timeout: 30000 });
cy.findByRole("heading", { name: "Welcome Alice" });
});
});
describe("logout", () => {
beforeEach(() => {
cy.startHomeserver("consent").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Erin");
});
});
it("should go to login page on logout", () => {
cy.findByRole("button", { name: "User menu" }).click();
// give a change for the outstanding requests queue to settle before logging out
cy.wait(2000);
cy.get(".mx_UserMenu_contextMenu").within(() => {
cy.findByRole("menuitem", { name: "Sign out" }).click();
});
cy.url().should("contain", "/#/login");
});
it("should respect logout_redirect_url", () => {
cy.tweakConfig({
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
// We could use example.org, matrix.org, or something else, however this puts dependency of external
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
// Using the decoder-ring is just as fine, and we can search for strategic names.
logout_redirect_url: "/decoder-ring/",
});
cy.findByRole("button", { name: "User menu" }).click();
// give a change for the outstanding requests queue to settle before logging out
cy.wait(2000);
cy.get(".mx_UserMenu_contextMenu").within(() => {
cy.findByRole("menuitem", { name: "Sign out" }).click();
});
cy.url().should("contains", "decoder-ring");
});
});
});

View file

@ -1,141 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { UserCredentials } from "../../support/login";
import { doTokenRegistration } from "./utils";
describe("Soft logout", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.task("startOAuthServer")
.then((oAuthServerPort: number) => {
return cy.startHomeserver({ template: "default", oAuthServerPort });
})
.then((data) => {
homeserver = data;
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
cy.task("stopOAuthServer");
});
describe("with password user", () => {
let testUserCreds: UserCredentials;
beforeEach(() => {
cy.initTestUser(homeserver, "Alice").then((creds) => {
testUserCreds = creds;
});
});
it("shows the soft-logout page when a request fails, and allows a re-login", () => {
interceptRequestsWithSoftLogout();
cy.findByText("You're signed out");
cy.findByPlaceholderText("Password").type(testUserCreds.password).type("{enter}");
// back to the welcome page
cy.url().should("contain", "/#/home", { timeout: 30000 });
cy.findByRole("heading", { name: "Welcome Alice" });
});
it("still shows the soft-logout page when the page is reloaded after a soft-logout", () => {
interceptRequestsWithSoftLogout();
cy.findByText("You're signed out");
cy.reload();
cy.findByText("You're signed out");
});
});
describe("with SSO user", () => {
beforeEach(() => {
doTokenRegistration(homeserver.baseUrl);
// Eventually, we should end up at the home screen.
cy.url().should("contain", "/#/home", { timeout: 30000 });
cy.findByRole("heading", { name: "Welcome Alice" });
});
it("shows the soft-logout page when a request fails, and allows a re-login", () => {
cy.findByRole("heading", { name: "Welcome Alice" });
interceptRequestsWithSoftLogout();
cy.findByText("You're signed out");
cy.findByRole("button", { name: "Continue with OAuth test" }).click();
// click the submit button
cy.findByRole("button", { name: "Submit" }).click();
// Synapse prompts us to grant permission to Element
cy.findByRole("heading", { name: "Continue to your account" });
cy.findByRole("link", { name: "Continue" }).click();
// back to the welcome page
cy.url().should("contain", "/#/home", { timeout: 30000 });
cy.findByRole("heading", { name: "Welcome Alice" });
});
});
});
/**
* Intercept calls to /sync and have them fail with a soft-logout
*
* Any further requests to /sync with the same access token are blocked.
*/
function interceptRequestsWithSoftLogout(): void {
let expiredAccessToken: string | null = null;
cy.intercept(
{
pathname: "/_matrix/client/*/sync",
},
(req) => {
const accessToken = req.headers["authorization"] as string;
// on the first request, record the access token
if (!expiredAccessToken) {
console.log(`Soft-logout on access token ${accessToken}`);
expiredAccessToken = accessToken;
}
// now, if the access token on this request matches the expired one, block it
if (expiredAccessToken && accessToken === expiredAccessToken) {
console.log(`Intercepting request with soft-logged-out access token`);
req.reply({
statusCode: 401,
body: {
errcode: "M_UNKNOWN_TOKEN",
error: "Soft logout",
soft_logout: true,
},
});
return;
}
// otherwise, pass through as normal
req.continue();
},
);
// do something to make the active /sync return: create a new room
cy.getClient().then((client) => {
// don't wait for this to complete: it probably won't, because of the broken sync
return client.createRoom({});
});
}

View file

@ -1,49 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
*/
export function doTokenRegistration(homeserverUrl: string) {
cy.visit("/#/login");
cy.findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl);
cy.findByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
cy.get(".mx_ServerPickerDialog").should("not.exist");
// click on "Continue with OAuth test"
cy.findByRole("button", { name: "Continue with OAuth test" }).click();
// wait for the Test OAuth Page to load
cy.findByText("Test OAuth page");
// click the submit button
cy.findByRole("button", { name: "Submit" }).click();
// Synapse prompts us to pick a user ID
cy.findByRole("heading", { name: "Create your account" });
cy.findByRole("textbox", { name: "Username (required)" }).type("alice");
// wait for username validation to start, and complete
cy.wait(50);
cy.get("#field-username-output").should("have.value", "");
cy.findByRole("button", { name: "Continue" }).click();
// Synapse prompts us to grant permission to Element
cy.findByRole("heading", { name: "Continue to your account" });
cy.findByRole("link", { name: "Continue" }).click();
}

View file

@ -26,17 +26,12 @@ import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver"; import { webserver } from "./webserver";
import { docker } from "./docker"; import { docker } from "./docker";
import { log } from "./log"; import { log } from "./log";
import { oAuthServer } from "./oauth_server";
/** /**
* @type {Cypress.PluginConfig} * @type {Cypress.PluginConfig}
*/ */
export default function (on: PluginEvents, config: PluginConfigOptions) { export default function (on: PluginEvents, config: PluginConfigOptions) {
initPlugins( initPlugins(on, [docker, synapseDocker, dendriteDocker, slidingSyncProxyDocker, webserver, log], config);
on,
[docker, synapseDocker, dendriteDocker, slidingSyncProxyDocker, webserver, oAuthServer, log],
config,
);
installLogsPrinter(on, { installLogsPrinter(on, {
printLogsToConsole: "never", printLogsToConsole: "never",

View file

@ -1,81 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import http from "http";
import express from "express";
import { AddressInfo } from "net";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
const servers: http.Server[] = [];
function startOAuthServer(html: string): number {
const app = express();
// static files. This includes the "authorization endpoint".
app.use(express.static(__dirname + "/res"));
// token endpoint (see https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint)
app.use("/oauth/token", express.urlencoded());
app.post("/oauth/token", (req, res) => {
// if the code is valid, accept it. Otherwise, return an error.
const code = req.body.code;
if (code === "valid_auth_code") {
res.send({
access_token: "oauth_access_token",
token_type: "Bearer",
expires_in: "3600",
});
} else {
res.send({ error: "bad auth code" });
}
});
// userinfo endpoint (see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
app.get("/oauth/userinfo", (req, res) => {
// TODO: validate that the request carries an auth header which matches the access token we issued above
// return an OAuth2 user info object
res.send({
sub: "alice",
name: "Alice",
});
});
const server = http.createServer(app);
server.listen();
servers.push(server);
const address = server.address() as AddressInfo;
console.log(`Started OAuth server at ${address.address}:${address.port}`);
return address.port;
}
function stopOAuthServer(): null {
console.log("Stopping OAuth servers");
for (const server of servers) {
const address = server.address() as AddressInfo;
server.close();
console.log(`Stopped OAuth server at ${address.address}:${address.port}`);
}
servers.splice(0, servers.length); // clear
return null;
}
export function oAuthServer(on: PluginEvents, config: PluginConfigOptions) {
on("task", { startOAuthServer, stopOAuthServer });
on("after:run", stopOAuthServer);
}

View file

@ -160,6 +160,7 @@
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/diff-match-patch": "^1.0.32", "@types/diff-match-patch": "^1.0.32",
"@types/escape-html": "^1.0.1", "@types/escape-html": "^1.0.1",
"@types/express": "^4.17.21",
"@types/file-saver": "^2.0.3", "@types/file-saver": "^2.0.3",
"@types/fs-extra": "^11.0.0", "@types/fs-extra": "^11.0.0",
"@types/glob-to-regexp": "^0.4.1", "@types/glob-to-regexp": "^0.4.1",

View file

@ -15,8 +15,9 @@ limitations under the License.
*/ */
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { doTokenRegistration } from "./utils";
test.describe("Consent", () => { test.describe("Login", () => {
test.describe("m.login.password", () => { test.describe("m.login.password", () => {
test.use({ startHomeserverOpts: "consent" }); test.use({ startHomeserverOpts: "consent" });
@ -75,4 +76,65 @@ test.describe("Consent", () => {
await expect(page).toHaveURL(/\/#\/home$/); await expect(page).toHaveURL(/\/#\/home$/);
}); });
}); });
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
test.describe("SSO login", () => {
test.use({
startHomeserverOpts: ({ oAuthServer }, use) =>
use({
template: "default",
oAuthServerPort: oAuthServer.port,
}),
});
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => {
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
// your firewall settings: Synapse is unable to reach the OIDC server.
//
// If you are using ufw, try something like:
// sudo ufw allow in on docker0
//
await doTokenRegistration(page, homeserver);
});
});
test.describe("logout", () => {
test.use({ startHomeserverOpts: "consent" });
test("should go to login page on logout", async ({ page, user }) => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
// Allow the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
});
});
test.describe("logout with logout_redirect_url", () => {
test.use({
startHomeserverOpts: "consent",
config: {
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
// We could use example.org, matrix.org, or something else, however this puts dependency of external
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
// Using the decoder-ring is just as fine, and we can search for strategic names.
logout_redirect_url: "/decoder-ring/",
},
});
test("should respect logout_redirect_url", async ({ page, user }) => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
// give a change for the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/decoder-ring\/$/);
});
});
}); });

View file

@ -0,0 +1,125 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { doTokenRegistration } from "./utils";
import { Credentials } from "../../plugins/utils/homeserver";
test.describe("Soft logout", () => {
test.use({
displayName: "Alice",
startHomeserverOpts: ({ oAuthServer }, use) =>
use({
template: "default",
oAuthServerPort: oAuthServer.port,
}),
});
test.describe("with password user", () => {
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByPlaceholder("Password").fill(user.password);
await page.getByPlaceholder("Password").press("Enter");
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
});
test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({
page,
user,
}) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.reload();
await expect(page.getByText("You're signed out")).toBeVisible();
});
});
test.describe("with SSO user", () => {
test.use({
user: async ({ page, homeserver }, use) => {
const user = await doTokenRegistration(page, homeserver);
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await use(user);
},
});
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
// click the submit button
await page.getByRole("button", { name: "Submit" }).click();
// Synapse prompts us to grant permission to Element
await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible();
await page.getByRole("link", { name: "Continue" }).click();
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
});
});
});
/**
* Intercept calls to /sync and have them fail with a soft-logout
*
* Any further requests to /sync with the same access token are blocked.
*/
async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise<void> {
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
const accessToken = await req.headerValue("Authorization");
// now, if the access token on this request matches the expired one, block it
if (accessToken === `Bearer ${user.accessToken}`) {
console.log("Intercepting request with soft-logged-out access token");
await route.fulfill({
status: 401,
json: {
errcode: "M_UNKNOWN_TOKEN",
error: "Soft logout",
soft_logout: true,
},
});
return;
}
// otherwise, pass through as normal
await route.continue();
});
// do something to make the active /sync return: create a new room
await page.evaluate(() => {
// don't wait for this to complete: it probably won't, because of the broken sync
window.mxMatrixClientPeg.get().createRoom({});
});
await page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401);
}

View file

@ -0,0 +1,68 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Page, expect } from "@playwright/test";
import { Credentials, HomeserverInstance } from "../../plugins/utils/homeserver";
/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
*/
export async function doTokenRegistration(
page: Page,
homeserver: HomeserverInstance,
): Promise<Credentials & { displayName: string }> {
await page.goto("/#/login");
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
// click on "Continue with OAuth test"
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
// wait for the Test OAuth Page to load
await expect(page.getByText("Test OAuth page")).toBeVisible();
// click the submit button
await page.getByRole("button", { name: "Submit" }).click();
// Synapse prompts us to pick a user ID
await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible();
await page.getByRole("textbox", { name: "Username (required)" }).type("alice");
// wait for username validation to start, and complete
await expect(page.locator("#field-username-output")).toHaveText("");
await page.getByRole("button", { name: "Continue" }).click();
// Synapse prompts us to grant permission to Element
page.getByRole("heading", { name: "Continue to your account" });
await page.getByRole("link", { name: "Continue" }).click();
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
return page.evaluate(() => ({
accessToken: window.mxMatrixClientPeg.get().getAccessToken(),
userId: window.mxMatrixClientPeg.get().getUserId(),
deviceId: window.mxMatrixClientPeg.get().getDeviceId(),
homeServer: window.mxMatrixClientPeg.get().getHomeserverUrl(),
password: null,
displayName: "Alice",
}));
}

View file

@ -16,12 +16,14 @@ limitations under the License.
import { test as base, expect } from "@playwright/test"; import { test as base, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright"; import AxeBuilder from "@axe-core/playwright";
import _ from "lodash";
import type mailhog from "mailhog"; import type mailhog from "mailhog";
import type { IConfigOptions } from "../src/IConfigOptions"; import type { IConfigOptions } from "../src/IConfigOptions";
import { HomeserverInstance, StartHomeserverOpts } from "./plugins/utils/homeserver"; import { Credentials, HomeserverInstance, StartHomeserverOpts } from "./plugins/utils/homeserver";
import { Synapse } from "./plugins/synapse"; import { Synapse } from "./plugins/synapse";
import { Instance } from "./plugins/mailhog"; import { Instance } from "./plugins/mailhog";
import { OAuthServer } from "./plugins/oauth_server";
const CONFIG_JSON: Partial<IConfigOptions> = { const CONFIG_JSON: Partial<IConfigOptions> = {
// This is deliberately quite a minimal config.json, so that we can test that the default settings // This is deliberately quite a minimal config.json, so that we can test that the default settings
@ -47,9 +49,16 @@ export const test = base.extend<
TestOptions & { TestOptions & {
axe: AxeBuilder; axe: AxeBuilder;
checkA11y: () => Promise<void>; checkA11y: () => Promise<void>;
// The contents of the config.json to send
config: typeof CONFIG_JSON; config: typeof CONFIG_JSON;
// The options with which to run the `homeserver` fixture
startHomeserverOpts: StartHomeserverOpts | string; startHomeserverOpts: StartHomeserverOpts | string;
homeserver: HomeserverInstance; homeserver: HomeserverInstance;
oAuthServer: { port: number };
user: Credentials & {
displayName: string;
};
displayName?: string;
mailhog?: { api: mailhog.API; instance: Instance }; mailhog?: { api: mailhog.API; instance: Instance };
} }
>({ >({
@ -57,7 +66,7 @@ export const test = base.extend<
config: CONFIG_JSON, config: CONFIG_JSON,
page: async ({ context, page, config, crypto }, use) => { page: async ({ context, page, config, crypto }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => { await context.route(`http://localhost:8080/config.json*`, async (route) => {
const json = { ...config }; const json = { ...CONFIG_JSON, ...config };
if (crypto === "rust") { if (crypto === "rust") {
json["features"] = { json["features"] = {
...json["features"], ...json["features"],
@ -66,6 +75,7 @@ export const test = base.extend<
} }
await route.fulfill({ json }); await route.fulfill({ json });
}); });
await use(page); await use(page);
}, },
@ -79,6 +89,49 @@ export const test = base.extend<
await use(await server.start(opts)); await use(await server.start(opts));
await server.stop(); await server.stop();
}, },
// eslint-disable-next-line no-empty-pattern
oAuthServer: async ({}, use) => {
const server = new OAuthServer();
const port = server.start();
await use({ port });
server.stop();
},
displayName: undefined,
user: async ({ page, homeserver, displayName: testDisplayName }, use) => {
const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"];
const username = _.uniqueId("user_");
const password = _.uniqueId("password_");
const displayName = testDisplayName ?? _.sample(names)!;
const credentials = await homeserver.registerUser(username, password, displayName);
console.log(`Registered test user ${username} with displayname ${displayName}`);
await page.addInitScript(
({ baseUrl, credentials }) => {
// Seed the localStorage with the required credentials
window.localStorage.setItem("mx_hs_url", baseUrl);
window.localStorage.setItem("mx_user_id", credentials.userId);
window.localStorage.setItem("mx_access_token", credentials.accessToken);
window.localStorage.setItem("mx_device_id", credentials.deviceId);
window.localStorage.setItem("mx_is_guest", "false");
window.localStorage.setItem("mx_has_pickle_key", "false");
window.localStorage.setItem("mx_has_access_token", "true");
// Ensure the language is set to a consistent value
window.localStorage.setItem("mx_local_settings", '{"language":"en"}');
},
{ baseUrl: homeserver.config.baseUrl, credentials },
);
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
await use({
...credentials,
displayName,
});
},
axe: async ({ page }, use) => { axe: async ({ page }, use) => {
await use(new AxeBuilder({ page })); await use(new AxeBuilder({ page }));
@ -98,4 +151,4 @@ export const test = base.extend<
test.use({}); test.use({});
export { expect } from "@playwright/test"; export { expect };

View file

@ -0,0 +1,72 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import http from "http";
import express from "express";
import { AddressInfo } from "net";
export class OAuthServer {
private server?: http.Server;
public start(): number {
if (this.server) this.stop();
const app = express();
// static files. This includes the "authorization endpoint".
app.use(express.static(__dirname + "/res"));
// token endpoint (see https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint)
app.use("/oauth/token", express.urlencoded({ extended: true }));
app.post("/oauth/token", (req, res) => {
// if the code is valid, accept it. Otherwise, return an error.
const code = req.body.code;
if (code === "valid_auth_code") {
res.send({
access_token: "oauth_access_token",
token_type: "Bearer",
expires_in: "3600",
});
} else {
res.send({ error: "bad auth code" });
}
});
// userinfo endpoint (see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo)
app.get("/oauth/userinfo", (req, res) => {
// TODO: validate that the request carries an auth header which matches the access token we issued above
// return an OAuth2 user info object
res.send({
sub: "alice",
name: "Alice",
});
});
this.server = http.createServer(app);
this.server.listen();
const address = this.server.address() as AddressInfo;
console.log(`Started OAuth server at ${address.address}:${address.port}`);
return address.port;
}
public stop(): void {
console.log("Stopping OAuth server");
const address = this.server.address() as AddressInfo;
this.server.close();
console.log(`Stopped OAuth server at ${address.address}:${address.port}`);
}
}

View file

@ -53,5 +53,5 @@ export interface Credentials {
userId: string; userId: string;
deviceId: string; deviceId: string;
homeServer: string; homeServer: string;
password: string; password: string | null; // null for password-less users
} }

View file

@ -8,5 +8,5 @@
"moduleResolution": "node", "moduleResolution": "node",
"module": "es2022" "module": "es2022"
}, },
"include": ["**/*.ts"] "include": ["**/*.ts", "../src/@types/global.d.ts"]
} }

View file

@ -2624,11 +2624,26 @@
dependencies: dependencies:
"@babel/types" "^7.20.7" "@babel/types" "^7.20.7"
"@types/body-parser@*":
version "1.19.5"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
dependencies:
"@types/connect" "*"
"@types/node" "*"
"@types/commonmark@^0.27.4": "@types/commonmark@^0.27.4":
version "0.27.9" version "0.27.9"
resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.9.tgz#2d2d42e72127c84525fbbc87aaefb5a43e1129d7" resolved "https://registry.yarnpkg.com/@types/commonmark/-/commonmark-0.27.9.tgz#2d2d42e72127c84525fbbc87aaefb5a43e1129d7"
integrity sha512-d3+57WgyPCcIc6oshmcPkmP4+JqRRot9eeZLsBsutWtIxwWivpoyc2wEcolOp8MyO3ZWN846mMdoR02kdHSMCw== integrity sha512-d3+57WgyPCcIc6oshmcPkmP4+JqRRot9eeZLsBsutWtIxwWivpoyc2wEcolOp8MyO3ZWN846mMdoR02kdHSMCw==
"@types/connect@*":
version "3.4.38"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
dependencies:
"@types/node" "*"
"@types/content-type@^1.1.5": "@types/content-type@^1.1.5":
version "1.1.8" version "1.1.8"
resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.8.tgz#319644d07ee6b4bfc734483008393b89b99f0219" resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.8.tgz#319644d07ee6b4bfc734483008393b89b99f0219"
@ -2654,6 +2669,26 @@
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.3.tgz#a8ef894305af28d1fc6d2dfdfc98e899591ea529" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.3.tgz#a8ef894305af28d1fc6d2dfdfc98e899591ea529"
integrity sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g== integrity sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==
"@types/express-serve-static-core@^4.17.33":
version "4.17.41"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6"
integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/send" "*"
"@types/express@^4.17.21":
version "4.17.21"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
dependencies:
"@types/body-parser" "*"
"@types/express-serve-static-core" "^4.17.33"
"@types/qs" "*"
"@types/serve-static" "*"
"@types/file-saver@^2.0.3": "@types/file-saver@^2.0.3":
version "2.0.7" version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d" resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.7.tgz#8dbb2f24bdc7486c54aa854eb414940bbd056f7d"
@ -2692,6 +2727,11 @@
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
"@types/http-errors@*":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44"
@ -2774,6 +2814,16 @@
"@types/mapbox__point-geometry" "*" "@types/mapbox__point-geometry" "*"
"@types/pbf" "*" "@types/pbf" "*"
"@types/mime@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
"@types/mime@^1":
version "1.3.5"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
"@types/minimist@^1.2.2": "@types/minimist@^1.2.2":
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.3.tgz#dd249cef80c6fff2ba6a0d4e5beca913e04e25f8" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.3.tgz#dd249cef80c6fff2ba6a0d4e5beca913e04e25f8"
@ -2841,6 +2891,16 @@
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
"@types/qs@*":
version "6.9.10"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.10.tgz#0af26845b5067e1c9a622658a51f60a3934d51e8"
integrity sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==
"@types/range-parser@*":
version "1.2.7"
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/react-beautiful-dnd@^13.0.0": "@types/react-beautiful-dnd@^13.0.0":
version "13.1.5" version "13.1.5"
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.5.tgz#04869f2ec4658aa963e56dc3cbb91f261587dedc" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.5.tgz#04869f2ec4658aa963e56dc3cbb91f261587dedc"
@ -2908,6 +2968,23 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.5.tgz#deed5ab7019756c9c90ea86139106b0346223f35" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.5.tgz#deed5ab7019756c9c90ea86139106b0346223f35"
integrity sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg== integrity sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==
"@types/send@*":
version "0.17.4"
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
dependencies:
"@types/mime" "^1"
"@types/node" "*"
"@types/serve-static@*":
version "1.15.5"
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
dependencies:
"@types/http-errors" "*"
"@types/mime" "*"
"@types/node" "*"
"@types/sinonjs__fake-timers@8.1.1": "@types/sinonjs__fake-timers@8.1.1":
version "8.1.1" version "8.1.1"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"