element-web/playwright/e2e/login/login-consent.spec.ts

264 lines
11 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
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 { Page } from "playwright-core";
import { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
// This test requires fixed credentials for the device signing keys below to work
const username = "user1234";
const password = "p4s5W0rD";
// Pre-generated dummy signing keys to create an account that has signing keys set.
// Note the signatures are specific to the username and must be valid or the HS will reject the keys.
const DEVICE_SIGNING_KEYS_BODY = {
master_key: {
keys: {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg": "6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"mvwqsYiGa2gPH6ueJsiJnceHMrZhf1pqIMGxkvKisN3ucz8sU7LwyzndbYaLkUKEDx1JuOKFfZ9Mb3mqc7PMBQ",
"ed25519:SRHVWTNVBH":
"HVGmVIzsJe3d+Un/6S9tXPsU7YA8HjZPdxogVzdjEFIU8OjLyElccvjupow0rVWgkEqU8sO21LIHw9cWRZEmDw",
},
},
usage: ["master"],
user_id: "@user1234:localhost",
},
self_signing_key: {
keys: {
"ed25519:eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8": "eqzRly4S1GvTA36v48hOKokHMtYBLm02zXRgPHue5/8",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"M2rt5xs+23egbVUwUcZuU7pMpn0chBNC5rpdyZGayfU3FDlx1DbopbakIcl5v4uOSGMbqUotyzkE6CchB+dgDw",
},
},
usage: ["self_signing"],
user_id: "@user1234:localhost",
},
user_signing_key: {
keys: {
"ed25519:h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4": "h6C7sonjKSSa/VMvmpmFnwMA02H2rKIMSYZ2ddwgJn4",
},
signatures: {
"@user1234:localhost": {
"ed25519:6qCouJsi2j7DzOmpxPTBALpvDTqa8p2mjrQR2P8wEbg":
"5ZMJ7SG2qr76vU2nITKap88AxLZ/RZQmF/mBcAcVZ9Bknvos3WQp8qN9jKuiqOHCq/XpPORA6XBmiDIyPqTFAA",
},
},
usage: ["user_signing"],
user_id: "@user1234:localhost",
},
auth: {
type: "m.login.password",
identifier: { type: "m.id.user", user: "@user1234:localhost" },
password: password,
},
};
async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) {
await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.baseUrl);
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
}
test.use(consentHomeserver);
test.use({
config: {
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
},
credentials: async ({ context, homeserver }, use) => {
const displayName = "Dave";
const credentials = await homeserver.registerUser(username, password, displayName);
console.log(`Registered test user @user:localhost with displayname ${displayName}`);
await use({
...credentials,
displayName,
});
},
});
test.describe("Login", () => {
test.describe("Password login", () => {
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
credentials,
page,
homeserver,
checkA11y,
}) => {
await page.goto("/");
// Should give us the welcome page initially
await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible();
// Start the login process
await page.getByRole("link", { name: "Sign in" }).click();
// first pick the homeserver, as otherwise the user picker won't be visible
await selectHomeserver(page, homeserver.baseUrl);
await page.getByRole("button", { name: "Edit" }).click();
// select the default server again
await page.locator(".mx_StyledRadioButton").first().click();
await page.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
await expect(page.locator(".mx_Spinner")).toHaveCount(0);
// name of default server
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
// switch back to the custom homeserver
await selectHomeserver(page, homeserver.baseUrl);
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
// cy.percySnapshot("Login");
await checkA11y();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/\/#\/home$/);
});
test("Follows the original link after login", async ({ page, homeserver, credentials }) => {
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
await login(page, homeserver, credentials);
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
});
test.describe("verification after login", () => {
test("Shows verification prompt after login if signing keys are set up, skippable by default", async ({
page,
homeserver,
request,
credentials,
}) => {
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
});
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});
test.describe("with force_verification off", () => {
test.use({
config: {
force_verification: false,
},
});
test("Shows skippable verification prompt after login if signing keys are set up", async ({
page,
homeserver,
request,
credentials,
}) => {
const res = await request.post(
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
},
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
});
});
test.describe("with force_verification on", () => {
test.use({
config: {
force_verification: true,
},
});
test("Shows unskippable verification prompt after login if signing keys are set up", async ({
page,
homeserver,
request,
credentials,
}) => {
console.log(`uid ${credentials.userId} body`, DEVICE_SIGNING_KEYS_BODY);
const res = await request.post(
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
},
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
await expect(h1).toBeVisible();
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
});
});
});
});
test.describe("logout", () => {
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$/);
});
});
});