CH-J Toolsδιαχείριση διακομιστή πάνω από SSH
Μενού
Δημοσίευση πηγής

CH-J Tools

Περιηγηθείτε σε καταλόγους και αρχεία για μια συγκεκριμένη έκδοση εφαρμογών.

Κατεβάστε την πηγή ZIP
CH-J / Server Manager 406 αρχεία
core-20261024T135205Z-7c487d719de2 Προστέθηκε: · Χτισμένο:
smokeFileManagerUi.js Προστέθηκε: Ημερομηνία που δεν έχει καταγραφεί 15,4 KB · 221 γραμμές
Κωδικός Λήψη αρχείου
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 // The assertions below check the Czech UI texts.
114 const runtime = new PluginRuntime({ BrowserWindow, registry, getLanguage: () => "cs", sessionManager, remoteFileService, transferManager,
115 preload: path.join(__dirname, "../src/preload/pluginPreload.js"), getMainWindow: () => null, isVaultUnlocked: () => true });
116 runtime.registerIpc(ipcMain);
117 runtime.open("chj.file-manager");
118 window = runtime.windows.get("chj.file-manager");
119 window.webContents.on("console-message", (event) => { if (event.level === "error") errors.push(event.message); });
120 window.setSize(1360, 900);
121 await until("document.querySelector('tr[data-path=\"/etc\"]')");
122 assert.equal(await js("document.querySelector('#transfers').hidden"), false, "transfer panel present");
123 await shot("01-root-listing");
124 await assertLayout(".app-header button, .app-header select, .navigation button, .navigation input, .toolbar button, .toolbar input, .transfers-header button, .transfers-header select", "main");
126 // Symlinks are shown with their target type.
127 await openPath("/etc/nginx/sites-enabled");
128 assert.match(await js("document.querySelector('#filesBody').textContent"), /odkaz → soubor/);
130 // Editing /etc/nginx/nginx.conf as a sudo user: save offers sudo, a wrong password is reported, metadata kept.
131 await openFile("/etc/nginx", "nginx.conf");
132 await until("!document.querySelector('#editor').hidden && window.monaco");
133 assert.match(await js("document.querySelector('#status').textContent"), /uložení bude vyžadovat sudo/);
134 await assertLayout("#editor .editor-header button, #editor .editor-header input, #editor .editor-header .pill, #editorPath", "editor");
135 await setEditorText("worker_processes 4;\n");
136 await until("!document.querySelector('#editorDirty').hidden");
137 await js("document.querySelector('#saveEditorButton').click()");
138 await sudo("wrong-password");
139 await until("document.querySelector('#sudoDialog').open && !document.querySelector('#sudoError').hidden", 30000);
140 await shot("02-sudo-wrong-password");
141 assert.equal(sh("cat /etc/nginx/nginx.conf"), "worker_processes 1;", "nothing written after a rejected password");
142 await sudo();
143 await until("document.querySelector('#status').textContent.includes('Atomicky uloženo')", 30000);
144 assert.equal(sh("cat /etc/nginx/nginx.conf"), "worker_processes 4;");
145 assert.equal(facts("/etc/nginx/nginx.conf"), before, "owner, mode, ACL and xattr unchanged");
146 assert.equal(await js("document.querySelector('#editorPrivilege').hidden"), false);
147 await shot("03-saved-with-sudo");
148 await js("document.querySelector('#closeEditorButton').click()");
150 // Symlink editing (password remembered in Main memory → no second prompt).
151 await openFile("/etc/nginx/sites-enabled", "example.conf");
152 await until("!document.querySelector('#editor').hidden && !document.querySelector('#editorTarget').hidden");
153 assert.match(await js("document.querySelector('#editorTarget').textContent"), /\/etc\/nginx\/sites-available\/example\.conf/);
154 await setEditorText("server { listen 8080; }\n");
155 await js("document.querySelector('#saveEditorButton').click()");
156 await until("document.querySelector('#status').textContent.includes('do cíle odkazu')", 30000);
157 assert.equal(sh("readlink /etc/nginx/sites-enabled/example.conf"), "../sites-available/example.conf");
158 assert.equal(sh("cat /etc/nginx/sites-available/example.conf"), "server { listen 8080; }");
159 await shot("04-symlink-saved");
160 await js("document.querySelector('#closeEditorButton').click()");
162 // Reading a root-only file uses the remembered sudo authorization.
163 await openFile("/etc", "chj-secret.conf");
164 await until("!document.querySelector('#editor').hidden && document.querySelector('#status').textContent.includes('se sudo')", 30000);
165 assert.equal(await js("monaco.editor.getModels()[0].getValue()"), "secret=1\n");
166 await js("document.querySelector('#closeEditorButton').click()");
168 // Upload into /etc/nginx: plan dialog with sudo section, real progress, privileged publication.
169 const conf = path.join(root, "upload.conf"); fs.writeFileSync(conf, "# uploaded\n".repeat(20000));
170 uploadPaths = [conf];
171 await openPath("/etc/nginx");
172 await js("document.querySelector('#uploadButton').click()");
173 await until("document.querySelector('#planDialog').open");
174 assert.equal(await js("document.querySelector('#planSudo').hidden"), false, "privileged route explained");
175 assert.equal(await js("getComputedStyle(document.querySelector('#planPasswordRow')).display"), "none", "no password field while sudo is remembered");
176 await shot("05-plan-dialog");
177 await js("document.querySelector('#planStart').click()");
178 await until("document.querySelector('.transfer .t-state.completed')", 60000);
179 assert.equal(sh("sha256sum /etc/nginx/upload.conf | cut -d' ' -f1"), crypto.createHash("sha256").update(fs.readFileSync(conf)).digest("hex"));
180 assert.equal(sh("stat -c '%u:%g:%a' /etc/nginx/upload.conf"), "0:0:644");
181 assert.match(await js("document.querySelector('.transfer').textContent"), /Dokončení na cíli: dokončeno/);
183 // Pause and resume a large upload from the transfer card.
184 const big = path.join(root, "big.bin"); fs.closeSync(fs.openSync(big, "w")); fs.truncateSync(big, 1024 * 1024 * 1024);
185 uploadPaths = [big];
186 await openPath("/home/admin");
187 await js("document.querySelector('#uploadButton').click()");
188 await until("document.querySelector('#planDialog').open");
189 await js("document.querySelector('#planStart').click()");
190 await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('[data-transfer-action=pause]'))", 30000);
191 await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && /Rychlost: [0-9]/.test(node.textContent))", 30000);
192 await shot("06-transferring");
193 await js("Array.from(document.querySelectorAll('.transfer')).find((node) => node.textContent.includes('big.bin')).querySelector('[data-transfer-action=pause]').click()");
194 await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('.t-state.paused'))", 30000);
195 await shot("07-paused");
196 await js("Array.from(document.querySelectorAll('.transfer')).find((node) => node.textContent.includes('big.bin')).querySelector('[data-transfer-action=resume]').click()");
197 await until("Array.from(document.querySelectorAll('.transfer')).some((node) => node.textContent.includes('big.bin') && node.querySelector('.t-state.completed'))", 120000);
198 assert.equal(sh("stat -c %s /home/admin/big.bin"), String(1024 * 1024 * 1024));
199 assert.match(await js("document.querySelector('#transferSummary').textContent"), /2 souborů · 2 dokončeno/);
200 assert.match(await js("document.querySelector('#transferOverallText').textContent"), /^100 %/);
201 await shot("08-completed");
203 // The panel survives closing and reopening the plugin window (jobs live in Main).
204 window.close();
205 await delay(500);
206 runtime.open("chj.file-manager");
207 window = runtime.windows.get("chj.file-manager");
208 window.setSize(1360, 900);
209 await until("document.querySelectorAll('.transfer').length === 2");
210 assert.deepEqual(errors, [], "no renderer errors");
211 transferManager.shutdown();
212 await sessionManager.disconnectAll("test");
213 console.log("chj.file-manager UI smoke passed");
216run().then(() => { cleanup(); app.exit(0); }, (error) => { console.error(error); cleanup(); app.exit(1); });
217function cleanup() {
218 const removed = spawnSync("docker", ["rm", "-f", container], { encoding: "utf8" });
219 if (removed.status !== 0) console.error(`Container ${container} could not be removed: ${removed.stderr}`);
220 fs.rmSync(root, { recursive: true, force: true });
SHA-256454aa24a20ca4c2a724e82c81b59277b22b1c789fdb3cc7a72af0cc54f07fb77

CH-J Proprietary Software License 1.14 · Η πηγή παρέχεται στο CH-J Proprietary Software License 1.14. Η διαθεσιμότητά του δεν αλλάζει τους όρους της άδειας χρήσης ούτε χορηγεί πρόσθετα δικαιώματα.

Archive SHA-256476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6
Αρχεία