""" π§ͺ Π’Π΅ΡΡ Π΄Π»Ρ ΠΈΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π±Π°Π³Π° ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ ΡΠ΅ΡΠ½ΠΎΠ²ΠΈΠΊΠΎΠ² ΠΡΠΎΠ²Π΅ΡΡΠ΅Ρ, ΡΡΠΎ ΠΏΠΎΡΠ»Π΅ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ ΡΠ΅ΡΠ½ΠΎΠ²ΠΈΠΊΠ° 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 Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΡΡΠ΅Π·Π½ΡΡΡ ΠΈΠ· ΡΠΏΠΈΡΠΊΠ° ΠΏΠΎΡΠ»Π΅ ΡΠ½ΡΡΠΈΡ Ρ ΠΏΡΠ±Π»ΠΈΠΊΠ°ΡΠΈΠΈ"