Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/setup-settings.test.js
T
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

373 lines
21 KiB
JavaScript

#!/usr/bin/env node
// Tests for the setup wizard's dependency probe and the settings writer.
//
// The interesting cases are the ones that cannot be exercised on a machine
// where everything is already installed: a missing required tool, and fprintd
// being present but having no enrolled finger.
//
// node tests/setup-settings.test.js
const fs = require("fs")
const path = require("path")
const Model = {}
new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
.replace(/^\.pragma library\s*$/m, "") + `
exports.parseDependencies = parseDependencies
exports.missingRequired = missingRequired
exports.dependencyCheckCommand = dependencyCheckCommand
exports.vaultListMode = vaultListMode
exports.vaultListBlockedMessage = vaultListBlockedMessage
exports.vaultListFailureMessage = vaultListFailureMessage
exports.sshCliMinVersion = sshCliMinVersion
exports.sshCliSupport = typeof sshCliSupport === "function" ? sshCliSupport : null
exports.sshUiAvailable = sshUiAvailable
exports.settingWriteCommand = settingWriteCommand
exports.boolSetting = boolSetting
exports.installPackagesCommand = installPackagesCommand
exports.SETTINGS_SCHEMA = SETTINGS_SCHEMA
exports.DEPENDENCIES = DEPENDENCIES
exports.groupedSettings = groupedSettings
exports.SETTINGS_GROUPS = SETTINGS_GROUPS
exports.validatePin = validatePin
exports.pinMinLength = pinMinLength
exports.pinRecommendedLength = pinRecommendedLength
exports.pinWeakWarning = pinWeakWarning
exports.isPinWeak = isPinWeak
exports.pinStoreCommand = pinStoreCommand
exports.pinUnlockCommand = pinUnlockCommand
`)(Model)
let pass = 0
const failures = []
const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`)
const byKey = (deps, k) => deps.items.find(d => d.key === k)
const dependencyProbe = Model.dependencyCheckCommand()[2]
const sshCliSupport = (version) => typeof Model.sshCliSupport === "function"
? Model.sshCliSupport(version)
: "__missing__"
// --- everything present -----------------------------------------------------
const all = Model.parseDependencies(
"bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("all present: nothing required is missing",
Model.missingRequired(all).length === 0,
`got [${Model.missingRequired(all).map(d => d.key)}]`)
check("all present: fprintd reported ready", byKey(all, "fprintd").ready === true, "expected ready")
check("jq is a required dependency alongside bw",
byKey(all, "jq") && byKey(all, "jq").required === true && byKey(all, "jq").pkg === "jq",
JSON.stringify(byKey(all, "jq")))
check("the dependency probe checks for jq before vault reads",
dependencyProbe.includes("command -v jq"),
dependencyProbe)
check("the dependency probe captures the bw CLI version for SSH gating",
/bw\s+--version|bw\s+-v/.test(dependencyProbe),
dependencyProbe)
check("sshCliMinVersion reports the verified floor",
Model.sshCliMinVersion() === "2025.1.2",
String(Model.sshCliMinVersion()))
check("sshCliSupport marks 2025.1.2 as supported",
sshCliSupport("2025.1.2") === "supported",
JSON.stringify(sshCliSupport("2025.1.2")))
check("sshCliSupport marks 2025.1.1 as unsupported",
sshCliSupport("2025.1.1") === "unsupported",
JSON.stringify(sshCliSupport("2025.1.1")))
check("sshCliSupport treats malformed versions as unknown",
sshCliSupport("development-build") === "unknown",
JSON.stringify(sshCliSupport("development-build")))
check("sshCliSupport treats a missing version as unknown",
sshCliSupport("") === "unknown",
JSON.stringify(sshCliSupport("")))
check("SSH surfaces stay hidden until the probe confirms a supported CLI",
Model.sshUiAvailable(all, true) === true
&& Model.sshUiAvailable(all, false) === false,
JSON.stringify({ checked: Model.sshUiAvailable(all, true), unchecked: Model.sshUiAvailable(all, false) }))
const oldCli = Model.parseDependencies(
"bw=1\nbw_version=2025.1.1\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("an unsupported CLI hides SSH but still reports why on the bw row",
Model.sshUiAvailable(oldCli, true) === false
&& byKey(oldCli, "bw").note.includes("2025.1.2")
&& byKey(oldCli, "bw").note.includes("2025.1.1"),
JSON.stringify(byKey(oldCli, "bw")))
const unreadableCli = Model.parseDependencies(
"bw=1\nbw_version=development-build\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("an unreadable CLI version hides SSH rather than assuming support",
Model.sshUiAvailable(unreadableCli, true) === false
&& unreadableCli.sshCliStatus === "unknown"
&& byKey(unreadableCli, "bw").note !== "",
JSON.stringify({ status: unreadableCli.sshCliStatus, bw: byKey(unreadableCli, "bw") }))
check("bw version metadata is preserved for feature gating",
byKey(all, "bw") && byKey(all, "bw").version === "2025.1.2" && all.sshCliStatus === "supported",
JSON.stringify({ bw: byKey(all, "bw"), sshCliStatus: all.sshCliStatus }))
// --- the case that matters: a required tool is absent -----------------------
const noBw = Model.parseDependencies(
"bw=0\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
const missing = Model.missingRequired(noBw)
check("missing bw is reported as required",
missing.length === 1 && missing[0].key === "bw" && missing[0].pkg === "bitwarden-cli",
`got [${missing.map(d => d.key + ":" + d.pkg)}]`)
check("missing bw is not marked installed", byKey(noBw, "bw").installed === false, "expected false")
const noJq = Model.parseDependencies(
"bw=1\nbw_version=2025.1.2\njq=0\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
const missingNoJq = Model.missingRequired(noJq)
check("missing jq is reported as required",
missingNoJq.length === 1 && missingNoJq[0].key === "jq" && missingNoJq[0].pkg === "jq",
`got [${missingNoJq.map(d => d.key + ":" + d.pkg)}]`)
check("missing jq does not alter optional dependency semantics",
byKey(noJq, "fprintd").required === false,
JSON.stringify(byKey(noJq, "fprintd")))
check("supported bw without jq blocks the vault list until setup finishes",
Model.vaultListMode(noJq) === "blocked"
&& Model.vaultListBlockedMessage(noJq).includes("jq"),
`${Model.vaultListMode(noJq)} / ${Model.vaultListBlockedMessage(noJq)}`)
// A whole-list failure on a CLI that predates the malformed-SSH-item fix is the
// one case where the panel can say something useful about a read it cannot
// repair. The attribution comes from the probed version, never from the failed
// read's own output, which can quote decrypted vault material.
const rawCliFailure = "TypeError: Cannot read properties of null (reading 'keyFingerprint') for item work-ssh"
check("a list failure on a pre-2026.8.0 CLI names the release that fixes it",
Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("2026.8.0"),
Model.vaultListFailureMessage(rawCliFailure, all, "sanitized"))
check("the failure message never echoes raw CLI output",
!Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("keyFingerprint")
&& !Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("TypeError")
&& !Model.vaultListFailureMessage(rawCliFailure, all, "sanitized").includes("work-ssh"),
Model.vaultListFailureMessage(rawCliFailure, all, "sanitized"))
const fixedCli = Model.parseDependencies(
"bw=1\nbw_version=2026.8.0\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("a list failure on a fixed CLI does not blame the SSH-item bug",
!Model.vaultListFailureMessage(rawCliFailure, fixedCli, "sanitized").includes("2026.8.0"),
Model.vaultListFailureMessage(rawCliFailure, fixedCli, "sanitized"))
check("a blocked list reports the missing tool instead of the SSH hint",
Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked").includes("jq")
&& !Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked").includes("2026.8.0"),
Model.vaultListFailureMessage(rawCliFailure, noJq, "blocked"))
// An optional tool going missing must not trigger the blocking wizard.
const noFprintd = Model.parseDependencies(
"bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=0\nfingerprint_ready=0\nomarchy=1")
check("missing optional tool does not block setup",
Model.missingRequired(noFprintd).length === 0,
`got [${Model.missingRequired(noFprintd).map(d => d.key)}]`)
// --- fprintd installed but no finger enrolled -------------------------------
const noFinger = Model.parseDependencies(
"bw=1\nbw_version=2025.1.2\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=0\nomarchy=1")
check("fprintd on PATH without an enrolled finger is installed-but-not-ready",
byKey(noFinger, "fprintd").installed === true && byKey(noFinger, "fprintd").ready === false,
`installed=${byKey(noFinger, "fprintd").installed} ready=${byKey(noFinger, "fprintd").ready}`)
const oldBw = Model.parseDependencies(
"bw=1\nbw_version=2025.1.1\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("older bw versions remain installed but are marked unsupported for SSH",
byKey(oldBw, "bw").installed === true
&& byKey(oldBw, "bw").version === "2025.1.1"
&& oldBw.sshCliStatus === "unsupported"
&& byKey(oldBw, "bw").note.includes(Model.sshCliMinVersion()),
JSON.stringify({ bw: byKey(oldBw, "bw"), sshCliStatus: oldBw.sshCliStatus }))
check("older bw with jq still uses the sanitized list path for ordinary items",
Model.vaultListMode(oldBw) === "sanitized",
JSON.stringify({ mode: Model.vaultListMode(oldBw), deps: oldBw }))
const unknownBw = Model.parseDependencies(
"bw=1\nbw_version=development-build\njq=1\nwlcopy=1\nhyprctl=1\nsecrettool=1\nfprintd=1\nfingerprint_ready=1\nomarchy=1")
check("unknown bw versions are reported separately from unsupported ones",
byKey(unknownBw, "bw").installed === true
&& byKey(unknownBw, "bw").version === ""
&& unknownBw.sshCliStatus === "unknown",
JSON.stringify({ bw: byKey(unknownBw, "bw"), sshCliStatus: unknownBw.sshCliStatus }))
check("unknown bw with jq still uses the sanitized list path and never falls back to a raw legacy read",
Model.vaultListMode(unknownBw) === "sanitized",
JSON.stringify({ mode: Model.vaultListMode(unknownBw), deps: unknownBw }))
// --- malformed / empty probe output -----------------------------------------
for (const [label, raw] of [["empty", ""], ["garbage", "???\n=\nbw\n"]]) {
const d = Model.parseDependencies(raw)
check(`${label} probe output degrades to all-missing`,
d.items.length === Model.DEPENDENCIES.length && d.items.every(i => !i.installed),
`got ${d.items.length} items, installed=[${d.items.filter(i => i.installed).map(i => i.key)}]`)
}
// --- settings writer --------------------------------------------------------
// Values must reach shell.json as real JSON types, not strings, or `setting()`
// hands the panel a string where it expects a number or a bool.
// The writer runs through bash so its diagnostic stderr can be capped, so the
// assertions read the script rather than an argv list.
const writeScript = (k, v, t) => Model.settingWriteCommand(k, v, t)[2]
// --- colorized menu-bar icon setting ----------------------------------------
const colorizeIcon = Model.SETTINGS_SCHEMA.find(e => e.key === "colorizeIcon")
check("colorized icon setting is declared in General", !!colorizeIcon
&& colorizeIcon.group === "general"
&& colorizeIcon.type === "bool",
JSON.stringify(colorizeIcon))
check("colorized icon defaults off", !!colorizeIcon && colorizeIcon.defaultValue === false,
JSON.stringify(colorizeIcon))
check("colorized icon accepts only actual booleans",
Model.boolSetting("colorizeIcon", true) === true
&& Model.boolSetting("colorizeIcon", false) === false
&& Model.boolSetting("colorizeIcon", "true") === false
&& Model.boolSetting("colorizeIcon", 1) === false,
"malformed colorizeIcon input was accepted")
check("boolean settings accept actual JSON booleans",
Model.boolSetting("fingerprintUnlock", true) === true
&& Model.boolSetting("fingerprintUnlock", false) === false,
"actual booleans were not preserved")
check("malformed strings cannot enable opt-in credential storage",
Model.boolSetting("fingerprintUnlock", "false") === false
&& Model.boolSetting("pinUnlock", "true") === false,
"a string enabled an opt-in unlock method")
check("malformed lock settings fail back to their secure defaults",
Model.boolSetting("lockOnScreenLock", "false") === true
&& Model.boolSetting("lockOnSuspend", 0) === true,
"a malformed setting disabled locking")
check("int setting is written with --json",
writeScript("autoLockMinutes", 15, "int")
.includes("omarchy bar set io.github.elevate08.qs-bitwarden-cli 'autoLockMinutes' '15' --json"),
writeScript("autoLockMinutes", 15, "int"))
for (const [v, want] of [[true, "true"], [false, "false"]]) {
const script = writeScript("closeOnCopy", v, "bool")
check(`bool ${v} is written as ${want}`,
script.includes(`'closeOnCopy' '${want}' --json`), `got ${script}`)
}
check("a zero int is written as 0, not dropped",
writeScript("autoLockMinutes", 0, "int").includes("'autoLockMinutes' '0' --json"),
writeScript("autoLockMinutes", 0, "int"))
// stderr from `omarchy bar set` is collected by the panel, so it needs the same
// producer-side cap as every other stream the long-lived shell buffers.
check("setting writer caps its diagnostic stderr",
writeScript("autoLockMinutes", 15, "int").includes("exec 2> >(head -c 8192 >&2)"),
writeScript("autoLockMinutes", 15, "int"))
// Every schema key must exist in the manifest, or the settings screen would
// write a key the plugin never reads.
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8"))
const manifestKeys = new Set(manifest.barWidget.schema.map(e => e.key))
for (const entry of Model.SETTINGS_SCHEMA) {
check(`schema key '${entry.key}' exists in manifest.json`,
manifestKeys.has(entry.key), `manifest has [${[...manifestKeys]}]`)
}
const colorizeManifest = manifest.barWidget.schema.find(e => e.key === "colorizeIcon")
check("manifest colorized icon schema matches the model contract",
!!colorizeManifest
&& colorizeManifest.type === "boolean"
&& colorizeManifest.label === colorizeIcon.label
&& colorizeManifest.description === colorizeIcon.description
&& colorizeManifest.defaultValue === false
&& manifest.barWidget.defaults.colorizeIcon === false,
JSON.stringify({ model: colorizeIcon, manifest: colorizeManifest }))
// --- install command --------------------------------------------------------
check("no packages yields no command", Model.installPackagesCommand([]) === null, "expected null")
const inst = Model.installPackagesCommand(["bitwarden-cli", "wl-clipboard"])
check("install goes through Omarchy's own floating-terminal installer",
inst.slice(0, 3).join(" ") === "omarchy install app" && inst[4] === "bitwarden-cli wl-clipboard",
inst.join(" "))
// The package list lands in an unquoted expansion inside omarchy-install-app,
// so anything that is not a plain package name must not reach it.
check("install refuses a package name that is not one",
Model.installPackagesCommand(["bitwarden-cli; rm -rf /"]) === null,
JSON.stringify(Model.installPackagesCommand(["bitwarden-cli; rm -rf /"])))
// The probe must be a single process, not one per tool.
check("dependency probe is one shell invocation",
Model.dependencyCheckCommand()[0] === "bash" && Model.dependencyCheckCommand().length === 3,
JSON.stringify(Model.dependencyCheckCommand().slice(0, 2)))
// --- settings grouping ------------------------------------------------------
const grouped = Model.groupedSettings()
check("grouping keeps every setting",
grouped.length === Model.SETTINGS_SCHEMA.length,
`${grouped.length} vs ${Model.SETTINGS_SCHEMA.length}`)
check("exactly one header per group",
grouped.filter(e => e.groupLabel !== "").length === Model.SETTINGS_GROUPS.length,
`got ${grouped.filter(e => e.groupLabel !== "").length} headers`)
check("entries are contiguous within a group",
JSON.stringify(grouped.map(e => e.group)) ===
JSON.stringify(grouped.map(e => e.group).slice().sort(
(a, b) => Model.SETTINGS_GROUPS.findIndex(g => g.id === a) - Model.SETTINGS_GROUPS.findIndex(g => g.id === b))),
grouped.map(e => e.group).join(","))
check("grouping does not mutate the schema",
Model.SETTINGS_SCHEMA.every(e => e.groupLabel === undefined), "schema was mutated")
// --- PIN validation ---------------------------------------------------------
check("minimum PIN length is 4", Model.pinMinLength() === 4, String(Model.pinMinLength()))
check("recommended PIN length is 6", Model.pinRecommendedLength() === 6, String(Model.pinRecommendedLength()))
// A short PIN is allowed -- the point is that it is flagged, not blocked.
check("a 4-digit PIN still validates", Model.validatePin("1234", "1234") === "", Model.validatePin("1234", "1234"))
check("a 5-digit PIN still validates", Model.validatePin("12345", "12345") === "", Model.validatePin("12345", "12345"))
check("but 4 digits is flagged weak", Model.isPinWeak("1234"), "expected weak")
check("and 5 digits is flagged weak", Model.isPinWeak("12345"), "expected weak")
check("6 digits is not flagged", !Model.isPinWeak("123456"), Model.pinWeakWarning("123456"))
check("longer than 6 is not flagged", !Model.isPinWeak("1234567890"), Model.pinWeakWarning("1234567890"))
// No warning while still typing towards a good PIN, or it would flash on
// every keystroke from the first digit onwards.
check("nothing is flagged before the floor is even reached",
!Model.isPinWeak("") && !Model.isPinWeak("1") && !Model.isPinWeak("123"),
"expected no warning below the minimum")
// The warning has to carry the actual number, not a vague 'weak'.
check("the warning names the search space for 4 digits",
Model.pinWeakWarning("1234").includes("10,000") && Model.pinWeakWarning("1234").includes("4-digit"),
Model.pinWeakWarning("1234"))
check("the warning names the search space for 5 digits",
Model.pinWeakWarning("12345").includes("100,000"), Model.pinWeakWarning("12345"))
check("the warning points at the recommendation",
Model.pinWeakWarning("1234").includes("6 or more"), Model.pinWeakWarning("1234"))
for (const [pin, confirm, wantErr] of [
["123", "123", true], // too short
["1234", "1234", false], // the minimum is accepted
["12345678901234", "12345678901234", false], // longer is allowed, no upper bound
["12a4", "12a4", true], // non-digits refused
["", "", true],
["1234", "4321", true], // mismatch
]) {
const err = Model.validatePin(pin, confirm)
check(`validatePin(${JSON.stringify(pin)}, ${JSON.stringify(confirm)})`,
(err !== "") === wantErr, `err=${JSON.stringify(err)}`)
}
check("confirm is optional when omitted", Model.validatePin("1234") === "", Model.validatePin("1234"))
// --- PIN crypto command shape ----------------------------------------------
// The whole point of PIN unlock over fingerprint unlock is that the keyring
// holds ciphertext, not the master password. Guard that property.
const store = Model.pinStoreCommand()[2]
check("store derives a key from the PIN rather than saving it",
store.includes("openssl enc") && store.includes("-pbkdf2") && store.includes("env:QSBW_PIN"), store)
check("store uses a high iteration count",
/-iter\s+(\d+)/.test(store) && Number(store.match(/-iter\s+(\d+)/)[1]) >= 600000, store)
check("store pins PBKDF2 to SHA-256 instead of relying on an OpenSSL default",
store.includes("-md sha256"), store)
check("store salts the ciphertext", store.includes("-salt"), store)
check("store reports encryption failures instead of saving an empty blob",
store.includes("set -o pipefail"), store)
check("store pipes straight into the keyring, never through argv",
store.includes("secret-tool store") && !store.includes("$QSBW_SECRET\" secret-tool"), store)
check("neither PIN nor secret appears as a literal argument",
!store.includes("--pass ") && store.includes("-pass env:"), store)
const unlock = Model.pinUnlockCommand()[2]
check("unlock decrypts with the PIN-derived key",
unlock.includes("openssl enc -d") && unlock.includes("env:QSBW_PIN"), unlock)
check("unlock fails loudly when the lookup fails (pipefail)",
unlock.includes("set -o pipefail"), unlock)
check("unlock iteration count matches store",
unlock.match(/-iter\s+(\d+)/)[1] === store.match(/-iter\s+(\d+)/)[1],
`${unlock.match(/-iter\s+(\d+)/)[1]} vs ${store.match(/-iter\s+(\d+)/)[1]}`)
check("unlock uses the same explicit PBKDF2 digest as store",
unlock.includes("-md sha256"), unlock)
console.log(`${pass} passed, ${failures.length} failed`)
if (failures.length) { console.error("\nFAILURES:\n " + failures.join("\n ")); process.exit(1) }