Skip to content

Commit

Permalink
Iterate
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Telatynski <[email protected]>
  • Loading branch information
t3chguy committed Jan 10, 2025
1 parent 8b3ffb4 commit 71f06cd
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 75 deletions.
29 changes: 10 additions & 19 deletions playwright/e2e/csAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,31 @@ import { APIRequestContext } from "playwright-core";
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";

import { HomeserverInstance } from "../plugins/homeserver";
import { ClientServerApi } from "../testcontainers/utils.ts";

/**
* A small subset of the Client-Server API used to manipulate the state of the
* account on the homeserver independently of the client under test.
*/
export class TestClientServerAPI {
export class TestClientServerAPI extends ClientServerApi {
public constructor(
private request: APIRequestContext,
private homeserver: HomeserverInstance,
request: APIRequestContext,
homeserver: HomeserverInstance,
private accessToken: string,
) {}
) {
super(`${homeserver.baseUrl}/_matrix/client`);
this.setRequest(request);
}

public async getCurrentBackupInfo(): Promise<KeyBackupInfo | null> {
const res = await this.request.get(`${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version`, {
headers: { Authorization: `Bearer ${this.accessToken}` },
});

return await res.json();
return this.request("GET", `/v3/room_keys/version`, this.accessToken);
}

/**
* Calls the API directly to delete the given backup version
* @param version The version to delete
*/
public async deleteBackupVersion(version: string): Promise<void> {
const res = await this.request.delete(
`${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version/${version}`,
{
headers: { Authorization: `Bearer ${this.accessToken}` },
},
);

if (!res.ok) {
throw new Error(`Failed to delete backup version: ${res.status}`);
}
await this.request("DELETE", `/v3/room_keys/version/${version}`, this.accessToken);
}
}
6 changes: 6 additions & 0 deletions playwright/e2e/login/login-consent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ async function login(page: Page, homeserver: HomeserverInstance, credentials: Cr
await page.getByRole("button", { name: "Sign in" }).click();
}

// This test suite uses the same userId for all tests in the suite
// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId,
// so we restart the Synapse container to make it forget everything.
test.use(consentHomeserver);
test.use({
config: {
Expand All @@ -97,6 +100,9 @@ test.use({
...credentials,
displayName,
});

// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
await homeserver.restart();
},
});

Expand Down
2 changes: 1 addition & 1 deletion playwright/e2e/oidc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function registerAccountMas(
expect(messages.items).toHaveLength(1);
}).toPass();
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
const [code] = messages.items[0].text.match(/(\d{6})/);
const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/);

await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click();
Expand Down
9 changes: 7 additions & 2 deletions playwright/e2e/oidc/oidc-native.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,23 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
mailhogClient,
mas,
}, testInfo) => {
await page.clock.install();

const tokenUri = `${mas.baseUrl}/oauth2/token`;
const tokenApiPromise = page.waitForRequest(
(request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code",
);

await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "[email protected]", "Pa$sW0rD!");

const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailhogClient, userId, "[email protected]", "Pa$sW0rD!");

// 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();
await expect(page.getByRole("heading", { name: `Welcome ${userId}`, exact: true })).toBeVisible();
await page.clock.runFor(20000); // run the timer so we see the token request

const tokenApiRequest = await tokenApiPromise;
expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code");
Expand Down
1 change: 0 additions & 1 deletion playwright/e2e/sliding-sync/sliding-sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ test.describe("Sliding Sync", () => {
await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click");
await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible();

await page.pause();
await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
});

Expand Down
10 changes: 5 additions & 5 deletions playwright/e2e/spotlight/spotlight.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,28 +366,28 @@ test.describe("Spotlight", () => {
await spotlight.search("b");

let resultLocator = spotlight.results;
await expect(resultLocator.count()).resolves.toBeGreaterThan(2);
await expect(resultLocator).toHaveCount(2);

Check failure on line 369 in playwright/e2e/spotlight/spotlight.spec.ts

View workflow job for this annotation

GitHub Actions / Run Tests [Chrome] 5/6

[Chrome] › spotlight/spotlight.spec.ts:362:5 › Spotlight › should be able to navigate results via keyboard

1) [Chrome] › spotlight/spotlight.spec.ts:362:5 › Spotlight › should be able to navigate results via keyboard Error: Timed out 5000ms waiting for expect(locator).toHaveCount(expected) Locator: locator('[role=dialog][aria-label="Search Dialog"]').locator('.mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option') Expected: 2 Received: 31 Call log: - expect.toHaveCount with timeout 5000ms - waiting for locator('[role=dialog][aria-label="Search Dialog"]').locator('.mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option') 2 × locator resolved to 1 element - unexpected value "1" 7 × locator resolved to 31 elements - unexpected value "31" 367 | 368 | let resultLocator = spotlight.results; > 369 | await expect(resultLocator).toHaveCount(2); | ^ 370 | await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true"); 371 | await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false"); 372 | at /home/runner/work/element-web/element-web/playwright/e2e/spotlight/spotlight.spec.ts:369:37
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false");

await spotlight.searchBox.press("ArrowDown");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true");

await spotlight.searchBox.press("ArrowDown");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false");

await spotlight.searchBox.press("ArrowUp");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true");

await spotlight.searchBox.press("ArrowUp");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false");
});
});
2 changes: 1 addition & 1 deletion playwright/element-web-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const CONFIG_JSON: Partial<IConfigOptions> = {
},
};

