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()