mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 14:23:16 +02:00
security: harden digital-wellbeing bypass vectors
- Screen locker: disable VT switching (Ctrl+Alt+Fn) via setxkbmap srvrkeys:none on startup; restore on close (production mode only). Gracefully skips if setxkbmap is not installed (shutil.which). Tests: 7 new tests, 100% branch coverage maintained. - Midnight shutdown: restore real schedule values (Mon-Wed 21:00, Thu-Sun 22:00, morning end 05:00); re-enable the three commented-out leniency checks in check_schedule_protection(); self-lock script with chattr +i at end of enable_midnight_shutdown(). - Hosts install: add UNBLOCK_STATE_FILE tracking for whitelisted domains; check_unblock_entries_protection() blocks installation if the unblock list grows; save state after install; self-lock install.sh and generate_hosts_file.sh with chattr +i.
This commit is contained in:
parent
8d7128241a
commit
b96f6801b6
@ -0,0 +1,18 @@
|
||||
{
|
||||
"title": "Harden digital-wellbeing bypass vectors",
|
||||
"objective": "Close three easy bypass vectors in the digital-wellbeing tooling: (1) screen locker bypassed by switching to a TTY with Ctrl+Alt+Fn, (2) midnight shutdown disabled by editing schedule constants or commenting out protection checks, (3) hosts-file unblock list silently expanded by adding sed commands. Success means each vector requires deliberate multi-step effort (manual chattr -i + code edits) rather than a single trivial command.",
|
||||
"acceptance_criteria": [
|
||||
"Screen locker calls setxkbmap -option srvrkeys:none on startup and restores on close in production mode only",
|
||||
"setup_midnight_shutdown.sh constants are 21/22/5 and all three leniency checks are active (uncommented)",
|
||||
"setup_midnight_shutdown.sh self-locks with chattr +i after enable_midnight_shutdown() completes",
|
||||
"install.sh tracks whitelisted domains in /etc/hosts.unblock-entries.state (chattr +i) and blocks install if the list grows",
|
||||
"install.sh and generate_hosts_file.sh self-lock with chattr +i after a successful install",
|
||||
"screen_locker package maintains 100% branch coverage (311 tests pass)"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Preventing root from bypassing chattr +i (requires kernel-level controls)",
|
||||
"Changes to the actual blocked or unblocked domain list",
|
||||
"Hardening against SIGKILL of the screen locker process"
|
||||
],
|
||||
"verifier": "pre-commit run --files <changed-files>; python -m pytest python_pkg/screen_locker/tests/ --cov=python_pkg.screen_locker --cov-branch"
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
{
|
||||
"intent": "Harden three digital-wellbeing tools against easy self-circumvention.",
|
||||
"scope": [
|
||||
"python_pkg/screen_locker/screen_lock.py",
|
||||
"python_pkg/screen_locker/tests/conftest.py",
|
||||
"python_pkg/screen_locker/tests/test_vt_switching.py",
|
||||
"linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh",
|
||||
"linux_configuration/scripts/periodic_background/hosts/install.sh",
|
||||
"Non-goal: changes to /etc/hosts contents or blocked-domain list"
|
||||
],
|
||||
"changes": [
|
||||
"Screen locker: call setxkbmap -option srvrkeys:none on startup to disable Ctrl+Alt+Fn VT switching; restore on close (production mode only); shutil.which used for full path (S607)",
|
||||
"Midnight shutdown: restored real schedule constants (21/22/5); re-enabled three commented-out leniency checks in check_schedule_protection(); added chattr +i self-lock at end of enable_midnight_shutdown()",
|
||||
"Hosts install: added UNBLOCK_STATE_FILE and check_unblock_entries_protection() to block expanding the whitelist; saves state after install; self-locks install.sh and generate_hosts_file.sh with chattr +i",
|
||||
"Tests: 7 new tests covering VT disable/restore in production vs demo mode and graceful handling when setxkbmap is absent; mock_subprocess_run autouse fixture added to conftest"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/screen_locker/screen_lock.py python_pkg/screen_locker/tests/conftest.py python_pkg/screen_locker/tests/test_vt_switching.py",
|
||||
"result": "pass",
|
||||
"evidence": "All hooks passed (ruff, ruff-format, mypy, pylint, bandit, shellcheck, codespell)"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh linux_configuration/scripts/periodic_background/hosts/install.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All hooks passed including shellcheck"
|
||||
},
|
||||
{
|
||||
"command": "python -m pytest python_pkg/screen_locker/tests/ -q --cov=python_pkg.screen_locker --cov-branch --cov-report=term-missing",
|
||||
"result": "pass",
|
||||
"evidence": "311 tests passed, screen_locker package at 100% branch coverage"
|
||||
},
|
||||
{
|
||||
"command": "sudo bash linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Script ran, schedule set to 21/22/5, PC shut down at 02:xx as expected"
|
||||
},
|
||||
{
|
||||
"command": "systemctl --user restart screen-locker.service",
|
||||
"result": "pass",
|
||||
"evidence": "Screen locker service restarted successfully; Ctrl+Alt+Fn blocked during active lock"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"setxkbmap -option srvrkeys:none disables ALL server-handled keys (not just VT switching); restored on clean close but NOT restored if the process is killed with SIGKILL",
|
||||
"chattr +i on install.sh means future maintenance requires manual sudo chattr -i first"
|
||||
],
|
||||
"rollback": [
|
||||
"Screen locker: revert _disable_vt_switching/_restore_vt_switching from screen_lock.py; run setxkbmap -option '' to restore keyboard state manually if needed",
|
||||
"Midnight shutdown: sudo chattr -i setup_midnight_shutdown.sh; restore SCHEDULE_MON_WED_HOUR=24 SCHEDULE_THU_SUN_HOUR=24 SCHEDULE_MORNING_END_HOUR=0 and re-comment the three if-blocks",
|
||||
"Hosts: sudo chattr -i /etc/hosts.unblock-entries.state && sudo rm /etc/hosts.unblock-entries.state; sudo chattr -i install.sh generate_hosts_file.sh"
|
||||
]
|
||||
}
|
||||
@ -13,9 +13,9 @@ source "$SCRIPT_DIR/../../lib/common.sh"
|
||||
|
||||
# Schedule constants (single source of truth for this script)
|
||||
# These values are written to /etc/shutdown-schedule.conf during setup
|
||||
SCHEDULE_MON_WED_HOUR=24
|
||||
SCHEDULE_THU_SUN_HOUR=24
|
||||
SCHEDULE_MORNING_END_HOUR=0
|
||||
SCHEDULE_MON_WED_HOUR=21
|
||||
SCHEDULE_THU_SUN_HOUR=22
|
||||
SCHEDULE_MORNING_END_HOUR=5
|
||||
|
||||
# ============================================================================
|
||||
# SCHEDULE PROTECTION MECHANISM
|
||||
@ -105,19 +105,19 @@ check_schedule_protection() {
|
||||
local violations=()
|
||||
|
||||
# Check if Mon-Wed hour is being made LATER (more lenient)
|
||||
#if [[ $SCHEDULE_MON_WED_HOUR -gt $canonical_mon_wed ]]; then
|
||||
# violations+=("Mon-Wed shutdown: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (later)")
|
||||
#fi
|
||||
#
|
||||
## Check if Thu-Sun hour is being made LATER (more lenient)
|
||||
#if [[ $SCHEDULE_THU_SUN_HOUR -gt $canonical_thu_sun ]]; then
|
||||
# violations+=("Thu-Sun shutdown: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (later)")
|
||||
#fi
|
||||
#
|
||||
## Check if morning end is being made EARLIER (more lenient - shorter shutdown window)
|
||||
#if [[ $SCHEDULE_MORNING_END_HOUR -lt $canonical_morning_end ]]; then
|
||||
# violations+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (earlier)")
|
||||
#fi
|
||||
if [[ $SCHEDULE_MON_WED_HOUR -gt $canonical_mon_wed ]]; then
|
||||
violations+=("Mon-Wed shutdown: ${canonical_mon_wed}:00 → ${SCHEDULE_MON_WED_HOUR}:00 (later)")
|
||||
fi
|
||||
|
||||
# Check if Thu-Sun hour is being made LATER (more lenient)
|
||||
if [[ $SCHEDULE_THU_SUN_HOUR -gt $canonical_thu_sun ]]; then
|
||||
violations+=("Thu-Sun shutdown: ${canonical_thu_sun}:00 → ${SCHEDULE_THU_SUN_HOUR}:00 (later)")
|
||||
fi
|
||||
|
||||
# Check if morning end is being made EARLIER (more lenient - shorter shutdown window)
|
||||
if [[ $SCHEDULE_MORNING_END_HOUR -lt $canonical_morning_end ]]; then
|
||||
violations+=("Morning end: 0${canonical_morning_end}:00 → 0${SCHEDULE_MORNING_END_HOUR}:00 (earlier)")
|
||||
fi
|
||||
|
||||
if [[ ${#violations[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
@ -1427,6 +1427,13 @@ enable_midnight_shutdown() {
|
||||
# Test setup
|
||||
test_setup
|
||||
|
||||
# Lock this setup script so values + checks can't be silently edited
|
||||
local this_script
|
||||
this_script="$(readlink -f "$0")"
|
||||
chattr +i "$this_script" 2>/dev/null \
|
||||
|| echo "⚠ Warning: Could not set immutable attribute on setup script"
|
||||
echo "✓ Setup script locked (chattr +i) — run 'sudo chattr -i $this_script' to unlock for future changes"
|
||||
|
||||
# Show instructions
|
||||
show_instructions
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ done
|
||||
# ============================================================================
|
||||
|
||||
CUSTOM_ENTRIES_STATE_FILE="/etc/hosts.custom-entries.state"
|
||||
UNBLOCK_STATE_FILE="/etc/hosts.unblock-entries.state"
|
||||
|
||||
# Extract custom blocked entries from a hosts file or heredoc section
|
||||
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
||||
@ -164,6 +165,105 @@ if ! check_custom_entries_protection; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# UNBLOCK ENTRIES PROTECTION MECHANISM
|
||||
# ============================================================================
|
||||
# This prevents silently expanding the whitelist (i.e. adding MORE domains to
|
||||
# the sed unblock list) by tracking which domains are whitelisted. Adding a
|
||||
# new domain here requires manually clearing the state file first.
|
||||
# ============================================================================
|
||||
#
|
||||
# PROTECTED_UNBLOCK_LIST_START
|
||||
# 4chan.com
|
||||
# www.4chan.com
|
||||
# 4chan.org
|
||||
# boards.4chan.org
|
||||
# sys.4chan.org
|
||||
# www.4chan.org
|
||||
# www.facebook.com
|
||||
# messenger.com
|
||||
# delio.com.pl
|
||||
# loverslab.com
|
||||
# linkedin.com
|
||||
# licdn.com
|
||||
# PROTECTED_UNBLOCK_LIST_END
|
||||
|
||||
# Extract whitelisted domains from the protected list embedded in this script
|
||||
extract_unblock_entries_from_script() {
|
||||
local script_path="$1"
|
||||
sed -n '/^# PROTECTED_UNBLOCK_LIST_START$/,/^# PROTECTED_UNBLOCK_LIST_END$/p' "$script_path" |
|
||||
grep -E '^# [a-zA-Z0-9._-]+$' |
|
||||
sed 's/^# //' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Save current unblock entries to immutable state file
|
||||
save_unblock_entries_state() {
|
||||
local entries="$1"
|
||||
chattr -i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
echo "$entries" | sort -u >"$UNBLOCK_STATE_FILE"
|
||||
chmod 644 "$UNBLOCK_STATE_FILE"
|
||||
chattr +i "$UNBLOCK_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Block installation if the unblock list has grown (more sites being whitelisted)
|
||||
check_unblock_entries_protection() {
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
|
||||
local new_entries
|
||||
new_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
if [[ ! -f $UNBLOCK_STATE_FILE ]]; then
|
||||
echo "ℹ️ First unblock-list run — no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local saved_entries
|
||||
saved_entries=$(sort -u "$UNBLOCK_STATE_FILE")
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# Entries added since last install
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Unblock Entries Protection Check:"
|
||||
echo " Previously whitelisted: $saved_count domains"
|
||||
echo " Currently in script: $new_count domains"
|
||||
echo " Newly added: $added_count"
|
||||
|
||||
if [[ $added_count -eq 0 ]]; then
|
||||
echo " ✅ No new unblocks — protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED — NEW UNBLOCK ENTRIES DETECTED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to WHITELIST these additional domains:"
|
||||
while IFS= read -r entry; do
|
||||
echo " + $entry"
|
||||
done <<<"$added_entries"
|
||||
echo ""
|
||||
echo "To proceed, manually delete the state file first:"
|
||||
echo " sudo chattr -i $UNBLOCK_STATE_FILE && sudo rm $UNBLOCK_STATE_FILE"
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run the unblock protection check
|
||||
if ! check_unblock_entries_protection; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enable systemd-resolved
|
||||
sudo systemctl enable systemd-resolved
|
||||
|
||||
@ -310,11 +410,14 @@ sudo sed -i 's/^0\.0\.0\.0 sys\.4chan\.org/#0.0.0.0 sys.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.4chan\.org/#0.0.0.0 www.4chan.org/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 www\.facebook\.com/#0.0.0.0 www.facebook.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 messenger\.com/#0.0.0.0 messenger.com/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 delio\.com.pl/#0.0.0.0 delio.com.pl/' /etc/hosts
|
||||
sudo sed -i 's/^0\.0\.0\.0 loverslab\.com/#0.0.0.0 loverslab.com/' /etc/hosts
|
||||
|
||||
# Allow LinkedIn and all subdomains (linkedin.com + licdn.com CDN)
|
||||
echo "Allowing LinkedIn by commenting out any blocking entries..."
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?linkedin\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?licdn\.com)/#\1/' /etc/hosts
|
||||
sudo sed -i -E 's/^(0\.0\.0\.0[[:space:]]+[a-zA-Z0-9._-]*\.?loverslab\.com)/#\1/' /etc/hosts
|
||||
|
||||
# Add custom entries for YouTube and Discord
|
||||
echo "Adding custom entries for YouTube and Discord..."
|
||||
@ -670,6 +773,11 @@ chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
save_custom_entries_state "$current_custom_entries"
|
||||
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
||||
|
||||
# Save unblock entries state for future protection checks
|
||||
current_unblock_entries=$(extract_unblock_entries_from_script "$script_path")
|
||||
save_unblock_entries_state "$current_unblock_entries"
|
||||
echo "✅ Unblock entries state saved to $UNBLOCK_STATE_FILE"
|
||||
|
||||
# Optionally flush DNS caches
|
||||
if [[ $FLUSH_DNS -eq 1 ]]; then
|
||||
echo "Flushing DNS caches..."
|
||||
@ -774,3 +882,19 @@ if [[ $BROWSERS_KILLED -eq 1 ]]; then
|
||||
else
|
||||
echo " No browsers were running."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# LOCK THIS SCRIPT AND generate_hosts_file.sh AGAINST SILENT EDITS
|
||||
# ============================================================================
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
INSTALL_SCRIPT="$(readlink -f "$0")"
|
||||
chattr +i "$INSTALL_SCRIPT" 2>/dev/null \
|
||||
|| echo "⚠ Warning: Could not set immutable attribute on install.sh"
|
||||
echo "✓ install.sh locked (chattr +i) — run 'sudo chattr -i $INSTALL_SCRIPT' to unlock for future changes"
|
||||
|
||||
GEN_SCRIPT="$SCRIPT_DIR/generate_hosts_file.sh"
|
||||
if [[ -f $GEN_SCRIPT ]]; then
|
||||
chattr +i "$GEN_SCRIPT" 2>/dev/null \
|
||||
|| echo "⚠ Warning: Could not set immutable attribute on generate_hosts_file.sh"
|
||||
echo "✓ generate_hosts_file.sh locked (chattr +i)"
|
||||
fi
|
||||
|
||||
@ -11,6 +11,8 @@ from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
@ -118,6 +120,25 @@ class ScreenLocker(
|
||||
self._start_phone_check()
|
||||
self._grab_input()
|
||||
|
||||
def _disable_vt_switching(self) -> None:
|
||||
"""Disable VT switching in X11 while the lock is active.
|
||||
|
||||
Prevents bypassing the lock by switching to a TTY with Ctrl+Alt+Fn.
|
||||
Best-effort: silently ignored if setxkbmap is unavailable.
|
||||
"""
|
||||
setxkbmap = shutil.which("setxkbmap")
|
||||
if setxkbmap is None:
|
||||
_logger.warning("setxkbmap not found; VT switching will not be disabled")
|
||||
return
|
||||
subprocess.run([setxkbmap, "-option", "srvrkeys:none"], check=False)
|
||||
|
||||
def _restore_vt_switching(self) -> None:
|
||||
"""Restore VT switching after the lock is dismissed."""
|
||||
setxkbmap = shutil.which("setxkbmap")
|
||||
if setxkbmap is None:
|
||||
return
|
||||
subprocess.run([setxkbmap, "-option", ""], check=False)
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
"""Configure the window for fullscreen lock."""
|
||||
screen_w = self.root.winfo_screenwidth()
|
||||
@ -127,6 +148,8 @@ class ScreenLocker(
|
||||
self.root.attributes(fullscreen=True)
|
||||
self.root.attributes(topmost=True)
|
||||
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
||||
if not self.demo_mode:
|
||||
self._disable_vt_switching()
|
||||
|
||||
def _setup_verify_window(self) -> None:
|
||||
"""Configure window for post-sick-day workout verification."""
|
||||
@ -483,6 +506,8 @@ class ScreenLocker(
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the application and exit."""
|
||||
if not self.demo_mode:
|
||||
self._restore_vt_switching()
|
||||
self.root.destroy()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@ -58,6 +58,26 @@ def _block_real_tk_and_exit() -> Iterator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_subprocess_run() -> Generator[MagicMock]:
|
||||
"""Block real subprocess calls (e.g. setxkbmap) for every test.
|
||||
|
||||
Also exposed as a named fixture so individual tests can assert
|
||||
on the calls made (e.g. VT switching tests).
|
||||
|
||||
``shutil.which`` is mocked to return a stable fake path so tests work
|
||||
regardless of whether setxkbmap is installed on the host machine.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value="/usr/bin/setxkbmap",
|
||||
),
|
||||
patch("python_pkg.screen_locker.screen_lock.subprocess.run") as mock,
|
||||
):
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
|
||||
"""Redirect SICK_HISTORY_FILE to tmp_path so tests cannot touch real state."""
|
||||
|
||||
136
python_pkg/screen_locker/tests/test_vt_switching.py
Normal file
136
python_pkg/screen_locker/tests/test_vt_switching.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""Tests for VT switching disable/restore during screen lock."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
_SETXKBMAP = "/usr/bin/setxkbmap"
|
||||
|
||||
|
||||
class TestVTSwitching:
|
||||
"""Tests for VT switching disable/restore behaviour."""
|
||||
|
||||
def test_vt_switching_disabled_in_production_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap srvrkeys:none is called when locker starts in production."""
|
||||
create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
[_SETXKBMAP, "-option", "srvrkeys:none"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_vt_switching_not_disabled_in_demo_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap is NOT called in demo mode."""
|
||||
create_locker(mock_tk, tmp_path, demo_mode=True)
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_vt_switching_restored_on_close_in_production(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap -option '' is called when close() runs in production."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
[_SETXKBMAP, "-option", ""],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_vt_switching_not_restored_in_demo_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""close() does NOT call setxkbmap in demo mode."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=True)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_disable_then_restore_are_complementary(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Full lifecycle: disable on init, restore on close in production."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
assert mock_subprocess_run.call_count == 1
|
||||
assert mock_subprocess_run.call_args_list[0] == call(
|
||||
[_SETXKBMAP, "-option", "srvrkeys:none"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
locker.close()
|
||||
|
||||
assert mock_subprocess_run.call_count == 2
|
||||
assert mock_subprocess_run.call_args_list[1] == call(
|
||||
[_SETXKBMAP, "-option", ""],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_disable_graceful_when_setxkbmap_missing(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No crash and no subprocess call when setxkbmap is not installed."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_restore_graceful_when_setxkbmap_missing(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No crash and no subprocess call on close when setxkbmap is not installed."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
Loading…
Reference in New Issue
Block a user