Files
ntfy/web/e2e/server.mjs
T
binwiederhier d7013703c1 Add Playwright end-to-end tests for the web app
Adds 56 Playwright tests in web/e2e/ that drive Chromium against a real
ntfy server: subscribing, publishing, notifications, signup/login/logout,
account settings, access tokens, reservations and languages. "make web-e2e"
builds the binary and runs them against a fresh local server; setting
NTFY_E2E_BASE_URL (plus an admin token and tier) runs them against an
existing server instead. docs/develop.md describes both.

The tests found two bugs, fixed here:

- Signup showed "Username is already taken" without the name, since it
  read the username from the error instead of the form.
- A poll that overlapped a WebSocket delivery could overwrite the stored
  message and drop its unread marker, or log a ConstraintError. Both
  write paths in SubscriptionManager now check and write in one
  transaction, and the poller skips messages that are already stored.
2026-10-07 17:26:35 +00:00

61 lines
2.5 KiB
JavaScript

import { execFileSync, spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
// Starts a throwaway ntfy server for the e2e run; Playwright's webServer launches this
// script (with the paths from env.js) and kills it when the run is over. State is wiped
// on every start.
const [binary, configFile, port, tier] = process.argv.slice(2);
const runDir = path.dirname(configFile);
const baseURL = `http://127.0.0.1:${port}`;
if (!fs.existsSync(binary)) {
console.error(`${binary} not found; run "make web-e2e" to build it`);
process.exit(1);
}
fs.rmSync(runDir, { recursive: true, force: true });
fs.mkdirSync(runDir, { recursive: true });
// behind-proxy lets every test pose as its own visitor via X-Forwarded-For (see fixtures.js),
// so per-IP limits like the 6/day account creation limit don't leak between tests
const config = {
"base-url": baseURL,
"listen-http": `127.0.0.1:${port}`,
"cache-file": path.join(runDir, "cache.db"),
"attachment-cache-dir": path.join(runDir, "attachments"),
"auth-file": path.join(runDir, "user.db"),
"auth-default-access": "read-write",
"enable-signup": true,
"enable-login": true,
"enable-reservations": true,
"behind-proxy": true,
"log-file": path.join(runDir, "ntfy.log"),
};
fs.writeFileSync(
configFile,
Object.entries(config)
.map(([k, v]) => `${k}: ${v}\n`)
.join(""),
);
const server = spawn(binary, ["serve", "--config", configFile], { stdio: "inherit" });
server.on("exit", (code) => process.exit(code ?? 1));
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => server.kill(signal));
}
// Wait for the server to create its database; the CLI refuses to run before that
while (!(await fetch(`${baseURL}/v1/health`).catch(() => null))?.ok) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
// Provision what a remote server is expected to already have: an admin token (specs create
// their users through the admin API) and a tier with reservations
const ntfy = (args, env = {}) => execFileSync(binary, args, { env: { ...process.env, ...env, NTFY_CONFIG_FILE: configFile } }).toString();
ntfy(["user", "add", "--role=admin", "e2eadmin"], { NTFY_PASSWORD: "e2eadmin-password" });
const token = ntfy(["token", "add", "e2eadmin"]).match(/token (tk_\w+) created/)[1];
ntfy(["tier", "add", "--name", "E2E", "--reservation-limit", "10", "--message-limit", "10000", tier]);
// Playwright waits for this line and exports the token as NTFY_E2E_ADMIN_TOKEN (see config)
console.log(`ntfy e2e server ready, admin token ${token}`);