@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.
Files changed (96) hide show
  1. package/Dockerfile +9 -0
  2. package/README.md +28 -0
  3. package/lib/element-web-test.d.ts +26 -0
  4. package/lib/element-web-test.d.ts.map +1 -0
  5. package/lib/element-web-test.js +54 -0
  6. package/lib/expect/axe.d.ts +11 -0
  7. package/lib/expect/axe.d.ts.map +1 -0
  8. package/lib/expect/axe.js +22 -0
  9. package/lib/expect/index.d.ts +6 -0
  10. package/lib/expect/index.d.ts.map +1 -0
  11. package/lib/expect/index.js +10 -0
  12. package/lib/expect/screenshot.d.ts +14 -0
  13. package/lib/expect/screenshot.d.ts.map +1 -0
  14. package/lib/expect/screenshot.js +48 -0
  15. package/lib/expect/toHaveNoViolations.d.ts +11 -0
  16. package/lib/expect/toHaveNoViolations.d.ts.map +1 -0
  17. package/lib/expect/toHaveNoViolations.js +22 -0
  18. package/lib/expect/toMatchScreenshot.d.ts +13 -0
  19. package/lib/expect/toMatchScreenshot.d.ts.map +1 -0
  20. package/lib/expect/toMatchScreenshot.js +44 -0
  21. package/lib/expect/toPassAxeCheck.d.ts +7 -0
  22. package/lib/expect/toPassAxeCheck.d.ts.map +1 -0
  23. package/lib/expect/toPassAxeCheck.js +22 -0
  24. package/lib/fixtures/axe.d.ts +8 -0
  25. package/lib/fixtures/axe.d.ts.map +1 -0
  26. package/lib/fixtures/axe.js +15 -0
  27. package/lib/fixtures/index.d.ts +10 -0
  28. package/lib/fixtures/index.d.ts.map +1 -0
  29. package/lib/fixtures/index.js +10 -0
  30. package/lib/fixtures/services.d.ts +57 -0
  31. package/lib/fixtures/services.d.ts.map +1 -0
  32. package/lib/fixtures/services.js +114 -0
  33. package/lib/fixtures/user.d.ts +31 -0
  34. package/lib/fixtures/user.d.ts.map +1 -0
  35. package/lib/fixtures/user.js +47 -0
  36. package/lib/index.d.ts +35 -0
  37. package/lib/index.d.ts.map +1 -0
  38. package/lib/index.js +54 -0
  39. package/lib/logger.d.ts +10 -0
  40. package/lib/logger.d.ts.map +1 -0
  41. package/lib/logger.js +59 -0
  42. package/lib/playwright.d.ts +2 -0
  43. package/lib/playwright.d.ts.map +1 -0
  44. package/lib/playwright.js +1 -0
  45. package/lib/testcontainers/HomeserverContainer.d.ts +70 -0
  46. package/lib/testcontainers/HomeserverContainer.d.ts.map +1 -0
  47. package/lib/testcontainers/HomeserverContainer.js +7 -0
  48. package/lib/testcontainers/index.d.ts +5 -0
  49. package/lib/testcontainers/index.d.ts.map +1 -0
  50. package/lib/testcontainers/index.js +9 -0
  51. package/lib/testcontainers/mailpit.d.ts +33 -0
  52. package/lib/testcontainers/mailpit.d.ts.map +1 -0
  53. package/lib/testcontainers/mailpit.js +52 -0
  54. package/lib/testcontainers/mas.d.ts +182 -0
  55. package/lib/testcontainers/mas.d.ts.map +1 -0
  56. package/lib/testcontainers/mas.js +311 -0
  57. package/lib/testcontainers/synapse.d.ts +229 -0
  58. package/lib/testcontainers/synapse.d.ts.map +1 -0
  59. package/lib/testcontainers/synapse.js +383 -0
  60. package/lib/utils/api.d.ts +49 -0
  61. package/lib/utils/api.d.ts.map +1 -0
  62. package/lib/utils/api.js +73 -0
  63. package/lib/utils/logger.d.ts +25 -0
  64. package/lib/utils/logger.d.ts.map +1 -0
  65. package/lib/utils/logger.js +74 -0
  66. package/lib/utils/object.d.ts +8 -0
  67. package/lib/utils/object.d.ts.map +1 -0
  68. package/lib/utils/object.js +15 -0
  69. package/lib/utils/port.d.ts +5 -0
  70. package/lib/utils/port.d.ts.map +1 -0
  71. package/lib/utils/port.js +20 -0
  72. package/lib/utils/rand.d.ts +6 -0
  73. package/lib/utils/rand.d.ts.map +1 -0
  74. package/lib/utils/rand.js +15 -0
  75. package/package.json +30 -0
  76. package/playwright-screenshots.sh +53 -0
  77. package/src/@types/playwright-core.d.ts +12 -0
  78. package/src/expect/axe.ts +37 -0
  79. package/src/expect/index.ts +21 -0
  80. package/src/expect/screenshot.ts +79 -0
  81. package/src/fixtures/axe.ts +22 -0
  82. package/src/fixtures/index.ts +15 -0
  83. package/src/fixtures/services.ts +188 -0
  84. package/src/fixtures/user.ts +93 -0
  85. package/src/index.ts +92 -0
  86. package/src/testcontainers/HomeserverContainer.ts +87 -0
  87. package/src/testcontainers/index.ts +15 -0
  88. package/src/testcontainers/mailpit.ts +62 -0
  89. package/src/testcontainers/mas.ts +382 -0
  90. package/src/testcontainers/synapse.ts +493 -0
  91. package/src/utils/api.ts +113 -0
  92. package/src/utils/logger.ts +79 -0
  93. package/src/utils/object.ts +16 -0
  94. package/src/utils/port.ts +22 -0
  95. package/src/utils/rand.ts +17 -0
  96. package/tsconfig.json +10 -0
