""" πŸ§ͺ ВСст для исправлСния Π±Π°Π³Π° ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠΎΠ² ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ послС ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° shout ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎ отобраТаСтся ΠΊΠ°ΠΊ Π² Π΅Π΄ΠΈΠ½ΠΈΡ‡Π½Ρ‹Ρ… запросах ΠΏΠΎ slug, Ρ‚Π°ΠΊ ΠΈ Π² списках load_shouts_by. """ import time from typing import Any from unittest.mock import patch, AsyncMock import pytest from sqlalchemy.orm import Session from orm.author import Author from orm.community import Community from orm.draft import Draft, DraftTopic from orm.shout import Shout from orm.topic import Topic from resolvers.draft import publish_draft from resolvers.reader import get_shout, load_shouts_by from storage.db import local_session # from tests.conftest import GraphQLResolveInfoMock class GraphQLResolveInfoMock: """Mock ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ для GraphQLResolveInfo Π² тСстах""" def __init__(self, context: dict | None = None): from unittest.mock import MagicMock self.context = context or {} self.field_nodes = [MagicMock()] self.field_nodes[0].selection_set = None self.field_name = "test_field" self.return_type = MagicMock() self.parent_type = MagicMock() self.path = MagicMock() self.schema = MagicMock() self.fragments = {} self.root_value = None self.operation = MagicMock() self.variable_values = {} self.is_awaitable = False @pytest.fixture(scope="function") def test_data() -> dict[str, Any]: """Π‘ΠΎΠ·Π΄Π°Π΅Ρ‚ тСстовыС Π΄Π°Π½Π½Ρ‹Π΅ для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ""" with local_session() as session: # πŸ” ΠžΡ‚Π»Π°Π΄ΠΊΠ°: провСряСм схСму Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ draft ΠΈ добавляСм Π½Π΅Π΄ΠΎΡΡ‚Π°ΡŽΡ‰ΡƒΡŽ ΠΊΠΎΠ»ΠΎΠ½ΠΊΡƒ from sqlalchemy import inspect, text inspector = inspect(session.bind) draft_columns = [col['name'] for col in inspector.get_columns('draft')] print(f"πŸ” Draft table columns in test: {draft_columns}") if 'shout' not in draft_columns: print("πŸ”§ Adding missing 'shout' column to draft table") session.execute(text("ALTER TABLE draft ADD COLUMN shout INTEGER")) session.commit() # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Π°Π²Ρ‚ΠΎΡ€Π° с ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΌ email import time import random timestamp = int(time.time()) + random.randint(1, 10000) author = Author( name="Test Author", slug=f"test-author-{timestamp}", email=f"test-{timestamp}@example.com", ) session.add(author) session.flush() # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ сообщСство с ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΌ slug community = Community( name="Test Community", slug=f"test-community-{timestamp}", created_by=author.id, ) session.add(community) session.flush() # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ с ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΌ slug draft = Draft( title="Test Draft Title", body="

Test draft content

", slug=f"test-draft-slug-{timestamp}", created_by=author.id, community=community.id, ) session.add(draft) session.flush() # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‚ΠΎΠΏΠΈΠΊ для Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° topic = Topic( title="Test Topic", slug=f"test-topic-{timestamp}", community=community.id, ) session.add(topic) session.flush() # БвязываСм Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ с Ρ‚ΠΎΠΏΠΈΠΊΠΎΠΌ draft_topic = DraftTopic(draft=draft.id, topic=topic.id, main=True) session.add(draft_topic) # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ shout для тСстирования обновлСния existing_shout = Shout( title="Old Title", body="

Old content

", slug=f"existing-shout-slug-{timestamp}", created_by=author.id, community=community.id, created_at=int(time.time()), published_at=None, # Π’Π°ΠΆΠ½ΠΎ: ΠΈΠ·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ Π½Π΅ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½ ) session.add(existing_shout) session.flush() # БвязываСм Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ с ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ shout draft_with_shout = Draft( title="Updated Draft Title", body="

Updated draft content

