Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
CH-J / Server Manager
377 files
core-20261019T111616Z-160a00bfcddc
Added: · Built:
1
"use strict";2
// End-to-end UI check of chj.file-manager: real plugin runtime, preload, IPC, SSH session, Monaco,3
// sudo dialog, transfer plan dialog and Transfer Manager panel against a disposable Docker container4
// built from test/fixtures/filesIntegration. Never point it at a real server.5
// Run: CHJ_FILES_IMAGE=chj-files-it-debian npx electron scripts/smokeFileManagerUi.js [screenshot-dir]6
const { app, BrowserWindow, ipcMain, protocol } = require("electron");7
const assert = require("node:assert/strict");8
const crypto = require("node:crypto");9
const fs = require("node:fs");10
const os = require("node:os");11
const path = require("node:path");12
const { execFileSync, spawnSync } = require("node:child_process");13
const { hardenSession } = require("../src/main/bootstrap/createMainWindow");14
const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");15
const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");16
const { SessionManager } = require("../src/main/sessions/sessionManager");17
const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");18
const { RemoteFileService } = require("../src/main/files/remoteFileService");19
const { TransferManager } = require("../src/main/files/transferManager");21
const image = process.env.CHJ_FILES_IMAGE || "chj-files-it-debian";22
const container = `chj-files-ui-${process.pid}`;23
const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-files-ui-"));24
const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null;25
app.setPath("userData", path.join(root, "userData"));26
// Closing and reopening the plugin window is part of the check: closing it must not quit the app.27
app.on("window-all-closed", () => {});28
protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);29
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));30
const errors = [];31
let window;32
const js = (source) => window.webContents.executeJavaScript(source);33
async function until(source, timeout = 30000) {34
const started = Date.now();35
while (Date.now() - started < timeout) { if (await js(`(() => { try { return Boolean(${source}); } catch { return false; } })()`)) return; await delay(60); }36
throw new Error(`UI condition timed out: ${source}\nstatus: ${await js("document.querySelector('#status').textContent")}\ntransfers: ${await js("document.querySelector('#transferList').innerText")}`);37
}38
async function shot(name) {39
if (!shots) return;40
fs.mkdirSync(shots, { recursive: true });41
fs.writeFileSync(path.join(shots, `${name}.png`), (await window.webContents.capturePage()).toPNG());42
}43
function sh(command) {44
const result = spawnSync("docker", ["exec", container, "sh", "-c", command], { encoding: "utf8" });45
if (result.status !== 0) throw new Error(`container command failed: ${command}\n${result.stderr}`);46
return result.stdout.trim();47
}48
const facts = (file) => sh(`stat -c '%u:%g:%a' ${file}; getfacl -cp ${file} | sort | tr '\\n' ' '; getfattr -d --absolute-names -m '^user\\.' ${file} | grep -v '^#' | tr '\\n' ' '`);49
async function sudo(password = "AdminPass-1") {50
await until("document.querySelector('#sudoDialog').open");51
await js(`document.querySelector('#sudoPassword').value = ${JSON.stringify(password)}; document.querySelector('#sudoConfirm').click();`);52
}53
async function openPath(target) {54
await js(`document.querySelector('#pathInput').value = ${JSON.stringify(target)}; document.querySelector('#pathForm').requestSubmit();`);55
await until(`document.querySelector('#pathInput').value === ${JSON.stringify(target)} && document.querySelector('#busyOverlay').hidden`);56
}57
async function openFile(directory, name) {58
await openPath(directory);59
await until(`document.querySelector('tr[data-path=${JSON.stringify(path.posix.join(directory, name))}] [data-row-action="open"]')`);60
await js(`document.querySelector('tr[data-path=${JSON.stringify(path.posix.join(directory, name))}] [data-row-action="open"]').click()`);61
}62
// Every visible control must be inside the window, below the Core window bar and not covered by anything.63
async function assertLayout(scope, label) {64
for (const [width, height] of [[1360, 900], [1024, 760], [760, 700], [600, 640]]) {65
window.setContentSize(width, height);66
await delay(250);67
const problems = await js(`(() => {68
const bar = document.querySelector('chj-plugin-chrome').getBoundingClientRect().bottom;69
const out = [];70
for (const el of document.querySelectorAll(${JSON.stringify(scope)})) {71
const r = el.getBoundingClientRect();72
if (!r.width || !r.height || getComputedStyle(el).visibility === 'hidden') continue;73
const name = (el.id || el.textContent.trim().slice(0, 20));74
if (r.top < bar - 0.5 || r.left < -0.5 || r.right > innerWidth + 0.5 || r.bottom > innerHeight + 0.5) { out.push(name + ' outside ' + [Math.round(r.top), Math.round(r.left), Math.round(r.right), Math.round(r.bottom), bar, innerWidth, innerHeight].join(',')); continue; }75
const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);76
if (!hit || !(hit === el || el.contains(hit) || hit.contains(el) && hit.tagName === 'LABEL')) out.push(name + ' covered by ' + (hit?.id || hit?.className || hit?.tagName));77
}78
return out;79
})()`);80
assert.deepEqual(problems, [], `${label} at ${width}x${height}`);81
await shot(`layout-${label}-${width}`);82
}83
window.setContentSize(1360, 900); await delay(200);84
}85
async function setEditorText(text) { await js(`monaco.editor.getModels()[0].setValue(${JSON.stringify(text)})`); }87
async function run() {88
execFileSync("docker", ["run", "-d", "--rm", "--name", container, "-p", "127.0.0.1::22", image]);89
const port = Number(execFileSync("docker", ["port", container, "22/tcp"], { encoding: "utf8" }).trim().split(":").pop());90
await delay(1500);91
sh(`set -e; mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled92
printf 'worker_processes 1;\\n' > /etc/nginx/nginx.conf; setfacl -m u:plain:r-- /etc/nginx/nginx.conf; setfattr -n user.chj -v kept /etc/nginx/nginx.conf93
printf 'server { listen 80; }\\n' > /etc/nginx/sites-available/example.conf; ln -s ../sites-available/example.conf /etc/nginx/sites-enabled/example.conf94
printf 'secret=1\\n' > /etc/chj-secret.conf; chmod 0600 /etc/chj-secret.conf`);95
const before = facts("/etc/nginx/nginx.conf");96
await app.whenReady(); hardenSession();97
const hostKeys = new Map();98
const profileService = {99
get: () => ({ id: "it", label: "Docker test", host: "127.0.0.1", port, username: "admin", authMethod: "password" }),100
getHostKey: () => hostKeys.get(port) || null, trustHostKey: (_host, _port, fingerprint) => hostKeys.set(port, fingerprint),101
getStoredPassword: () => "AdminPass-1", markUsed() {}102
};103
const sessionManager = new SessionManager({ profileService, latencyMonitor: { start: () => () => {} } });104
try { await sessionManager.connect({ sessionId: "ui", profileId: "it" }); }105
catch (error) { sessionManager.trustPending({ sessionId: "ui", profileId: "it", fingerprint: error.fingerprint }); await sessionManager.connect({ sessionId: "ui", profileId: "it" }); }106
const privilegedExecutor = new PrivilegedExecutor({ sessionManager });107
sessionManager.privilegedExecutor = privilegedExecutor;108
let uploadPaths = [];109
const remoteFileService = new RemoteFileService({ sessionManager, privilegedExecutor, selectUploadPath: async () => ({ canceled: false, paths: uploadPaths }),110
selectDownloadPath: async (name) => ({ canceled: false, path: path.join(root, name) }), selectDownloadDirectory: async () => ({ canceled: false, path: root }) });111
const transferManager = new TransferManager({ sessionManager, remoteFileService, storageRoot: path.join(root, "userData") });112
const registry = new PluginRegistry(path.join(root, "userData"), null, { bundledRoot: path.join(__dirname, "../src/main/firstPartyPlugins") });113
const runtime = new PluginRuntime({ BrowserWindow, registry, sessionManager, remoteFileService, transferManager,114
preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true });115
runtime.registerIpc(ipcMain);116
runtime.open("chj.file-manager");117
window = runtime.windows.get("chj.file-manager");118
window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); });119
window.setSize(1360, 900);120
await until("document.querySelector('tr[data-path=\"/etc\"]')");121
assert.equal(await js("document.querySelector('#transfers').hidden"), false, "transfer panel present");122
await shot("01-root-listing");123
await assertLayout(".app-header button, .app-header select, .navigation button, .navigation input, .toolbar button, .toolbar input, .transfers-header button, .transfers-header select", "main");125
// Symlinks are shown with their target type.126
await openPath("/etc/nginx/sites-enabled");127
assert.match(await js("document.querySelector('#filesBody').textContent"), /odkaz → soubor/);129
// Editing /etc/nginx/nginx.conf as a sudo user: save offers sudo, a wrong password is reported, metadata kept.130
await openFile("/etc/nginx", "nginx.conf");131
await until("!document.querySelector('#editor').hidden && window.monaco");132
assert.match(await js("document.querySelector('#status').textContent"), /uložení bude vyžadovat sudo/);133
await assertLayout("#editor .editor-header button, #editor .editor-header input, #editor .editor-header .pill, #editorPath", "editor");134
await setEditorText("worker_processes 4;\n");135
await until("!document.querySelector('#editorDirty').hidden");136
await js("document.querySelector('#saveEditorButton').click()");137
await sudo("wrong-password");138
await until("document.querySelector('#sudoDialog').open && !document.querySelector('#sudoError').hidden", 30000);139
await shot("02-sudo-wrong-password");140
assert.equal(sh("cat /etc/nginx/nginx.conf"), "worker_processes 1;", "nothing written after a rejected password");141
await sudo();142
await until("document.querySelector('#status').textContent.includes('Atomicky uloženo')", 30000);143
assert.equal(sh("cat /etc/nginx/nginx.conf"), "worker_processes 4;");144
assert.equal(facts("/etc/nginx/nginx.conf"), before, "owner, mode, ACL and xattr unchanged");145
assert.equal(await js("document.querySelector('#editorPrivilege').hidden"), false);146
await shot("03-saved-with-sudo");147
await js("document.querySelector('#closeEditorButton').click()");149
// Symlink editing (password remembered in Main memory → no second prompt).150
await openFile("/etc/nginx/sites-enabled", "example.conf");151
await until("!document.querySelector('#editor').hidden && !document.querySelector('#editorTarget').hidden");152
assert.match(await js("document.querySelector('#editorTarget').textContent"), /\/etc\/nginx\/sites-available\/example\.conf/);153
await setEditorText("server { listen 8080; }\n");154
await js("document.querySelector('#saveEditorButton').click()");155
await until("document.querySelector('#status').textContent.includes('do cíle odkazu')", 30000);156
assert.equal(sh("readlink /etc/nginx/sites-enabled/example.conf"), "../sites-available/example.conf");157
assert.equal(sh("cat /etc/nginx/sites-available/example.conf"), "server { listen 8080; }");158
await shot("04-symlink-saved");159
await js("document.querySelector('#closeEditorButton').click()");161
// Reading a root-only file uses the remembered sudo authorization.162
await openFile("/etc", "chj-secret.conf");163
await until("!document.querySelector('#editor').hidden && document.querySelector('#status').textContent.includes('se sudo')", 30000);164
assert.equal(await js("monaco.editor.getModels()[0].getValue()"), "secret=1\n");165
await js("document.querySelector('#closeEditorButton').click()");167
// Upload into /etc/nginx: plan dialog with sudo section, real progress, privileged publication.168
const conf = path.join(root, "upload.conf"); fs.writeFileSync(conf, "# uploaded\n".repeat(20000));169
uploadPaths = [conf];170
await openPath("/etc/nginx");171
await js("document.querySelector('#uploadButton').click()");172
await until("document.querySelector('#planDialog').open");173
assert.equal(await js("document.querySelector('#planSudo').hidden"), false, "privileged route explained");174
assert.equal(await js("getComputedStyle(document.querySelector('#planPasswordRow')).display"), "none", "no password field while sudo is remembered");175
await shot("05-plan-dialog");176
await js("document.querySelector('#planStart').click()");177
await until("document.querySelector('.transfer .t-state.completed')", 60000);178
assert.equal(sh("sha256sum /etc/nginx/upload.conf | cut -d' ' -f1"), crypto.createHash("sha256").update(fs.readFileSync(conf)).digest("hex"));179
assert.equal(sh("stat -c '%u:%g:%a' /etc/nginx/upload.conf"), "0:0:644");180
assert.match(await js("document.querySelector('.transfer').textContent"), /Dokončení na cíli: dokončeno/);182
// Pause and resume a large upload from the transfer card.183
const big = path.join(root, "big.bin"); fs.closeSync(fs.openSync(big, "w")); fs.truncateSync(big, 1024 * 1024 * 1024);184
uploadPaths = [big];185
await openPath("/home/admin");186
await js("document.querySelector('#uploadButton').click()");187
await until("document.querySelector('#planDialog').open");188
await js("document.querySelector('#planStart').click()");189
await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('[data-transfer-action=pause]'))", 30000);190
await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && /Rychlost: [0-9]/.test(node.textContent))", 30000);191
await shot("06-transferring");192
await js("Array.from(document.querySelectorAll('.transfer')).find((node) => node.textContent.includes('big.bin')).querySelector('[data-transfer-action=pause]').click()");193
await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('.t-state.paused'))", 30000);194
await shot("07-paused");195
await js("Array.from(document.querySelectorAll('.transfer')).find((node) => node.textContent.includes('big.bin')).querySelector('[data-transfer-action=resume]').click()");196
await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('.t-state.completed'))", 120000);197
assert.equal(sh("stat -c %s /home/admin/big.bin"), String(1024 * 1024 * 1024));198
assert.match(await js("document.querySelector('#transferSummary').textContent"), /2 souborů · 2 dokončeno/);199
assert.match(await js("document.querySelector('#transferOverallText').textContent"), /^100 %/);200
await shot("08-completed");202
// The panel survives closing and reopening the plugin window (jobs live in Main).203
window.close();204
await delay(500);205
runtime.open("chj.file-manager");206
window = runtime.windows.get("chj.file-manager");207
window.setSize(1360, 900);208
await until("document.querySelectorAll('.transfer').length === 2");209
assert.deepEqual(errors, [], "no renderer errors");210
transferManager.shutdown();211
await sessionManager.disconnectAll("test");212
console.log("chj.file-manager UI smoke passed");213
}215
run().then(() => { cleanup(); app.exit(0); }, (error) => { console.error(error); cleanup(); app.exit(1); });216
function cleanup() {217
const removed = spawnSync("docker", ["rm", "-f", container], { encoding: "utf8" });218
if (removed.status !== 0) console.error(`Container ${container} could not be removed: ${removed.stderr}`);219
fs.rmSync(root, { recursive: true, force: true });220
}SHA-256
40e6e37d31fb8db0d606301a582ab7d2e2a444e4811778f39c6c988097d399fbCH-J Proprietary Software License 1.14 · Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
Archive SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a