invalidate-new-follower
All checks were successful
Deploy on push / deploy (push) Successful in 5m45s
All checks were successful
Deploy on push / deploy (push) Successful in 5m45s
This commit is contained in:
@@ -15,6 +15,7 @@ from orm.author import Author, AuthorFollower
|
|||||||
from orm.community import Community, CommunityFollower
|
from orm.community import Community, CommunityFollower
|
||||||
from orm.shout import Shout, ShoutReactionsFollower
|
from orm.shout import Shout, ShoutReactionsFollower
|
||||||
from orm.topic import Topic, TopicFollower
|
from orm.topic import Topic, TopicFollower
|
||||||
|
from resolvers.author import invalidate_authors_cache
|
||||||
from services.auth import login_required
|
from services.auth import login_required
|
||||||
from services.notify import notify_follower
|
from services.notify import notify_follower
|
||||||
from storage.db import local_session
|
from storage.db import local_session
|
||||||
@@ -133,6 +134,10 @@ async def follow(
|
|||||||
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")
|
await notify_follower(follower=follower_dict, author_id=entity_id, action="follow")
|
||||||
|
|
||||||
|
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
|
||||||
|
logger.debug("Инвалидируем кеш статистики авторов")
|
||||||
|
await invalidate_authors_cache(entity_id)
|
||||||
|
|
||||||
# Всегда получаем актуальный список подписок для возврата клиенту
|
# Всегда получаем актуальный список подписок для возврата клиенту
|
||||||
if get_cached_follows_method and isinstance(follower_id, int):
|
if get_cached_follows_method and isinstance(follower_id, int):
|
||||||
logger.debug("Получение актуального списка подписок из кэша")
|
logger.debug("Получение актуального списка подписок из кэша")
|
||||||
@@ -260,6 +265,10 @@ async def unfollow(
|
|||||||
if what == "AUTHOR" and isinstance(follower_dict, dict):
|
if what == "AUTHOR" and isinstance(follower_dict, dict):
|
||||||
await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow")
|
await notify_follower(follower=follower_dict, author_id=entity_id, action="unfollow")
|
||||||
|
|
||||||
|
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
|
||||||
|
logger.debug("Инвалидируем кеш статистики авторов после отписки")
|
||||||
|
await invalidate_authors_cache(entity_id)
|
||||||
|
|
||||||
return {f"{entity_type}s": follows, "error": None}
|
return {f"{entity_type}s": follows, "error": None}
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
162
tests/test_follower_counters.py
Normal file
162
tests/test_follower_counters.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Тест обновления счетчиков подписчиков в статистике авторов
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from resolvers.author import get_authors_with_stats, invalidate_authors_cache
|
||||||
|
from resolvers.stat import get_followers_count
|
||||||
|
from orm.author import Author, AuthorFollower
|
||||||
|
from storage.db import local_session
|
||||||
|
from storage.redis import redis
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_follower_counters_update():
|
||||||
|
"""
|
||||||
|
Тест обновления счетчиков подписчиков после инвалидации кеша
|
||||||
|
"""
|
||||||
|
# Создаем тестовых пользователей
|
||||||
|
with local_session() as session:
|
||||||
|
# Создаем автора, у которого будут подписчики
|
||||||
|
target_author = Author(
|
||||||
|
name="Popular Author",
|
||||||
|
slug=f"popular-author-{int(time.time())}",
|
||||||
|
email=f"popular-{int(time.time())}@test.com"
|
||||||
|
)
|
||||||
|
session.add(target_author)
|
||||||
|
|
||||||
|
# Создаем подписчиков
|
||||||
|
follower1 = Author(
|
||||||
|
name="Follower 1",
|
||||||
|
slug=f"follower-1-{int(time.time())}",
|
||||||
|
email=f"follower-1-{int(time.time())}@test.com"
|
||||||
|
)
|
||||||
|
session.add(follower1)
|
||||||
|
|
||||||
|
follower2 = Author(
|
||||||
|
name="Follower 2",
|
||||||
|
slug=f"follower-2-{int(time.time())}",
|
||||||
|
email=f"follower-2-{int(time.time())}@test.com"
|
||||||
|
)
|
||||||
|
session.add(follower2)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
target_author_id = target_author.id
|
||||||
|
follower1_id = follower1.id
|
||||||
|
follower2_id = follower2.id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Очищаем кеш авторов
|
||||||
|
await invalidate_authors_cache()
|
||||||
|
|
||||||
|
# 2. Получаем начальную статистику (должно быть 0 подписчиков)
|
||||||
|
initial_stats = await get_authors_with_stats(limit=100, offset=0)
|
||||||
|
target_stats = next((author for author in initial_stats if author["id"] == target_author_id), None)
|
||||||
|
|
||||||
|
assert target_stats is not None, "Автор должен быть найден в статистике"
|
||||||
|
assert target_stats["stat"]["followers"] == 0, "Изначально подписчиков быть не должно"
|
||||||
|
|
||||||
|
# 3. Добавляем подписчика в БД
|
||||||
|
with local_session() as session:
|
||||||
|
subscription1 = AuthorFollower(
|
||||||
|
follower=follower1_id,
|
||||||
|
following=target_author_id
|
||||||
|
)
|
||||||
|
session.add(subscription1)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# 4. Проверяем, что прямой запрос к БД возвращает 1 подписчика
|
||||||
|
direct_count = get_followers_count("author", target_author_id)
|
||||||
|
assert direct_count == 1, "Прямой запрос должен вернуть 1 подписчика"
|
||||||
|
|
||||||
|
# 5. Инвалидируем кеш (имитируя логику follow)
|
||||||
|
await invalidate_authors_cache(target_author_id)
|
||||||
|
|
||||||
|
# 6. Получаем обновленную статистику
|
||||||
|
updated_stats = await get_authors_with_stats(limit=100, offset=0)
|
||||||
|
updated_target_stats = next((author for author in updated_stats if author["id"] == target_author_id), None)
|
||||||
|
|
||||||
|
assert updated_target_stats is not None, "Автор должен быть найден после обновления"
|
||||||
|
assert updated_target_stats["stat"]["followers"] == 1, "После инвалидации кеша должен быть 1 подписчик"
|
||||||
|
|
||||||
|
# 7. Добавляем второго подписчика
|
||||||
|
with local_session() as session:
|
||||||
|
subscription2 = AuthorFollower(
|
||||||
|
follower=follower2_id,
|
||||||
|
following=target_author_id
|
||||||
|
)
|
||||||
|
session.add(subscription2)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# 8. Инвалидируем кеш снова
|
||||||
|
await invalidate_authors_cache(target_author_id)
|
||||||
|
|
||||||
|
# 9. Проверяем финальную статистику
|
||||||
|
final_stats = await get_authors_with_stats(limit=100, offset=0)
|
||||||
|
final_target_stats = next((author for author in final_stats if author["id"] == target_author_id), None)
|
||||||
|
|
||||||
|
assert final_target_stats is not None, "Автор должен быть найден в финальной статистике"
|
||||||
|
assert final_target_stats["stat"]["followers"] == 2, "В финальной статистике должно быть 2 подписчика"
|
||||||
|
|
||||||
|
print("✅ Тест обновления счетчиков подписчиков прошел успешно!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Очистка тестовых данных
|
||||||
|
with local_session() as session:
|
||||||
|
session.query(AuthorFollower).filter(
|
||||||
|
AuthorFollower.following == target_author_id
|
||||||
|
).delete()
|
||||||
|
session.query(Author).filter(Author.id.in_([target_author_id, follower1_id, follower2_id])).delete()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Очищаем кеш
|
||||||
|
await invalidate_authors_cache()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_follower_counter_edge_cases():
|
||||||
|
"""
|
||||||
|
Тест крайних случаев для счетчиков подписчиков
|
||||||
|
"""
|
||||||
|
with local_session() as session:
|
||||||
|
author = Author(
|
||||||
|
name="Edge Case Author",
|
||||||
|
slug=f"edge-case-{int(time.time())}",
|
||||||
|
email=f"edge-case-{int(time.time())}@test.com"
|
||||||
|
)
|
||||||
|
session.add(author)
|
||||||
|
session.commit()
|
||||||
|
author_id = author.id
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Тест 1: Автор без подписчиков
|
||||||
|
await invalidate_authors_cache()
|
||||||
|
stats = await get_authors_with_stats(limit=100, offset=0)
|
||||||
|
author_stats = next((a for a in stats if a["id"] == author_id), None)
|
||||||
|
|
||||||
|
if author_stats: # Автор может не попасть в топ-100, это нормально
|
||||||
|
assert author_stats["stat"]["followers"] == 0, "Автор без подписчиков должен иметь 0 в счетчике"
|
||||||
|
|
||||||
|
# Тест 2: Проверяем прямой запрос для несуществующего автора
|
||||||
|
nonexistent_count = get_followers_count("author", 999999)
|
||||||
|
assert nonexistent_count == 0, "Несуществующий автор должен иметь 0 подписчиков"
|
||||||
|
|
||||||
|
print("✅ Тест крайних случаев прошел успешно!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
with local_session() as session:
|
||||||
|
session.query(Author).filter(Author.id == author_id).delete()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
await invalidate_authors_cache()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(test_follower_counters_update())
|
||||||
|
asyncio.run(test_follower_counter_edge_cases())
|
||||||
|
print("🎯 Все тесты счетчиков подписчиков прошли успешно!")
|
||||||
Reference in New Issue
Block a user