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>
196 lines
7.2 KiB
Python
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
|