Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,626 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user