Critical fixes:
- Fix admin_delete_user missing current_user_id argument (TypeError on every delete)
- Fix self-signed cert OOM: bytes(2130706433) → IPv4Address("127.0.0.1")
- Add @login_required to attestation routes (attest, log); verify stays public
- Add auth guards to fieldkit (@admin_required on killswitch) and keys blueprints
- Fix cleanup_temp_files NameError in generate() route
Security hardening:
- Unify temp storage to ~/.soosef/temp/ so killswitch purge covers web uploads
- Replace Path.unlink() with secure deletion (shred fallback) in temp_storage
- Add structured audit log (audit.jsonl) for admin, key, and killswitch actions
New features:
- Dead man's switch background enforcement thread in serve + check-deadman CLI
- Key rotation: soosef keys rotate-identity/rotate-channel with archiving
- Batch attestation: soosef attest batch <dir> with progress and error handling
- Geofence CLI: set/check/clear commands with config persistence
- USB CLI: snapshot/check commands against device whitelist
- Verification receipt download (/verify/receipt JSON endpoint + UI button)
- IdentityInfo.created_at populated from sidecar meta.json (mtime fallback)
Data layer:
- ChainStore.get() now O(1) via byte-offset index built during state rebuild
- Add federation module (chain, models, serialization, entropy)
Includes 45+ new tests across chain, deadman, key rotation, killswitch, and
serialization modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
293 lines
9.7 KiB
Python
293 lines
9.7 KiB
Python
"""
|
|
Tests for dead man's switch background enforcement and CLI command.
|
|
|
|
Covers:
|
|
- _deadman_enforcement_loop: does not call execute_purge when not armed
|
|
- _deadman_enforcement_loop: calls execute_purge when armed and overdue
|
|
- _deadman_enforcement_loop: exits after firing so execute_purge is not called twice
|
|
- _start_deadman_thread: returns a live daemon thread
|
|
- check-deadman CLI: exits 0 when disarmed
|
|
- check-deadman CLI: exits 0 and prints OK when armed but not overdue
|
|
- check-deadman CLI: exits 0 and prints OVERDUE warning when past interval but in grace
|
|
- check-deadman CLI: exits 2 when fully expired (past interval + grace)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
|
|
# ── Fixtures ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture()
|
|
def soosef_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Redirect soosef paths to a tmp directory."""
|
|
import soosef.paths as paths
|
|
|
|
data_dir = tmp_path / ".soosef"
|
|
data_dir.mkdir()
|
|
monkeypatch.setattr(paths, "BASE_DIR", data_dir)
|
|
return data_dir
|
|
|
|
|
|
def _write_deadman_state(
|
|
state_file: Path,
|
|
*,
|
|
armed: bool,
|
|
last_checkin: datetime,
|
|
interval_hours: int = 24,
|
|
grace_hours: int = 2,
|
|
) -> None:
|
|
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
state = {
|
|
"armed": armed,
|
|
"last_checkin": last_checkin.isoformat(),
|
|
"interval_hours": interval_hours,
|
|
"grace_hours": grace_hours,
|
|
}
|
|
state_file.write_text(json.dumps(state))
|
|
|
|
|
|
# ── Unit tests: enforcement loop ─────────────────────────────────────────────
|
|
|
|
|
|
def test_enforcement_loop_no_op_when_disarmed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Loop should not call check() when the switch is not armed."""
|
|
from soosef.cli import _deadman_enforcement_loop
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
|
|
# Redirect the module-level DEADMAN_STATE constant so DeadmanSwitch() default is our tmp file
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
check_calls = []
|
|
|
|
def fake_check(self):
|
|
check_calls.append("fired")
|
|
|
|
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
|
|
|
|
iterations = [0]
|
|
|
|
def one_shot_sleep(n):
|
|
iterations[0] += 1
|
|
if iterations[0] >= 2:
|
|
raise StopIteration("stop test loop")
|
|
|
|
monkeypatch.setattr(time, "sleep", one_shot_sleep)
|
|
|
|
with pytest.raises(StopIteration):
|
|
_deadman_enforcement_loop(interval_seconds=0)
|
|
|
|
assert check_calls == []
|
|
|
|
|
|
def test_enforcement_loop_fires_when_overdue(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Loop must call DeadmanSwitch.check() when armed and past interval + grace."""
|
|
from soosef.cli import _deadman_enforcement_loop
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
last_checkin = datetime.now(UTC) - timedelta(hours=100)
|
|
_write_deadman_state(
|
|
state_file,
|
|
armed=True,
|
|
last_checkin=last_checkin,
|
|
interval_hours=24,
|
|
grace_hours=2,
|
|
)
|
|
|
|
check_calls = []
|
|
|
|
def fake_check(self):
|
|
check_calls.append("fired")
|
|
|
|
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
|
|
monkeypatch.setattr(time, "sleep", lambda n: None)
|
|
|
|
_deadman_enforcement_loop(interval_seconds=0)
|
|
|
|
assert len(check_calls) == 1
|
|
|
|
|
|
def test_enforcement_loop_exits_after_firing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""After firing, the loop must return and not call check() a second time."""
|
|
from soosef.cli import _deadman_enforcement_loop
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
last_checkin = datetime.now(UTC) - timedelta(hours=100)
|
|
_write_deadman_state(state_file, armed=True, last_checkin=last_checkin)
|
|
|
|
check_calls = []
|
|
|
|
def fake_check(self):
|
|
check_calls.append("fired")
|
|
|
|
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
|
|
monkeypatch.setattr(time, "sleep", lambda n: None)
|
|
|
|
_deadman_enforcement_loop(interval_seconds=0)
|
|
|
|
# Called exactly once — loop exited after firing
|
|
assert len(check_calls) == 1
|
|
|
|
|
|
def test_enforcement_loop_tolerates_exceptions(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
|
"""Transient errors in check() must not kill the loop."""
|
|
from soosef.cli import _deadman_enforcement_loop
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
call_count = [0]
|
|
|
|
def counting_sleep(n):
|
|
call_count[0] += 1
|
|
if call_count[0] >= 3:
|
|
raise StopIteration("stop test loop")
|
|
|
|
monkeypatch.setattr(time, "sleep", counting_sleep)
|
|
|
|
error_calls = [0]
|
|
|
|
def flaky_is_armed(self):
|
|
error_calls[0] += 1
|
|
if error_calls[0] == 1:
|
|
raise OSError("state file temporarily unreadable")
|
|
return False # not armed — loop just skips
|
|
|
|
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "is_armed", flaky_is_armed)
|
|
|
|
with pytest.raises(StopIteration):
|
|
_deadman_enforcement_loop(interval_seconds=0)
|
|
|
|
# Should have survived the first exception and continued
|
|
assert call_count[0] >= 2
|
|
|
|
|
|
# ── Unit tests: _start_deadman_thread ────────────────────────────────────────
|
|
|
|
|
|
def test_start_deadman_thread_is_daemon(monkeypatch: pytest.MonkeyPatch):
|
|
"""Thread must be a daemon so it dies with the process."""
|
|
from soosef.cli import _start_deadman_thread
|
|
|
|
# Patch the loop to exit immediately so the thread doesn't hang in tests
|
|
import soosef.cli as cli_mod
|
|
|
|
monkeypatch.setattr(cli_mod, "_deadman_enforcement_loop", lambda interval_seconds: None)
|
|
|
|
t = _start_deadman_thread(interval_seconds=60)
|
|
assert t is not None
|
|
assert t.daemon is True
|
|
assert t.name == "deadman-enforcement"
|
|
t.join(timeout=2)
|
|
|
|
|
|
# ── CLI integration: check-deadman ───────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture()
|
|
def cli_runner():
|
|
return CliRunner()
|
|
|
|
|
|
def test_check_deadman_disarmed(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
|
"""check-deadman exits 0 and prints helpful message when not armed."""
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
from soosef.cli import main
|
|
|
|
# Point at an empty tmp dir so the real ~/.soosef/fieldkit/deadman.json isn't read
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
|
|
assert result.exit_code == 0
|
|
assert "not armed" in result.output
|
|
|
|
|
|
def test_check_deadman_armed_ok(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
|
"""check-deadman exits 0 when armed and check-in is current."""
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
from soosef.cli import main
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
last_checkin = datetime.now(UTC) - timedelta(hours=1)
|
|
_write_deadman_state(
|
|
state_file,
|
|
armed=True,
|
|
last_checkin=last_checkin,
|
|
interval_hours=24,
|
|
grace_hours=2,
|
|
)
|
|
|
|
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
|
|
assert result.exit_code == 0
|
|
assert "OK" in result.output
|
|
|
|
|
|
def test_check_deadman_overdue_in_grace(tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch):
|
|
"""check-deadman exits 0 but prints OVERDUE warning when past interval but in grace."""
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
from soosef.cli import main
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
# Past 24h interval but within 26h total (grace=2)
|
|
last_checkin = datetime.now(UTC) - timedelta(hours=25)
|
|
_write_deadman_state(
|
|
state_file,
|
|
armed=True,
|
|
last_checkin=last_checkin,
|
|
interval_hours=24,
|
|
grace_hours=2,
|
|
)
|
|
|
|
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
|
|
# Not yet fired (grace not expired), so exit code is 0
|
|
assert result.exit_code == 0
|
|
assert "OVERDUE" in result.output
|
|
|
|
|
|
def test_check_deadman_fires_when_expired(
|
|
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
"""check-deadman exits 2 when the switch has fully expired."""
|
|
from soosef.fieldkit import deadman as deadman_mod
|
|
from soosef.cli import main
|
|
|
|
state_file = tmp_path / "deadman.json"
|
|
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
|
|
|
|
last_checkin = datetime.now(UTC) - timedelta(hours=100)
|
|
_write_deadman_state(
|
|
state_file,
|
|
armed=True,
|
|
last_checkin=last_checkin,
|
|
interval_hours=24,
|
|
grace_hours=2,
|
|
)
|
|
|
|
# Patch check() so we don't invoke the real killswitch during tests
|
|
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", lambda self: None)
|
|
|
|
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
|
|
assert result.exit_code == 2
|
|
assert "killswitch triggered" in result.output.lower() or "expired" in result.output.lower()
|