fieldwitness/tests/test_deadman_enforcement.py
Aaron D. Lee 490f9d4a1d Rebrand SooSeF to FieldWitness
Complete project rebrand for better positioning in the press freedom
and digital security space. FieldWitness communicates both field
deployment and evidence testimony — appropriate for the target audience
of journalists, NGOs, and human rights organizations.

Rename mapping:
- soosef → fieldwitness (package, CLI, all imports)
- soosef.stegasoo → fieldwitness.stego
- soosef.verisoo → fieldwitness.attest
- ~/.soosef/ → ~/.fwmetadata/ (innocuous data dir name)
- SOOSEF_DATA_DIR → FIELDWITNESS_DATA_DIR
- SoosefConfig → FieldWitnessConfig
- SoosefError → FieldWitnessError

Also includes:
- License switch from MIT to GPL-3.0
- C2PA bridge module (Phase 0-2 MVP): cert.py, export.py, vendor_assertions.py
- README repositioned to lead with provenance/federation, stego backgrounded
- Threat model skeleton at docs/security/threat-model.md
- Planning docs: docs/planning/c2pa-integration.md, docs/planning/gtm-feasibility.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:05:13 -04:00

297 lines
10 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 fieldwitness_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect fieldwitness paths to a tmp directory."""
import fieldwitness.paths as paths
data_dir = tmp_path / ".fieldwitness"
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 fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.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, "_paths", type("P", (), {"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 fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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 fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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 fieldwitness.cli import _deadman_enforcement_loop
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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."""
# Patch the loop to exit immediately so the thread doesn't hang in tests
import fieldwitness.cli as cli_mod
from fieldwitness.cli import _start_deadman_thread
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 fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
# Point at an empty tmp dir so the real ~/.fieldwitness/fieldkit/deadman.json isn't read
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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 fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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 fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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 fieldwitness.cli import main
from fieldwitness.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "_paths", type("P", (), {"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()