@element-hq/element-web-playwright-common 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +9 -0
- package/README.md +28 -0
- package/lib/element-web-test.d.ts +26 -0
- package/lib/element-web-test.d.ts.map +1 -0
- package/lib/element-web-test.js +54 -0
- package/lib/expect/axe.d.ts +11 -0
- package/lib/expect/axe.d.ts.map +1 -0
- package/lib/expect/axe.js +22 -0
- package/lib/expect/index.d.ts +6 -0
- package/lib/expect/index.d.ts.map +1 -0
- package/lib/expect/index.js +10 -0
- package/lib/expect/screenshot.d.ts +14 -0
- package/lib/expect/screenshot.d.ts.map +1 -0
- package/lib/expect/screenshot.js +48 -0
- package/lib/expect/toHaveNoViolations.d.ts +11 -0
- package/lib/expect/toHaveNoViolations.d.ts.map +1 -0
- package/lib/expect/toHaveNoViolations.js +22 -0
- package/lib/expect/toMatchScreenshot.d.ts +13 -0
- package/lib/expect/toMatchScreenshot.d.ts.map +1 -0
- package/lib/expect/toMatchScreenshot.js +44 -0
- package/lib/expect/toPassAxeCheck.d.ts +7 -0
- package/lib/expect/toPassAxeCheck.d.ts.map +1 -0
- package/lib/expect/toPassAxeCheck.js +22 -0
- package/lib/fixtures/axe.d.ts +8 -0
- package/lib/fixtures/axe.d.ts.map +1 -0
- package/lib/fixtures/axe.js +15 -0
- package/lib/fixtures/index.d.ts +10 -0
- package/lib/fixtures/index.d.ts.map +1 -0
- package/lib/fixtures/index.js +10 -0
- package/lib/fixtures/services.d.ts +57 -0
- package/lib/fixtures/services.d.ts.map +1 -0
- package/lib/fixtures/services.js +114 -0
- package/lib/fixtures/user.d.ts +31 -0
- package/lib/fixtures/user.d.ts.map +1 -0
- package/lib/fixtures/user.js +47 -0
- package/lib/index.d.ts +35 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +54 -0
- package/lib/logger.d.ts +10 -0
- package/lib/logger.d.ts.map +1 -0
- package/lib/logger.js +59 -0
- package/lib/playwright.d.ts +2 -0
- package/lib/playwright.d.ts.map +1 -0
- package/lib/playwright.js +1 -0
- package/lib/testcontainers/HomeserverContainer.d.ts +70 -0
- package/lib/testcontainers/HomeserverContainer.d.ts.map +1 -0
- package/lib/testcontainers/HomeserverContainer.js +7 -0
- package/lib/testcontainers/index.d.ts +5 -0
- package/lib/testcontainers/index.d.ts.map +1 -0
- package/lib/testcontainers/index.js +9 -0
- package/lib/testcontainers/mailpit.d.ts +33 -0
- package/lib/testcontainers/mailpit.d.ts.map +1 -0
- package/lib/testcontainers/mailpit.js +52 -0
- package/lib/testcontainers/mas.d.ts +182 -0
- package/lib/testcontainers/mas.d.ts.map +1 -0
- package/lib/testcontainers/mas.js +311 -0
- package/lib/testcontainers/synapse.d.ts +229 -0
- package/lib/testcontainers/synapse.d.ts.map +1 -0
- package/lib/testcontainers/synapse.js +383 -0
- package/lib/utils/api.d.ts +49 -0
- package/lib/utils/api.d.ts.map +1 -0
- package/lib/utils/api.js +73 -0
- package/lib/utils/logger.d.ts +25 -0
- package/lib/utils/logger.d.ts.map +1 -0
- package/lib/utils/logger.js +74 -0
- package/lib/utils/object.d.ts +8 -0
- package/lib/utils/object.d.ts.map +1 -0
- package/lib/utils/object.js +15 -0
- package/lib/utils/port.d.ts +5 -0
- package/lib/utils/port.d.ts.map +1 -0
- package/lib/utils/port.js +20 -0
- package/lib/utils/rand.d.ts +6 -0
- package/lib/utils/rand.d.ts.map +1 -0
- package/lib/utils/rand.js +15 -0
- package/package.json +30 -0
- package/playwright-screenshots.sh +53 -0
- package/src/@types/playwright-core.d.ts +12 -0
- package/src/expect/axe.ts +37 -0
- package/src/expect/index.ts +21 -0
- package/src/expect/screenshot.ts +79 -0
- package/src/fixtures/axe.ts +22 -0
- package/src/fixtures/index.ts +15 -0
- package/src/fixtures/services.ts +188 -0
- package/src/fixtures/user.ts +93 -0
- package/src/index.ts +92 -0
- package/src/testcontainers/HomeserverContainer.ts +87 -0
- package/src/testcontainers/index.ts +15 -0
- package/src/testcontainers/mailpit.ts +62 -0
- package/src/testcontainers/mas.ts +382 -0
- package/src/testcontainers/synapse.ts +493 -0
- package/src/utils/api.ts +113 -0
- package/src/utils/logger.ts +79 -0
- package/src/utils/object.ts +16 -0
- package/src/utils/port.ts +22 -0
- package/src/utils/rand.ts +17 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 New Vector Ltd.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
5
|
+
Please see LICENSE files in the repository root for full details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AbstractStartedContainer,
|
|
10
|
+
GenericContainer,
|
|
11
|
+
type RestartOptions,
|
|
12
|
+
type StartedTestContainer,
|
|
13
|
+
Wait,
|
|
14
|
+
} from "testcontainers";
|
|
15
|
+
import { type APIRequestContext, type TestInfo } from "@playwright/test";
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import * as YAML from "yaml";
|
|
18
|
+
import { set } from "lodash-es";
|
|
19
|
+
|
|
20
|
+
import { getFreePort } from "../utils/port.js";
|
|
21
|
+
import { randB64Bytes } from "../utils/rand.js";
|
|
22
|
+
import { deepCopy } from "../utils/object.js";
|
|
23
|
+
import { type HomeserverContainer, type StartedHomeserverContainer } from "./HomeserverContainer.js";
|
|
24
|
+
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.js";
|
|
25
|
+
import { Api, ClientServerApi, type Verb, type Credentials } from "../utils/api.js";
|
|
26
|
+
import { StartedMailpitContainer } from "./mailpit.js";
|
|
27
|
+
|
|
28
|
+
const TAG = "develop@sha256:8d0049e8e0524ad6817cf7737453fe47de1ed3b8d04704f0c2fd6c136414c9d7";
|
|
29
|
+
|
|
30
|
+
const DEFAULT_CONFIG = {
|
|
31
|
+
server_name: "localhost",
|
|
32
|
+
public_baseurl: "", // set by start method
|
|
33
|
+
pid_file: "/homeserver.pid",
|
|
34
|
+
web_client: false,
|
|
35
|
+
soft_file_limit: 0,
|
|
36
|
+
// Needs to be configured to log to the console like a good docker process
|
|
37
|
+
log_config: "/data/log.config",
|
|
38
|
+
listeners: [
|
|
39
|
+
{
|
|
40
|
+
// Listener is always port 8008 (configured in the container)
|
|
41
|
+
port: 8008,
|
|
42
|
+
tls: false,
|
|
43
|
+
bind_addresses: ["::"],
|
|
44
|
+
type: "http",
|
|
45
|
+
x_forwarded: true,
|
|
46
|
+
resources: [
|
|
47
|
+
{
|
|
48
|
+
names: ["client"],
|
|
49
|
+
compress: false,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
database: {
|
|
55
|
+
// An sqlite in-memory database is fast & automatically wipes each time
|
|
56
|
+
name: "sqlite3",
|
|
57
|
+
args: {
|
|
58
|
+
database: ":memory:",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
rc_messages_per_second: 10000,
|
|
62
|
+
rc_message_burst_count: 10000,
|
|
63
|
+
rc_registration: {
|
|
64
|
+
per_second: 10000,
|
|
65
|
+
burst_count: 10000,
|
|
66
|
+
},
|
|
67
|
+
rc_joins: {
|
|
68
|
+
local: {
|
|
69
|
+
per_second: 9999,
|
|
70
|
+
burst_count: 9999,
|
|
71
|
+
},
|
|
72
|
+
remote: {
|
|
73
|
+
per_second: 9999,
|
|
74
|
+
burst_count: 9999,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
rc_joins_per_room: {
|
|
78
|
+
per_second: 9999,
|
|
79
|
+
burst_count: 9999,
|
|
80
|
+
},
|
|
81
|
+
rc_3pid_validation: {
|
|
82
|
+
per_second: 1000,
|
|
83
|
+
burst_count: 1000,
|
|
84
|
+
},
|
|
85
|
+
rc_invites: {
|
|
86
|
+
per_room: {
|
|
87
|
+
per_second: 1000,
|
|
88
|
+
burst_count: 1000,
|
|
89
|
+
},
|
|
90
|
+
per_user: {
|
|
91
|
+
per_second: 1000,
|
|
92
|
+
burst_count: 1000,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
rc_login: {
|
|
96
|
+
address: {
|
|
97
|
+
per_second: 10000,
|
|
98
|
+
burst_count: 10000,
|
|
99
|
+
},
|
|
100
|
+
account: {
|
|
101
|
+
per_second: 10000,
|
|
102
|
+
burst_count: 10000,
|
|
103
|
+
},
|
|
104
|
+
failed_attempts: {
|
|
105
|
+
per_second: 10000,
|
|
106
|
+
burst_count: 10000,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
media_store_path: "/tmp/media_store",
|
|
110
|
+
max_upload_size: "50M",
|
|
111
|
+
max_image_pixels: "32M",
|
|
112
|
+
dynamic_thumbnails: false,
|
|
113
|
+
enable_registration: true,
|
|
114
|
+
enable_registration_without_verification: true,
|
|
115
|
+
disable_msisdn_registration: false,
|
|
116
|
+
registrations_require_3pid: [],
|
|
117
|
+
enable_metrics: false,
|
|
118
|
+
report_stats: false,
|
|
119
|
+
// These placeholders will be replaced with values generated at start
|
|
120
|
+
registration_shared_secret: "secret",
|
|
121
|
+
macaroon_secret_key: "secret",
|
|
122
|
+
form_secret: "secret",
|
|
123
|
+
// Signing key must be here: it will be generated to this file
|
|
124
|
+
signing_key_path: "/data/localhost.signing.key",
|
|
125
|
+
trusted_key_servers: [],
|
|
126
|
+
password_config: {
|
|
127
|
+
enabled: true,
|
|
128
|
+
},
|
|
129
|
+
ui_auth: {},
|
|
130
|
+
background_updates: {
|
|
131
|
+
// Inhibit background updates as this Synapse isn't long-lived
|
|
132
|
+
min_batch_size: 100000,
|
|
133
|
+
sleep_duration_ms: 100000,
|
|
134
|
+
},
|
|
135
|
+
enable_authenticated_media: true,
|
|
136
|
+
email: undefined as
|
|
137
|
+
| undefined
|
|
138
|
+
| {
|
|
139
|
+
enable_notifs: boolean;
|
|
140
|
+
smtp_host: string;
|
|
141
|
+
smtp_port: number;
|
|
142
|
+
smtp_user: string;
|
|
143
|
+
smtp_pass: string;
|
|
144
|
+
require_transport_security: false;
|
|
145
|
+
notif_from: string;
|
|
146
|
+
app_name: string;
|
|
147
|
+
notif_template_html: string;
|
|
148
|
+
notif_template_text: string;
|
|
149
|
+
notif_for_new_users: boolean;
|
|
150
|
+
client_base_url: string;
|
|
151
|
+
},
|
|
152
|
+
user_consent: undefined as
|
|
153
|
+
| undefined
|
|
154
|
+
| {
|
|
155
|
+
template_dir: string;
|
|
156
|
+
version: string;
|
|
157
|
+
server_notice_content: Record<string, unknown>;
|
|
158
|
+
send_server_notice_to_guests: boolean;
|
|
159
|
+
block_events_error: string;
|
|
160
|
+
require_at_registration: boolean;
|
|
161
|
+
},
|
|
162
|
+
server_notices: undefined as
|
|
163
|
+
| undefined
|
|
164
|
+
| {
|
|
165
|
+
system_mxid_localpart: string;
|
|
166
|
+
system_mxid_display_name: string;
|
|
167
|
+
system_mxid_avatar_url: string;
|
|
168
|
+
room_name: string;
|
|
169
|
+
},
|
|
170
|
+
allow_guest_access: false,
|
|
171
|
+
experimental_features: {},
|
|
172
|
+
oidc_providers: [],
|
|
173
|
+
serve_server_wellknown: true,
|
|
174
|
+
presence: {
|
|
175
|
+
enabled: true,
|
|
176
|
+
include_offline_users_on_sync: true,
|
|
177
|
+
},
|
|
178
|
+
room_list_publication_rules: [{ action: "allow" }],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Incomplete type describing the configuration for a Synapse homeserver
|
|
183
|
+
*/
|
|
184
|
+
export type SynapseConfig = typeof DEFAULT_CONFIG;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A Synapse testcontainer
|
|
188
|
+
*
|
|
189
|
+
* Exposes port 8008.
|
|
190
|
+
* Waits for HTTP /health 8008 to 200.
|
|
191
|
+
*/
|
|
192
|
+
export class SynapseContainer extends GenericContainer implements HomeserverContainer<SynapseConfig> {
|
|
193
|
+
private config: SynapseConfig;
|
|
194
|
+
private mas?: StartedMatrixAuthenticationServiceContainer;
|
|
195
|
+
|
|
196
|
+
public constructor() {
|
|
197
|
+
super(`ghcr.io/element-hq/synapse:${TAG}`);
|
|
198
|
+
|
|
199
|
+
this.config = deepCopy(DEFAULT_CONFIG);
|
|
200
|
+
this.config.registration_shared_secret = randB64Bytes(16);
|
|
201
|
+
this.config.macaroon_secret_key = randB64Bytes(16);
|
|
202
|
+
this.config.form_secret = randB64Bytes(16);
|
|
203
|
+
|
|
204
|
+
const signingKey = randB64Bytes(32);
|
|
205
|
+
this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([
|
|
206
|
+
{ target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` },
|
|
207
|
+
{
|
|
208
|
+
target: this.config.log_config,
|
|
209
|
+
content: YAML.stringify({
|
|
210
|
+
version: 1,
|
|
211
|
+
formatters: {
|
|
212
|
+
precise: {
|
|
213
|
+
format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
handlers: {
|
|
217
|
+
console: {
|
|
218
|
+
class: "logging.StreamHandler",
|
|
219
|
+
formatter: "precise",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
loggers: {
|
|
223
|
+
"synapse.storage.SQL": {
|
|
224
|
+
level: "DEBUG",
|
|
225
|
+
},
|
|
226
|
+
"twisted": {
|
|
227
|
+
handlers: ["console"],
|
|
228
|
+
propagate: false,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
root: {
|
|
232
|
+
level: "DEBUG",
|
|
233
|
+
handlers: ["console"],
|
|
234
|
+
},
|
|
235
|
+
disable_existing_loggers: false,
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public withConfigField(key: string, value: unknown): this {
|
|
242
|
+
set(this.config, key, value);
|
|
243
|
+
return this;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public withConfig(config: Partial<SynapseConfig>): this {
|
|
247
|
+
this.config = {
|
|
248
|
+
...this.config,
|
|
249
|
+
...config,
|
|
250
|
+
};
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public withSmtpServer(mailpit: StartedMailpitContainer): this {
|
|
255
|
+
this.config.email = {
|
|
256
|
+
enable_notifs: false,
|
|
257
|
+
smtp_host: mailpit.internalHost,
|
|
258
|
+
smtp_port: mailpit.internalSmtpPort,
|
|
259
|
+
smtp_user: "username",
|
|
260
|
+
smtp_pass: "password",
|
|
261
|
+
require_transport_security: false,
|
|
262
|
+
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
|
|
263
|
+
app_name: "Matrix",
|
|
264
|
+
notif_template_html: "notif_mail.html",
|
|
265
|
+
notif_template_text: "notif_mail.txt",
|
|
266
|
+
notif_for_new_users: true,
|
|
267
|
+
client_base_url: "http://localhost/element",
|
|
268
|
+
};
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
|
|
273
|
+
this.mas = mas;
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public override async start(): Promise<StartedSynapseContainer> {
|
|
278
|
+
// Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually
|
|
279
|
+
const port = await getFreePort();
|
|
280
|
+
|
|
281
|
+
this.withExposedPorts({
|
|
282
|
+
container: 8008,
|
|
283
|
+
host: port,
|
|
284
|
+
})
|
|
285
|
+
.withConfig({
|
|
286
|
+
public_baseurl: `http://localhost:${port}`,
|
|
287
|
+
})
|
|
288
|
+
.withCopyContentToContainer([
|
|
289
|
+
{
|
|
290
|
+
target: "/data/homeserver.yaml",
|
|
291
|
+
content: YAML.stringify(this.config),
|
|
292
|
+
},
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
const container = await super.start();
|
|
296
|
+
const baseUrl = `http://localhost:${port}`;
|
|
297
|
+
if (this.mas) {
|
|
298
|
+
return new StartedSynapseWithMasContainer(
|
|
299
|
+
container,
|
|
300
|
+
baseUrl,
|
|
301
|
+
this.config.registration_shared_secret,
|
|
302
|
+
this.mas,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return new StartedSynapseContainer(container, baseUrl, this.config.registration_shared_secret);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A started Synapse testcontainer
|
|
312
|
+
*/
|
|
313
|
+
export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer {
|
|
314
|
+
protected adminTokenPromise?: Promise<string>;
|
|
315
|
+
protected readonly adminApi: Api;
|
|
316
|
+
public readonly csApi: ClientServerApi;
|
|
317
|
+
|
|
318
|
+
public constructor(
|
|
319
|
+
container: StartedTestContainer,
|
|
320
|
+
public readonly baseUrl: string,
|
|
321
|
+
private readonly registrationSharedSecret: string,
|
|
322
|
+
) {
|
|
323
|
+
super(container);
|
|
324
|
+
this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`);
|
|
325
|
+
this.csApi = new ClientServerApi(this.baseUrl);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Restart the container
|
|
330
|
+
* Useful to reset the state of the homeserver between tests
|
|
331
|
+
* @param options - options to pass to the restart
|
|
332
|
+
*/
|
|
333
|
+
public restart(options?: Partial<RestartOptions>): Promise<void> {
|
|
334
|
+
this.adminTokenPromise = undefined;
|
|
335
|
+
return super.restart(options);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public setRequest(request: APIRequestContext): void {
|
|
339
|
+
this.csApi.setRequest(request);
|
|
340
|
+
this.adminApi.setRequest(request);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public async onTestFinished(testInfo: TestInfo): Promise<void> {
|
|
344
|
+
// Clean up the server to prevent rooms leaking between tests
|
|
345
|
+
await this.deletePublicRooms();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected async deletePublicRooms(): Promise<void> {
|
|
349
|
+
const token = await this.getAdminToken();
|
|
350
|
+
// We hide the rooms from the room directory to save time between tests and for portability between homeservers
|
|
351
|
+
const { chunk: rooms } = await this.csApi.request<{
|
|
352
|
+
chunk: { room_id: string }[];
|
|
353
|
+
}>("GET", "/v3/publicRooms", token, {});
|
|
354
|
+
await Promise.all(
|
|
355
|
+
rooms.map((room) =>
|
|
356
|
+
this.csApi.request("PUT", `/v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }),
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async registerUserInternal(
|
|
362
|
+
username: string,
|
|
363
|
+
password: string,
|
|
364
|
+
displayName?: string,
|
|
365
|
+
admin = false,
|
|
366
|
+
): Promise<Credentials> {
|
|
367
|
+
const path = "/v1/register";
|
|
368
|
+
const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {});
|
|
369
|
+
const mac = crypto
|
|
370
|
+
.createHmac("sha1", this.registrationSharedSecret)
|
|
371
|
+
.update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
|
|
372
|
+
.digest("hex");
|
|
373
|
+
const data = await this.adminApi.request<{
|
|
374
|
+
home_server: string;
|
|
375
|
+
access_token: string;
|
|
376
|
+
user_id: string;
|
|
377
|
+
device_id: string;
|
|
378
|
+
}>("POST", path, undefined, {
|
|
379
|
+
nonce,
|
|
380
|
+
username,
|
|
381
|
+
password,
|
|
382
|
+
mac,
|
|
383
|
+
admin,
|
|
384
|
+
displayname: displayName,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
homeServer: data.home_server || data.user_id.split(":").slice(1).join(":"),
|
|
389
|
+
accessToken: data.access_token,
|
|
390
|
+
userId: data.user_id,
|
|
391
|
+
deviceId: data.device_id,
|
|
392
|
+
password,
|
|
393
|
+
displayName,
|
|
394
|
+
username,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
protected async getAdminToken(): Promise<string> {
|
|
399
|
+
if (this.adminTokenPromise === undefined) {
|
|
400
|
+
this.adminTokenPromise = this.registerUserInternal(
|
|
401
|
+
"admin",
|
|
402
|
+
"totalyinsecureadminpassword",
|
|
403
|
+
undefined,
|
|
404
|
+
true,
|
|
405
|
+
).then((res) => res.accessToken);
|
|
406
|
+
}
|
|
407
|
+
return this.adminTokenPromise;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async adminRequest<R extends object>(verb: "GET", path: string, data?: never): Promise<R>;
|
|
411
|
+
private async adminRequest<R extends object>(verb: Verb, path: string, data?: object): Promise<R>;
|
|
412
|
+
private async adminRequest<R extends object>(verb: Verb, path: string, data?: object): Promise<R> {
|
|
413
|
+
const adminToken = await this.getAdminToken();
|
|
414
|
+
return this.adminApi.request(verb, path, adminToken, data);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Register a user on the given Homeserver using the shared registration secret.
|
|
419
|
+
* @param username - the username of the user to register
|
|
420
|
+
* @param password - the password of the user to register
|
|
421
|
+
* @param displayName - optional display name to set on the newly registered user
|
|
422
|
+
*/
|
|
423
|
+
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
|
424
|
+
return this.registerUserInternal(username, password, displayName, false);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Logs into synapse with the given username/password
|
|
429
|
+
* @param userId - login username
|
|
430
|
+
* @param password - login password
|
|
431
|
+
*/
|
|
432
|
+
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
|
433
|
+
return this.csApi.loginUser(userId, password);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Binds a 3pid
|
|
438
|
+
* @param userId - the username of the user to bind the 3pid to
|
|
439
|
+
* @param medium - the medium of the 3pid to bind
|
|
440
|
+
* @param address - the address of the 3pid to bind
|
|
441
|
+
*/
|
|
442
|
+
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
|
|
443
|
+
await this.adminRequest("PUT", `/v2/users/${userId}`, {
|
|
444
|
+
threepids: [
|
|
445
|
+
{
|
|
446
|
+
medium,
|
|
447
|
+
address,
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A started Synapse container when delegating auth to MAS
|
|
456
|
+
*/
|
|
457
|
+
export class StartedSynapseWithMasContainer extends StartedSynapseContainer {
|
|
458
|
+
public constructor(
|
|
459
|
+
container: StartedTestContainer,
|
|
460
|
+
baseUrl: string,
|
|
461
|
+
registrationSharedSecret: string,
|
|
462
|
+
private readonly mas: StartedMatrixAuthenticationServiceContainer,
|
|
463
|
+
) {
|
|
464
|
+
super(container, baseUrl, registrationSharedSecret);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
protected async getAdminToken(): Promise<string> {
|
|
468
|
+
if (this.adminTokenPromise === undefined) {
|
|
469
|
+
this.adminTokenPromise = this.mas.getAdminToken();
|
|
470
|
+
}
|
|
471
|
+
return this.adminTokenPromise;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Register a user on the given Homeserver using the shared registration secret.
|
|
476
|
+
* @param username - the username of the user to register
|
|
477
|
+
* @param password - the password of the user to register
|
|
478
|
+
* @param displayName - optional display name to set on the newly registered user
|
|
479
|
+
*/
|
|
480
|
+
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
|
481
|
+
return this.mas.registerUser(username, password, displayName);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Binds a 3pid
|
|
486
|
+
* @param userId - the username of the user to bind the 3pid to
|
|
487
|
+
* @param medium - the medium of the 3pid to bind
|
|
488
|
+
* @param address - the address of the 3pid to bind
|
|
489
|
+
*/
|
|
490
|
+
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
|
|
491
|
+
return this.mas.setThreepid(userId, medium, address);
|
|
492
|
+
}
|
|
493
|
+
}
|
package/src/utils/api.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 New Vector Ltd.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
5
|
+
Please see LICENSE files in the repository root for full details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type APIRequestContext } from "@playwright/test";
|
|
9
|
+
|
|
10
|
+
export type Verb = "GET" | "POST" | "PUT" | "DELETE";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A generic API client.
|
|
14
|
+
*/
|
|
15
|
+
export class Api {
|
|
16
|
+
private _request?: APIRequestContext;
|
|
17
|
+
|
|
18
|
+
public constructor(private readonly baseUrl: string) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set the request context to use for making requests.
|
|
22
|
+
* @param request - The request context to use.
|
|
23
|
+
*/
|
|
24
|
+
public setRequest(request: APIRequestContext): void {
|
|
25
|
+
this._request = request;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Make a request to the API.
|
|
30
|
+
* @param verb - The HTTP verb to use.
|
|
31
|
+
* @param path - The path to request.
|
|
32
|
+
* @param token - The access token to use for the request.
|
|
33
|
+
* @param data - The data to send with the request.
|
|
34
|
+
*/
|
|
35
|
+
public async request<R extends object>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
|
|
36
|
+
public async request<R extends object>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
|
|
37
|
+
public async request<R extends object>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
|
|
38
|
+
if (!this._request) {
|
|
39
|
+
throw new Error("No request context set");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = `${this.baseUrl}${path}`;
|
|
43
|
+
const res = await this._request.fetch(url, {
|
|
44
|
+
data,
|
|
45
|
+
method: verb,
|
|
46
|
+
headers: token
|
|
47
|
+
? {
|
|
48
|
+
Authorization: `Bearer ${token}`,
|
|
49
|
+
}
|
|
50
|
+
: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!res.ok()) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return res.json();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Credentials for a user.
|
|
65
|
+
*/
|
|
66
|
+
export interface Credentials {
|
|
67
|
+
accessToken: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
deviceId: string;
|
|
70
|
+
homeServer: string;
|
|
71
|
+
password: string | null; // null for password-less users
|
|
72
|
+
displayName?: string;
|
|
73
|
+
username: string; // the localpart of the userId
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A client-server API for interacting with a Matrix homeserver.
|
|
78
|
+
*/
|
|
79
|
+
export class ClientServerApi extends Api {
|
|
80
|
+
public constructor(baseUrl: string) {
|
|
81
|
+
super(`${baseUrl}/_matrix/client`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register a user on the homeserver.
|
|
86
|
+
* @param userId - The user ID to register.
|
|
87
|
+
* @param password - The password to use for the user.
|
|
88
|
+
*/
|
|
89
|
+
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
|
90
|
+
const json = await this.request<{
|
|
91
|
+
access_token: string;
|
|
92
|
+
user_id: string;
|
|
93
|
+
device_id: string;
|
|
94
|
+
home_server: string;
|
|
95
|
+
}>("POST", "/v3/login", undefined, {
|
|
96
|
+
type: "m.login.password",
|
|
97
|
+
identifier: {
|
|
98
|
+
type: "m.id.user",
|
|
99
|
+
user: userId,
|
|
100
|
+
},
|
|
101
|
+
password: password,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
password,
|
|
106
|
+
accessToken: json.access_token,
|
|
107
|
+
userId: json.user_id,
|
|
108
|
+
deviceId: json.device_id,
|
|
109
|
+
homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"),
|
|
110
|
+
username: userId.slice(1).split(":")[0],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024-2025 New Vector Ltd.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
5
|
+
Please see LICENSE files in the repository root for full details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
|
9
|
+
import { type Readable } from "node:stream";
|
|
10
|
+
import stripAnsi from "strip-ansi";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A logger that captures console logs from pages and testcontainers.
|
|
14
|
+
*/
|
|
15
|
+
export class Logger {
|
|
16
|
+
private pages: Page[] = [];
|
|
17
|
+
private logs: Record<string, string> = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get a consumer function that captures logs for a given container.
|
|
21
|
+
* @param container - the human-readable name of the container.
|
|
22
|
+
*/
|
|
23
|
+
public getConsumer(container: string) {
|
|
24
|
+
this.logs[container] = "";
|
|
25
|
+
return (stream: Readable) => {
|
|
26
|
+
stream.on("data", (chunk) => {
|
|
27
|
+
this.logs[container] += chunk.toString();
|
|
28
|
+
});
|
|
29
|
+
stream.on("err", (chunk) => {
|
|
30
|
+
this.logs[container] += "ERR " + chunk.toString();
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hook to call when a test starts.
|
|
37
|
+
* @param context - the browser context of the test.
|
|
38
|
+
*/
|
|
39
|
+
public async onTestStarted(context: BrowserContext) {
|
|
40
|
+
this.pages = [];
|
|
41
|
+
for (const id in this.logs) {
|
|
42
|
+
if (id.startsWith("page-")) {
|
|
43
|
+
delete this.logs[id];
|
|
44
|
+
} else {
|
|
45
|
+
this.logs[id] = "";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
context.on("console", (msg) => {
|
|
50
|
+
const page = msg.page();
|
|
51
|
+
if (!page) return;
|
|
52
|
+
let pageIdx = this.pages.indexOf(page);
|
|
53
|
+
if (pageIdx === -1) {
|
|
54
|
+
this.pages.push(page);
|
|
55
|
+
pageIdx = this.pages.length - 1;
|
|
56
|
+
this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`;
|
|
57
|
+
}
|
|
58
|
+
const type = msg.type();
|
|
59
|
+
const text = msg.text();
|
|
60
|
+
this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hook to call when a test finishes.
|
|
66
|
+
* @param testInfo - the info about the test that just finished.
|
|
67
|
+
*/
|
|
68
|
+
public async onTestFinished(testInfo: TestInfo) {
|
|
69
|
+
if (testInfo.status !== "passed") {
|
|
70
|
+
for (const id in this.logs) {
|
|
71
|
+
if (!this.logs[id]) continue;
|
|
72
|
+
await testInfo.attach(id, {
|
|
73
|
+
body: stripAnsi(this.logs[id]),
|
|
74
|
+
contentType: "text/plain",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024-2025 New Vector Ltd.
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
5
|
+
Please see LICENSE files in the repository root for full details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deep copy the given object. The object MUST NOT have circular references and
|
|
10
|
+
* MUST NOT have functions.
|
|
11
|
+
* @param obj - The object to deep copy.
|
|
12
|
+
* @returns A copy of the object without any references to the original.
|
|
13
|
+
*/
|
|
14
|
+
export function deepCopy<T>(obj: T): T {
|
|
15
|
+
return JSON.parse(JSON.stringify(obj));
|
|
16
|
+
}
|