interface CredentialsWithDisplayName extends Credentials {
export interface CredentialsWithDisplayName extends Credentials {
displayName: string;
}

Expand Down
3 changes: 3 additions & 0 deletions playwright/plugins/homeserver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import { ClientServerApi } from "../../testcontainers/utils.ts";

export interface HomeserverInstance {
readonly baseUrl: string;
readonly csApi: ClientServerApi;

/**
* Register a user on the given Homeserver using the shared registration secret.
Expand Down
3 changes: 1 addition & 2 deletions playwright/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,9 @@ export const test = base.extend<{}, Services>({
.withNetworkAliases("homeserver")
.withLogConsumer(logger.getConsumer("synapse"))
.withConfig(synapseConfigOptions)
.withMatrixAuthenticationService(mas)
.start();

container.setMatrixAuthenticationService(mas);

await use(container);
await container.stop();
},
Expand Down
2 changes: 1 addition & 1 deletion playwright/testcontainers/HomeserverContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
export interface HomeserverContainer<Config> extends GenericContainer {
withConfigField(key: string, value: any): this;
withConfig(config: Partial<Config>): this;
withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this;
start(): Promise<StartedHomeserverContainer>;
}

export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
setRequest(request: APIRequestContext): void;
setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void;
onTestFinished(testInfo: TestInfo): Promise<void>;
}
5 changes: 5 additions & 0 deletions playwright/testcontainers/dendrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { randB64Bytes } from "../plugins/utils/rand.ts";
import { StartedSynapseContainer } from "./synapse.ts";
import { deepCopy } from "../plugins/utils/object.ts";
import { HomeserverContainer } from "./HomeserverContainer.ts";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";

const DEFAULT_CONFIG = {
version: 2,
Expand Down Expand Up @@ -235,6 +236,10 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
return this;
}

public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
throw new Error("Dendrite does not support MAS.");
}

public override async start(): Promise<StartedDendriteContainer> {
this.withCopyContentToContainer([
{
Expand Down
80 changes: 63 additions & 17 deletions playwright/testcontainers/mas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait, ExecResult } from "testcontainers";
import { StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import * as YAML from "yaml";

import { getFreePort } from "../plugins/utils/port.ts";
import { deepCopy } from "../plugins/utils/object.ts";
import { Credentials } from "../plugins/homeserver";
import { ClientServerApi } from "./utils.ts";

const DEFAULT_CONFIG = {
http: {
Expand Down Expand Up @@ -227,10 +226,9 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted
super(container);
}

public async getAdminToken(csApi: ClientServerApi): Promise<string> {
public async getAdminToken(): Promise<string> {
if (this.adminTokenPromise === undefined) {
this.adminTokenPromise = this.registerUserInternal(
csApi,
"admin",
"totalyinsecureadminpassword",
undefined,
Expand All @@ -240,46 +238,94 @@ export class StartedMatrixAuthenticationServiceContainer extends AbstractStarted
return this.adminTokenPromise;
}

private async registerUserInternal(
csApi: ClientServerApi,
private async manage(cmd: string, ...args: string[]): Promise<ExecResult> {
const result = await this.exec(["mas-cli", "manage", cmd, ...this.args, ...args]);
if (result.exitCode !== 0) {
throw new Error(`Failed mas-cli manage ${cmd}: ${result.output}`);
}
return result;
}

private async manageRegisterUser(
username: string,
password: string,
displayName?: string,
admin = false,
): Promise<Credentials> {
): Promise<string> {
const args: string[] = [];
if (admin) args.push("-a");
await this.exec([
"mas-cli",
"manage",
const result = await this.manage(
"register-user",
...this.args,
...args,
"-y",
"-p",
password,
"-d",
displayName ?? "",
username,
]);
);

return csApi.loginUser(username, password);
const registerLines = result.output.trim().split("\n");
const userId = registerLines
.find((line) => line.includes("Matrix ID: "))
?.split(": ")
.pop();

if (!userId) {
throw new Error(`Failed to register user: ${result.output}`);
}

return userId;
}

public async registerUser(
csApi: ClientServerApi,
private async manageIssueCompatibilityToken(
username: string,
admin = false,
): Promise<{ accessToken: string; deviceId: string }> {
const args: string[] = [];
if (admin) args.push("--yes-i-want-to-grant-synapse-admin-privileges");
const result = await this.manage("issue-compatibility-token", ...args, username);

const parts = result.output.trim().split(/\s+/);
const accessToken = parts.find((part) => part.startsWith("mct_"));
const deviceId = parts.find((part) => part.startsWith("compat_session.device="))?.split("=")[1];

if (!accessToken || !deviceId) {
throw new Error(`Failed to issue compatibility token: ${result.output}`);
}

return { accessToken, deviceId };
}

private async registerUserInternal(
username: string,
password: string,
displayName?: string,
admin = false,
): Promise<Credentials> {
return this.registerUserInternal(csApi, username, password, displayName, false);
const userId = await this.manageRegisterUser(username, password, displayName, admin);
const { deviceId, accessToken } = await this.manageIssueCompatibilityToken(username, admin);

return {
userId,
accessToken,
deviceId,
homeServer: userId.slice(1).split(":").slice(1).join(":"),
displayName,
username,
password,
};
}

public async registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
return this.registerUserInternal(username, password, displayName, false);
}

public async setThreepid(username: string, medium: string, address: string): Promise<void> {
if (medium !== "email") {
throw new Error("Only email threepids are supported by MAS");
}

await this.exec(["mas-cli", "manage", "add-email", ...this.args, username, address]);
await this.manage("add-email", username, address);
}
}
Loading

0 comments on commit 71f06cd

Please sign in to comment.