171 lines
6.2 KiB
Python
171 lines
6.2 KiB
Python
import asyncio
|
||
import base64
|
||
import json
|
||
import os
|
||
import random
|
||
import string
|
||
from contextlib import asynccontextmanager
|
||
from dataclasses import asdict
|
||
from typing import Any, Dict, Generator, AsyncGenerator, Callable, Optional
|
||
|
||
import pytest
|
||
|
||
# Инициализация менеджера Ed25519, если доступен
|
||
try:
|
||
from app.core.crypto import init_ed25519_manager, get_ed25519_manager, ContentCipher
|
||
except Exception: # при статическом анализе или изолированном запуске тестов
|
||
init_ed25519_manager = None # type: ignore
|
||
get_ed25519_manager = None # type: ignore
|
||
ContentCipher = None # type: ignore
|
||
|
||
# FastAPI тест-клиент
|
||
try:
|
||
from fastapi import FastAPI
|
||
from fastapi.testclient import TestClient
|
||
# Основной FastAPI вход
|
||
from app.fastapi_main import app as fastapi_app # type: ignore
|
||
except Exception:
|
||
FastAPI = None # type: ignore
|
||
TestClient = None # type: ignore
|
||
fastapi_app = None # type: ignore
|
||
|
||
|
||
@pytest.fixture(scope="session", autouse=True)
|
||
def seed_random() -> None:
|
||
random.seed(1337)
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
|
||
# pytest-asyncio: собственный event loop с session scope
|
||
loop = asyncio.new_event_loop()
|
||
yield loop
|
||
loop.close()
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def ed25519_manager() -> Any:
|
||
"""
|
||
Глобальный менеджер Ed25519 для подписей. Если доступна функция инициализации — вызываем.
|
||
"""
|
||
if init_ed25519_manager:
|
||
init_ed25519_manager()
|
||
if get_ed25519_manager:
|
||
return get_ed25519_manager()
|
||
class _Dummy: # fallback на случай отсутствия
|
||
public_key_hex = "00"*32
|
||
def sign_message(self, payload: Dict[str, Any]) -> str:
|
||
data = json.dumps(payload, sort_keys=True).encode("utf-8")
|
||
return base64.b64encode(data) .decode("ascii")
|
||
def verify_signature(self, payload: Dict[str, Any], signature: str, pub: str) -> bool:
|
||
try:
|
||
_ = base64.b64decode(signature.encode("ascii"))
|
||
return True
|
||
except Exception:
|
||
return False
|
||
return _Dummy()
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def content_cipher() -> Any:
|
||
"""
|
||
Экземпляр AES-256-GCM шифратора контента.
|
||
"""
|
||
if ContentCipher:
|
||
return ContentCipher()
|
||
class _DummyCipher:
|
||
KEY_SIZE = 32
|
||
NONCE_SIZE = 12
|
||
def generate_content_key(self, seed: Optional[bytes] = None) -> bytes:
|
||
return os.urandom(self.KEY_SIZE)
|
||
def encrypt_content(self, plaintext: bytes, key: bytes, metadata: Optional[Dict[str, Any]] = None,
|
||
associated_data: Optional[bytes] = None, sign_with_ed25519: bool = True) -> Dict[str, Any]:
|
||
# Псевдо-шифрование для fallback
|
||
ct = base64.b64encode(plaintext).decode("ascii")
|
||
nonce = base64.b64encode(b"\x00" * 12).decode("ascii")
|
||
tag = base64.b64encode(b"\x00" * 16).decode("ascii")
|
||
return {"ciphertext_b64": ct, "nonce_b64": nonce, "tag_b64": tag, "content_id": "deadbeef", "metadata": metadata or {}}
|
||
def decrypt_content(self, ciphertext_b64: str, nonce_b64: str, tag_b64: str, key: bytes,
|
||
associated_data: Optional[bytes] = None) -> bytes:
|
||
return base64.b64decode(ciphertext_b64.encode("ascii"))
|
||
def verify_content_integrity(self, encrypted_obj: Dict[str, Any], expected_metadata: Optional[Dict[str, Any]] = None,
|
||
verify_signature: bool = True):
|
||
return True, None
|
||
return _DummyCipher()
|
||
|
||
|
||
@pytest.fixture(scope="session")
|
||
def fastapi_client() -> Any:
|
||
"""
|
||
Тестовый HTTP клиент FastAPI. Если приложение недоступно — пропускаем API тесты.
|
||
"""
|
||
if fastapi_app is None or TestClient is None:
|
||
pytest.skip("FastAPI app is not importable in this environment")
|
||
return TestClient(fastapi_app)
|
||
|
||
|
||
@pytest.fixture
|
||
def temp_large_bytes() -> bytes:
|
||
"""
|
||
Большой буфер для нагрузочных тестов ( ~10 MiB ).
|
||
"""
|
||
size = 10 * 1024 * 1024
|
||
return os.urandom(size)
|
||
|
||
|
||
@pytest.fixture
|
||
def small_sample_bytes() -> bytes:
|
||
return b"The quick brown fox jumps over the lazy dog."
|
||
|
||
|
||
@pytest.fixture
|
||
def random_content_key(content_cipher) -> bytes:
|
||
return content_cipher.generate_content_key()
|
||
|
||
|
||
class MockTONManager:
|
||
"""
|
||
Мок TON NFT менеджера/клиента: имитирует выдачу/проверку лицензий.
|
||
"""
|
||
def __init__(self) -> None:
|
||
self._store: Dict[str, Dict[str, Any]] = {}
|
||
|
||
def issue_license(self, content_id: str, owner_address: str) -> Dict[str, Any]:
|
||
lic_id = "LIC_" + ''.join(random.choices(string.ascii_uppercase + string.digits, k=12))
|
||
nft_addr = "EQ" + ''.join(random.choices(string.ascii_letters + string.digits, k=40))
|
||
lic = {
|
||
"license_id": lic_id,
|
||
"content_id": content_id,
|
||
"owner_address": owner_address,
|
||
"nft_address": nft_addr,
|
||
}
|
||
self._store[lic_id] = lic
|
||
return lic
|
||
|
||
def get_license(self, license_id: str) -> Optional[Dict[str, Any]]:
|
||
return self._store.get(license_id)
|
||
|
||
def verify_access(self, license_id: str, content_id: str, owner_address: str) -> bool:
|
||
lic = self._store.get(license_id)
|
||
return bool(lic and lic["content_id"] == content_id and lic["owner_address"] == owner_address)
|
||
|
||
|
||
@pytest.fixture
|
||
def ton_mock() -> MockTONManager:
|
||
return MockTONManager()
|
||
|
||
|
||
class MockConverter:
|
||
"""
|
||
Мок конвертера: имитация успешной/ошибочной конвертации.
|
||
"""
|
||
def convert(self, content: bytes, fmt: str = "mp3") -> bytes:
|
||
if not content:
|
||
raise ValueError("empty content")
|
||
# Имитация преобразования: добавим префикс для отладки
|
||
return f"[converted:{fmt}]".encode("utf-8") + content
|
||
|
||
|
||
@pytest.fixture
|
||
def converter_mock() -> MockConverter:
|
||
return MockConverter() |