Skip to content

Commit e6228d5

Browse files
Add Synapse end-to-end test workflow
1 parent e510a56 commit e6228d5

13 files changed

Lines changed: 364 additions & 4 deletions

.github/workflows/e2e-test.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: e2e-test
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: ["master"]
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
e2e:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 30
16+
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v6
20+
21+
- name: Setup node
22+
uses: actions/setup-node@v6
23+
with:
24+
node-version: 24
25+
26+
- name: Install dependencies
27+
run: yarn --immutable
28+
29+
- name: Cache Playwright browsers
30+
id: playwright-cache
31+
uses: actions/cache@v4
32+
with:
33+
path: ~/.cache/ms-playwright
34+
key: ${{ runner.os }}-playwright-${{ hashFiles('**/yarn.lock') }}
35+
restore-keys: |
36+
${{ runner.os }}-playwright-
37+
38+
- name: Install Playwright browser
39+
if: steps.playwright-cache.outputs.cache-hit != 'true'
40+
run: yarn playwright install --with-deps chromium
41+
42+
- name: Run Synapse end-to-end test
43+
run: yarn test:e2e:ci
44+
45+
- name: Collect docker logs on failure
46+
if: failure()
47+
run: docker compose -f docker-compose.yml --project-name synapse-admin-e2e logs --no-color > e2e-compose.log
48+
49+
- name: Upload failure artifacts
50+
if: failure()
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: e2e-synapse-artifacts
54+
path: |
55+
e2e-compose.log
56+
playwright-report
57+
test-results

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ lib-cov
2626
# Coverage directory used by tools like istanbul
2727
coverage
2828
*.lcov
29+
playwright-report/
30+
test-results/
31+
e2e-compose.log
2932

3033
# nyc test coverage
3134
.nyc_output

docker-compose.yml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ services:
22
synapse-admin:
33
container_name: synapse-admin
44
hostname: synapse-admin
5-
image: awesometechnologies/synapse-admin:latest
6-
# build:
7-
# context: .
5+
# Use a prebuilt image:
6+
#image: awesometechnologies/synapse-admin:latest
7+
# or build from source:
8+
build:
9+
context: .
810

911
# to use the docker-compose as standalone without a local repo clone,
1012
# replace the context definition with this:
@@ -16,10 +18,33 @@ services:
1618
# to define a maximum ram for node. otherwise the build will fail.
1719
# - NODE_OPTIONS="--max_old_space_size=1024"
1820
# - BASE_PATH="/synapse-admin"
21+
depends_on:
22+
synapse:
23+
condition: service_healthy
1924
ports:
2025
- "8080:80"
2126
restart: unless-stopped
2227
healthcheck:
2328
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/config.json >/dev/null 2>&1 || exit 1"]
2429
interval: 5s
2530
timeout: 5s
31+
32+
synapse:
33+
image: matrixdotorg/synapse:v1.141.0
34+
environment:
35+
SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
36+
SYNAPSE_REPORT_STATS: "no"
37+
SYNAPSE_SERVER_NAME: localhost
38+
ports:
39+
- "8008:8008"
40+
volumes:
41+
- ${SYNAPSE_DATA_DIR:-/tmp/synapse}:/data
42+
restart: unless-stopped
43+
healthcheck:
44+
test:
45+
[
46+
"CMD-SHELL",
47+
"python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8008/_matrix/client/versions', timeout=5)\"",
48+
]
49+
interval: 5s
50+
timeout: 5s

