288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""
|
|
Tests pour le module de chiffrement.
|
|
|
|
Exigences : 17.4
|
|
"""
|
|
|
|
import base64
|
|
import json
|
|
|
|
import pytest
|
|
from cryptography.fernet import InvalidToken
|
|
|
|
from pipeline_mco_pmsi.security.encryption import AuditEncryptor, EncryptionKey
|
|
|
|
|
|
class TestEncryptionKey:
|
|
"""Tests pour EncryptionKey."""
|
|
|
|
def test_generate_key(self):
|
|
"""Test génération de clé."""
|
|
key = EncryptionKey.generate()
|
|
assert key.key is not None
|
|
assert len(key.key) > 0
|
|
|
|
def test_generate_unique_keys(self):
|
|
"""Test que chaque génération produit une clé unique."""
|
|
key1 = EncryptionKey.generate()
|
|
key2 = EncryptionKey.generate()
|
|
assert key1.key != key2.key
|
|
|
|
def test_from_string(self):
|
|
"""Test création de clé depuis string."""
|
|
key = EncryptionKey.generate()
|
|
key_string = key.to_string()
|
|
|
|
# Recréer depuis string
|
|
key2 = EncryptionKey.from_string(key_string)
|
|
assert key2.key == key.key
|
|
|
|
def test_to_string(self):
|
|
"""Test conversion de clé en string."""
|
|
key = EncryptionKey.generate()
|
|
key_string = key.to_string()
|
|
|
|
assert isinstance(key_string, str)
|
|
assert len(key_string) > 0
|
|
|
|
def test_roundtrip_string_conversion(self):
|
|
"""Test conversion aller-retour string."""
|
|
key = EncryptionKey.generate()
|
|
key_string = key.to_string()
|
|
key2 = EncryptionKey.from_string(key_string)
|
|
|
|
assert key.key == key2.key
|
|
|
|
|
|
class TestAuditEncryptor:
|
|
"""Tests pour AuditEncryptor."""
|
|
|
|
@pytest.fixture
|
|
def encryption_key(self):
|
|
"""Fixture pour clé de chiffrement."""
|
|
return EncryptionKey.generate()
|
|
|
|
@pytest.fixture
|
|
def encryptor(self, encryption_key):
|
|
"""Fixture pour encryptor."""
|
|
return AuditEncryptor(encryption_key)
|
|
|
|
@pytest.fixture
|
|
def sample_audit_data(self):
|
|
"""Fixture pour données d'audit de test."""
|
|
return {
|
|
"stay_id": "STAY001",
|
|
"codes": [
|
|
{"code": "I10", "label": "Hypertension essentielle", "type": "dp"},
|
|
{"code": "E11.9", "label": "Diabète de type 2", "type": "das"},
|
|
],
|
|
"timestamp": "2026-02-11T10:30:00",
|
|
"user": "tim_user123",
|
|
}
|
|
|
|
def test_encrypt_audit_data(self, encryptor, sample_audit_data):
|
|
"""Test chiffrement de données d'audit."""
|
|
encrypted = encryptor.encrypt_audit_data(sample_audit_data)
|
|
|
|
assert isinstance(encrypted, bytes)
|
|
assert len(encrypted) > 0
|
|
# Les données chiffrées ne doivent pas contenir le texte original
|
|
assert b"STAY001" not in encrypted
|
|
assert b"I10" not in encrypted
|
|
|
|
def test_decrypt_audit_data(self, encryptor, sample_audit_data):
|
|
"""Test déchiffrement de données d'audit."""
|
|
encrypted = encryptor.encrypt_audit_data(sample_audit_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == sample_audit_data
|
|
|
|
def test_roundtrip_encryption(self, encryptor, sample_audit_data):
|
|
"""Test chiffrement/déchiffrement aller-retour."""
|
|
encrypted = encryptor.encrypt_audit_data(sample_audit_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == sample_audit_data
|
|
assert decrypted["stay_id"] == "STAY001"
|
|
assert len(decrypted["codes"]) == 2
|
|
|
|
def test_encrypt_to_base64(self, encryptor, sample_audit_data):
|
|
"""Test chiffrement vers base64."""
|
|
encrypted_b64 = encryptor.encrypt_to_base64(sample_audit_data)
|
|
|
|
assert isinstance(encrypted_b64, str)
|
|
assert len(encrypted_b64) > 0
|
|
# Vérifier que c'est du base64 valide
|
|
base64.b64decode(encrypted_b64)
|
|
|
|
def test_decrypt_from_base64(self, encryptor, sample_audit_data):
|
|
"""Test déchiffrement depuis base64."""
|
|
encrypted_b64 = encryptor.encrypt_to_base64(sample_audit_data)
|
|
decrypted = encryptor.decrypt_from_base64(encrypted_b64)
|
|
|
|
assert decrypted == sample_audit_data
|
|
|
|
def test_roundtrip_base64(self, encryptor, sample_audit_data):
|
|
"""Test chiffrement/déchiffrement base64 aller-retour."""
|
|
encrypted_b64 = encryptor.encrypt_to_base64(sample_audit_data)
|
|
decrypted = encryptor.decrypt_from_base64(encrypted_b64)
|
|
|
|
assert decrypted == sample_audit_data
|
|
|
|
def test_different_keys_cannot_decrypt(self, sample_audit_data):
|
|
"""Test qu'une clé différente ne peut pas déchiffrer."""
|
|
key1 = EncryptionKey.generate()
|
|
key2 = EncryptionKey.generate()
|
|
|
|
encryptor1 = AuditEncryptor(key1)
|
|
encryptor2 = AuditEncryptor(key2)
|
|
|
|
encrypted = encryptor1.encrypt_audit_data(sample_audit_data)
|
|
|
|
# Tentative de déchiffrement avec une clé différente
|
|
with pytest.raises(InvalidToken):
|
|
encryptor2.decrypt_audit_data(encrypted)
|
|
|
|
def test_tampered_data_fails_decryption(self, encryptor, sample_audit_data):
|
|
"""Test que des données altérées échouent au déchiffrement."""
|
|
encrypted = encryptor.encrypt_audit_data(sample_audit_data)
|
|
|
|
# Altérer les données
|
|
tampered = encrypted[:-1] + b"X"
|
|
|
|
with pytest.raises(InvalidToken):
|
|
encryptor.decrypt_audit_data(tampered)
|
|
|
|
def test_encrypt_empty_data(self, encryptor):
|
|
"""Test chiffrement de données vides."""
|
|
empty_data = {}
|
|
encrypted = encryptor.encrypt_audit_data(empty_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == empty_data
|
|
|
|
def test_encrypt_complex_nested_data(self, encryptor):
|
|
"""Test chiffrement de données complexes imbriquées."""
|
|
complex_data = {
|
|
"stay_id": "STAY001",
|
|
"metadata": {
|
|
"admission_date": "2026-01-15",
|
|
"discharge_date": "2026-01-20",
|
|
"specialty": "Cardiologie",
|
|
},
|
|
"codes": [
|
|
{
|
|
"code": "I10",
|
|
"evidence": [
|
|
{"document_id": "DOC001", "text": "HTA connue"},
|
|
{"document_id": "DOC002", "text": "Traitement antihypertenseur"},
|
|
],
|
|
}
|
|
],
|
|
"versions": {
|
|
"cim10": {"version": "2026", "hash": "abc123"},
|
|
"ccam": {"version": "V81", "hash": "def456"},
|
|
},
|
|
}
|
|
|
|
encrypted = encryptor.encrypt_audit_data(complex_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == complex_data
|
|
assert decrypted["metadata"]["specialty"] == "Cardiologie"
|
|
assert len(decrypted["codes"][0]["evidence"]) == 2
|
|
|
|
def test_encrypt_unicode_data(self, encryptor):
|
|
"""Test chiffrement de données avec caractères Unicode."""
|
|
unicode_data = {
|
|
"stay_id": "STAY001",
|
|
"diagnosis": "Œdème pulmonaire aigu",
|
|
"notes": "Patient présentant dyspnée sévère",
|
|
"symbols": "→ ≥ ≤ ± °",
|
|
}
|
|
|
|
encrypted = encryptor.encrypt_audit_data(unicode_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == unicode_data
|
|
assert decrypted["diagnosis"] == "Œdème pulmonaire aigu"
|
|
|
|
def test_encrypt_large_data(self, encryptor):
|
|
"""Test chiffrement de grandes quantités de données."""
|
|
large_data = {
|
|
"stay_id": "STAY001",
|
|
"documents": [
|
|
{
|
|
"document_id": f"DOC{i:03d}",
|
|
"content": "Lorem ipsum dolor sit amet " * 100,
|
|
}
|
|
for i in range(50)
|
|
],
|
|
}
|
|
|
|
encrypted = encryptor.encrypt_audit_data(large_data)
|
|
decrypted = encryptor.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == large_data
|
|
assert len(decrypted["documents"]) == 50
|
|
|
|
def test_encrypted_data_is_different_each_time(self, encryptor, sample_audit_data):
|
|
"""Test que le chiffrement produit des résultats différents à chaque fois."""
|
|
# Fernet utilise un IV aléatoire, donc même données = chiffrement différent
|
|
encrypted1 = encryptor.encrypt_audit_data(sample_audit_data)
|
|
encrypted2 = encryptor.encrypt_audit_data(sample_audit_data)
|
|
|
|
# Les données chiffrées doivent être différentes
|
|
assert encrypted1 != encrypted2
|
|
|
|
# Mais les deux doivent déchiffrer vers les mêmes données
|
|
decrypted1 = encryptor.decrypt_audit_data(encrypted1)
|
|
decrypted2 = encryptor.decrypt_audit_data(encrypted2)
|
|
assert decrypted1 == decrypted2 == sample_audit_data
|
|
|
|
|
|
class TestEncryptionIntegration:
|
|
"""Tests d'intégration pour le chiffrement."""
|
|
|
|
def test_key_persistence_and_reuse(self):
|
|
"""Test persistance et réutilisation de clé."""
|
|
# Générer une clé
|
|
key = EncryptionKey.generate()
|
|
key_string = key.to_string()
|
|
|
|
# Simuler sauvegarde/chargement de clé
|
|
# (dans un vrai système, on sauvegarderait dans un fichier sécurisé)
|
|
|
|
# Chiffrer avec la clé originale
|
|
encryptor1 = AuditEncryptor(key)
|
|
data = {"test": "data"}
|
|
encrypted = encryptor1.encrypt_audit_data(data)
|
|
|
|
# Charger la clé depuis string et déchiffrer
|
|
loaded_key = EncryptionKey.from_string(key_string)
|
|
encryptor2 = AuditEncryptor(loaded_key)
|
|
decrypted = encryptor2.decrypt_audit_data(encrypted)
|
|
|
|
assert decrypted == data
|
|
|
|
def test_multiple_encryptions_with_same_key(self):
|
|
"""Test multiples chiffrements avec la même clé."""
|
|
key = EncryptionKey.generate()
|
|
encryptor = AuditEncryptor(key)
|
|
|
|
data_list = [
|
|
{"stay_id": "STAY001", "code": "I10"},
|
|
{"stay_id": "STAY002", "code": "E11.9"},
|
|
{"stay_id": "STAY003", "code": "J18.9"},
|
|
]
|
|
|
|
# Chiffrer toutes les données
|
|
encrypted_list = [encryptor.encrypt_audit_data(data) for data in data_list]
|
|
|
|
# Déchiffrer toutes les données
|
|
decrypted_list = [
|
|
encryptor.decrypt_audit_data(enc) for enc in encrypted_list
|
|
]
|
|
|
|
assert decrypted_list == data_list
|