from __future__ import annotations import asyncio from unittest.mock import AsyncMock import pytest from cryptography.fernet import Fernet from app import opencloud_token_store as store from app.opencloud_token_store import OpenCloudTokenStoreError class FakeResult: def __init__(self, usernames: list[str]) -> None: self._usernames = usernames def scalars(self): return self def all(self): return self._usernames class FakeRow: def __init__(self, username: str, encrypted_token: str, added_by=None) -> None: self.username = username self.encrypted_token = encrypted_token self.added_by = added_by class FakeSession: """Мини-модель БД: {username: FakeRow}, без реального движка.""" def __init__(self, table: dict[str, FakeRow]) -> None: self._table = table async def get(self, model, username): # noqa: ANN001 _ = model return self._table.get(username) def add(self, obj: FakeRow) -> None: self._table[obj.username] = obj async def delete(self, obj: FakeRow) -> None: self._table.pop(obj.username, None) async def commit(self) -> None: pass async def execute(self, stmt): # noqa: ANN001 _ = stmt return FakeResult(sorted(self._table.keys())) async def __aenter__(self) -> FakeSession: return self async def __aexit__(self, exc_type, exc, tb) -> bool: # noqa: ANN001 return False class FakeSessionLocal: def __init__(self, table: dict[str, FakeRow]) -> None: self._table = table def __call__(self) -> FakeSession: return FakeSession(self._table) @pytest.fixture(autouse=True) def _fake_store(monkeypatch): """Подменяет SessionLocal/engine и FERNET_KEY на in-memory заглушки. Каждый вызов add_token/get_token/remove_token/list_usernames в проде оборачивается ОТДЕЛЬНЫМ asyncio.run() (см. app/app_config.py) — новый event loop на каждый вызов. Тесты ниже воспроизводят это же (несколько независимых asyncio.run() подряд в одном тесте) и проверяют, что engine.dispose() вызывается после каждой операции. """ table: dict[str, FakeRow] = {} fake_engine = AsyncMock() monkeypatch.setattr(store, "SessionLocal", FakeSessionLocal(table)) monkeypatch.setattr(store, "engine", fake_engine) monkeypatch.setattr(store.settings, "FERNET_KEY", Fernet.generate_key().decode()) return table, fake_engine def test_add_token_then_get_token_across_separate_event_loops(_fake_store) -> None: table, fake_engine = _fake_store asyncio.run(store.add_token("raykovmse", "coherent effects plausibly apple")) assert fake_engine.dispose.await_count == 1 token = asyncio.run(store.get_token("raykovmse")) assert fake_engine.dispose.await_count == 2 assert token == "coherent effects plausibly apple" def test_add_token_updates_existing_row(_fake_store) -> None: table, fake_engine = _fake_store asyncio.run(store.add_token("raykovmse", "first token", added_by="admin")) asyncio.run(store.add_token("raykovmse", "second token", added_by="raykovmse")) assert fake_engine.dispose.await_count == 2 assert len(table) == 1 assert asyncio.run(store.get_token("raykovmse")) == "second token" def test_get_token_missing_username_disposes_engine(_fake_store) -> None: _, fake_engine = _fake_store result = asyncio.run(store.get_token("no-such-user")) assert result is None assert fake_engine.dispose.await_count == 1 def test_remove_token_disposes_engine(_fake_store) -> None: table, fake_engine = _fake_store asyncio.run(store.add_token("raykovmse", "token-123")) fake_engine.dispose.reset_mock() removed = asyncio.run(store.remove_token("raykovmse")) assert removed is True assert "raykovmse" not in table assert fake_engine.dispose.await_count == 1 def test_list_usernames_disposes_engine(_fake_store) -> None: _, fake_engine = _fake_store asyncio.run(store.add_token("raykovmse", "token-123")) fake_engine.dispose.reset_mock() usernames = asyncio.run(store.list_usernames()) assert usernames == ["raykovmse"] assert fake_engine.dispose.await_count == 1 def test_add_token_disposes_engine_even_when_commit_fails( _fake_store, monkeypatch ) -> None: _, fake_engine = _fake_store async def _failing_commit(self) -> None: # noqa: ANN001 raise RuntimeError("boom") monkeypatch.setattr(FakeSession, "commit", _failing_commit) with pytest.raises(RuntimeError): asyncio.run(store.add_token("raykovmse", "token-123")) assert fake_engine.dispose.await_count == 1 def test_add_token_requires_username_and_token(_fake_store) -> None: with pytest.raises(OpenCloudTokenStoreError): asyncio.run(store.add_token("", "token-123")) with pytest.raises(OpenCloudTokenStoreError): asyncio.run(store.add_token("raykovmse", ""))