e2e/synapse-login.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("logs into Synapse and loads the users list", async ({ page }) => {
4+
// Navigate to start page
5+
await page.goto("/");
6+
// Wait for redirect to login page
7+
await page.waitForURL("**/login");
8+
9+
// Fill login data
10+
await page.getByLabel("Username").fill("admin");
11+
await page.locator('input[name="password"]').fill("supersecret");
12+
await page.getByLabel("Homeserver URL").fill("http://localhost:8008");
13+
// Expect server version
14+
await expect (page.getByText("1.141.0")).toBeVisible();
15+
16+
// Sign in
17+
await page.getByRole("button", { name: "Sign in", exact: true }).click();
18+
// Expect users table
19+
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
20+
await expect(page.getByRole("cell", { name: "@admin:localhost" })).toBeVisible();
21+
await expect(page.getByRole("cell", { name: "admin", exact: true })).toBeVisible();
22+
});

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"devDependencies": {
1515
"@mui/system": "^7.1.0",
1616
"@mui/utils": "^7.1.0",
17+
"@playwright/test": "^1.58.2",
1718
"@testing-library/dom": "^10.4.1",
1819
"@testing-library/react": "^16.3.2",
1920
"@testing-library/user-event": "^14.6.1",
@@ -89,7 +90,9 @@
8990
"lint": "eslint --ignore-path .gitignore --ext .ts,.tsx,.yml,.yaml .",
9091
"fix": "yarn lint --fix",
9192
"test": "vitest run",
92-
"test:watch": "vitest watch"
93+
"test:watch": "vitest watch",
94+
"test:e2e": "playwright test",
95+
"test:e2e:ci": "./scripts/e2e/run.sh"
9396
},
9497
"eslintConfig": {
9598
"env": {

playwright.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from "@playwright/test";
2+
3+
export default defineConfig({
4+
testDir: "./e2e",
5+
fullyParallel: false,
6+
reporter: [["html", { open: "never" }]],
7+
retries: process.env.CI ? 1 : 0,
8+
use: {
9+
baseURL: process.env.E2E_BASE_URL ?? "http://127.0.0.1:8080",
10+
headless: true,
11+
locale: "en-US",
12+
trace: "retain-on-failure",
13+
},
14+
});

scripts/e2e/configure-synapse.mjs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import fs from "node:fs";
2+
3+
const filePath = process.argv[2];
4+
5+
if (!filePath) {
6+
throw new Error("Usage: node configure-synapse.mjs <homeserver.yaml>");
7+
}
8+
9+
const upsertScalar = (content, key, value) => {
10+
const pattern = new RegExp(`^${key}:.*$`, "m");
11+
const line = `${key}: ${value}`;
12+
return pattern.test(content) ? content.replace(pattern, line) : `${content.trimEnd()}\n${line}\n`;
13+
};
14+
15+
const removeScalar = (content, key) => content.replace(new RegExp(`^${key}:.*\n`, "m"), "");
16+
17+
const replaceTrustedKeyServers = content => {
18+
const pattern = /^trusted_key_servers:\n(?:[ \t].*\n)*/m;
19+
const block = "trusted_key_servers: []\n";
20+
return pattern.test(content) ? content.replace(pattern, block) : `${content.trimEnd()}\n${block}`;
21+
};
22+
23+
let content = fs.readFileSync(filePath, "utf8");
24+
25+
content = removeScalar(content, "registration_shared_secret");
26+
content = upsertScalar(content, "enable_registration", "false");
27+
content = upsertScalar(content, "registration_shared_secret_path", '"/data/registration_shared_secret"');
28+
content = upsertScalar(content, "suppress_key_server_warning", "true");
29+
content = replaceTrustedKeyServers(content);
30+
31+
fs.writeFileSync(filePath, content);

scripts/e2e/prepare-synapse.sh

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6+
export SYNAPSE_DATA_DIR="${SYNAPSE_DATA_DIR:-/tmp/synapse}"
7+
8+
mkdir -p "${SYNAPSE_DATA_DIR}"
9+
10+
if [[ ! -f "${SYNAPSE_DATA_DIR}/homeserver.yaml" ]]; then
11+
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e run --rm synapse generate
12+
fi
13+
14+
docker run --rm \
15+
-v "${ROOT_DIR}:/workspace" \
16+
-v "${SYNAPSE_DATA_DIR}:/data" \
17+
-w /workspace \
18+
node:lts \
19+
node ./scripts/e2e/configure-synapse.mjs /data/homeserver.yaml

scripts/e2e/register-admin.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import crypto from "node:crypto";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
5+
const baseUrl = process.env.SYNAPSE_BASE_URL ?? "http://127.0.0.1:8008";
6+
const dataDir = process.env.SYNAPSE_DATA_DIR ?? "/tmp/synapse";
7+
const username = process.env.SYNAPSE_E2E_ADMIN_USER ?? "admin";
8+
const password = process.env.SYNAPSE_E2E_ADMIN_PASSWORD ?? "supersecret";
9+
const sharedSecretPath = process.env.SYNAPSE_E2E_SHARED_SECRET_PATH ?? path.join(dataDir, "registration_shared_secret");
10+
11+
const loginPayload = {
12+
type: "m.login.password",
13+
user: username,
14+
password,
15+
identifier: {
16+
type: "m.id.user",
17+
user: username,
18+
},
19+
};
20+
21+
const loginResponse = await fetch(`${baseUrl}/_matrix/client/r0/login`, {
22+
method: "POST",
23+
headers: {
24+
"Content-Type": "application/json",
25+
},
26+
body: JSON.stringify(loginPayload),
27+
});
28+
29+
if (loginResponse.ok) {
30+
process.exit(0);
31+
}
32+
33+
const sharedSecret = fs.readFileSync(sharedSecretPath, "utf8").trim();
34+
const nonceResponse = await fetch(`${baseUrl}/_synapse/admin/v1/register`);
35+
36+
if (!nonceResponse.ok) {
37+
throw new Error(`Failed to fetch Synapse registration nonce: HTTP ${nonceResponse.status}`);
38+
}
39+
40+
const { nonce } = await nonceResponse.json();
41+
const mac = crypto
42+
.createHmac("sha1", sharedSecret)
43+
.update(nonce)
44+
.update("\u0000")
45+
.update(username)
46+
.update("\u0000")
47+
.update(password)
48+
.update("\u0000")
49+
.update("admin")
50+
.digest("hex");
51+
52+
const registerResponse = await fetch(`${baseUrl}/_synapse/admin/v1/register`, {
53+
method: "POST",
54+
headers: {
55+
"Content-Type": "application/json",
56+
},
57+
body: JSON.stringify({
58+
nonce,
59+
username,
60+
password,
61+
admin: true,
62+
mac,
63+
}),
64+
});
65+
66+
if (!registerResponse.ok) {
67+
throw new Error(`Failed to register Synapse admin user: HTTP ${registerResponse.status}`);
68+
}

scripts/e2e/run.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6+
export SYNAPSE_DATA_DIR="${SYNAPSE_DATA_DIR:-/tmp/synapse}"
7+
export SYNAPSE_ADMIN_BASE_URL="${SYNAPSE_BASE_URL:-http://127.0.0.1:8080}"
8+
export SYNAPSE_BASE_URL="${SYNAPSE_BASE_URL:-http://127.0.0.1:8008}"
9+
export E2E_BASE_URL="${E2E_BASE_URL:-${SYNAPSE_ADMIN_BASE_URL}}"
10+
11+
cleanup() {
12+
if [[ "${KEEP_E2E_STACK:-0}" == "1" ]]; then return; fi
13+
if [[ "${E2E_RUN_STATUS:-0}" != "0" ]]; then return; fi
14+
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e down -v --remove-orphans
15+
}
16+
17+
trap cleanup EXIT
18+
19+
E2E_RUN_STATUS=1
20+
21+
rm -rf "${SYNAPSE_DATA_DIR}"
22+
23+
"${ROOT_DIR}/scripts/e2e/prepare-synapse.sh"
24+
25+
docker compose -f "${ROOT_DIR}/docker-compose.yml" --project-name synapse-admin-e2e up -d --build
26+
27+
echo "Wait for ${SYNAPSE_ADMIN_BASE_URL}/config.json"
28+
node "${ROOT_DIR}/scripts/e2e/wait-for-url.mjs" "${SYNAPSE_ADMIN_BASE_URL}/config.json" 120000
29+
echo "Wait for ${SYNAPSE_BASE_URL}/_matrix/client/versions"
30+
node "${ROOT_DIR}/scripts/e2e/wait-for-url.mjs" "${SYNAPSE_BASE_URL}/_matrix/client/versions" 120000
31+
echo "Register admin account"
32+
node "${ROOT_DIR}/scripts/e2e/register-admin.mjs"
33+
34+
echo "Start playwright tests"
35+
yarn playwright test
36+
37+
E2E_RUN_STATUS=0

0 commit comments

Comments
 (0)