fieldwitness/tests/test_tor.py
Aaron D. Lee 16318daea3
Some checks failed
CI / lint (push) Failing after 12s
CI / typecheck (push) Failing after 12s
Add comprehensive test suite: integration tests + Playwright e2e
Integration tests (350 passing):
- test_evidence_summary.py: HTML/PDF generation, XSS safety, anchor rendering
- test_tor.py: Tor module unit tests (mocked, no Tor needed)
- test_c2pa_importer.py: Import result dataclass, trust evaluation, graceful degradation
- test_file_attestation.py: All file types (PNG, PDF, CSV, empty, large), determinism
- test_paths.py: Registry correctness, env var override, all paths under BASE_DIR
- test_killswitch_coverage.py: Tor keys, trusted keys, carrier history destruction

Playwright e2e infrastructure:
- tests/e2e/ with conftest (live server, auth fixtures), helpers (test file generators)
- test_auth.py: Setup flow, login/logout, protected routes
- test_attest.py: Image/PDF/CSV attestation, verify, attestation log
- test_dropbox.py: Token creation, source upload, branding check
- test_keys.py: Identity display, trust store
- test_fieldkit.py: Status dashboard, killswitch page
- test_navigation.py: All nav links, responsive layout

Run: pytest (unit/integration) or pytest -m e2e tests/e2e/ (browser)

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

196 lines
7.2 KiB
Python

"""Unit tests for fieldwitness.fieldkit.tor — all run without Tor installed."""
from __future__ import annotations
import sys
import types
from dataclasses import fields
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _reload_tor_module(monkeypatch: pytest.MonkeyPatch, *, stem_available: bool):
"""Reload the tor module with stem either importable or not.
Because the module checks `import stem` at module scope, we must manipulate
sys.modules before importing (or re-importing) the module.
"""
# Remove the cached module so the import guard re-evaluates.
for key in list(sys.modules.keys()):
if key == "stem" or key.startswith("fieldwitness.fieldkit.tor"):
del sys.modules[key]
if not stem_available:
# Make `import stem` raise ImportError
monkeypatch.setitem(sys.modules, "stem", None) # type: ignore[call-overload]
else:
# Install a minimal stub that satisfies `import stem`
stub = types.ModuleType("stem")
monkeypatch.setitem(sys.modules, "stem", stub)
stub_control = types.ModuleType("stem.control")
stub_control.Controller = MagicMock() # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "stem.control", stub_control)
import fieldwitness.fieldkit.tor as tor_module
return tor_module
# ---------------------------------------------------------------------------
# test_has_tor_returns_false_without_stem
# ---------------------------------------------------------------------------
class TestHasTorWithoutStem:
def test_has_tor_false_when_stem_absent(self, monkeypatch: pytest.MonkeyPatch):
"""has_tor() must return False when stem is not importable."""
# Remove any cached stem import
for key in list(sys.modules.keys()):
if key == "stem" or key.startswith("stem."):
del sys.modules[key]
monkeypatch.setitem(sys.modules, "stem", None) # type: ignore[call-overload]
# Re-import _availability after evicting the cached module
for key in list(sys.modules.keys()):
if key == "fieldwitness._availability":
del sys.modules[key]
from fieldwitness._availability import has_tor
assert has_tor() is False
# ---------------------------------------------------------------------------
# test_start_onion_service_without_stem_raises
# ---------------------------------------------------------------------------
class TestStartOnionServiceWithoutStem:
def test_raises_tor_not_available(self, monkeypatch: pytest.MonkeyPatch):
"""start_onion_service() must raise TorNotAvailableError when stem is absent."""
tor = _reload_tor_module(monkeypatch, stem_available=False)
with pytest.raises(tor.TorNotAvailableError):
tor.start_onion_service(target_port=5000)
def test_error_message_mentions_install(self, monkeypatch: pytest.MonkeyPatch):
"""The error message must guide the operator to install stem."""
tor = _reload_tor_module(monkeypatch, stem_available=False)
with pytest.raises(tor.TorNotAvailableError, match="pip install"):
tor.start_onion_service(target_port=5000)
def test_tor_not_available_is_not_tor_control_error(self, monkeypatch: pytest.MonkeyPatch):
"""TorNotAvailableError and TorControlError must be distinct exception types."""
tor = _reload_tor_module(monkeypatch, stem_available=False)
assert tor.TorNotAvailableError is not tor.TorControlError
# ---------------------------------------------------------------------------
# test_onion_service_info_dataclass
# ---------------------------------------------------------------------------
class TestOnionServiceInfoDataclass:
def test_fields_exist(self):
from fieldwitness.fieldkit.tor import OnionServiceInfo
field_names = {f.name for f in fields(OnionServiceInfo)}
assert "onion_address" in field_names
assert "target_port" in field_names
assert "is_persistent" in field_names
def test_onion_url_property(self):
from fieldwitness.fieldkit.tor import OnionServiceInfo
info = OnionServiceInfo(
onion_address="abc123.onion",
target_port=5000,
is_persistent=True,
)
assert info.onion_url == "http://abc123.onion"
def test_frozen_dataclass_rejects_mutation(self):
from fieldwitness.fieldkit.tor import OnionServiceInfo
info = OnionServiceInfo(
onion_address="abc123.onion",
target_port=5000,
is_persistent=False,
)
with pytest.raises((AttributeError, TypeError)):
info.onion_address = "evil.onion" # type: ignore[misc]
def test_is_persistent_false(self):
from fieldwitness.fieldkit.tor import OnionServiceInfo
info = OnionServiceInfo(
onion_address="xyz.onion",
target_port=8080,
is_persistent=False,
)
assert info.is_persistent is False
def test_target_port_stored(self):
from fieldwitness.fieldkit.tor import OnionServiceInfo
info = OnionServiceInfo(
onion_address="test.onion",
target_port=9999,
is_persistent=True,
)
assert info.target_port == 9999
# ---------------------------------------------------------------------------
# test_persistent_key_storage_path
# ---------------------------------------------------------------------------
class TestPersistentKeyStoragePath:
def test_key_stored_under_tor_hidden_service_dir(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
):
"""The persistent key must be written inside paths.TOR_HIDDEN_SERVICE_DIR."""
import fieldwitness.paths as paths
# Redirect BASE_DIR to a temp location
monkeypatch.setattr(paths, "BASE_DIR", tmp_path / ".fwmetadata")
tor_dir = paths.TOR_HIDDEN_SERVICE_DIR
# Verify the resolved path sits under BASE_DIR / fieldkit / tor / hidden_service
assert str(tor_dir).startswith(str(tmp_path))
assert "fieldkit" in str(tor_dir)
assert "tor" in str(tor_dir)
assert "hidden_service" in str(tor_dir)
def test_tor_dir_is_child_of_base_dir(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
import fieldwitness.paths as paths
monkeypatch.setattr(paths, "BASE_DIR", tmp_path / ".fwmetadata")
assert str(paths.TOR_HIDDEN_SERVICE_DIR).startswith(str(paths.BASE_DIR))
def test_key_filename_in_expected_location(
self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
):
"""The key file used by _start_persistent_service must be 'hs_ed25519_secret_key'."""
import fieldwitness.paths as paths
monkeypatch.setattr(paths, "BASE_DIR", tmp_path / ".fwmetadata")
expected_key_file = paths.TOR_HIDDEN_SERVICE_DIR / "hs_ed25519_secret_key"
# We're verifying the path structure, not the file's existence
assert expected_key_file.name == "hs_ed25519_secret_key"
assert expected_key_file.parent == paths.TOR_HIDDEN_SERVICE_DIR