@@ -0,0 +1,87 @@
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 AbstractStartedContainer, type GenericContainer } from "testcontainers";
9
+ import { type APIRequestContext, type TestInfo } from "@playwright/test";
10
+
11
+ import { type StartedMatrixAuthenticationServiceContainer } from "./mas";
12
+ import { ClientServerApi, Credentials } from "../utils/api";
13
+ import { StartedMailpitContainer } from "./mailpit";
14
+
15
+ export interface HomeserverInstance {
16
+ readonly baseUrl: string;
17
+ readonly csApi: ClientServerApi;
18
+
19
+ /**
20
+ * Register a user on the given Homeserver using the shared registration secret.
21
+ * @param username the username of the user to register
22
+ * @param password the password of the user to register
23
+ * @param displayName optional display name to set on the newly registered user
24
+ */
25
+ registerUser(username: string, password: string, displayName?: string): Promise<Credentials>;
26
+
27
+ /**
28
+ * Logs into synapse with the given username/password
29
+ * @param userId login username
30
+ * @param password login password
31
+ */
32
+ loginUser(userId: string, password: string): Promise<Credentials>;
33
+
34
+ /**
35
+ * Sets a third party identifier for the given user. This only supports setting a single 3pid and will
36
+ * replace any others.
37
+ * @param userId The full ID of the user to edit (as returned from registerUser)
38
+ * @param medium The medium of the 3pid to set
39
+ * @param address The address of the 3pid to set
40
+ */
41
+ setThreepid(userId: string, medium: string, address: string): Promise<void>;
42
+ }
43
+
44
+ export interface HomeserverContainer<Config> extends GenericContainer {
45
+ /**
46
+ * Set a configuration field in the config
47
+ * @param key - the key to set
48
+ * @param value - the value to set
49
+ */
50
+ withConfigField<Key extends keyof Config>(key: Key, value: Config[Key]): this;
51
+
52
+ /**
53
+ * Merge a partial configuration into the config
54
+ * @param config - the partial configuration to merge
55
+ */
56
+ withConfig(config: Partial<Config>): this;
57
+
58
+ /**
59
+ * Set the SMTP server to use for sending emails
60
+ * @param mailpit - the mailpit container to use
61
+ */
62
+ withSmtpServer(mailpit: StartedMailpitContainer): this;
63
+ /**
64
+ * Set the MAS server to use for delegated auth
65
+ * @param mas - the MAS container to use
66
+ */
67
+ withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this;
68
+
69
+ /**
70
+ * Start the container
71
+ */
72
+ start(): Promise<StartedHomeserverContainer>;
73
+ }
74
+
75
+ export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
76
+ /**
77
+ * Set the request context for the APIs
78
+ * @param request - the request context to set
79
+ */
80
+ setRequest(request: APIRequestContext): void;
81
+
82
+ /**
83
+ * Clean up the server to prevent rooms leaking between tests
84
+ * @param testInfo - the test info for the test that just finished
85
+ */
86
+ onTestFinished(testInfo: TestInfo): Promise<void>;
87
+ }
@@ -0,0 +1,15 @@
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
+ export type { HomeserverInstance, HomeserverContainer, StartedHomeserverContainer } from "./HomeserverContainer.js";
9
+ export { type SynapseConfig, SynapseContainer, StartedSynapseContainer } from "./synapse.js";
10
+ export {
11
+ type MasConfig,
12
+ MatrixAuthenticationServiceContainer,
13
+ StartedMatrixAuthenticationServiceContainer,
14
+ } from "./mas.js";
15
+ export { MailpitClient, MailpitContainer, StartedMailpitContainer } from "./mailpit.js";
@@ -0,0 +1,62 @@
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 { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
9
+ import { MailpitClient } from "mailpit-api";
10
+
11
+ export type { MailpitClient };
12
+
13
+ /**
14
+ * A testcontainer for Mailpit.
15
+ *
16
+ * Exposes port 8025.
17
+ * Waits for listening ports.
18
+ * Disables SMTP authentication.
19
+ */
20
+ export class MailpitContainer extends GenericContainer {
21
+ public constructor() {
22
+ super("axllent/mailpit:latest");
23
+
24
+ this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()).withEnvironment({
25
+ MP_SMTP_AUTH_ALLOW_INSECURE: "true",
26
+ MP_SMTP_AUTH_ACCEPT_ANY: "true",
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Start the Mailpit container.
32
+ */
33
+ public override async start(): Promise<StartedMailpitContainer> {
34
+ return new StartedMailpitContainer(await super.start());
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A started Mailpit container.
40
+ */
41
+ export class StartedMailpitContainer extends AbstractStartedContainer {
42
+ public readonly client: MailpitClient;
43
+
44
+ public constructor(container: StartedTestContainer) {
45
+ super(container);
46
+ this.client = new MailpitClient(`http://${container.getHost()}:${container.getMappedPort(8025)}`);
47
+ }
48
+
49
+ /**
50
+ * Get the hostname to use to connect to the Mailpit container from inside the docker network.
51
+ */
52
+ public get internalHost(): string {
53
+ return "mailpit";
54
+ }
55
+
56
+ /**
57
+ * Get the port to use to connect to the Mailpit container from inside the docker network.
58
+ */
59
+ public get internalSmtpPort(): number {
60
+ return 1025;
61
+ }
62
+ }
@@ -0,0 +1,382 @@
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 {
9
+ AbstractStartedContainer,
10
+ GenericContainer,
11
+ type StartedTestContainer,
12
+ Wait,
13
+ type ExecResult,
14
+ } from "testcontainers";
15
+ import { type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
16
+ import * as YAML from "yaml";
17
+
18
+ import { getFreePort } from "../utils/port.js";
19
+ import { deepCopy } from "../utils/object.js";
20
+ import { type Credentials } from "../utils/api.js";
21
+
22
+ const DEFAULT_CONFIG = {
23
+ http: {
24
+ listeners: [
25
+ {
26
+ name: "web",
27
+ resources: [
28
+ { name: "discovery" },
29
+ { name: "human" },
30
+ { name: "oauth" },
31
+ { name: "compat" },
32
+ {
33
+ name: "graphql",
34
+ playground: true,
35
+ },
36
+ {
37
+ name: "assets",
38
+ path: "/usr/local/share/mas-cli/assets/",
39
+ },
40
+ ],
41
+ binds: [
42
+ {
43
+ address: "[::]:8080",
44
+ },
45
+ ],
46
+ proxy_protocol: false,
47
+ },
48
+ {
49
+ name: "internal",
50
+ resources: [
51
+ {
52
+ name: "health",
53
+ },
54
+ ],
55
+ binds: [
56
+ {
57
+ address: "[::]:8081",
58
+ },
59
+ ],
60
+ proxy_protocol: false,
61
+ },
62
+ ],
63
+ trusted_proxies: ["192.128.0.0/16", "172.16.0.0/12", "10.0.0.0/10", "127.0.0.1/8", "fd00::/8", "::1/128"],
64
+ public_base: "", // Needs to be set
65
+ issuer: "", // Needs to be set
66
+ },
67
+ database: {
68
+ host: "postgres",
69
+ port: 5432,
70
+ database: "postgres",
71
+ username: "postgres",
72
+ password: "p4S5w0rD",
73
+ max_connections: 10,
74
+ min_connections: 0,
75
+ connect_timeout: 30,
76
+ idle_timeout: 600,
77
+ max_lifetime: 1800,
78
+ },
79
+ telemetry: {
80
+ tracing: {
81
+ exporter: "none",
82
+ propagators: [],
83
+ },
84
+ metrics: {
85
+ exporter: "none",
86
+ },
87
+ sentry: {
88
+ dsn: null,
89
+ },
90
+ },
91
+ templates: {
92
+ path: "/usr/local/share/mas-cli/templates/",
93
+ assets_manifest: "/usr/local/share/mas-cli/manifest.json",
94
+ translations_path: "/usr/local/share/mas-cli/translations/",
95
+ },
96
+ email: {
97
+ from: '"Authentication Service" <root@localhost>',
98
+ reply_to: '"Authentication Service" <root@localhost>',
99
+ transport: "smtp",
100
+ mode: "plain",
101
+ hostname: "mailpit",
102
+ port: 1025,
103
+ username: "username",
104
+ password: "password",
105
+ },
106
+ secrets: {
107
+ encryption: "984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5",
108
+ keys: [
109
+ {
110
+ kid: "YEAhzrKipJ",
111
+ key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B\nS79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/\n+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki\nOXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW\nR+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA\nuiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83\nCdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8\nz8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv\nx2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w\nVkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK\nUdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F\nvYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7\nXnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4\ncgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V\n4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT\nhr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V\n5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN\nyO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ\nNghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw\nb4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/\n/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH\nfjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt\n+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ\n1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m\nMC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq\n-----END RSA PRIVATE KEY-----\n",
112
+ },
113
+ {
114
+ kid: "8J1AxrlNZT",
115
+ key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49\nAwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW\ndE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==\n-----END EC PRIVATE KEY-----\n",
116
+ },
117
+ {
118
+ kid: "3BW6un1EBi",
119
+ key: "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2\nq3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK\nmZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P\n9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=\n-----END EC PRIVATE KEY-----\n",
120
+ },
121
+ {
122
+ kid: "pkZ0pTKK0X",
123
+ key: "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK\noUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl\nAer+6PMZpPc8ycyeH9N+U9NAyliBhQ==\n-----END EC PRIVATE KEY-----\n",
124
+ },
125
+ ],
126
+ },
127
+ passwords: {
128
+ enabled: true,
129
+ schemes: [
130
+ {
131
+ version: 1,
132
+ algorithm: "argon2id",
133
+ },
134
+ ],
135
+ minimum_complexity: 0,
136
+ },
137
+ policy: {
138
+ wasm_module: "/usr/local/share/mas-cli/policy.wasm",
139
+ client_registration_entrypoint: "client_registration/violation",
140
+ register_entrypoint: "register/violation",
141
+ authorization_grant_entrypoint: "authorization_grant/violation",
142
+ password_entrypoint: "password/violation",
143
+ email_entrypoint: "email/violation",
144
+ data: {
145
+ client_registration: {
146
+ // allow non-SSL and localhost URIs
147
+ allow_insecure_uris: true,
148
+ // EW doesn't have contacts at this time
149
+ allow_missing_contacts: true,
150
+ },
151
+ },
152
+ },
153
+ upstream_oauth2: {
154
+ providers: [],
155
+ },
156
+ branding: {
157
+ service_name: null,
158
+ policy_uri: null,
159
+ tos_uri: null,
160
+ imprint: null,
161
+ logo_uri: null,
162
+ },
163
+ account: {
164
+ password_registration_enabled: true,
165
+ },
166
+ experimental: {
167
+ access_token_ttl: 300,
168
+ compat_token_ttl: 300,
169
+ },
170
+ rate_limiting: {
171
+ login: {
172
+ burst: 10,
173
+ per_second: 1,
174
+ },
175
+ registration: {
176
+ burst: 10,
177
+ per_second: 1,
178
+ },
179
+ },
180
+ };
181
+
182
+ /**
183
+ * Incomplete type for the MAS configuration.
184
+ */
185
+ export type MasConfig = typeof DEFAULT_CONFIG;
186
+
187
+ /**
188
+ * A container running the Matrix Authentication Service.
189
+ *
190
+ * Exposes the MAS API on port 8080 and the health check on port 8081.
191
+ * Waits for HTTP /health on port 8081 to be available.
192
+ */
193
+ export class MatrixAuthenticationServiceContainer extends GenericContainer {
194
+ private config: MasConfig;
195
+ private readonly args = ["-c", "/config/config.yaml"];
196
+
197
+ public constructor(db: StartedPostgreSqlContainer) {
198
+ // We rely on `mas-cli manage add-email` which isn't in a release yet
199
+ // https://github.com/element-hq/matrix-authentication-service/pull/3235
200
+ super("ghcr.io/element-hq/matrix-authentication-service:sha-0b90c33");
201
+
202
+ this.config = deepCopy(DEFAULT_CONFIG);
203
+ this.config.database.username = db.getUsername();
204
+ this.config.database.password = db.getPassword();
205
+
206
+ this.withExposedPorts(8080, 8081)
207
+ .withWaitStrategy(Wait.forHttp("/health", 8081))
208
+ .withCommand(["server", ...this.args]);
209
+ }
210
+
211
+ /**
212
+ * Adds additional configuration to the MAS config.
213
+ * @param config - additional config fields to add
214
+ */
215
+ public withConfig(config: object): this {
216
+ this.config = {
217
+ ...this.config,
218
+ ...config,
219
+ };
220
+ return this;
221
+ }
222
+
223
+ /**
224
+ * Starts the MAS container
225
+ */
226
+ public override async start(): Promise<StartedMatrixAuthenticationServiceContainer> {
227
+ // MAS config issuer needs to know what URL it'll be accessed from, so we have to map the port manually
228
+ const port = await getFreePort();
229
+
230
+ this.config.http.public_base = `http://localhost:${port}/`;
231
+ this.config.http.issuer = `http://localhost:${port}/`;
232
+
233
+ this.withExposedPorts({
234
+ container: 8080,
235
+ host: port,
236
+ }).withCopyContentToContainer([
237
+ {
238
+ target: "/config/config.yaml",
239
+ content: YAML.stringify(this.config),
240
+ },
241
+ ]);
242
+
243
+ return new StartedMatrixAuthenticationServiceContainer(
244
+ await super.start(),
245
+ `http://localhost:${port}`,
246
+ this.args,
247
+ );
248
+ }
249
+ }
250
+
251
+ /**
252
+ * A started Matrix Authentication Service container.
253
+ */
254
+ export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer {
255
+ private adminTokenPromise?: Promise<string>;
256
+
257
+ public constructor(
258
+ container: StartedTestContainer,
259
+ public readonly baseUrl: string,
260
+ private readonly args: string[],
261
+ ) {
262
+ super(container);
263
+ }
264
+
265
+ /**
266
+ * Retrieves a valid HS admin token
267
+ */
268
+ public async getAdminToken(): Promise<string> {
269
+ if (this.adminTokenPromise === undefined) {
270
+ this.adminTokenPromise = this.registerUserInternal(
271
+ "admin",
272
+ "totalyinsecureadminpassword",
273
+ undefined,
274
+ true,
275
+ ).then((res) => res.accessToken);
276
+ }
277
+ return this.adminTokenPromise;
278
+ }
279
+
280
+ private async manage(cmd: string, ...args: string[]): Promise<ExecResult> {
281
+ const result = await this.exec(["mas-cli", "manage", cmd, ...this.args, ...args]);
282
+ if (result.exitCode !== 0) {
283
+ throw new Error(`Failed mas-cli manage ${cmd}: ${result.output}`);
284
+ }
285
+ return result;
286
+ }
287
+
288
+ private async manageRegisterUser(
289
+ username: string,
290
+ password: string,
291
+ displayName?: string,
292
+ admin = false,
293
+ ): Promise<string> {
294
+ const args: string[] = [];
295
+ if (admin) args.push("-a");
296
+ const result = await this.manage(
297
+ "register-user",
298
+ ...args,
299
+ "-y",
300
+ "-p",
301
+ password,
302
+ "-d",
303
+ displayName ?? "",
304
+ username,
305
+ );
306
+
307
+ const registerLines = result.output.trim().split("\n");
308
+ const userId = registerLines
309
+ .find((line) => line.includes("Matrix ID: "))
310
+ ?.split(": ")
311
+ .pop();
312
+
313
+ if (!userId) {
314
+ throw new Error(`Failed to register user: ${result.output}`);
315
+ }
316
+
317
+ return userId;
318
+ }
319
+
320
+ private async manageIssueCompatibilityToken(
321
+ username: string,
322
+ admin = false,
323
+ ): Promise<{ accessToken: string; deviceId: string }> {
324
+ const args: string[] = [];
325
+ if (admin) args.push("--yes-i-want-to-grant-synapse-admin-privileges");
326
+ const result = await this.manage("issue-compatibility-token", ...args, username);
327
+
328
+ const parts = result.output.trim().split(/\s+/);
329
+ const accessToken = parts.find((part) => part.startsWith("mct_"));
330
+ const deviceId = parts.find((part) => part.startsWith("compat_session.device="))?.split("=")[1];
331
+
332
+ if (!accessToken || !deviceId) {
333
+ throw new Error(`Failed to issue compatibility token: ${result.output}`);
334
+ }
335
+
336
+ return { accessToken, deviceId };
337
+ }
338
+
339
+ private async registerUserInternal(
340
+ username: string,
341
+ password: string,
342
+ displayName?: string,
343
+ admin = false,
344
+ ): Promise<Credentials> {
345
+ const userId = await this.manageRegisterUser(username, password, displayName, admin);
346
+ const { deviceId, accessToken } = await this.manageIssueCompatibilityToken(username, admin);
347
+
348
+ return {
349
+ userId,
350
+ accessToken,
351
+ deviceId,
352
+ homeServer: userId.slice(1).split(":").slice(1).join(":"),
353
+ displayName,
354
+ username,
355
+ password,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Registers a user
361
+ * @param username - the username of the user to register
362
+ * @param password - the password of the user to register
363
+ * @param displayName - optional display name to set on the newly registered user
364
+ */
365
+ public async registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
366
+ return this.registerUserInternal(username, password, displayName, false);
367
+ }
368
+
369
+ /**
370
+ * Binds a 3pid
371
+ * @param username - the username of the user to bind the 3pid to
372
+ * @param medium - the medium of the 3pid to bind
373
+ * @param address - the address of the 3pid to bind
374
+ */
375
+ public async setThreepid(username: string, medium: string, address: string): Promise<void> {
376
+ if (medium !== "email") {
377
+ throw new Error("Only email threepids are supported by MAS");
378
+ }
379
+
380
+ await this.manage("add-email", username, address);
381
+ }
382
+ }