Release / verify (push) Canceled after 0s
CI / checks (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / backend (push) Canceled after 0s
Release / test (push) Canceled after 0s
Release / publish (backend) (push) Canceled after 0s
Release / publish (frontend) (push) Canceled after 0s
Release / publish (proxy) (push) Canceled after 0s
Diagnose von der Firewall aus sowie VXLAN und GIF. Gegen die OPNsense-Core-Quellen geprüft und live auf den Test-Firewalls getestet, siehe Handbuch „Erweiterung 1.14.0“: - **Host-System › Diagnose:** Ping, Traceroute, Port-Prüfung und DNS-Abfrage, ausgeführt von der ausgewählten Firewall. Nur für Admins, strenge Eingabeprüfung, jeder Lauf im Audit-Log, Ping-Jobs werden immer wieder aufgeräumt, der Banner der Gegenstelle wird nicht weitergegeben. Packet-Capture gibt es bewusst nicht (Mitschnitte enthalten fremde Nutzdaten). - **Netzwerk › Geräte:** VXLAN und GIF-Tunnel (anlegen, ändern, löschen) über dieselbe abgesicherte Engine wie die übrigen Geräte. - **Fehlerkorrektur:** Auf den Host-System-Seiten war „Übersicht › Hosts“ markiert und der Seitentitel falsch, weil `/hostsystem` mit `/hosts` beginnt.
153 lines
7.9 KiB
JavaScript
153 lines
7.9 KiB
JavaScript
// Browser verification for the DE/EN language switch. Logs in, switches to English
|
|
// through the real settings page, then walks every read-only page (and the primary
|
|
// "new" dialogs) and lists any visible text that still looks German.
|
|
//
|
|
// Safety: only GET requests reach the backend, plus the login and the preferences
|
|
// PUT. Every other write is answered locally, so no firewall is ever changed.
|
|
// Requires OPNEXUS_TEST_PASSWORD (and optionally OPNEXUS_TEST_USER / OPNEXUS_BASE_URL).
|
|
// Deliberately skips /updates: opening that page starts update checks on the firewalls.
|
|
// Text that comes from live firewall data (alias/rule names) can legitimately be German and is
|
|
// reported as GERMAN too — judge those lines by eye.
|
|
import fs from "node:fs";
|
|
|
|
const { chromium } = await import(process.env.PLAYWRIGHT_MODULE || "/root/.npm/_npx/e41f203b7505f1fb/node_modules/playwright/index.mjs");
|
|
const base = process.env.OPNEXUS_BASE_URL || "http://localhost:3100";
|
|
const password = process.env.OPNEXUS_TEST_PASSWORD;
|
|
if (!password) throw new Error("Set OPNEXUS_TEST_PASSWORD to run browser verification.");
|
|
const outDir = process.env.OUT_DIR || "/tmp/opnexus-i18n-verification";
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
|
|
const GERMAN = /[äöüÄÖÜß]|\b(Keine?|Noch|Fehler|werden|wird|nicht|für|und|oder|mit|von|auf|nach|Regel|Regeln|Neue[rsn]?|Löschen|Bearbeiten|Speichern|Abbrechen|Erneut|Zurück|Weiter|Lädt|Benutzer|Passwort|Einstellungen|Gruppe|Gruppen|Zertifikate?|Sicherung|Wartung|Anmeldung|Übersicht|Hinzufügen|Aktion|Ziel|Quelle|verwaltet|geladen)\b/;
|
|
|
|
const browser = await chromium.launch({ args: ["--no-sandbox"] });
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
|
const page = await context.newPage();
|
|
page.setDefaultTimeout(30000);
|
|
const browserErrors = [];
|
|
page.on("pageerror", (error) => browserErrors.push(`pageerror: ${error.message}`));
|
|
// The 404 probe for a non-existent host is intentional; its failed-resource line is not a page fault.
|
|
page.on("console", (message) => { if (message.type() === "error" && !/status of 404/.test(message.text())) browserErrors.push(`console: ${message.text()}`); });
|
|
|
|
const blockedWrites = [];
|
|
await page.route("**/api/**", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
const method = request.method();
|
|
const allowed = method === "GET" || method === "HEAD" || method === "OPTIONS" || path === "/api/auth/login" || (path === "/api/settings" && method === "PUT");
|
|
if (allowed) return route.continue();
|
|
blockedWrites.push(`${method} ${path}`);
|
|
return route.fulfill({ status: 200, contentType: "application/json", body: "{}", headers: { "access-control-allow-origin": base, "access-control-allow-credentials": "true" } });
|
|
});
|
|
|
|
async function login() {
|
|
await page.goto(`${base}/login`);
|
|
await page.locator('input[type="text"],input:not([type])').first().fill(process.env.OPNEXUS_TEST_USER || "admin");
|
|
await page.locator('input[type="password"]').fill(password);
|
|
await page.locator('button[type="submit"]').click();
|
|
await page.waitForFunction(() => !location.pathname.startsWith("/login"));
|
|
}
|
|
|
|
async function setLanguage(code) {
|
|
await page.goto(`${base}/settings`);
|
|
await page.locator('input[name="language"]').nth(code === "en" ? 1 : 0).check();
|
|
await page.waitForTimeout(800);
|
|
}
|
|
|
|
async function visibleText() {
|
|
return page.evaluate(() => document.body.innerText);
|
|
}
|
|
|
|
function scan(text) {
|
|
return text.split("\n").map((line) => line.trim()).filter((line) => line && GERMAN.test(line));
|
|
}
|
|
|
|
const findings = [];
|
|
async function inspect(name, { dialog } = {}) {
|
|
await page.waitForTimeout(1800);
|
|
const hits = scan(await visibleText());
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 1);
|
|
findings.push({ name, hits, overflow });
|
|
if (dialog) {
|
|
const button = page.getByRole("button", { name: dialog }).first();
|
|
if (await button.count()) {
|
|
await button.click();
|
|
await page.waitForTimeout(600);
|
|
findings.push({ name: `${name} · dialog`, hits: scan(await visibleText()), overflow: false });
|
|
await page.keyboard.press("Escape");
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
}
|
|
|
|
let originalLanguage = "de";
|
|
try {
|
|
await login();
|
|
originalLanguage = (await page.evaluate(() => localStorage.getItem("opnexus-language"))) || "de";
|
|
await setLanguage("en");
|
|
const lang = await page.evaluate(() => document.documentElement.lang);
|
|
if (lang !== "en") throw new Error(`<html lang> is ${lang}, expected en`);
|
|
|
|
await page.reload();
|
|
await page.waitForTimeout(1500);
|
|
const persisted = await page.evaluate(() => document.documentElement.lang);
|
|
if (persisted !== "en") throw new Error("English did not persist across reload");
|
|
|
|
const hostId = await page.evaluate(async () => {
|
|
const api = `${location.protocol}//${location.hostname}:8100/api/hosts`;
|
|
const rows = await (await fetch(api, { credentials: "include" })).json();
|
|
return rows[0]?.id ?? null;
|
|
});
|
|
|
|
const pages = [
|
|
["/dashboard"], ["/hosts", /^add host/i], ["/cluster"], ["/rules", /^new rule/i], ["/policy-hierarchy", /^new group/i],
|
|
["/network/interfaces"], ["/network/vlans"], ["/network/vips"], ["/network/gateways"], ["/network/routes"], ["/network/dns"], ["/network/dhcp"], ["/network/devices"], ["/network/compare"], ["/hostsystem/tunables"], ["/hostsystem/cron"], ["/hostsystem/syslog"], ["/hostsystem/snapshots"], ["/hostsystem/diagnostics"],
|
|
["/groups", /^new group/i], ["/aliases", /^new alias/i], ["/nat", /^new rule/i], ["/ipsec", /^new tunnel/i],
|
|
["/openvpn", /^new instance/i], ["/wireguard", /^new tunnel/i], ["/certificates", /^new certificate/i], ["/logs"], ["/audit"],
|
|
["/users", /^new user/i], ["/settings"], ["/about"],
|
|
];
|
|
if (hostId) pages.splice(3, 0, [`/hosts/${hostId}`]);
|
|
for (const [path, dialog] of pages) {
|
|
await page.goto(`${base}${path}`);
|
|
await inspect(path, { dialog });
|
|
}
|
|
|
|
await page.goto(`${base}/hosts/00000000-0000-4000-8000-000000000000`);
|
|
await page.waitForTimeout(1500);
|
|
const notFound = await visibleText();
|
|
if (!/Host not found/.test(notFound)) throw new Error(`English error text expected, got: ${notFound.slice(0, 200)}`);
|
|
|
|
await page.setViewportSize({ width: 375, height: 800 });
|
|
for (const path of ["/dashboard", "/rules", "/settings"]) {
|
|
await page.goto(`${base}${path}`);
|
|
await page.waitForTimeout(1500);
|
|
findings.push({ name: `${path} @375`, hits: [], overflow: await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth + 1) });
|
|
await page.screenshot({ path: `${outDir}/en-${path.slice(1)}-375.png` });
|
|
}
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
await page.goto(`${base}/dashboard`);
|
|
await page.waitForTimeout(1500);
|
|
await page.screenshot({ path: `${outDir}/en-dashboard-1440.png` });
|
|
|
|
await setLanguage("de");
|
|
await page.goto(`${base}/rules`);
|
|
await page.waitForTimeout(1500);
|
|
const germanBody = await visibleText();
|
|
if (!/Firewall-Regeln/.test(germanBody)) throw new Error("German rules page did not render German title");
|
|
if ((await page.evaluate(() => document.documentElement.lang)) !== "de") throw new Error("lang did not return to de");
|
|
await page.screenshot({ path: `${outDir}/de-rules-1440.png` });
|
|
} finally {
|
|
try { await setLanguage(originalLanguage === "en" ? "en" : "de"); } catch { /* best effort */ }
|
|
await browser.close();
|
|
}
|
|
|
|
let failed = false;
|
|
for (const { name, hits, overflow } of findings) {
|
|
const flag = hits.length ? "GERMAN" : overflow ? "OVERFLOW" : "ok";
|
|
console.log(`${flag.padEnd(8)} ${name}`);
|
|
for (const hit of hits.slice(0, 25)) console.log(` ${hit.slice(0, 160)}`);
|
|
if (hits.length || overflow) failed = true;
|
|
}
|
|
console.log(`blocked writes: ${blockedWrites.length ? blockedWrites.join(", ") : "none"}`);
|
|
console.log(`browser errors: ${browserErrors.length ? "\n" + [...new Set(browserErrors)].join("\n") : "none"}`);
|
|
process.exit(failed || browserErrors.length ? 1 : 0);
|