Files
RustProxy/tools/checks/web_service_ui_test.mjs
T
2026-10-09 13:11:33 +02:00

232 lines
14 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import vm from "node:vm";
class ClassList {
constructor() { this.values = new Set(); }
toggle(name, active) { active ? this.values.add(name) : this.values.delete(name); }
}
class Element {
constructor(id = "") {
this.id = id;
this.value = "";
this.checked = false;
this.disabled = false;
this.hidden = false;
this.open = false;
this.dataset = {};
this.children = [];
this.listeners = new Map();
this.classList = new ClassList();
this.attributes = new Map();
this.tabIndex = 0;
this.textContent = "";
this.type = "";
this.readOnly = false;
this.elements = [];
}
addEventListener(name, callback) { this.listeners.set(name, callback); }
dispatch(name) { return this.listeners.get(name)?.({ target: this, preventDefault() {} }); }
setAttribute(name, value) { this.attributes.set(name, String(value)); }
append(...items) { this.children.push(...items); }
replaceChildren(...items) { this.children = [...items]; }
showModal() { this.open = true; }
close() { this.open = false; }
reset() {}
checkValidity() { return true; }
}
const ids = [
"htaccess-help-close","htaccess-help-dialog","php-help-close","php-help-dialog","web-service-cache-control","web-service-cache-rules",
"web-service-close","web-service-default-mime","web-service-dialog","web-service-download-extensions",
"web-service-editing","web-service-form","web-service-hardening-attacks","web-service-hardening-headers",
"web-service-htaccess-enabled","web-service-htaccess-filename","web-service-htaccess-help","web-service-htaccess-recursive",
"web-service-htaccess-strict","web-service-id","web-service-index","web-service-list","web-service-listing",
"web-service-mime-types","web-service-name","web-service-new","web-service-php-concurrency",
"web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions",
"web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-path-info","web-service-php-body-limit","web-service-php-header-limit",
"web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-admin-values","web-service-php-help",
"web-service-compression","web-service-compression-options","web-service-compression-zstd","web-service-compression-br","web-service-compression-gzip","web-service-compression-deflate","web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip",
"web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset",
"web-service-response-headers","web-service-rewrites","web-service-root","web-service-search",
"web-service-symlink-policy","web-service-tabs","web-service-try-files",
"web-service-php-profile", "web-service-php-profiles", "web-service-php-profiles-open", "php-fpm-profiles-dialog",
"php-fpm-profiles-list", "php-fpm-profiles-close", "php-fpm-profile-add"
];
const elements = new Map(ids.map(id => [id, new Element(id)]));
const pages = ["general","routing","htaccess","mime","cache","delivery","php","security"].map(name => {
const page = new Element(); page.dataset.webServicePage = name; return page;
});
elements.get("web-service-php-endpoint").value = "unix:/run/php/php-fpm.sock";
elements.get("web-service-php-connect-timeout").value = "1000";
elements.get("web-service-php-request-timeout").value = "30000";
elements.get("web-service-php-concurrency").value = "64";
elements.get("web-service-php-pass-authorization").checked = true;
elements.get("web-service-php-path-info").checked = false;
elements.get("web-service-php-body-limit").value = "0";
elements.get("web-service-php-header-limit").value = "65536";
elements.get("web-service-php-stderr-limit").value = "4096";
elements.get("web-service-compression").value = "";
for (const name of ["zstd", "br", "gzip", "deflate"]) elements.get(`web-service-compression-${name}`).value = "";
const document = {
body: new Element("body"),
createElement: tag => { const node = new Element(); node.type = tag; return node; },
querySelectorAll(selector) {
if (selector === "#web-service-tabs button") return elements.get("web-service-tabs").children;
if (selector === "[data-web-service-page]") return pages;
return [];
}
};
let savedSection = null;
const context = {
console,
structuredClone,
document,
window: {},
envelope: null,
busy: false,
$: id => {
const value = elements.get(id);
if (!value) throw new Error(`missing fake element ${id}`);
return value;
},
t: key => key,
el: (_tag, text = "", className = "") => {
const node = new Element();
node.textContent = text ?? "";
if (className) node.classList.values.add(className);
return node;
},
toast: () => {},
saveSection: async (name, value) => { savedSection = { name, value: structuredClone(value) }; return true; },
appConfirm: async () => true,
};
vm.createContext(context);
vm.runInContext(fs.readFileSync("src/admin/web/drafts.js", "utf8"), context, { filename: "drafts.js" });
vm.runInContext(fs.readFileSync("src/admin/web/web-server-profiles.js", "utf8"), context, { filename: "web-server-profiles.js" });
vm.runInContext(fs.readFileSync("src/admin/web/web-server.js", "utf8"), context, { filename: "web-server.js" });
// Explicitly disabling scoped dynamic compression must hide and disable codec
// inheritance controls without erasing their saved selections.
const compressionMode = elements.get("web-service-compression");
const compressionOptions = elements.get("web-service-compression-options");
const compressionZstd = elements.get("web-service-compression-zstd");
compressionZstd.value = "off";
compressionMode.value = "off";
compressionMode.dispatch("change");
if (!compressionOptions.hidden) throw new Error("Disabled Web Service compression did not hide codec overrides");
for (const name of ["zstd", "br", "gzip", "deflate"]) {
if (!elements.get(`web-service-compression-${name}`).disabled) throw new Error(`Disabled Web Service compression left ${name} override active`);
}
compressionMode.value = "on";
compressionMode.dispatch("change");
if (compressionOptions.hidden) throw new Error("Re-enabled Web Service compression did not restore codec overrides");
if (compressionZstd.value !== "off") throw new Error("Compression visibility toggle erased the saved codec selection");
const preset = elements.get("web-service-preset");
preset.value = "wordpress";
preset.dispatch("change");
const expectedIndex = "index.php\nindex.html\nindex.htm";
const expectedTry = "$uri\n$uri/\n/index.php?$query_string";
if (elements.get("web-service-index").value !== expectedIndex) throw new Error("WordPress preset did not populate index files");
if (elements.get("web-service-try-files").value !== expectedTry) throw new Error("WordPress preset did not populate try_files");
if (!elements.get("web-service-php-enabled").checked) throw new Error("WordPress preset did not enable PHP-FPM");
if (!elements.get("web-service-precompressed").checked) throw new Error("WordPress preset did not enable precompressed sidecars");
if (!elements.get("web-service-cache-rules").value.includes("ext:css = public, max-age=604800")) throw new Error("WordPress preset did not populate cache rules");
preset.value = "downloads";
preset.dispatch("change");
if (elements.get("web-service-index").value !== "") throw new Error("Downloads preset should clear index files");
if (!elements.get("web-service-listing").checked) throw new Error("Downloads preset should enable directory listing");
if (elements.get("web-service-php-enabled").checked) throw new Error("Downloads preset should disable PHP-FPM");
if (elements.get("web-service-cache-control").value !== "public, max-age=3600") throw new Error("Downloads preset cache policy missing");
// The save path must serialize the values materialized by preset selection, not
// the previous/custom values that were present when the dialog opened.
context.envelope = { config: { web_services: [] } };
elements.get("web-service-id").value = "web1";
elements.get("web-service-name").value = "web1";
elements.get("web-service-root").value = "/srv/web1";
elements.get("web-service-php-pass-authorization").checked = false;
elements.get("web-service-php-path-info").checked = true;
elements.get("web-service-php-body-limit").value = "1048576";
elements.get("web-service-php-header-limit").value = "131072";
elements.get("web-service-php-stderr-limit").value = "8192";
elements.get("web-service-php-custom-params").value = "APP_ENV = production";
elements.get("web-service-php-values").value = "memory_limit = 256M";
preset.value = "wordpress";
preset.dispatch("change");
elements.get("web-service-compression").value = "on";
elements.get("web-service-compression-gzip").value = "off";
await elements.get("web-service-form").dispatch("submit");
if (savedSection?.name !== "web-services" || savedSection.value.length !== 1) throw new Error("WordPress preset save did not persist a Web Service");
const saved = savedSection.value[0];
if (saved.index.join("\n") !== expectedIndex) throw new Error("Saved WordPress preset lost index files");
if (saved.try_files.join("\n") !== expectedTry) throw new Error("Saved WordPress preset lost try_files");
if (!saved.php.enabled) throw new Error("Saved WordPress preset lost PHP-FPM enablement");
if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement");
if (saved.compression?.enabled !== true || saved.compression?.gzip !== false) throw new Error("Saved WordPress preset lost compression/codec override");
if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice");
if (saved.php.path_info !== true) throw new Error("Saved WordPress preset lost PATH_INFO choice");
if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits");
if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters");
if (saved.php.php_values.memory_limit !== "256M") throw new Error("Saved WordPress preset lost PHP_VALUE settings");
if (!saved.cache_rules.some(rule => rule.matcher === "ext:css" && rule.cache_control.includes("604800"))) throw new Error("Saved WordPress preset lost cache rules");
console.log("PASS: Web Service preset selection materializes and persists CMS/download settings.");
// The shared editor lists profiles, saves them under the new revisioned
// section, and Web Services can attach without losing local PHP settings.
context.envelope.config.php_fpm_profiles = [{
id: "wp-pool", name: "WordPress pool", php: {
enabled: true, path_info: true, pass_authorization: true,
extensions: ["php"], endpoint: "tcp:127.0.0.1:9000",
connect_timeout_ms: 1000, request_timeout_ms: 30000,
max_concurrent_requests: 64, max_request_body_bytes: 0,
max_response_header_bytes: 65536, stderr_log_bytes: 4096,
custom_params: {}, php_values: {}, php_admin_values: {}
}
}];
// Both entry points must share the existing modal; the toolbar shortcut
// belongs next to New Web Service rather than inside the service editor.
const html = fs.readFileSync("src/admin/web/index.html", "utf8");
const webPanel = html.split('id="panel-web-server"')[1]?.split("</section>")[0] ?? "";
const toolbar = webPanel.match(/<div class="vhost-list-tools">([\s\S]*?)<\/div>/)?.[1] ?? "";
const shortcutAt = toolbar.indexOf('id="web-service-php-profiles-open"');
const newAt = toolbar.indexOf('id="web-service-new"');
if (shortcutAt < 0 || newAt < 0 || shortcutAt > newAt) {
throw new Error("PHP-FPM profiles shortcut must be adjacent to New Web Service in the toolbar");
}
elements.get("web-service-php-profiles-open").dispatch("click");
if (!elements.get("php-fpm-profiles-dialog").open) throw new Error("Toolbar PHP-FPM profile shortcut did not open the modal");
elements.get("php-fpm-profiles-close").dispatch("click");
if (elements.get("php-fpm-profiles-dialog").open) throw new Error("PHP-FPM profile modal failed to close");
elements.get("web-service-php-profiles").dispatch("click");
if (!elements.get("php-fpm-profiles-dialog").open) throw new Error("PHP-FPM profile modal was not opened");
const profileDetail = elements.get("php-fpm-profiles-list").children[0];
if (!profileDetail || profileDetail.children[0].textContent !== "WordPress pool") throw new Error("Shared profile list missing");
const profileForm = profileDetail.children[1];
profileForm.children[1].children[1].value = "Updated WordPress pool";
await profileForm.dispatch("submit");
if (savedSection?.name !== "php-fpm-profiles" || savedSection.value[0].name !== "Updated WordPress pool") {
throw new Error("Shared profile edit did not save to the profile configuration section");
}
const profileSelector = elements.get("web-service-php-profile");
const localEndpoint = elements.get("web-service-php-endpoint").value;
profileSelector.value = "wp-pool";
profileSelector.dispatch("change");
if (!elements.get("web-service-php-endpoint").disabled) throw new Error("Shared profile left local PHP controls editable");
elements.get("web-service-id").value = "wp-shared";
elements.get("web-service-name").value = "WordPress shared";
await elements.get("web-service-form").dispatch("submit");
if (savedSection?.name !== "web-services" || savedSection.value[0].php_profile_id !== "wp-pool") {
throw new Error("Web Service did not persist selected shared PHP-FPM profile");
}
if (savedSection.value[0].php.endpoint !== localEndpoint) {
throw new Error("Attaching a shared FPM profile changed local settings");
}
console.log("PASS: Toolbar and editor PHP-FPM shortcuts open the shared modal; profile edits and Web Service attachment work.");