Files
omarchy/plugins/io.github.elevate08.qs-bitwarden-cli/tests/lock-triggers.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

278 lines
11 KiB
JavaScript

#!/usr/bin/env node
// Tests for the two events that lock the vault without waiting out the
// auto-lock countdown, and for the window in which a terminal login's session
// key is accepted.
//
// node tests/lock-triggers.test.js
const fs = require("fs")
const path = require("path")
const { execFileSync } = require("child_process")
const Model = {}
new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
.replace(/^\.pragma library\s*$/m, "") + `
exports.screenLockStateCommand = screenLockStateCommand
exports.screenIsLocked = screenIsLocked
exports.screenLockPollMs = screenLockPollMs
exports.sleepMonitorCommand = sleepMonitorCommand
exports.sleepSignalToken = sleepSignalToken
exports.wakeSignalToken = wakeSignalToken
exports.sessionHandoffReadCommand = sessionHandoffReadCommand
exports.handoffWindowOpen = handoffWindowOpen
exports.handoffWindowMs = handoffWindowMs
exports.groupedSettings = groupedSettings
`)(Model)
let pass = 0
const failures = []
const check = (l, ok, d) => ok ? pass++ : failures.push(`${l}\n ${d}`)
// -------------------------------------------------------------------------
// Screen lock
// -------------------------------------------------------------------------
// Only "true" is locked. A shell with the lock plugin disabled answers
// "Target not found." and exits non-zero; that is "no answer", and a vault
// that reads it as "locked" would relock itself every few seconds forever.
const LOCK_ANSWERS = [
["true", true],
["true\n", true],
[" true ", true],
["false", false],
["", false],
["Target not found.", false],
["TRUE", false],
["truthy", false],
[null, false],
[undefined, false],
]
for (const [raw, want] of LOCK_ANSWERS) {
check(`screenIsLocked(${JSON.stringify(raw)}) is ${want}`,
Model.screenIsLocked(raw) === want, String(Model.screenIsLocked(raw)))
}
const lockCmd = Model.screenLockStateCommand()
check("screen lock state is asked of the shell's own lock plugin",
lockCmd.join(" ").includes("omarchy-shell lock isLocked"), lockCmd.join(" "))
check("screen lock state bounds what it will read back",
/head -c \d+/.test(lockCmd.join(" ")), lockCmd.join(" "))
check("screen lock poll is a sane interval",
Model.screenLockPollMs() >= 1000 && Model.screenLockPollMs() <= 15000,
String(Model.screenLockPollMs()))
// -------------------------------------------------------------------------
// Suspend
// -------------------------------------------------------------------------
const sleepScript = Model.sleepMonitorCommand()[2]
check("suspend is taken from logind's PrepareForSleep",
sleepScript.includes("PrepareForSleep") && sleepScript.includes("org.freedesktop.login1"),
sleepScript)
// Without a delay inhibitor logind announces the sleep and suspends without
// waiting, so the lock would be racing the freeze.
check("a delay inhibitor is held so the lock lands before the freeze",
sleepScript.includes("--what=sleep") && sleepScript.includes("--mode=delay"),
sleepScript)
// sed quits on the match, but the monitor would then keep the pipeline open
// until it next wrote -- which is on the far side of the suspend. Killing it
// is what lets the inhibitor be released and the loop come round again.
check("the monitor is killed rather than left to a broken pipe",
sleepScript.includes("kill \"$g\""), sleepScript)
check("the loop never exits, so a failure cannot become a hot restart",
sleepScript.includes("while :; do") && /sleep 300/.test(sleepScript) && /sleep 5/.test(sleepScript),
sleepScript)
check("sed is unbuffered, so the token is not held back past the suspend",
/sed -une/.test(sleepScript), sleepScript)
// The end-to-end behaviour, against stubs standing in for gdbus and
// systemd-inhibit: the announcement has to produce exactly one token, the
// inhibitor has to be released about a second later, and the loop has to come
// round for the suspend after this one.
const os = require("os")
const work = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-lock-"))
try {
const log = path.join(work, "inhibit.log")
fs.writeFileSync(log, "")
// Real gdbus is a single process, so the stub execs its wait rather than
// backgrounding it -- otherwise the stub would leak a child that the real
// thing does not have.
fs.writeFileSync(path.join(work, "gdbus"), `#!/bin/bash
echo "Monitoring signals on object /org/freedesktop/login1 owned by org.freedesktop.login1"
echo "/org/freedesktop/login1: org.freedesktop.login1.Manager.PrepareForSleep (false,)"
echo "/org/freedesktop/login1: org.freedesktop.login1.Manager.PrepareForSleep (true,)"
exec sleep 600
`)
fs.writeFileSync(path.join(work, "systemd-inhibit"), `#!/bin/bash
while [[ "$1" == --* ]]; do shift; done
echo "ACQUIRED $(date +%s%3N)" >> "${log}"
"$@"; rc=$?
echo "RELEASED $(date +%s%3N)" >> "${log}"
exit $rc
`)
for (const f of ["gdbus", "systemd-inhibit"]) fs.chmodSync(path.join(work, f), 0o755)
const script = path.join(work, "cmd.sh")
fs.writeFileSync(script, sleepScript)
let out = ""
try {
out = execFileSync("bash", ["-c",
`PATH=${work}:$PATH timeout 4 bash ${script}`], { encoding: "utf8" })
} catch (e) {
out = String(e.stdout || "") // timeout always kills it; that is the point
}
const tokens = out.split("\n").map(s => s.trim()).filter(Boolean)
check("an announcement produces the sleep token",
tokens[0] === Model.sleepSignalToken(), JSON.stringify(tokens))
check("resuming produces the wake token",
tokens[1] === Model.wakeSignalToken(), JSON.stringify(tokens))
// A `false` announcement is a resume, not a sleep. Two tokens per cycle, so
// an odd count would mean the resume line matched as well.
check("only a true announcement counts as a sleep",
tokens.filter(t => t === Model.sleepSignalToken()).length
=== tokens.filter(t => t === Model.wakeSignalToken()).length
|| tokens[tokens.length - 1] === Model.sleepSignalToken(),
JSON.stringify(tokens))
// The bug this shape exists to avoid: the loop coming round only once.
check("the loop detects more than one suspend per session",
tokens.filter(t => t === Model.sleepSignalToken()).length >= 2,
JSON.stringify(tokens))
const entries = fs.readFileSync(log, "utf8").trim().split("\n").filter(Boolean)
const acquired = entries.filter(l => l.startsWith("ACQUIRED")).length
const released = entries.filter(l => l.startsWith("RELEASED")).length
check("an inhibitor is taken for every cycle",
acquired >= 2 && released >= 1 && acquired - released <= 1,
entries.join(" | "))
// Held about a second past the announcement, which is well inside logind's
// InhibitDelayMaxSec (5s by default) and long enough for the panel to drop
// the key and for the keyring clear it spawns to finish.
const firstAcquire = Number(entries[0].split(" ")[1])
const firstRelease = Number(entries.find(l => l.startsWith("RELEASED")).split(" ")[1])
const held = firstRelease - firstAcquire
check("the inhibitor is released promptly, not held across the suspend",
held >= 900 && held < 4000, `${held}ms`)
} finally {
fs.rmSync(work, { recursive: true, force: true })
}
// -------------------------------------------------------------------------
// Session handoff window
// -------------------------------------------------------------------------
const NOW = 1_700_000_000_000
const WINDOW = Model.handoffWindowMs()
check("the window is closed when no terminal login was ever launched",
Model.handoffWindowOpen(0, NOW) === false, "0")
check("the window is open right after launching one",
Model.handoffWindowOpen(NOW, NOW) === true, "same instant")
check("the window is still open partway through a slow login",
Model.handoffWindowOpen(NOW - WINDOW / 2, NOW) === true, "half the window")
check("the window is open at the boundary",
Model.handoffWindowOpen(NOW - WINDOW, NOW) === true, "exactly the window")
check("the window is closed past the boundary",
Model.handoffWindowOpen(NOW - WINDOW - 1, NOW) === false, "one ms past")
// A clock stepped backwards is evidence the clock moved, not that the login
// was recent, so it must not reopen the window.
check("a clock stepped backwards closes the window rather than reopening it",
Model.handoffWindowOpen(NOW + 60_000, NOW) === false, "start in the future")
check("a window long enough for a real 2FA login",
WINDOW >= 5 * 60 * 1000, `${WINDOW}ms`)
const expecting = Model.sessionHandoffReadCommand(true)[2]
const discarding = Model.sessionHandoffReadCommand(false)[2]
check("an expected handoff is read out",
expecting.includes("head -c"), expecting)
check("an unexpected handoff is not read out",
!discarding.includes("head -c"), discarding)
// Not reading it is not the same as leaving it there. A live session key in
// the runtime directory is the worse of the two outcomes.
check("an unexpected handoff is still removed",
discarding.includes("rm -f"), discarding)
check("an expected handoff is removed once consumed",
expecting.includes("rm -f"), expecting)
// Both forms, run for real against a planted file.
{
const runtime = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-handoff-"))
try {
const dir = path.join(runtime, "qs-bitwarden-cli")
fs.mkdirSync(dir)
const file = path.join(dir, "session-handoff")
const KEY = "A".repeat(88)
for (const [label, script, wantOut] of [
["expected", expecting, KEY],
["unexpected", discarding, ""],
]) {
fs.writeFileSync(file, KEY)
const out = execFileSync("bash", ["-c", script],
{ encoding: "utf8", env: { ...process.env, XDG_RUNTIME_DIR: runtime } })
check(`an ${label} handoff returns ${wantOut ? "the key" : "nothing"}`,
out.trim() === wantOut, JSON.stringify(out))
check(`an ${label} handoff leaves no file behind`,
!fs.existsSync(file), "file still present")
}
} finally {
fs.rmSync(runtime, { recursive: true, force: true })
}
}
// -------------------------------------------------------------------------
// Both settings reach the settings screen
// -------------------------------------------------------------------------
const keys = Model.groupedSettings().map(e => e.key)
for (const k of ["lockOnScreenLock", "lockOnSuspend"]) {
check(`${k} appears in the settings screen`, keys.includes(k), keys.join(", "))
const entry = Model.groupedSettings().find(e => e.key === k)
check(`${k} is a toggle in the Security group`,
entry.type === "bool" && entry.group === "security", JSON.stringify(entry))
}
// The manifest is what the shell reads defaults from, so the two have to agree.
const manifest = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "manifest.json"), "utf8"))
for (const k of ["lockOnScreenLock", "lockOnSuspend"]) {
check(`${k} has a manifest default`,
manifest.barWidget.defaults[k] === true, JSON.stringify(manifest.barWidget.defaults[k]))
check(`${k} has a manifest schema entry`,
manifest.barWidget.schema.some(e => e.key === k && e.type === "boolean"), k)
}
// -------------------------------------------------------------------------
if (failures.length) {
console.error(`\n${failures.length} failure(s):\n`)
for (const f of failures) console.error(` ${f}\n`)
process.exit(1)
}
console.log(`lock-triggers.test.js: ${pass} checks passed`)