CH-J Server Managerġestjoni tas-server fuq SSH
Menu
Sors ippubblikat

CH-J Server Manager

direttorji browse u fajls għal rilaxx applikazzjoni speċifika.

Sors ta' tniżżil ZIP
CH-J / Server Manager 377 fajls
core-20261019T111616Z-160a00bfcddc Miżjud: · Buillt:
smokeFileManagerUi.js Miżjud: Data mhux irreġistrata 15.4 KB · 220 linji
Kodiċi Fajl tat-tniżżil
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 container
4// 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]
6const { app, BrowserWindow, ipcMain, protocol } = require("electron");
7const assert = require("node:assert/strict");
8const crypto = require("node:crypto");
9const fs = require("node:fs");
10const os = require("node:os");
11const path = require("node:path");
12const { execFileSync, spawnSync } = require("node:child_process");
13const { hardenSession } = require("../src/main/bootstrap/createMainWindow");
14const { PluginRegistry } = require("../src/main/plugins/pluginRegistry");
15const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");
16const { SessionManager } = require("../src/main/sessions/sessionManager");
17const { PrivilegedExecutor } = require("../src/main/sessions/privilegedExec");
18const { RemoteFileService } = require("../src/main/files/remoteFileService");
19const { TransferManager } = require("../src/main/files/transferManager");
21const image = process.env.CHJ_FILES_IMAGE || "chj-files-it-debian";
22const container = `chj-files-ui-${process.pid}`;
23const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-files-ui-"));
24const shots = process.argv.find((arg, index) => index > 1 && !arg.startsWith("-") && !arg.endsWith(".js")) || null;
25app.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.
27app.on("window-all-closed", () => {});
28protocol.registerSchemesAsPrivileged([{ scheme: "chj-plugin", privileges: { standard: true, secure: true, supportFetchAPI: true } }]);
29const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
30const errors = [];
31let window;
32const js = (source) => window.webContents.executeJavaScript(source);
33async 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")}`);
38async 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());
43function 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();
48const 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' ' '`);
49async 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();`);
53async 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`);
57async 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()`);
62// Every visible control must be inside the window, below the Core window bar and not covered by anything.
63async 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);
85async function setEditorText(text) { await js(`monaco.editor.getModels()[0].setValue(${JSON.stringify(text)})`); }
87async 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-enabled
92 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.conf
93 printf 'server { listen 80; }\\n' > /etc/nginx/sites-available/example.conf; ln -s ../sites-available/example.conf /etc/nginx/sites-enabled/example.conf
94 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");
215run().then(() => { cleanup(); app.exit(0); }, (error) => { console.error(error); cleanup(); app.exit(1); });
216function 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 });
SHA-25640e6e37d31fb8db0d606301a582ab7d2e2a444e4811778f39c6c988097d399fb

CH-J Proprietary Software License 1.14 · Sors huwa pprovdut taħt iż-CH-J Proprietary Software License 1.14. Id-disponibbiltà tagħha ma tibdilx it-termini tal-liċenzja jew tagħti drittijiet addizzjonali.

Archive SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
Fajls