mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
188 lines
9.7 KiB
JavaScript
188 lines
9.7 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');
|
|
}
|
|
});
|