Fix more flakes

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2025-01-10 12:51:32 +00:00
parent db47e68903
commit 8f68dbb4f3
7 changed files with 189 additions and 91 deletions

View File

@@ -366,28 +366,28 @@ test.describe("Spotlight", () => {
await spotlight.search("b");
let resultLocator = spotlight.results;
await expect(resultLocator).toHaveCount(2);
await expect(resultLocator.count()).resolves.toBeGreaterThan(2);
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.last()).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true");
await spotlight.searchBox.press("ArrowDown");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false");
await spotlight.searchBox.press("ArrowUp");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "true");
await spotlight.searchBox.press("ArrowUp");
resultLocator = spotlight.results;
await expect(resultLocator.first()).toHaveAttribute("aria-selected", "true");
await expect(resultLocator.last()).toHaveAttribute("aria-selected", "false");
await expect(resultLocator.nth(2)).toHaveAttribute("aria-selected", "false");
});
});

View File

@@ -116,6 +116,8 @@ export const test = base.extend<{}, Services>({
.withConfig(synapseConfigOptions)
.start();
container.setMatrixAuthenticationService(mas);
await use(container);
await container.stop();
},

View File

@@ -9,6 +9,7 @@ import { AbstractStartedContainer, GenericContainer } from "testcontainers";
import { APIRequestContext, TestInfo } from "@playwright/test";
import { HomeserverInstance } from "../plugins/homeserver";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
export interface HomeserverContainer<Config> extends GenericContainer {
withConfigField(key: string, value: any): this;
@@ -18,5 +19,6 @@ export interface HomeserverContainer<Config> extends GenericContainer {
export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
setRequest(request: APIRequestContext): void;
setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void;
onTestFinished(testInfo: TestInfo): Promise<void>;
}

View File

@@ -258,5 +258,5 @@ export class PineconeContainer extends DendriteContainer {
}
}
// Surprisingly, Dendrite implements the same register user Admin API Synapse, so we can just extend it
// Surprisingly, Dendrite implements the same register user Synapse Admin API, so we can just extend it
export class StartedDendriteContainer extends StartedSynapseContainer {}

View File

@@ -11,6 +11,8 @@ 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: {
@@ -18,18 +20,11 @@ const DEFAULT_CONFIG = {
{
name: "web",
resources: [
{
name: "discovery",
},
{
name: "human",
},
{
name: "oauth",
},
{
name: "compat",
},
{ name: "discovery" },
{ name: "human" },
{ name: "oauth" },
{ name: "compat" },
{ name: "adminapi" },
{
name: "graphql",
playground: true,
@@ -172,9 +167,12 @@ const DEFAULT_CONFIG = {
export class MatrixAuthenticationServiceContainer extends GenericContainer {
private config: typeof DEFAULT_CONFIG;
private readonly args = ["-c", "/config/config.yaml"];
constructor(db: StartedPostgreSqlContainer) {
super("ghcr.io/element-hq/matrix-authentication-service:0.12.0");
// We rely on `mas-cli manage add-email` which isn't in a release yet
// https://github.com/element-hq/matrix-authentication-service/pull/3235
super("ghcr.io/element-hq/matrix-authentication-service:sha-0b90c33");
this.config = deepCopy(DEFAULT_CONFIG);
this.config.database.username = db.getUsername();
@@ -182,7 +180,7 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer {
this.withExposedPorts(8080, 8081)
.withWaitStrategy(Wait.forHttp("/health", 8081))
.withCommand(["server", "--config", "/config/config.yaml"]);
.withCommand(["server", ...this.args]);
}
public withConfig(config: object): this {
@@ -210,15 +208,78 @@ export class MatrixAuthenticationServiceContainer extends GenericContainer {
},
]);
return new StartedMatrixAuthenticationServiceContainer(await super.start(), `http://localhost:${port}`);
return new StartedMatrixAuthenticationServiceContainer(
await super.start(),
`http://localhost:${port}`,
this.args,
);
}
}
export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer {
private adminTokenPromise?: Promise<string>;
constructor(
container: StartedTestContainer,
public readonly baseUrl: string,
private readonly args: string[],
) {
super(container);
}
public async getAdminToken(csApi: ClientServerApi): Promise<string> {
if (this.adminTokenPromise === undefined) {
this.adminTokenPromise = this.registerUserInternal(
csApi,
"admin",
"totalyinsecureadminpassword",
undefined,
true,
).then((res) => res.accessToken);
}
return this.adminTokenPromise;
}
private async registerUserInternal(
csApi: ClientServerApi,
username: string,
password: string,
displayName?: string,
admin = false,
): Promise<Credentials> {
const args: string[] = [];
if (admin) args.push("-a");
await this.exec([
"mas-cli",
"manage",
"register-user",
...this.args,
...args,
"-y",
"-p",
password,
"-d",
displayName ?? "",
username,
]);
return csApi.loginUser(username, password);
}
public async registerUser(
csApi: ClientServerApi,
username: string,
password: string,
displayName?: string,
): Promise<Credentials> {
return this.registerUserInternal(csApi, 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]);
}
}

View File

@@ -16,6 +16,8 @@ import { randB64Bytes } from "../plugins/utils/rand.ts";
import { Credentials } from "../plugins/homeserver";
import { deepCopy } from "../plugins/utils/object.ts";
import { HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.ts";
import { StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
import { Api, ClientServerApi, Verb } from "./utils.ts";
const TAG = "develop@sha256:b69222d98abe9625d46f5d3cb01683d5dc173ae339215297138392cfeec935d9";
@@ -138,8 +140,6 @@ const DEFAULT_CONFIG = {
},
};
type Verb = "GET" | "POST" | "PUT" | "DELETE";
export type SynapseConfigOptions = Partial<typeof DEFAULT_CONFIG>;
export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
@@ -231,7 +231,10 @@ export class SynapseContainer extends GenericContainer implements HomeserverCont
export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer {
private adminTokenPromise?: Promise<string>;
private _mas?: StartedMatrixAuthenticationServiceContainer;
protected _request?: APIRequestContext;
protected csApi: ClientServerApi;
protected adminApi: Api;
constructor(
container: StartedTestContainer,
@@ -239,10 +242,17 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
private readonly registrationSharedSecret: string,
) {
super(container);
this.csApi = new ClientServerApi(this.baseUrl);
this.adminApi = new Api(`${this.baseUrl}/_synapse/admin/`);
}
public setRequest(request: APIRequestContext): void {
this._request = request;
this.csApi.setRequest(request);
}
public setMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): void {
this._mas = mas;
}
public async onTestFinished(testInfo: TestInfo): Promise<void> {
@@ -251,13 +261,14 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
}
protected async deletePublicRooms(): Promise<void> {
const token = await this.getAdminToken();
// We hide the rooms from the room directory to save time between tests and for portability between homeservers
const { chunk: rooms } = await this.request<{
const { chunk: rooms } = await this.csApi.request<{
chunk: { room_id: string }[];
}>("GET", "v3/publicRooms", {});
}>("GET", "v3/publicRooms", token, {});
await Promise.all(
rooms.map((room) =>
this.request("PUT", `v3/directory/list/room/${room.room_id}`, { visibility: "private" }),
this.csApi.request("PUT", `v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }),
),
);
}
@@ -268,13 +279,18 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
displayName?: string,
admin = false,
): Promise<Credentials> {
const url = `${this.baseUrl}/_synapse/admin/v1/register`;
const { nonce } = await this._request.get(url).then((r) => r.json());
const path = `v1/register`;
const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {});
const mac = crypto
.createHmac("sha1", this.registrationSharedSecret)
.update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
.digest("hex");
const res = await this._request.post(url, {
const data = await this.adminApi.request<{
home_server: string;
access_token: string;
user_id: string;
device_id: string;
}>("POST", path, undefined, {
data: {
nonce,
username,
@@ -285,11 +301,6 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
},
});
if (!res.ok()) {
throw await res.json();
}
const data = await res.json();
return {
homeServer: data.home_server,
accessToken: data.access_token,
@@ -303,6 +314,10 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
protected async getAdminToken(): Promise<string> {
if (this.adminTokenPromise === undefined) {
if (this._mas) {
return (this.adminTokenPromise = this._mas.getAdminToken(this.csApi));
}
this.adminTokenPromise = this.registerUserInternal(
"admin",
"totalyinsecureadminpassword",
@@ -317,72 +332,24 @@ export class StartedSynapseContainer extends AbstractStartedContainer implements
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
const adminToken = await this.getAdminToken();
const url = `${this.baseUrl}/_synapse/admin/${path}`;
const res = await this._request.fetch(url, {
data,
method: verb,
headers: {
Authorization: `Bearer ${adminToken}`,
},
});
if (!res.ok()) {
throw await res.json();
}
return res.json();
}
public async request<R extends {}>(verb: "GET", path: string, data?: never): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
const token = await this.getAdminToken();
const url = `${this.baseUrl}/_matrix/client/${path}`;
const res = await this._request.fetch(url, {
data,
method: verb,
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok()) {
throw await res.json();
}
return res.json();
return this.adminApi.request(verb, path, adminToken, data);
}
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
if (this._mas) {
return this._mas.registerUser(this.csApi, username, password, displayName);
}
return this.registerUserInternal(username, password, displayName, false);
}
public async loginUser(userId: string, password: string): Promise<Credentials> {
const json = await this.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>("POST", "v3/login", {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: userId,
},
password: password,
});
return {
password,
accessToken: json.access_token,
userId: json.user_id,
deviceId: json.device_id,
homeServer: json.home_server,
username: userId.slice(1).split(":")[0],
};
return this.csApi.loginUser(userId, password);
}
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
if (this._mas) {
return this._mas.setThreepid(userId, medium, address);
}
await this.adminRequest("PUT", `v2/users/${userId}`, {
threepids: [
{

View File

@@ -5,10 +5,12 @@ 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 { TestInfo } from "@playwright/test";
import { APIRequestContext, TestInfo } from "@playwright/test";
import { Readable } from "stream";
import stripAnsi from "strip-ansi";
import { Credentials } from "../plugins/homeserver";
export class ContainerLogger {
private logs: Record<string, string> = {};
@@ -41,3 +43,67 @@ export class ContainerLogger {
}
}
}
export type Verb = "GET" | "POST" | "PUT" | "DELETE";
export class Api {
private _request?: APIRequestContext;
constructor(private readonly baseUrl: string) {}
public setRequest(request: APIRequestContext): void {
this._request = request;
}
public async request<R extends {}>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
const url = `${this.baseUrl}/_matrix/client/${path}`;
const res = await this._request.fetch(url, {
data,
method: verb,
headers: token
? {
Authorization: `Bearer ${token}`,
}
: undefined,
});
if (!res.ok()) {
throw await res.json();
}
return res.json();
}
}
export class ClientServerApi extends Api {
constructor(baseUrl: string) {
super(`${baseUrl}/_matrix/client/`);
}
public async loginUser(userId: string, password: string): Promise<Credentials> {
const json = await this.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>("POST", "v3/login", undefined, {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: userId,
},
password: password,
});
return {
password,
accessToken: json.access_token,
userId: json.user_id,
deviceId: json.device_id,
homeServer: json.home_server,
username: userId.slice(1).split(":")[0],
};
}
}