diff --git a/CHANGELOG.md b/CHANGELOG.md index 41364af7..be0aa577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.9.10] - 2025-01-23 + +### 🐛 Fixed +- **Исправлена критическая ошибка с уведомлениями**: Устранена ошибка `null value in column "kind" of relation "notification" violates not-null constraint` +- **Исправлен возвращаемый формат publish_draft**: Теперь возвращается `{"draft": draft_dict}` вместо `{"shout": shout}` для соответствия GraphQL схеме +- **Фронтенд получает корректные данные**: При публикации черновика фронтенд теперь получает ожидаемое поле `draft` вместо `null` + +### 🏗️ Changed +- **Обновлена функция save_notification**: Добавлено обязательное поле `kind` для создания уведомлений +- **Исправлена типизация**: Поле `kind` теперь корректно преобразуется из `action` в `NotificationAction` enum +- **Убрано неиспользуемое значение PUBLISHED**: Из enum `NotificationAction` убрано значение, которое не использовалось + +### 📦 Added +- **Добавлен fallback для нестандартных действий**: Если `action` не соответствует enum, используется `NotificationAction.CREATE` +- **Созданы тесты для уведомлений**: Добавлены тесты проверки корректного создания уведомлений +- **Созданы тесты для publish_draft**: Добавлены тесты проверки правильного возвращаемого формата + +### 🧪 Tests +- **test_notification_fix.py**: Тесты для проверки создания уведомлений с валидными действиями +- **test_draft_publish_fix.py**: Тесты для проверки возвращаемого формата в `publish_draft` + ## [0.9.9] - 2025-08-21 ### 🐛 Fixed diff --git a/resolvers/draft.py b/resolvers/draft.py index c7765f26..f770d75a 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -502,7 +502,11 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic logger.info(f"Successfully published shout #{shout.id} from draft #{draft_id}") logger.debug(f"Shout data: {shout.dict()}") - return {"shout": shout} + # Возвращаем обновленный черновик с информацией о shout + draft_dict = draft.dict() + draft_dict["shout"] = {"id": shout.id, "slug": shout.slug, "published_at": shout.published_at} + + return {"draft": draft_dict} except Exception as e: logger.error(f"Failed to publish draft {draft_id}: {e}", exc_info=True) diff --git a/services/notify.py b/services/notify.py index 377328eb..bd709fca 100644 --- a/services/notify.py +++ b/services/notify.py @@ -28,7 +28,7 @@ def save_notification(action: str, entity: str, payload: dict[Any, Any] | str | # Fallback: создаем NotificationAction с пользовательским значением # TODO: базовое значение для нестандартных действий kind = NotificationAction.CREATE - + n = Notification(action=action, entity=entity, payload=payload, kind=kind) session.add(n) session.commit() diff --git a/tests/test_draft_publish_fix.py b/tests/test_draft_publish_fix.py index 243b174a..d607a6f7 100644 --- a/tests/test_draft_publish_fix.py +++ b/tests/test_draft_publish_fix.py @@ -74,3 +74,75 @@ class TestDraftPublishFix: # Assert assert not hasattr(shout, 'draft') + +""" +Тест для проверки исправления возвращаемого значения в publish_draft. +""" + +import pytest +from unittest.mock import patch, MagicMock, AsyncMock + +from resolvers.draft import publish_draft + + +@pytest.mark.asyncio +async def test_publish_draft_returns_draft(): + """Тест что publish_draft возвращает draft в правильном формате""" + # Мокаем контекст + mock_info = MagicMock() + mock_info.context = {"author": {"id": 1}} + + # Мокаем session + mock_session = MagicMock() + mock_session_instance = MagicMock() + mock_session.return_value.__enter__.return_value = mock_session_instance + + # Мокаем draft + mock_draft = MagicMock() + mock_draft.id = 1 + mock_draft.body = "

Test content

" + mock_draft.shout = None + mock_draft.authors = [] + mock_draft.topics = [] + mock_draft.dict.return_value = {"id": 1, "title": "Test Draft"} + + # Мокаем shout + mock_shout = MagicMock() + mock_shout.id = 100 + mock_shout.slug = "test-shout" + mock_shout.published_at = 1234567890 + mock_shout.dict.return_value = {"id": 100, "slug": "test-shout"} + + # Настраиваем моки + mock_session_instance.query.return_value.options.return_value.where.return_value.first.side_effect = [ + mock_draft, # Первый вызов для draft + None, # Второй вызов для существующего shout + ] + + # Мокаем create_shout_from_draft + with patch('resolvers.draft.create_shout_from_draft', return_value=mock_shout): + with patch('resolvers.draft.validate_html_content', return_value=(True, None)): + with patch('resolvers.draft.invalidate_shouts_cache', new_callable=AsyncMock): + with patch('resolvers.draft.invalidate_shout_related_cache', new_callable=AsyncMock): + with patch('resolvers.draft.notify_shout', new_callable=AsyncMock): + with patch('resolvers.draft.search_service.index'): + with patch('resolvers.draft.local_session', mock_session): + # Вызываем функцию + result = await publish_draft(None, mock_info, 1) + + # Проверяем результат + assert "draft" in result + assert "error" not in result + + draft_data = result["draft"] + assert draft_data["id"] == 1 + assert "shout" in draft_data + + shout_data = draft_data["shout"] + assert shout_data["id"] == 100 + assert shout_data["slug"] == "test-shout" + assert shout_data["published_at"] == 1234567890 + + +if __name__ == "__main__": + pytest.main([__file__])