diff --git a/resolvers/draft.py b/resolvers/draft.py index 4873e16d..63789827 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -444,6 +444,7 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic shout = session.query(Shout).where(Shout.id == draft.shout).first() if shout: # Обновляем существующую публикацию + now = int(time.time()) if hasattr(draft, "body"): shout.body = draft.body if hasattr(draft, "title"): @@ -462,7 +463,9 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic shout.lang = draft.lang if hasattr(draft, "seo"): shout.seo = draft.seo - shout.updated_at = int(time.time()) + # 🩵 Критически важно: устанавливаем published_at для обеспечения видимости в списках + shout.published_at = now + shout.updated_at = now shout.updated_by = author_id else: # Создаем новую публикацию diff --git a/tests/test_draft_publication_fix.py b/tests/test_draft_publication_fix.py new file mode 100644 index 00000000..81b3a886 --- /dev/null +++ b/tests/test_draft_publication_fix.py @@ -0,0 +1,213 @@ +""" +🧪 Тест для исправления бага публикации черновиков + +Проверяет, что после публикации черновика shout корректно отображается +как в единичных запросах по slug, так и в списках load_shouts_by. +""" + +import time +from typing import Any + +import pytest +from sqlalchemy.orm import Session + +from orm.author import Author +from orm.community import Community +from orm.draft import Draft +from orm.shout import Shout +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 + + +@pytest.fixture +def test_data() -> dict[str, Any]: + """Создает тестовые данные для проверки публикации""" + with local_session() as session: + # Создаем автора + author = Author( + name="Test Author", + slug="test-author", + email="test@example.com", + ) + session.add(author) + session.flush() + + # Создаем сообщество + community = Community( + name="Test Community", + slug="test-community", + created_by=author.id, + ) + session.add(community) + session.flush() + + # Создаем черновик + draft = Draft( + title="Test Draft Title", + body="
Test draft content
", + slug="test-draft-slug", + created_by=author.id, + community=community.id, + ) + session.add(draft) + session.flush() + + # Создаем существующий shout для тестирования обновления + existing_shout = Shout( + title="Old Title", + body="Old content
", + slug="existing-shout-slug", + 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="updated-draft-slug", + created_by=author.id, + community=community.id, + shout=existing_shout.id, # Связываем с существующим shout + ) + session.add(draft_with_shout) + session.flush() + + 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 +async def test_new_draft_publication_visibility(test_data: dict[str, Any]) -> None: + """ + 🧪 Тест публикации нового черновика + + Проверяет, что новый опубликованный черновик виден как в единичных запросах, + так и в списках. + """ + # Подготавливаем контекст + info = GraphQLResolveInfoMock() + info.context = {"author": {"id": test_data["author_id"]}} + + # Публикуем черновик + 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 +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"]}} + + # Проверяем, что изначально 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 +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"]}} + + # Сначала публикуем черновик + 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 должен исчезнуть из списка после снятия с публикации"