627 lines
24 KiB
Python
627 lines
24 KiB
Python
"""Unit-Tests für SecretEncryptionManager (Task 4.6).
|
||
|
||
Testet die folgenden Aspekte, die NICHT bereits in test_encryption.py abgedeckt sind:
|
||
1. encrypt_file() – Erfolg, Autorisierung, fehlende Datei
|
||
2. decrypt_file() – Erfolg, Autorisierung, fehlende Datei, Kontextauflösung
|
||
3. is_authorized() – autorisierte und nicht-autorisierte Kontexte
|
||
4. Fehlermeldungen enthalten KEINEN Dateiinhalt (keine Offenlegung)
|
||
5. setup_gitcrypt_filters() – .gitattributes-Generierung pro Kontext
|
||
6. Edge Cases: shared-Kontext, Dateien außerhalb von Kontextordnern
|
||
|
||
_Requirements: 9.1, 9.3, 9.4, 9.8, 9.10, 9.11_
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
|
||
from monorepo.encryption import (
|
||
DecryptionResult,
|
||
EncryptionResult,
|
||
GitCryptNotAvailableError,
|
||
SecretEncryptionManager,
|
||
)
|
||
from monorepo.models import MachineContext
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Fixtures
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
def tmp_monorepo(tmp_path: Path) -> Path:
|
||
"""Erstellt eine minimale Monorepo-Struktur."""
|
||
for ctx in ("privat", "dhive", "bahn", "shared"):
|
||
(tmp_path / ctx).mkdir()
|
||
return tmp_path
|
||
|
||
|
||
@pytest.fixture
|
||
def full_context() -> MachineContext:
|
||
"""MachineContext mit Zugriff auf alle Kontexte."""
|
||
return MachineContext(
|
||
name="andre-hauptrechner",
|
||
description="Voller Zugriff",
|
||
authorized_contexts=["privat", "dhive", "bahn"],
|
||
key_source="keyring",
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def dhive_only_context() -> MachineContext:
|
||
"""MachineContext nur für dhive autorisiert."""
|
||
return MachineContext(
|
||
name="dhive-laptop",
|
||
description="Nur dhive",
|
||
authorized_contexts=["dhive"],
|
||
key_source="keyring",
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def no_context() -> MachineContext:
|
||
"""MachineContext ohne jede Autorisierung."""
|
||
return MachineContext(
|
||
name="fremder-rechner",
|
||
description="Kein Zugriff",
|
||
authorized_contexts=[],
|
||
key_source="keyring",
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def manager(tmp_monorepo: Path, full_context: MachineContext) -> SecretEncryptionManager:
|
||
"""SecretEncryptionManager mit vollem Zugriff."""
|
||
return SecretEncryptionManager(tmp_monorepo, full_context)
|
||
|
||
|
||
@pytest.fixture
|
||
def limited_manager(
|
||
tmp_monorepo: Path, dhive_only_context: MachineContext
|
||
) -> SecretEncryptionManager:
|
||
"""SecretEncryptionManager nur für dhive."""
|
||
return SecretEncryptionManager(tmp_monorepo, dhive_only_context)
|
||
|
||
|
||
@pytest.fixture
|
||
def unauthorized_manager(
|
||
tmp_monorepo: Path, no_context: MachineContext
|
||
) -> SecretEncryptionManager:
|
||
"""SecretEncryptionManager ohne Autorisierung."""
|
||
return SecretEncryptionManager(tmp_monorepo, no_context)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: encrypt_file()
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestEncryptFile:
|
||
"""Tests für SecretEncryptionManager.encrypt_file()."""
|
||
|
||
def test_encrypt_success_with_authorized_context(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselung gelingt bei autorisiertem Kontext und existierender Datei."""
|
||
secret_file = tmp_monorepo / "privat" / ".env"
|
||
secret_file.write_text("SECRET_KEY=super-geheim-123")
|
||
|
||
# Mock git-crypt Aufruf, da git-crypt nicht installiert ist
|
||
with patch.object(manager, "_run_gitcrypt") as mock_gc:
|
||
mock_gc.return_value = None
|
||
result = manager.encrypt_file(secret_file, "privat")
|
||
|
||
assert isinstance(result, EncryptionResult)
|
||
assert result.success is True
|
||
assert result.file_path == secret_file
|
||
assert result.context == "privat"
|
||
assert result.error is None
|
||
|
||
def test_encrypt_fails_with_unauthorized_context(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselung schlägt fehl bei nicht-autorisiertem Kontext."""
|
||
secret_file = tmp_monorepo / "privat" / ".env"
|
||
secret_file.write_text("SECRET=value")
|
||
|
||
result = limited_manager.encrypt_file(secret_file, "privat")
|
||
|
||
assert result.success is False
|
||
assert "nicht" in result.error.lower() or "autorisiert" in result.error.lower()
|
||
|
||
def test_encrypt_fails_when_file_does_not_exist(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselung schlägt fehl wenn die Datei nicht existiert."""
|
||
nonexistent = tmp_monorepo / "privat" / "nonexistent.env"
|
||
|
||
result = manager.encrypt_file(nonexistent, "privat")
|
||
|
||
assert result.success is False
|
||
assert "existiert nicht" in result.error
|
||
|
||
def test_encrypt_fails_when_gitcrypt_not_available(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselung schlägt fehl wenn git-crypt nicht installiert ist."""
|
||
secret_file = tmp_monorepo / "dhive" / "credentials.pem"
|
||
secret_file.write_text("-----BEGIN PRIVATE KEY-----\n...")
|
||
|
||
with patch.object(manager, "_run_gitcrypt") as mock_gc:
|
||
mock_gc.side_effect = GitCryptNotAvailableError(
|
||
"git-crypt ist nicht installiert"
|
||
)
|
||
result = manager.encrypt_file(secret_file, "dhive")
|
||
|
||
assert result.success is False
|
||
assert "git-crypt" in result.error.lower()
|
||
|
||
def test_encrypt_for_bahn_context(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselung funktioniert auch für Bahn-Kontext."""
|
||
bahn_secret = tmp_monorepo / "bahn" / "api-token.key"
|
||
bahn_secret.write_text("token-abc-123")
|
||
|
||
with patch.object(manager, "_run_gitcrypt"):
|
||
result = manager.encrypt_file(bahn_secret, "bahn")
|
||
|
||
assert result.success is True
|
||
assert result.context == "bahn"
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: decrypt_file()
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestDecryptFile:
|
||
"""Tests für SecretEncryptionManager.decrypt_file()."""
|
||
|
||
def test_decrypt_success_with_authorized_context(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung gelingt bei autorisiertem Kontext."""
|
||
secret_file = tmp_monorepo / "privat" / ".env"
|
||
secret_content = b"DB_PASSWORD=geheim123"
|
||
secret_file.write_bytes(secret_content)
|
||
|
||
with patch.object(manager, "_run_gitcrypt"):
|
||
result = manager.decrypt_file(secret_file)
|
||
|
||
assert isinstance(result, DecryptionResult)
|
||
assert result.success is True
|
||
assert result.content == secret_content
|
||
|
||
def test_decrypt_fails_with_unauthorized_context(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung schlägt fehl bei nicht-autorisiertem Kontext."""
|
||
privat_secret = tmp_monorepo / "privat" / ".env"
|
||
privat_secret.write_text("PRIVATE_SECRET=xyz")
|
||
|
||
result = limited_manager.decrypt_file(privat_secret)
|
||
|
||
assert result.success is False
|
||
assert result.content is None
|
||
assert "verweigert" in result.error.lower() or "autorisiert" in result.error.lower()
|
||
|
||
def test_decrypt_succeeds_for_own_context(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Eingeschränkter Manager kann eigenen Kontext entschlüsseln."""
|
||
dhive_secret = tmp_monorepo / "dhive" / ".env"
|
||
dhive_content = b"DHIVE_API_KEY=abc"
|
||
dhive_secret.write_bytes(dhive_content)
|
||
|
||
with patch.object(limited_manager, "_run_gitcrypt"):
|
||
result = limited_manager.decrypt_file(dhive_secret)
|
||
|
||
assert result.success is True
|
||
assert result.content == dhive_content
|
||
|
||
def test_decrypt_fails_when_file_does_not_exist(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung schlägt fehl wenn die Datei nicht existiert."""
|
||
nonexistent = tmp_monorepo / "dhive" / "missing.key"
|
||
|
||
result = manager.decrypt_file(nonexistent)
|
||
|
||
assert result.success is False
|
||
assert "existiert nicht" in result.error
|
||
|
||
def test_decrypt_resolves_context_from_path(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung ermittelt den Kontext automatisch aus dem Dateipfad."""
|
||
bahn_secret = tmp_monorepo / "bahn" / "project-x" / "secret.key"
|
||
bahn_secret.parent.mkdir(parents=True, exist_ok=True)
|
||
bahn_secret.write_bytes(b"key-data")
|
||
|
||
with patch.object(manager, "_run_gitcrypt"):
|
||
result = manager.decrypt_file(bahn_secret)
|
||
|
||
assert result.success is True
|
||
assert result.content == b"key-data"
|
||
|
||
def test_decrypt_fails_for_file_outside_context_dirs(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung schlägt fehl für Dateien außerhalb der Kontextordner."""
|
||
outside_file = tmp_monorepo / "unknown-dir" / "secrets.env"
|
||
outside_file.parent.mkdir(parents=True, exist_ok=True)
|
||
outside_file.write_text("SHOULD_NOT_DECRYPT=true")
|
||
|
||
result = manager.decrypt_file(outside_file)
|
||
|
||
assert result.success is False
|
||
assert "kontext" in result.error.lower()
|
||
|
||
def test_decrypt_fails_for_root_level_file(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselung schlägt fehl für Datei direkt im Root (kein Kontext)."""
|
||
root_file = tmp_monorepo / ".env"
|
||
root_file.write_text("ROOT_SECRET=hidden")
|
||
|
||
result = manager.decrypt_file(root_file)
|
||
|
||
# .env im Root hat keinen gültigen Kontext als erstes Pfadsegment
|
||
assert result.success is False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: is_authorized()
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestIsAuthorized:
|
||
"""Tests für SecretEncryptionManager.is_authorized()."""
|
||
|
||
def test_authorized_for_all_contexts_full_access(
|
||
self, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Full-Access-Manager ist für alle drei Kontexte autorisiert."""
|
||
assert manager.is_authorized("privat") is True
|
||
assert manager.is_authorized("dhive") is True
|
||
assert manager.is_authorized("bahn") is True
|
||
|
||
def test_authorized_only_for_own_context(
|
||
self, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Eingeschränkter Manager ist nur für seinen Kontext autorisiert."""
|
||
assert limited_manager.is_authorized("dhive") is True
|
||
assert limited_manager.is_authorized("privat") is False
|
||
assert limited_manager.is_authorized("bahn") is False
|
||
|
||
def test_not_authorized_for_any_context(
|
||
self, unauthorized_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Manager ohne Autorisierung ist für keinen Kontext autorisiert."""
|
||
assert unauthorized_manager.is_authorized("privat") is False
|
||
assert unauthorized_manager.is_authorized("dhive") is False
|
||
assert unauthorized_manager.is_authorized("bahn") is False
|
||
|
||
def test_not_authorized_for_nonexistent_context(
|
||
self, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Nicht existierender Kontext ist nie autorisiert."""
|
||
assert manager.is_authorized("nonexistent") is False
|
||
assert manager.is_authorized("") is False
|
||
|
||
def test_shared_context_not_in_authorized_list(
|
||
self, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""'shared' ist per Design nicht in der Autorisierungsliste."""
|
||
# shared wird über andere Mechanismen gesteuert
|
||
assert manager.is_authorized("shared") is False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: Fehlermeldungen offenlegen KEINEN Dateiinhalt (Requirement 9.8)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestErrorMessagesSecurity:
|
||
"""Fehlermeldungen dürfen KEINEN Dateiinhalt offenlegen."""
|
||
|
||
def test_encrypt_error_does_not_reveal_content(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Verschlüsselungs-Fehlermeldung enthält keinen Dateiinhalt."""
|
||
secret_file = tmp_monorepo / "privat" / ".env"
|
||
secret_content = "SUPER_SECRET_API_KEY=sk-12345abcde"
|
||
secret_file.write_text(secret_content)
|
||
|
||
result = limited_manager.encrypt_file(secret_file, "privat")
|
||
|
||
assert result.success is False
|
||
# Fehlermeldung darf KEINEN Teil des Dateiinhalts enthalten
|
||
assert "SUPER_SECRET_API_KEY" not in (result.error or "")
|
||
assert "sk-12345abcde" not in (result.error or "")
|
||
assert secret_content not in (result.error or "")
|
||
|
||
def test_decrypt_error_does_not_reveal_content(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Entschlüsselungs-Fehlermeldung enthält keinen Dateiinhalt."""
|
||
secret_file = tmp_monorepo / "privat" / "credentials.key"
|
||
secret_content = "-----BEGIN RSA PRIVATE KEY-----\nMIIE...very-secret"
|
||
secret_file.write_text(secret_content)
|
||
|
||
result = limited_manager.decrypt_file(secret_file)
|
||
|
||
assert result.success is False
|
||
assert result.content is None
|
||
# Fehlermeldung darf KEINEN Teil des Dateiinhalts enthalten
|
||
assert "BEGIN RSA PRIVATE KEY" not in (result.error or "")
|
||
assert "MIIE" not in (result.error or "")
|
||
assert "very-secret" not in (result.error or "")
|
||
|
||
def test_decrypt_error_for_unauthorized_binary_file(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Fehlermeldung für verschlüsselte Binärdatei enthält keine Bytes."""
|
||
binary_file = tmp_monorepo / "bahn" / "cert.pem"
|
||
binary_content = b"\x00\x01\x02\x03PRIVATE_DATA\xff\xfe"
|
||
binary_file.write_bytes(binary_content)
|
||
|
||
result = limited_manager.decrypt_file(binary_file)
|
||
|
||
assert result.success is False
|
||
assert result.content is None
|
||
# Keine binären Daten oder Schlüsselwörter in der Fehlermeldung
|
||
assert "PRIVATE_DATA" not in (result.error or "")
|
||
|
||
def test_encrypt_error_has_useful_context_info(
|
||
self, tmp_monorepo: Path, limited_manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Fehlermeldung enthält nützliche Kontextinformationen (ohne Secrets)."""
|
||
secret_file = tmp_monorepo / "privat" / "token.secret"
|
||
secret_file.write_text("geheim-token-value")
|
||
|
||
result = limited_manager.encrypt_file(secret_file, "privat")
|
||
|
||
assert result.success is False
|
||
# Fehlermeldung sollte den Maschinennamen oder Kontext benennen
|
||
assert "dhive-laptop" in result.error or "privat" in result.error
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: setup_gitcrypt_filters() – .gitattributes-Generierung
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestSetupGitcryptFilters:
|
||
"""Tests für SecretEncryptionManager.setup_gitcrypt_filters()."""
|
||
|
||
def test_generates_gitattributes_for_privat(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Erzeugt .gitattributes im privat-Kontext mit korrekten Filtern."""
|
||
manager.setup_gitcrypt_filters("privat")
|
||
|
||
gitattr_path = tmp_monorepo / "privat" / ".gitattributes"
|
||
assert gitattr_path.exists()
|
||
|
||
content = gitattr_path.read_text(encoding="utf-8")
|
||
assert "filter=git-crypt-privat" in content
|
||
assert "diff=git-crypt-privat" in content
|
||
|
||
def test_generates_gitattributes_for_dhive(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Erzeugt .gitattributes im dhive-Kontext."""
|
||
manager.setup_gitcrypt_filters("dhive")
|
||
|
||
gitattr_path = tmp_monorepo / "dhive" / ".gitattributes"
|
||
assert gitattr_path.exists()
|
||
|
||
content = gitattr_path.read_text(encoding="utf-8")
|
||
assert "filter=git-crypt-dhive" in content
|
||
assert "diff=git-crypt-dhive" in content
|
||
|
||
def test_generates_gitattributes_for_bahn(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Erzeugt .gitattributes im bahn-Kontext."""
|
||
manager.setup_gitcrypt_filters("bahn")
|
||
|
||
gitattr_path = tmp_monorepo / "bahn" / ".gitattributes"
|
||
assert gitattr_path.exists()
|
||
|
||
content = gitattr_path.read_text(encoding="utf-8")
|
||
assert "filter=git-crypt-bahn" in content
|
||
assert "diff=git-crypt-bahn" in content
|
||
|
||
def test_contains_all_secret_patterns(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Die generierten .gitattributes enthalten alle Secret-Patterns."""
|
||
manager.setup_gitcrypt_filters("privat")
|
||
|
||
content = (tmp_monorepo / "privat" / ".gitattributes").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
# Alle SECRET_PATTERNS müssen als gitattributes-Patterns vorhanden sein
|
||
assert ".env" in content
|
||
assert "*.pem" in content
|
||
assert "*.key" in content
|
||
assert "*token*" in content
|
||
assert "*secret*" in content
|
||
|
||
def test_filter_names_are_context_specific(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Jeder Kontext hat seinen eigenen Filter-Namen."""
|
||
manager.setup_gitcrypt_filters("privat")
|
||
manager.setup_gitcrypt_filters("dhive")
|
||
manager.setup_gitcrypt_filters("bahn")
|
||
|
||
privat_content = (tmp_monorepo / "privat" / ".gitattributes").read_text()
|
||
dhive_content = (tmp_monorepo / "dhive" / ".gitattributes").read_text()
|
||
bahn_content = (tmp_monorepo / "bahn" / ".gitattributes").read_text()
|
||
|
||
# Jeder Kontext referenziert nur seinen eigenen Filter
|
||
assert "git-crypt-privat" in privat_content
|
||
assert "git-crypt-dhive" not in privat_content
|
||
|
||
assert "git-crypt-dhive" in dhive_content
|
||
assert "git-crypt-privat" not in dhive_content
|
||
|
||
assert "git-crypt-bahn" in bahn_content
|
||
assert "git-crypt-privat" not in bahn_content
|
||
|
||
def test_creates_context_directory_if_missing(
|
||
self, tmp_path: Path, full_context: MachineContext
|
||
) -> None:
|
||
"""Erstellt den Kontextordner falls er nicht existiert."""
|
||
mgr = SecretEncryptionManager(tmp_path, full_context)
|
||
mgr.setup_gitcrypt_filters("privat")
|
||
|
||
assert (tmp_path / "privat" / ".gitattributes").exists()
|
||
|
||
def test_overwrites_existing_gitattributes(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Überschreibt bestehende .gitattributes-Datei."""
|
||
gitattr_path = tmp_monorepo / "privat" / ".gitattributes"
|
||
gitattr_path.write_text("# alte Konfiguration\n*.old filter=old")
|
||
|
||
manager.setup_gitcrypt_filters("privat")
|
||
|
||
content = gitattr_path.read_text(encoding="utf-8")
|
||
assert "alte Konfiguration" not in content
|
||
assert "filter=git-crypt-privat" in content
|
||
|
||
def test_gitattributes_has_header_comment(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Die .gitattributes-Datei enthält einen informativen Header."""
|
||
manager.setup_gitcrypt_filters("bahn")
|
||
|
||
content = (tmp_monorepo / "bahn" / ".gitattributes").read_text(encoding="utf-8")
|
||
assert "# git-crypt Filter" in content
|
||
assert "bahn" in content
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Tests: Edge Cases – shared-Kontext und Dateien außerhalb
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestEdgeCases:
|
||
"""Edge Cases für die Verschlüsselungs-Operationen."""
|
||
|
||
def test_shared_context_file_decrypt_unauthorized(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Datei im shared-Kontext: 'shared' ist nicht in authorized_contexts."""
|
||
shared_file = tmp_monorepo / "shared" / "config" / "secrets.env"
|
||
shared_file.parent.mkdir(parents=True, exist_ok=True)
|
||
shared_file.write_text("SHARED_SECRET=123")
|
||
|
||
# 'shared' ist nicht in der authorized_contexts-Liste des full_context
|
||
result = manager.decrypt_file(shared_file)
|
||
|
||
# Da 'shared' nicht in authorized_contexts ist, schlägt es fehl
|
||
assert result.success is False
|
||
|
||
def test_deeply_nested_file_resolves_context(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Tief verschachtelte Datei wird korrekt dem Kontext zugeordnet."""
|
||
deep_file = tmp_monorepo / "dhive" / "proj" / "module" / "sub" / ".env"
|
||
deep_file.parent.mkdir(parents=True, exist_ok=True)
|
||
deep_file.write_bytes(b"DEEP_SECRET=value")
|
||
|
||
with patch.object(manager, "_run_gitcrypt"):
|
||
result = manager.decrypt_file(deep_file)
|
||
|
||
assert result.success is True
|
||
assert result.content == b"DEEP_SECRET=value"
|
||
|
||
def test_encrypt_file_with_empty_content(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Leere Datei kann verschlüsselt werden."""
|
||
empty_file = tmp_monorepo / "privat" / "empty.key"
|
||
empty_file.write_text("")
|
||
|
||
with patch.object(manager, "_run_gitcrypt"):
|
||
result = manager.encrypt_file(empty_file, "privat")
|
||
|
||
assert result.success is True
|
||
|
||
def test_encrypt_file_path_outside_monorepo(
|
||
self, tmp_path: Path, full_context: MachineContext
|
||
) -> None:
|
||
"""Datei komplett außerhalb des Monorepo-Roots."""
|
||
# Erstelle einen separaten Pfad als "Monorepo-Root"
|
||
mono_root = tmp_path / "monorepo"
|
||
mono_root.mkdir()
|
||
(mono_root / "privat").mkdir()
|
||
|
||
mgr = SecretEncryptionManager(mono_root, full_context)
|
||
|
||
# Datei außerhalb
|
||
outside_file = tmp_path / "outside" / "secret.env"
|
||
outside_file.parent.mkdir(parents=True, exist_ok=True)
|
||
outside_file.write_text("OUTSIDE=true")
|
||
|
||
# encrypt_file prüft nur Existenz und Autorisierung, nicht den Pfad
|
||
# aber es sollte nicht abstürzen
|
||
with patch.object(mgr, "_run_gitcrypt"):
|
||
result = mgr.encrypt_file(outside_file, "privat")
|
||
|
||
assert result.success is True # encrypt prüft Pfad nicht, nur Kontext-Param
|
||
|
||
def test_decrypt_file_path_outside_monorepo(
|
||
self, tmp_path: Path, full_context: MachineContext
|
||
) -> None:
|
||
"""Datei außerhalb des Monorepo-Roots kann nicht entschlüsselt werden."""
|
||
mono_root = tmp_path / "monorepo"
|
||
mono_root.mkdir()
|
||
|
||
mgr = SecretEncryptionManager(mono_root, full_context)
|
||
|
||
outside_file = tmp_path / "somewhere-else" / "secret.env"
|
||
outside_file.parent.mkdir(parents=True, exist_ok=True)
|
||
outside_file.write_text("OUTSIDE=true")
|
||
|
||
result = mgr.decrypt_file(outside_file)
|
||
|
||
# Kontext kann nicht aus dem Pfad ermittelt werden
|
||
assert result.success is False
|
||
assert "kontext" in result.error.lower()
|
||
|
||
def test_resolve_merge_for_shared_context_file(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""Merge-Auflösung für Datei im shared-Ordner: ours gewinnt (shared nicht autorisiert)."""
|
||
shared_file = tmp_monorepo / "shared" / "config.yaml"
|
||
shared_file.write_text("config: value")
|
||
|
||
result = manager.resolve_merge(shared_file, b"ours", b"theirs")
|
||
|
||
# shared ist nicht in authorized_contexts → ours beibehalten
|
||
assert result == b"ours"
|
||
|
||
def test_setup_gitcrypt_filters_for_shared_context(
|
||
self, tmp_monorepo: Path, manager: SecretEncryptionManager
|
||
) -> None:
|
||
"""setup_gitcrypt_filters funktioniert auch für den shared-Kontext."""
|
||
manager.setup_gitcrypt_filters("shared")
|
||
|
||
gitattr_path = tmp_monorepo / "shared" / ".gitattributes"
|
||
assert gitattr_path.exists()
|
||
|
||
content = gitattr_path.read_text(encoding="utf-8")
|
||
assert "filter=git-crypt-shared" in content
|