This commit is contained in:
@@ -132,7 +132,9 @@ async def follow(
|
|||||||
if what == "AUTHOR" and not existing_sub:
|
if what == "AUTHOR" and not existing_sub:
|
||||||
logger.debug("Отправка уведомления автору о подписке")
|
logger.debug("Отправка уведомления автору о подписке")
|
||||||
if isinstance(follower_dict, dict) and isinstance(entity_id, int):
|
if isinstance(follower_dict, dict) and isinstance(entity_id, int):
|
||||||
await notify_follower(follower=follower_dict, author_id=entity_id, action="follow")
|
# Получаем ID созданной записи подписки
|
||||||
|
subscription_id = getattr(sub, 'id', None) if 'sub' in locals() else None
|
||||||
|
await notify_follower(follower=follower_dict, author_id=entity_id, action="follow", subscription_id=subscription_id)
|
||||||
|
|
||||||
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
|
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
|
||||||
logger.debug("Инвалидируем кеш статистики авторов")
|
logger.debug("Инвалидируем кеш статистики авторов")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from collections.abc import Collection
|
from collections.abc import Collection
|
||||||
|
from datetime import UTC
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
@@ -72,12 +73,26 @@ async def notify_shout(shout: dict[str, Any], action: str = "update") -> None:
|
|||||||
logger.error(f"Failed to publish to channel {channel_name}: {e}")
|
logger.error(f"Failed to publish to channel {channel_name}: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def notify_follower(follower: dict[str, Any], author_id: int, action: str = "follow") -> None:
|
async def notify_follower(follower: dict[str, Any], author_id: int, action: str = "follow", subscription_id: int | None = None) -> None:
|
||||||
channel_name = f"follower:{author_id}"
|
channel_name = f"follower:{author_id}"
|
||||||
try:
|
try:
|
||||||
# Simplify dictionary before publishing
|
# Simplify dictionary before publishing
|
||||||
simplified_follower = {k: follower[k] for k in ["id", "name", "slug", "pic"]}
|
simplified_follower = {k: follower[k] for k in ["id", "name", "slug", "pic"]}
|
||||||
data = {"payload": simplified_follower, "action": action}
|
|
||||||
|
# Формат данных для фронтенда согласно обновленной спецификации SSE
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"action": "create" if action == "follow" else "delete",
|
||||||
|
"entity": "follower",
|
||||||
|
"payload": {
|
||||||
|
"id": subscription_id or 999, # ID записи подписки из БД
|
||||||
|
"follower_id": simplified_follower["id"],
|
||||||
|
"following_id": author_id,
|
||||||
|
"created_at": datetime.now(UTC).isoformat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# save in channel
|
# save in channel
|
||||||
payload = data.get("payload")
|
payload = data.get("payload")
|
||||||
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
|
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
|
||||||
@@ -91,6 +106,7 @@ async def notify_follower(follower: dict[str, Any], author_id: int, action: str
|
|||||||
if json_data:
|
if json_data:
|
||||||
# Use the 'await' keyword when publishing
|
# Use the 'await' keyword when publishing
|
||||||
await redis.publish(channel_name, json_data)
|
await redis.publish(channel_name, json_data)
|
||||||
|
logger.debug(f"📡 Отправлено SSE уведомление о подписке: author_id={author_id}, follower={simplified_follower.get('name')}")
|
||||||
|
|
||||||
except (ConnectionError, TimeoutError, KeyError, ValueError) as e:
|
except (ConnectionError, TimeoutError, KeyError, ValueError) as e:
|
||||||
# Log the error and re-raise it
|
# Log the error and re-raise it
|
||||||
|
|||||||
145
tests/test_follow_sse_notifications.py
Normal file
145
tests/test_follow_sse_notifications.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
Тест SSE уведомлений о новых подписчиках
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from services.notify import notify_follower
|
||||||
|
from storage.redis import redis
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_follow_sse_notification_format():
|
||||||
|
"""
|
||||||
|
Тест формата SSE уведомления о новой подписке
|
||||||
|
"""
|
||||||
|
# Мокаем Redis publish, чтобы перехватить отправляемые данные
|
||||||
|
published_data = []
|
||||||
|
|
||||||
|
async def mock_publish(channel: str, data: bytes) -> None:
|
||||||
|
published_data.append((channel, orjson.loads(data)))
|
||||||
|
|
||||||
|
with patch.object(redis, 'publish', side_effect=mock_publish):
|
||||||
|
# Данные подписавшегося пользователя
|
||||||
|
follower_data = {
|
||||||
|
"id": 123,
|
||||||
|
"name": "Test Follower",
|
||||||
|
"slug": "test-follower",
|
||||||
|
"pic": "https://example.com/avatar.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
target_author_id = 456
|
||||||
|
|
||||||
|
# Отправляем уведомление
|
||||||
|
await notify_follower(
|
||||||
|
follower=follower_data,
|
||||||
|
author_id=target_author_id,
|
||||||
|
action="follow"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем, что данные отправлены правильно
|
||||||
|
assert len(published_data) == 1
|
||||||
|
channel, data = published_data[0]
|
||||||
|
|
||||||
|
# Проверяем канал
|
||||||
|
assert channel == f"follower:{target_author_id}"
|
||||||
|
|
||||||
|
# Проверяем формат данных согласно обновленной спецификации фронтенда
|
||||||
|
assert data["action"] == "create"
|
||||||
|
assert data["entity"] == "follower"
|
||||||
|
assert data["payload"]["follower_id"] == 123
|
||||||
|
assert data["payload"]["following_id"] == target_author_id
|
||||||
|
assert "id" in data["payload"]
|
||||||
|
assert "created_at" in data["payload"]
|
||||||
|
|
||||||
|
print(f"✅ SSE уведомление отправлено правильно: {data}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unfollow_sse_notification_format():
|
||||||
|
"""
|
||||||
|
Тест формата SSE уведомления об отписке
|
||||||
|
"""
|
||||||
|
published_data = []
|
||||||
|
|
||||||
|
async def mock_publish(channel: str, data: bytes) -> None:
|
||||||
|
published_data.append((channel, orjson.loads(data)))
|
||||||
|
|
||||||
|
with patch.object(redis, 'publish', side_effect=mock_publish):
|
||||||
|
# Данные отписавшегося пользователя
|
||||||
|
follower_data = {
|
||||||
|
"id": 789,
|
||||||
|
"name": "Test Unfollower",
|
||||||
|
"slug": "test-unfollower",
|
||||||
|
"pic": "https://example.com/avatar2.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
target_author_id = 101
|
||||||
|
|
||||||
|
# Отправляем уведомление об отписке
|
||||||
|
await notify_follower(
|
||||||
|
follower=follower_data,
|
||||||
|
author_id=target_author_id,
|
||||||
|
action="unfollow"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем формат для отписки
|
||||||
|
assert len(published_data) == 1
|
||||||
|
channel, data = published_data[0]
|
||||||
|
|
||||||
|
assert channel == f"follower:{target_author_id}"
|
||||||
|
|
||||||
|
# Для отписки action должен быть "delete"
|
||||||
|
assert data["action"] == "delete"
|
||||||
|
assert data["entity"] == "follower"
|
||||||
|
assert data["payload"]["follower_id"] == 789
|
||||||
|
assert data["payload"]["following_id"] == target_author_id
|
||||||
|
assert "id" in data["payload"]
|
||||||
|
assert "created_at" in data["payload"]
|
||||||
|
|
||||||
|
print(f"✅ SSE уведомление об отписке отправлено правильно: {data}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_custom_subscription_id():
|
||||||
|
"""
|
||||||
|
Тест передачи пользовательского ID подписки
|
||||||
|
"""
|
||||||
|
published_data = []
|
||||||
|
|
||||||
|
async def mock_publish(channel: str, data: bytes) -> None:
|
||||||
|
published_data.append((channel, orjson.loads(data)))
|
||||||
|
|
||||||
|
with patch.object(redis, 'publish', side_effect=mock_publish):
|
||||||
|
# Данные подписчика
|
||||||
|
follower_data = {
|
||||||
|
"id": 777,
|
||||||
|
"name": "Test User",
|
||||||
|
"slug": "test-user",
|
||||||
|
"pic": "https://example.com/avatar.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
target_author_id = 333
|
||||||
|
custom_subscription_id = 12345
|
||||||
|
|
||||||
|
# Отправляем уведомление с пользовательским ID
|
||||||
|
await notify_follower(
|
||||||
|
follower=follower_data,
|
||||||
|
author_id=target_author_id,
|
||||||
|
action="follow",
|
||||||
|
subscription_id=custom_subscription_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Проверяем, что передается правильный subscription_id
|
||||||
|
assert len(published_data) == 1
|
||||||
|
channel, data = published_data[0]
|
||||||
|
|
||||||
|
assert data["payload"]["id"] == custom_subscription_id
|
||||||
|
assert data["payload"]["follower_id"] == 777
|
||||||
|
assert data["payload"]["following_id"] == target_author_id
|
||||||
|
|
||||||
|
print(f"✅ Передан пользовательский subscription_id: {custom_subscription_id}")
|
||||||
Reference in New Issue
Block a user