mirror of
https://github.com/Awesome-Technologies/synapse-admin.git
synced 2026-05-29 18:54:33 +00:00
Add Synapse end-to-end test workflow
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
Executable
+19
@@ -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
|
||||
@@ -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}`);
|
||||
}
|
||||
Executable
+37
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user