", slug=f"updated-draft-slug-{timestamp}", created_by=author.id, community=community.id, shout=existing_shout.id, # БвязываСм с ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ shout ) session.add(draft_with_shout) session.flush() # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Ρ‚ΠΎΠΏΠΈΠΊ для Π²Ρ‚ΠΎΡ€ΠΎΠ³ΠΎ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° topic2 = Topic( title="Updated Topic", slug=f"updated-topic-{timestamp}", community=community.id, ) session.add(topic2) session.flush() # БвязываСм Π²Ρ‚ΠΎΡ€ΠΎΠΉ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ с Ρ‚ΠΎΠΏΠΈΠΊΠΎΠΌ draft_topic2 = DraftTopic(draft=draft_with_shout.id, topic=topic2.id, main=True) session.add(draft_topic2) session.commit() return { "author_id": author.id, "community_id": community.id, "draft_id": draft.id, "draft_with_shout_id": draft_with_shout.id, "existing_shout_id": existing_shout.id, } @pytest.mark.asyncio @patch('resolvers.draft.notify_shout', new=AsyncMock()) @patch('resolvers.draft.invalidate_shouts_cache', new=AsyncMock()) @patch('resolvers.draft.invalidate_shout_related_cache', new=AsyncMock()) async def test_new_draft_publication_visibility(test_data: dict[str, Any]) -> None: """ πŸ§ͺ ВСст ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π½ΠΎΠ²ΠΎΠ³ΠΎ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ Π½ΠΎΠ²Ρ‹ΠΉ ΠΎΠΏΡƒΠ±Π»ΠΈΠΊΠΎΠ²Π°Π½Π½Ρ‹ΠΉ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ Π²ΠΈΠ΄Π΅Π½ ΠΊΠ°ΠΊ Π² Π΅Π΄ΠΈΠ½ΠΈΡ‡Π½Ρ‹Ρ… запросах, Ρ‚Π°ΠΊ ΠΈ Π² списках. """ # ΠŸΠΎΠ΄Π³ΠΎΡ‚Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ контСкст info = GraphQLResolveInfoMock() info.context = { "author": {"id": test_data["author_id"]}, "roles": ["author", "reader"] } # ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠ΅ΠΌ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ result = await publish_draft(None, info, test_data["draft_id"]) assert "error" not in result or result["error"] is None assert "draft" in result shout_info = result["draft"]["shout"] shout_id = shout_info["id"] shout_slug = shout_info["slug"] # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ published_at установлСн assert shout_info["published_at"] is not None # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ Π² Π΅Π΄ΠΈΠ½ΠΈΡ‡Π½ΠΎΠΌ запросС single_shout = await get_shout(None, info, slug=shout_slug) assert single_shout is not None assert single_shout["id"] == shout_id # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ Π² спискС shouts_list = await load_shouts_by(None, info, {"limit": 10, "offset": 0}) shout_ids_in_list = [shout["id"] for shout in shouts_list] assert shout_id in shout_ids_in_list @pytest.mark.asyncio @patch('resolvers.draft.notify_shout', new=AsyncMock()) @patch('resolvers.draft.invalidate_shouts_cache', new=AsyncMock()) @patch('resolvers.draft.invalidate_shout_related_cache', new=AsyncMock()) async def test_existing_shout_update_visibility(test_data: dict[str, Any]) -> None: """ πŸ§ͺ ВСст обновлСния ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ shout Ρ‡Π΅Ρ€Π΅Π· Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ ΠΏΡ€ΠΈ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΈ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π³ΠΎ shout Ρ‡Π΅Ρ€Π΅Π· ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΡŽ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊΠ° shout становится Π²ΠΈΠ΄ΠΈΠΌΡ‹ΠΌ Π² списках (устанавливаСтся published_at). """ # ΠŸΠΎΠ΄Π³ΠΎΡ‚Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ контСкст info = GraphQLResolveInfoMock() info.context = { "author": {"id": test_data["author_id"]}, "roles": ["author", "reader"] } # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ ΠΈΠ·Π½Π°Ρ‡Π°Π»ΡŒΠ½ΠΎ shout Π½Π΅ Π²ΠΈΠ΄Π΅Π½ Π² списках (published_at = None) with local_session() as session: existing_shout = session.query(Shout).filter( Shout.id == test_data["existing_shout_id"] ).first() assert existing_shout is not None assert existing_shout.published_at is None # ΠŸΡƒΠ±Π»ΠΈΠΊΡƒΠ΅ΠΌ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ обновляСт ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ shout result = await publish_draft(None, info, test_data["draft_with_shout_id"]) assert "error" not in result or result["error"] is None assert "draft" in result shout_info = result["draft"]["shout"] shout_id = shout_info["id"] shout_slug = shout_info["slug"] # 🎯 ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠ°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°: published_at Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ установлСн assert shout_info["published_at"] is not None # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… published_at Π΄Π΅ΠΉΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ установлСн with local_session() as session: updated_shout = session.query(Shout).filter(Shout.id == shout_id).first() assert updated_shout is not None assert updated_shout.published_at is not None assert updated_shout.title == "Updated Draft Title" # ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ Π² Π΅Π΄ΠΈΠ½ΠΈΡ‡Π½ΠΎΠΌ запросС single_shout = await get_shout(None, info, slug=shout_slug) assert single_shout is not None assert single_shout["id"] == shout_id # 🎯 Главная ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°: Π²ΠΈΠ΄ΠΈΠΌΠΎΡΡ‚ΡŒ Π² спискС (это ΠΈ Π±Ρ‹Π»ΠΎ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠΎΠΉ) shouts_list = await load_shouts_by(None, info, {"limit": 10, "offset": 0}) shout_ids_in_list = [shout["id"] for shout in shouts_list] assert shout_id in shout_ids_in_list, f"Shout {shout_id} Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ Π²ΠΈΠ΄Π΅Π½ Π² спискС послС ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ" @pytest.mark.asyncio @patch('resolvers.draft.notify_shout', new=AsyncMock()) @patch('resolvers.draft.invalidate_shouts_cache', new=AsyncMock()) @patch('resolvers.draft.invalidate_shout_related_cache', new=AsyncMock()) async def test_unpublish_draft_removes_from_lists(test_data: dict[str, Any]) -> None: """ πŸ§ͺ ВСст снятия с ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ послС снятия с ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ shout исчСзаСт ΠΈΠ· списков, Π½ΠΎ остаСтся доступным ΠΏΠΎ прямому запросу (Ссли это ΠΏΠΎΠ²Π΅Π΄Π΅Π½ΠΈΠ΅ Π½ΡƒΠΆΠ½ΠΎ ΠΈΠ·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ). """ from resolvers.draft import unpublish_draft # ΠŸΠΎΠ΄Π³ΠΎΡ‚Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ контСкст info = GraphQLResolveInfoMock() info.context = { "author": {"id": test_data["author_id"]}, "roles": ["author", "reader"] } # Π‘Π½Π°Ρ‡Π°Π»Π° ΠΏΡƒΠ±Π»ΠΈΠΊΡƒΠ΅ΠΌ Ρ‡Π΅Ρ€Π½ΠΎΠ²ΠΈΠΊ publish_result = await publish_draft(None, info, test_data["draft_id"]) assert "error" not in publish_result or publish_result["error"] is None shout_info = publish_result["draft"]["shout"] shout_id = shout_info["id"] # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ shout Π²ΠΈΠ΄Π΅Π½ Π² спискС shouts_list_before = await load_shouts_by(None, info, {"limit": 10, "offset": 0}) shout_ids_before = [shout["id"] for shout in shouts_list_before] assert shout_id in shout_ids_before # Π‘Π½ΠΈΠΌΠ°Π΅ΠΌ с ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ unpublish_result = await unpublish_draft(None, info, test_data["draft_id"]) assert "error" not in unpublish_result or unpublish_result["error"] is None # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ shout исчСз ΠΈΠ· списка shouts_list_after = await load_shouts_by(None, info, {"limit": 10, "offset": 0}) shout_ids_after = [shout["id"] for shout in shouts_list_after] assert shout_id not in shout_ids_after, "Shout Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΡΡ‡Π΅Π·Π½ΡƒΡ‚ΡŒ ΠΈΠ· списка послС снятия с ΠΏΡƒΠ±Π»ΠΈΠΊΠ°Ρ†ΠΈΠΈ"