import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { copyFileSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; // Real QML Process/FileView lifecycle, isolated from the desktop, clipboard and user settings. const directory = mkdtempSync(join(tmpdir(), "nuguland-vault-runtime-test-")); const profile = { name: "personal", serverUrl: "https://vault.example.com", account: "initial@example.com", unlockTimeoutSeconds: 900, clipboardTimeoutSeconds: 30, }; try { for (const file of ["Vault.qml", "vault.js", "VaultPanel.qml", "Theme.qml", "NuguText.qml", "PopupCard.qml", "PopupButton.qml", "PopupTextField.qml", "PopupToolTip.qml", "SetupField.qml"]) copyFileSync(new URL(file, import.meta.url), join(directory, file)); mkdirSync(join(directory, "state")); mkdirSync(join(directory, "vault-native")); writeFileSync(join(directory, "state", "vault.json"), JSON.stringify(profile)); writeFileSync(join(directory, "Clipboard.qml"), `import QtQuick Item { signal vaultCapturePaused function pauseForVaultCopy(seconds) {} function resumeAfterVaultCopy() {} } `); writeFileSync(join(directory, "old.cjs"), `const fs = require("node:fs"); const lines = require("node:readline").createInterface({ input: process.stdin }); lines.on("line", line => { if (JSON.parse(line).op === "configure-lock") { fs.writeFileSync(${JSON.stringify(join(directory, "obsolete-request"))}, "rejected"); console.log(JSON.stringify({ event: "protocol-error" })); } }); `); const binary = resolve("vault-native/target/debug/nuguland-vault"); writeFileSync(join(directory, "vault-native", "run.sh"), `#!/bin/sh printf x >> '${directory}/launches' if [ ! -f '${directory}/old-started' ]; then touch '${directory}/old-started' exec node '${directory}/old.cjs' fi exec '${binary}' `, { mode: 0o700 }); writeFileSync(join(directory, "shell.qml"), `//@ pragma StateDir ${directory}/state import QtQuick import Quickshell import Quickshell.Io ShellRoot { id: test property int phase: 0 property string savedAccount: "" property var retainedField: null property var fixtureDetail: ({ event: "detail", id: "fixture", fields: [{ id: "login.password", label: "password", value: "synthetic", sensitive: true, kind: "text" }], totp: { current: "123456", next: "654321", remaining: 1, currentSlot: 0, nextSlot: 1 } }) function find(item, name) { if (item.objectName === name) return item; for (const child of item.children) { const found = find(child, name); if (found) return found; } return null; } Clipboard { id: clipboard } Vault { id: vault; clipboardService: clipboard } Theme { id: theme } Window { visible: true width: 980 height: 620 VaultPanel { id: panel; anchors.fill: parent; theme: theme; vaultService: vault } } FileView { id: saved path: vault.configPath watchChanges: true onFileChanged: reload() onLoaded: test.savedAccount = JSON.parse(text()).account } Timer { interval: 20 running: true repeat: true onTriggered: { if (vault.setupError) { console.error("vault runtime test phase " + test.phase + ": " + vault.setupError); Qt.quit(); } else if (test.phase === 0 && !vault.setupSaving && vault.profile.account === "initial@example.com") { vault.beginSetup(); test.phase = 1; } else if (test.phase === 1 && !vault.lockPending) { vault.setupUrlDraft = "other.example.com"; vault.setupAccountDraft = "saved@example.com"; vault.saveSetup(); test.phase = 2; } else if (test.phase === 2 && !vault.setupSaving && !vault.setupEditing && vault.status === "locked" && test.savedAccount === "saved@example.com") { vault.beginSetup(); test.phase = 3; } else if (test.phase === 3 && !vault.lockPending) { vault.setupUrlDraft = "http://invalid.example.com"; vault.testConnection(); test.phase = 4; } else if (test.phase === 4 && !vault.connectionBusy && vault.connectionResult.indexOf("https") >= 0) { if (vault.profile.serverUrl !== "https://other.example.com" || test.savedAccount !== "saved@example.com") console.error("connection test changed saved settings"); else { vault.handleLine(JSON.stringify({ event: "state", status: "second-factor", items: [], challenge: { providers: [{ id: 1, label: "email code", codeSupported: true }] } })); vault.panelOpen = true; vault.selectSecondFactor(1); vault.secondFactorDraft = "123"; vault.panelOpen = false; test.phase = 5; } } else if (test.phase === 5) { vault.panelOpen = true; if (vault.status !== "second-factor" || vault.secondFactorProvider !== 1 || vault.secondFactorDraft !== "123" || vault.secondFactorHint().indexOf("check your inbox") < 0) console.error("closing lost verification state"); else console.info("vault runtime test: saved"); vault.cancelUnlock(); if (vault.secondFactorDraft !== "") console.error("cancel retained verification code"); test.phase = 6; } else if (test.phase === 6 && !vault.lockPending && vault.status === "locked") { vault.cancelSetup(); vault.status = "unlocked"; vault.selectedId = "fixture"; vault.handleLine(JSON.stringify(test.fixtureDetail)); test.phase = 7; } else if (test.phase === 7) { test.retainedField = test.find(panel, "vault-field-login.password"); if (!test.retainedField) { console.error("fixture field delegate missing"); Qt.quit(); return; } test.retainedField.forceActiveFocus(); const update = Object.assign({}, test.fixtureDetail, { totp: { current: "654321", next: "987654", remaining: 30, currentSlot: 1, nextSlot: 2 } }); vault.handleLine(JSON.stringify(update)); test.phase = 8; } else if (test.phase === 8) { if (test.find(panel, "vault-field-login.password") !== test.retainedField || !test.retainedField.activeFocus) console.error("TOTP rollover replaced the field delegate or lost keyboard focus"); else console.info("vault runtime test: field focus retained"); vault.lock(); test.phase = 9; } else if (test.phase === 9 && !vault.lockPending) { if (!test.find(panel, "vaultMasterPassword").input.activeFocus) console.error("locking failed to focus the master password input"); else console.info("vault runtime test: unlock focused"); Qt.quit(); } } } } `); mkdirSync(join(directory, "runtime"), { mode: 0o700 }); const environment = { ...process.env, QT_QPA_PLATFORM: "offscreen", QT_QUICK_BACKEND: "software", QT_QPA_PLATFORMTHEME: "", QT_NO_XDG_DESKTOP_PORTAL: "1", QS_NO_RELOAD_POPUP: "1", XDG_STATE_HOME: join(directory, "state"), XDG_CACHE_HOME: join(directory, "cache"), XDG_DATA_HOME: join(directory, "data"), XDG_RUNTIME_DIR: join(directory, "runtime"), }; delete environment.WAYLAND_DISPLAY; delete environment.DISPLAY; const result = spawnSync("qs", ["--no-color", "-p", join(directory, "shell.qml")], { cwd: directory, env: environment, encoding: "utf8", timeout: 15_000, }); const output = result.stdout + result.stderr + "\nlaunches: " + (existsSync(join(directory, "launches")) ? readFileSync(join(directory, "launches"), "utf8") : "none"); assert.equal(result.error, undefined, output); assert.equal(result.status, 0, output); assert.doesNotMatch(output, /WARN|ERROR|Error:|TypeError|ReferenceError/, output); assert.match(output, /vault runtime test: saved/); assert.match(output, /vault runtime test: field focus retained/); assert.match(output, /vault runtime test: unlock focused/); assert.equal(existsSync(join(directory, "obsolete-request")), false, "old protocol must not receive configuration requests"); assert.equal(readFileSync(join(directory, "launches"), "utf8"), "xxxx", "replace obsolete boundary, replace on save, test in a separate boundary"); assert.deepEqual(JSON.parse(readFileSync(join(directory, "state", "vault.json"), "utf8")), { ...profile, serverUrl: "https://other.example.com", account: "saved@example.com", }); console.log("vault-runtime.test.mjs: ok"); } finally { rmSync(directory, { recursive: true, force: true }); }