Add Synapse end-to-end test workflow

This commit is contained in:
Manuel Stahl
2026-03-08 12:35:07 +01:00
parent e510a5665b
commit e6228d5bd1
13 changed files with 364 additions and 4 deletions
+57
View File
@@ -0,0 +1,57 @@
name: e2e-test
on:
workflow_dispatch:
push:
branches: ["master"]
pull_request:
permissions:
contents: read
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup node
uses: actions/setup-node@v6
with:
node-version: 24
- name: Install dependencies
run: yarn --immutable
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-playwright-
- name: Install Playwright browser
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps chromium
- name: Run Synapse end-to-end test
run: yarn test:e2e:ci
- name: Collect docker logs on failure
if: failure()
run: docker compose -f docker-compose.yml --project-name synapse-admin-e2e logs --no-color > e2e-compose.log
- name: Upload failure artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: e2e-synapse-artifacts
path: |
e2e-compose.log
playwright-report
test-results
+3
View File
@@ -26,6 +26,9 @@ lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
playwright-report/
test-results/
e2e-compose.log
# nyc test coverage
.nyc_output
+28 -3
View File
@@ -2,9 +2,11 @@ services:
synapse-admin:
container_name: synapse-admin
hostname: synapse-admin
image: awesometechnologies/synapse-admin:latest
# build:
# context: .
# Use a prebuilt image:
#image: awesometechnologies/synapse-admin:latest
# or build from source:
build:
context: .
# to use the docker-compose as standalone without a local repo clone,
# replace the context definition with this:
@@ -16,6 +18,9 @@ services:
# to define a maximum ram for node. otherwise the build will fail.
# - NODE_OPTIONS="--max_old_space_size=1024"
# - BASE_PATH="/synapse-admin"
depends_on:
synapse:
condition: service_healthy
ports:
- "8080:80"
restart: unless-stopped
@@ -23,3 +28,23 @@ services:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/config.json >/dev/null 2>&1 || exit 1"]
interval: 5s
timeout: 5s
synapse:
image: matrixdotorg/synapse:v1.141.0
environment:
SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
SYNAPSE_REPORT_STATS: "no"
SYNAPSE_SERVER_NAME: localhost
ports:
- "8008:8008"
volumes:
- ${SYNAPSE_DATA_DIR:-/tmp/synapse}:/data
restart: unless-stopped
healthcheck:
test:
[
"CMD-SHELL",
"python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8008/_matrix/client/versions', timeout=5)\"",
]
interval: 5s
timeout: 5s
+22
View File
@@ -0,0 +1,22 @@
import { expect, test } from "@playwright/test";
test("logs into Synapse and loads the users list", async ({ page }) => {
// Navigate to start page
await page.goto("/");
// Wait for redirect to login page
await page.waitForURL("**/login");
// Fill login data
await page.getByLabel("Username").fill("admin");
await page.locator('input[name="password"]').fill("supersecret");
await page.getByLabel("Homeserver URL").fill("http://localhost:8008");
// Expect server version
await expect (page.getByText("1.141.0")).toBeVisible();
// Sign in
await page.getByRole("button", { name: "Sign in", exact: true }).click();
// Expect users table
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
await expect(page.getByRole("cell", { name: "@admin:localhost" })).toBeVisible();
await expect(page.getByRole("cell", { name: "admin", exact: true })).toBeVisible();
});
+4 -1
View File
@@ -14,6 +14,7 @@
"devDependencies": {
"@mui/system": "^7.1.0",
"@mui/utils": "^7.1.0",
"@playwright/test": "^1.58.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
@@ -89,7 +90,9 @@
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
"fix": "yarn lint --fix",
"test": "vitest run",
"test:watch": "vitest watch"
"test:watch": "vitest watch",
"test:e2e": "playwright test",
"test:e2e:ci": "./scripts/e2e/run.sh"
},
"eslintConfig": {
"env": {
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: false,
reporter: [["html", { open: "never" }]],
retries: process.env.CI ? 1 : 0,
use: {
baseURL: process.env.E2E_BASE_URL ?? "http://127.0.0.1:8080",
headless: true,
locale: "en-US",
trace: "retain-on-failure",
},
});
+31
View File
@@ -0,0 +1,31 @@
import fs from "node:fs";
const filePath = process.argv[2];
if (!filePath) {
throw new Error("Usage: node configure-synapse.mjs <homeserver.yaml>");
}
const upsertScalar = (content, key, value) => {
const pattern = new RegExp(`^${key}:.*$`, "m");
const line = `${key}: ${value}`;
return pattern.test(content) ? content.replace(pattern, line) : `${content.trimEnd()}\n${line}\n`;
};
const removeScalar = (content, key) => content.replace(new RegExp(`^${key}:.*\n`, "m"), "");
const replaceTrustedKeyServers = content => {
const pattern = /^trusted_key_servers:\n(?:[ \t].*\n)*/m;
const block = "trusted_key_servers: []\n";
return pattern.test(content) ? content.replace(pattern, block) : `${content.trimEnd()}\n${block}`;
};
let content = fs.readFileSync(filePath, "utf8");
content = removeScalar(content, "registration_shared_secret");
content = upsertScalar(content, "enable_registration", "false");
content = upsertScalar(content, "registration_shared_secret_path", '"/data/registration_shared_secret"');
content = upsertScalar(content, "suppress_key_server_warning", "true");
content = replaceTrustedKeyServers(content);
fs.writeFileSync(filePath, content);
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
export SYNAPSE_DATA_DIR="${SYNAPSE_DATA_DIR:-/tmp/synapse}"
mkdir -p "${SYNAPSE_DATA_DIR}"
if [[ ! -f "${SYNAPSE_DATA_DIR}/homeserver.yaml" ]]; then
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e run --rm synapse generate
fi
docker run --rm \
-v "${ROOT_DIR}:/workspace" \
-v "${SYNAPSE_DATA_DIR}:/data" \
-w /workspace \
node:lts \
node ./scripts/e2e/configure-synapse.mjs /data/homeserver.yaml
+68
View File
@@ -0,0 +1,68 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
const baseUrl = process.env.SYNAPSE_BASE_URL ?? "http://127.0.0.1:8008";
const dataDir = process.env.SYNAPSE_DATA_DIR ?? "/tmp/synapse";
const username = process.env.SYNAPSE_E2E_ADMIN_USER ?? "admin";
const password = process.env.SYNAPSE_E2E_ADMIN_PASSWORD ?? "supersecret";
const sharedSecretPath = process.env.SYNAPSE_E2E_SHARED_SECRET_PATH ?? path.join(dataDir, "registration_shared_secret");
const loginPayload = {
type: "m.login.password",
user: username,
password,
identifier: {
type: "m.id.user",
user: username,
},
};
const loginResponse = await fetch(`${baseUrl}/_matrix/client/r0/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(loginPayload),
});
if (loginResponse.ok) {
process.exit(0);
}
const sharedSecret = fs.readFileSync(sharedSecretPath, "utf8").trim();
const nonceResponse = await fetch(`${baseUrl}/_synapse/admin/v1/register`);
if (!nonceResponse.ok) {
throw new Error(`Failed to fetch Synapse registration nonce: HTTP ${nonceResponse.status}`);
}
const { nonce } = await nonceResponse.json();
const mac = crypto
.createHmac("sha1", sharedSecret)
.update(nonce)
.update("\u0000")
.update(username)
.update("\u0000")
.update(password)
.update("\u0000")
.update("admin")
.digest("hex");
const registerResponse = await fetch(`${baseUrl}/_synapse/admin/v1/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nonce,
username,
password,
admin: true,
mac,
}),
});
if (!registerResponse.ok) {
throw new Error(`Failed to register Synapse admin user: HTTP ${registerResponse.status}`);
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
export SYNAPSE_DATA_DIR="${SYNAPSE_DATA_DIR:-/tmp/synapse}"
export SYNAPSE_ADMIN_BASE_URL="${SYNAPSE_BASE_URL:-http://127.0.0.1:8080}"
export SYNAPSE_BASE_URL="${SYNAPSE_BASE_URL:-http://127.0.0.1:8008}"
export E2E_BASE_URL="${E2E_BASE_URL:-${SYNAPSE_ADMIN_BASE_URL}}"
cleanup() {
if [[ "${KEEP_E2E_STACK:-0}" == "1" ]]; then return; fi
if [[ "${E2E_RUN_STATUS:-0}" != "0" ]]; then return; fi
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e down -v --remove-orphans
}
trap cleanup EXIT
E2E_RUN_STATUS=1
rm -rf "${SYNAPSE_DATA_DIR}"
"${ROOT_DIR}/scripts/e2e/prepare-synapse.sh"
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e up -d --build
echo "Wait for ${SYNAPSE_ADMIN_BASE_URL}/config.json"
node "${ROOT_DIR}/scripts/e2e/wait-for-url.mjs" "${SYNAPSE_ADMIN_BASE_URL}/config.json" 120000
echo "Wait for ${SYNAPSE_BASE_URL}/_matrix/client/versions"
node "${ROOT_DIR}/scripts/e2e/wait-for-url.mjs" "${SYNAPSE_BASE_URL}/_matrix/client/versions" 120000
echo "Register admin account"
node "${ROOT_DIR}/scripts/e2e/register-admin.mjs"
echo "Start playwright tests"
yarn playwright test
E2E_RUN_STATUS=0
+25
View File
@@ -0,0 +1,25 @@
const [url, timeoutArg] = process.argv.slice(2);
if (!url) {
throw new Error("Usage: node wait-for-url.mjs <url> [timeoutMs]");
}
const timeoutMs = Number(timeoutArg ?? "60000");
const startedAt = Date.now();
for (;;) {
try {
const response = await fetch(url);
if (response.ok) {
process.exit(0);
}
} catch {
// Keep polling until the timeout is reached.
}
if (Date.now() - startedAt > timeoutMs) {
throw new Error(`Timed out waiting for ${url}`);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
+1
View File
@@ -17,6 +17,7 @@ export default defineConfig(({ mode }) => ({
globals: true,
environment: 'happy-dom',
setupFiles: "./src/vitest.setup.ts",
exclude: ["e2e/**"],
},
ssr: {
noExternal: ['react-dropzone', 'react-admin', 'ra-ui-materialui'],
+55
View File
@@ -1148,6 +1148,17 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:^1.58.2":
version: 1.58.2
resolution: "@playwright/test@npm:1.58.2"
dependencies:
playwright: "npm:1.58.2"
bin:
playwright: cli.js
checksum: 10c0/2164c03ad97c3653ff02e8818a71f3b2bbc344ac07924c9d8e31cd57505d6d37596015a41f51396b3ed8de6840f59143eaa9c21bf65515963da20740119811da
languageName: node
linkType: hard
"@popperjs/core@npm:^2.11.8":
version: 2.11.8
resolution: "@popperjs/core@npm:2.11.8"
@@ -3527,6 +3538,16 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.2":
version: 2.3.2
resolution: "fsevents@npm:2.3.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
@@ -3537,6 +3558,15 @@ __metadata:
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>":
version: 2.3.2
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
dependencies:
node-gyp: "npm:latest"
conditions: os=darwin
languageName: node
linkType: hard
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin<compat/fsevents>":
version: 2.3.3
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
@@ -5138,6 +5168,30 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.58.2":
version: 1.58.2
resolution: "playwright-core@npm:1.58.2"
bin:
playwright-core: cli.js
checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b
languageName: node
linkType: hard
"playwright@npm:1.58.2":
version: 1.58.2
resolution: "playwright@npm:1.58.2"
dependencies:
fsevents: "npm:2.3.2"
playwright-core: "npm:1.58.2"
dependenciesMeta:
fsevents:
optional: true
bin:
playwright: cli.js
checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727
languageName: node
linkType: hard
"possible-typed-array-names@npm:^1.0.0":
version: 1.0.0
resolution: "possible-typed-array-names@npm:1.0.0"
@@ -6270,6 +6324,7 @@ __metadata:
"@mui/material": "npm:^7.1.0"
"@mui/system": "npm:^7.1.0"
"@mui/utils": "npm:^7.1.0"
"@playwright/test": "npm:^1.58.2"
"@tanstack/react-query": "npm:5.90.20"
"@testing-library/dom": "npm:^10.4.1"
"@testing-library/react": "npm:^16.3.2"