mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
239 lines
10 KiB
JavaScript
239 lines
10 KiB
JavaScript
// Opt-in Windows integration test. Requires NSIS, gh, and handlebars installed
|
|
// in an isolated fixture directory (npm install --prefix <directory> handlebars).
|
|
// Run: node scripts/windows-installer.integration.mjs <directory>
|
|
// Uses real runas elevation; approve UAC if Windows prompts. The full production
|
|
// installer template installs a disposable test app, never the user's DBX.
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import assert from "node:assert/strict";
|
|
import { createRequire } from "node:module";
|
|
import { fileURLToPath } from "node:url";
|
|
assert.equal(process.platform, "win32", "This integration test requires Windows");
|
|
assert.ok(process.argv[2], "Pass an isolated fixture directory containing handlebars");
|
|
const root = path.resolve(process.argv[2]);
|
|
const hb = createRequire(path.join(root, "package.json"))("handlebars");
|
|
const repo = fileURLToPath(new URL("..", import.meta.url));
|
|
const compiler = path.join(process.env.LOCALAPPDATA, "tauri/NSIS/makensis.exe");
|
|
const product = `DBX Issue10895 Test ${Date.now()}`;
|
|
const manufacturer = "DBXInstallerIntegrationTest";
|
|
const target = path.join(root, "protected DBX folder");
|
|
const report = path.join(root, "report.ini");
|
|
const mainName = "dbx-10895-test-app";
|
|
const registry = `Software\\${manufacturer}\\${product}`;
|
|
const uninstallRegistry = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${product}`;
|
|
const ref = "9b17a7aeae9a83222ffe829aa4e2d8a5ba6bed8c";
|
|
// Refuse to overwrite existing fixture installs. Cleanup only these exact paths
|
|
// and the unique test registry keys, leaving source files and reports for review.
|
|
assert.equal(fs.existsSync(target), false, "Fixture destination already exists");
|
|
assert.equal(fs.existsSync(path.join(root, "writable DBX folder")), false, "Writable fixture destination already exists");
|
|
fs.rmSync(report, { force: true });
|
|
function compile(name, source) {
|
|
const file = path.join(root, `${name}.nsi`);
|
|
fs.writeFileSync(file, source);
|
|
const out = execFileSync(compiler, ["/V1", "/INPUTCHARSET", "UTF8", file], { cwd: root, encoding: "utf8", timeout: 30000 });
|
|
if (out.trim()) console.log(out.trim());
|
|
}
|
|
function run(name, args = []) {
|
|
const exe = path.join(root, `${name}.exe`);
|
|
execFileSync(exe, args, { cwd: root, argv0: `"${exe}"`, windowsVerbatimArguments: true, timeout: 30000 });
|
|
}
|
|
function base(name, level = "user") {
|
|
return `Unicode true\n!include MUI2.nsh\n!include FileFunc.nsh\nName "${name}"\nOutFile "${root}\\${name}.exe"\nRequestExecutionLevel ${level}\nSilentInstall silent\n`;
|
|
}
|
|
async function waitFor(check) {
|
|
const end = Date.now() + 30000;
|
|
while (Date.now() < end) {
|
|
if (check()) return;
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
}
|
|
throw new Error("Timed out waiting for elevated installer: " + (fs.existsSync(report) ? fs.readFileSync(report, "utf8") : ""));
|
|
}
|
|
async function main() {
|
|
for (const file of ["utils.nsh", "FileAssociation.nsh", "languages/English.nsh", "languages/SimpChinese.nsh", "languages/TradChinese.nsh"]) {
|
|
const data = JSON.parse(execFileSync("gh", ["api", `repos/tauri-apps/tauri/contents/crates/tauri-bundler/src/bundle/windows/nsis/${file}?ref=${ref}`], { encoding: "utf8" }));
|
|
fs.writeFileSync(path.join(root, path.basename(file)), Buffer.from(data.content, "base64"));
|
|
}
|
|
for (const version of ["old", "new"]) {
|
|
compile(
|
|
`${mainName}-${version}`,
|
|
base(`${mainName}-${version}`) +
|
|
`
|
|
Function .onInit
|
|
WriteINIStr "${report}" "app" "version" "${version}"
|
|
WriteINIStr "${report}" "app" "cmdline" "$CMDLINE"
|
|
System::Call 'shell32::IsUserAnAdmin() i .r0'
|
|
WriteINIStr "${report}" "app" "admin" "$0"
|
|
\u0024{GetOptions} $CMDLINE "/HOLD" $0
|
|
\u0024{IfNot} \u0024{Errors}
|
|
WriteINIStr "${report}" "app" "holding" "1"
|
|
Sleep 120000
|
|
\u0024{EndIf}
|
|
SetErrorLevel 0
|
|
Quit
|
|
FunctionEnd
|
|
Section
|
|
SectionEnd
|
|
`,
|
|
);
|
|
}
|
|
fs.copyFileSync(path.join(root, `${mainName}-new.exe`), path.join(root, `${mainName}.exe`));
|
|
fs.writeFileSync(path.join(root, "payload.txt"), "updated resource payload");
|
|
fs.writeFileSync(
|
|
path.join(root, "hooks.nsh"),
|
|
`
|
|
!macro NSIS_HOOK_PREINSTALL
|
|
System::Call 'shell32::IsUserAnAdmin() i .r0'
|
|
WriteINIStr "${report}" "installer" "admin" "$0"
|
|
WriteINIStr "${report}" "installer" "elevation" "$DbxElevationCode"
|
|
WriteINIStr "${report}" "installer" "cmdline" "$CMDLINE"
|
|
WriteINIStr "${report}" "installer" "language" "$LANGUAGE"
|
|
WriteINIStr "${report}" "installer" "update" "$UpdateMode"
|
|
WriteINIStr "${report}" "installer" "passive" "$PassiveMode"
|
|
WriteINIStr "${report}" "installer" "noShortcut" "$NoShortcutMode"
|
|
!macroend
|
|
!macro NSIS_HOOK_POSTINSTALL
|
|
WriteINIStr "${report}" "installer" "complete" "1"
|
|
!macroend
|
|
`,
|
|
);
|
|
const data = {
|
|
compression: "lzma",
|
|
installer_hooks: path.join(root, "hooks.nsh"),
|
|
manufacturer,
|
|
product_name: product,
|
|
version: "1.0.1",
|
|
version_with_build: "1.0.1.0",
|
|
install_mode: "currentUser",
|
|
main_binary_name: mainName,
|
|
main_binary_path: path.join(root, `${mainName}.exe`),
|
|
bundle_id: "com.dbx.integration10895",
|
|
copyright: "DBX installer test",
|
|
out_file: path.join(root, "installer.exe"),
|
|
arch: "x64",
|
|
additional_plugins_path: path.join(process.env.LOCALAPPDATA, "tauri/NSIS/Plugins/x86-unicode/additional"),
|
|
allow_downgrades: "true",
|
|
display_language_selector: "false",
|
|
install_webview2_mode: "",
|
|
estimated_size: 1024,
|
|
start_menu_folder: "",
|
|
languages: ["English", "SimpChinese", "TradChinese"],
|
|
language_files: ["English.nsh", "SimpChinese.nsh", "TradChinese.nsh"],
|
|
resources: { [path.join(root, "payload.txt")]: [path.join(root, "payload.txt"), "payload.txt"] },
|
|
};
|
|
hb.registerHelper("no-escape", (value) => new hb.SafeString(value));
|
|
const template = fs.readFileSync(path.join(repo, "src-tauri/windows/nsis/installer.nsi"), "utf8");
|
|
compile("installer", hb.compile(template, { noEscape: true })(data));
|
|
compile(
|
|
"seed",
|
|
base("seed", "admin") +
|
|
`
|
|
Section
|
|
CreateDirectory "${target}"
|
|
SetOutPath "${target}"
|
|
File /oname=${mainName}.exe "${root}\\${mainName}-old.exe"
|
|
FileOpen $0 "${target}\\payload.txt" w
|
|
FileWrite $0 "old resource payload"
|
|
FileClose $0
|
|
WriteRegStr HKCU "${registry}" "" "${target}"
|
|
WriteRegStr HKCU "${uninstallRegistry}" "" "${target}"
|
|
WriteRegStr HKCU "${uninstallRegistry}" "DisplayVersion" "1.0.0"
|
|
WriteRegStr HKCU "${uninstallRegistry}" "UninstallString" '"${target}\\uninstall.exe"'
|
|
ExecWait '"$SYSDIR\\icacls.exe" "${target}" /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-545:(OI)(CI)RX"' $0
|
|
WriteINIStr "${report}" "seed" "aclExit" "$0"
|
|
System::Call 'shell32::IsUserAnAdmin() i .r0'
|
|
WriteINIStr "${report}" "seed" "admin" "$0"
|
|
SectionEnd
|
|
`,
|
|
);
|
|
compile(
|
|
"driver",
|
|
base("driver") +
|
|
`
|
|
Function .onInit
|
|
System::Call 'shell32::IsUserAnAdmin() i .r0'
|
|
WriteINIStr "${report}" "driver" "admin" "$0"
|
|
ExecShellWait "runas" "${root}\\seed.exe"
|
|
SetErrorLevel 0
|
|
Quit
|
|
FunctionEnd
|
|
Section
|
|
SectionEnd
|
|
`,
|
|
);
|
|
compile(
|
|
"cleanup",
|
|
base("cleanup", "admin") +
|
|
`
|
|
Section
|
|
RMDir /r "${target}"
|
|
RMDir /r "${root}\\writable DBX folder"
|
|
DeleteRegKey HKCU "${registry}"
|
|
DeleteRegKey /ifempty HKCU "Software\\${manufacturer}"
|
|
DeleteRegKey HKCU "${uninstallRegistry}"
|
|
SectionEnd
|
|
`,
|
|
);
|
|
compile(
|
|
"cleanup-driver",
|
|
base("cleanup-driver") +
|
|
`
|
|
Function .onInit
|
|
ExecShellWait "runas" "${root}\\cleanup.exe"
|
|
SetErrorLevel 0
|
|
Quit
|
|
FunctionEnd
|
|
Section
|
|
SectionEnd
|
|
`,
|
|
);
|
|
run("driver");
|
|
console.log(fs.readFileSync(report, "utf8"));
|
|
assert.throws(() => fs.openSync(path.join(target, `${mainName}.exe`), "r+"), /EACCES|EPERM/);
|
|
const oldApp = spawn(path.join(target, `${mainName}.exe`), ["/HOLD"], { windowsHide: true, stdio: "ignore" });
|
|
await waitFor(() => fs.readFileSync(report, "utf8").includes("holding=1"));
|
|
assert.equal(oldApp.exitCode, null);
|
|
run("installer", ["/UPDATE", "/P", "/NS", "/R", "/ARGS", '"argument with spaces"']);
|
|
await waitFor(() => fs.existsSync(report) && fs.readFileSync(report, "utf8").includes("version=new"));
|
|
const output = fs.readFileSync(report, "utf8");
|
|
console.log(output);
|
|
assert.match(output, /\[driver\]\r?\nadmin=0/);
|
|
assert.match(output, /\[installer\]\r?\nadmin=1/);
|
|
assert.match(output, /elevation=2/);
|
|
assert.match(output, /complete=1/);
|
|
assert.match(output, /update=1/);
|
|
assert.match(output, /passive=1/);
|
|
assert.match(output, /noShortcut=1/);
|
|
assert.match(output, /\/DBX_ELEVATED/);
|
|
assert.match(output, /\[app\][\s\S]*version=new[\s\S]*argument with spaces/);
|
|
assert.deepEqual(fs.readFileSync(path.join(target, `${mainName}.exe`)), fs.readFileSync(path.join(root, `${mainName}.exe`)));
|
|
assert.equal(fs.readFileSync(path.join(target, "payload.txt"), "utf8"), "updated resource payload");
|
|
assert.ok(fs.existsSync(path.join(target, "uninstall.exe")));
|
|
assert.notEqual(oldApp.exitCode, null, "old application was stopped before overwrite");
|
|
console.log("REAL ELEVATION AND FULL TEMPLATE UPDATE WITH RUNNING OLD APP: PASS");
|
|
fs.copyFileSync(report, path.join(root, "protected-update-report.ini"));
|
|
fs.unlinkSync(report);
|
|
const writableTarget = path.join(root, "writable DBX folder");
|
|
run("installer", ["/S", "/UPDATE", "/NS", "/R", "/ARGS", '"writable update argument"', `/D=${writableTarget}`]);
|
|
await waitFor(() => fs.existsSync(report) && fs.readFileSync(report, "utf8").includes("version=new"));
|
|
const writableOutput = fs.readFileSync(report, "utf8");
|
|
assert.match(writableOutput, /elevation=0/);
|
|
console.log(writableOutput);
|
|
assert.match(writableOutput, /\[installer\]\r?\nadmin=0/);
|
|
assert.doesNotMatch(writableOutput, /\/DBX_ELEVATED/);
|
|
assert.equal(fs.readFileSync(path.join(writableTarget, "payload.txt"), "utf8"), "updated resource payload");
|
|
console.log("FULL TEMPLATE INSTALL TO WRITABLE FOLDER WITHOUT ELEVATION: PASS");
|
|
}
|
|
main()
|
|
.catch((e) => {
|
|
console.error(e);
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(() => {
|
|
if (fs.existsSync(path.join(root, "cleanup-driver.exe"))) {
|
|
run("cleanup-driver");
|
|
assert.equal(fs.existsSync(target), false, "Protected fixture was not removed");
|
|
assert.equal(fs.existsSync(path.join(root, "writable DBX folder")), false, "Writable fixture was not removed");
|
|
}
|
|
});
|