following-debug
All checks were successful
Deploy on push / deploy (push) Successful in 5m46s

This commit is contained in:
2025-08-30 18:23:15 +03:00
parent f6253f2007
commit f891b73608
4 changed files with 408 additions and 2 deletions

View File

@@ -0,0 +1,174 @@
"""
Простой тест кеша подписок без GraphQL и авторизации
"""
from __future__ import annotations
import asyncio
import time
import pytest
from cache.cache import get_cached_follower_authors
from orm.author import Author, AuthorFollower
from storage.db import local_session
from storage.redis import redis
@pytest.mark.asyncio
async def test_cache_invalidation_logic():
"""
Тест логики инвалидации кеша при прямых операциях с БД
"""
# Создаем тестовых пользователей
with local_session() as session:
follower = Author(
name="Cache Test Follower",
slug=f"cache-test-follower-{int(time.time())}",
email=f"cache-follower-{int(time.time())}@test.com"
)
session.add(follower)
target_author = Author(
name="Cache Test Target",
slug=f"cache-test-target-{int(time.time())}",
email=f"cache-target-{int(time.time())}@test.com"
)
session.add(target_author)
session.commit()
follower_id = follower.id
target_author_id = target_author.id
try:
# 1. Очищаем кеш
cache_key = f"author:follows-authors:{follower_id}"
await redis.execute("DEL", cache_key)
# 2. Проверяем начальное состояние (пустой список)
initial_follows = await get_cached_follower_authors(follower_id)
assert len(initial_follows) == 0, "Изначально подписок быть не должно"
# 3. Создаем подписку в БД напрямую
with local_session() as session:
subscription = AuthorFollower(
follower=follower_id,
following=target_author_id
)
session.add(subscription)
session.commit()
# 4. Инвалидируем кеш (имитируя логику follow)
await redis.execute("DEL", cache_key)
# 5. Проверяем, что кеш обновился
after_follow_follows = await get_cached_follower_authors(follower_id)
assert len(after_follow_follows) == 1, "После подписки должна быть 1 запись"
assert after_follow_follows[0]["id"] == target_author_id, "ID должен совпадать"
# 6. Проверяем, что второй запрос берется из кеша
cached_follows = await get_cached_follower_authors(follower_id)
assert len(cached_follows) == 1, "Кеш должен содержать 1 запись"
assert cached_follows[0]["id"] == target_author_id, "ID в кеше должен совпадать"
# 7. Удаляем подписку из БД
with local_session() as session:
session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id,
AuthorFollower.following == target_author_id
).delete()
session.commit()
# 8. Инвалидируем кеш (имитируя логику unfollow)
await redis.execute("DEL", cache_key)
# 9. Проверяем, что кеш снова пустой
after_unfollow_follows = await get_cached_follower_authors(follower_id)
assert len(after_unfollow_follows) == 0, "После отписки кеш должен быть пустым"
print("✅ Тест логики кеширования прошел успешно!")
finally:
# Очистка
with local_session() as session:
session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id
).delete()
session.query(Author).filter(Author.id.in_([follower_id, target_author_id])).delete()
session.commit()
await redis.execute("DEL", cache_key)
@pytest.mark.asyncio
async def test_cache_miss_behavior():
"""
Тест поведения при промахе кеша - данные должны браться из БД
"""
with local_session() as session:
follower = Author(
name="Cache Miss Test",
slug=f"cache-miss-{int(time.time())}",
email=f"cache-miss-{int(time.time())}@test.com"
)
session.add(follower)
target1 = Author(
name="Target 1",
slug=f"target-1-{int(time.time())}",
email=f"target-1-{int(time.time())}@test.com"
)
session.add(target1)
target2 = Author(
name="Target 2",
slug=f"target-2-{int(time.time())}",
email=f"target-2-{int(time.time())}@test.com"
)
session.add(target2)
session.commit()
follower_id = follower.id
target1_id = target1.id
target2_id = target2.id
# Создаем подписки в БД
sub1 = AuthorFollower(follower=follower_id, following=target1_id)
sub2 = AuthorFollower(follower=follower_id, following=target2_id)
session.add_all([sub1, sub2])
session.commit()
try:
cache_key = f"author:follows-authors:{follower_id}"
# Убеждаемся, что кеша нет
await redis.execute("DEL", cache_key)
# Запрашиваем данные (должно произойти cache miss и загрузка из БД)
follows = await get_cached_follower_authors(follower_id)
assert len(follows) == 2, "Должно быть 2 подписки"
follow_ids = {f["id"] for f in follows}
assert target1_id in follow_ids, "Должна быть подписка на target1"
assert target2_id in follow_ids, "Должна быть подписка на target2"
# Второй запрос должен брать из кеша
cached_follows = await get_cached_follower_authors(follower_id)
assert len(cached_follows) == 2, "Кеш должен содержать 2 записи"
print("✅ Тест cache miss поведения прошел успешно!")
finally:
# Очистка
with local_session() as session:
session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id
).delete()
session.query(Author).filter(Author.id.in_([follower_id, target1_id, target2_id])).delete()
session.commit()
await redis.execute("DEL", cache_key)
if __name__ == "__main__":
asyncio.run(test_cache_invalidation_logic())
asyncio.run(test_cache_miss_behavior())
print("🎯 Все тесты кеша прошли успешно!")

View File

@@ -0,0 +1,221 @@
"""
Тест консистентности кеша подписок после операций follow/unfollow
"""
from __future__ import annotations
import asyncio
import pytest
import time
from unittest.mock import patch
from cache.cache import get_cached_follower_authors
from orm.author import Author, AuthorFollower
from resolvers.follower import follow, unfollow
from storage.db import local_session
from storage.redis import redis
class MockRequest:
"""Mock объект для HTTP request"""
def __init__(self):
self.method = "POST"
self.url = type('MockURL', (), {"path": "/graphql"})()
class MockGraphQLResolveInfo:
"""Mock объект для GraphQL resolve info"""
def __init__(self, author_id: int):
self.context = {
"author": {"id": author_id},
"request": MockRequest()
}
@pytest.mark.asyncio
async def test_follow_cache_consistency():
"""
Тест консистентности кеша после операции подписки:
1. Подписываемся на автора
2. Проверяем, что кеш корректно инвалидирован и обновлен
3. Проверяем, что следующий запрос возвращает актуальные данные
"""
# Создаем тестовых пользователей
with local_session() as session:
# Создаем подписчика
follower = Author(
name="Test Follower",
slug=f"test-follower-{int(time.time())}",
email=f"follower-{int(time.time())}@test.com"
)
session.add(follower)
# Создаем автора для подписки
target_author = Author(
name="Target Author",
slug=f"target-author-{int(time.time())}",
email=f"target-{int(time.time())}@test.com"
)
session.add(target_author)
session.commit()
follower_id = follower.id
target_author_id = target_author.id
target_author_slug = target_author.slug
try:
# Очищаем кеш перед тестом
cache_key = f"author:follows-authors:{follower_id}"
await redis.execute("DEL", cache_key)
# 1. Проверяем начальное состояние (пустой список подписок)
initial_follows = await get_cached_follower_authors(follower_id)
assert len(initial_follows) == 0, "Изначально подписок быть не должно"
# 2. Выполняем подписку (обходим авторизацию)
mock_info = MockGraphQLResolveInfo(follower_id)
# Патчим декоратор авторизации
with patch('resolvers.follower.login_required', lambda func: func):
result = await follow(
None,
mock_info,
what="AUTHOR",
slug=target_author_slug
)
# 3. Проверяем результат операции
assert result.get("error") is None, f"Операция подписки завершилась с ошибкой: {result.get('error')}"
returned_authors = result.get("authors", [])
assert len(returned_authors) == 1, "Должен вернуться 1 автор в списке подписок"
assert any(
author.get("id") == target_author_id for author in returned_authors
), "Целевой автор должен быть в списке подписок"
# 4. Проверяем кеш напрямую (должен быть обновлен)
fresh_follows = await get_cached_follower_authors(follower_id)
assert len(fresh_follows) == 1, "Кеш должен содержать 1 подписку"
assert fresh_follows[0]["id"] == target_author_id, "ID автора в кеше должен совпадать"
# 5. Проверяем консистентность с БД
with local_session() as session:
db_follows = session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id,
AuthorFollower.following == target_author_id
).all()
assert len(db_follows) == 1, "В БД должна быть запись о подписке"
# 6. Тестируем отписку (обходим авторизацию)
with patch('resolvers.follower.login_required', lambda func: func):
unfollow_result = await unfollow(
None,
mock_info,
what="AUTHOR",
slug=target_author_slug
)
assert unfollow_result.get("error") is None, f"Операция отписки завершилась с ошибкой: {unfollow_result.get('error')}"
# 7. Проверяем кеш после отписки
after_unfollow_follows = await get_cached_follower_authors(follower_id)
assert len(after_unfollow_follows) == 0, "После отписки кеш должен быть пустым"
# 8. Проверяем БД после отписки
with local_session() as session:
db_follows_after = session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id,
AuthorFollower.following == target_author_id
).all()
assert len(db_follows_after) == 0, "В БД не должно быть записи о подписке после отписки"
finally:
# Очистка тестовых данных
with local_session() as session:
# Удаляем подписки
session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id
).delete()
# Удаляем авторов
session.query(Author).filter(Author.id.in_([follower_id, target_author_id])).delete()
session.commit()
# Очищаем кеш
await redis.execute("DEL", f"author:follows-authors:{follower_id}")
@pytest.mark.asyncio
async def test_follow_already_following():
"""
Тест попытки повторной подписки на того же автора
"""
# Создаем тестовых пользователей
with local_session() as session:
follower = Author(
name="Test Follower 2",
slug=f"test-follower-2-{int(time.time())}",
email=f"follower-2-{int(time.time())}@test.com"
)
session.add(follower)
target_author = Author(
name="Target Author 2",
slug=f"target-author-2-{int(time.time())}",
email=f"target-2-{int(time.time())}@test.com"
)
session.add(target_author)
session.commit()
follower_id = follower.id
target_author_id = target_author.id
target_author_slug = target_author.slug
# Создаем изначальную подписку
subscription = AuthorFollower(
follower=follower_id,
following=target_author_id
)
session.add(subscription)
session.commit()
try:
# Очищаем кеш
cache_key = f"author:follows-authors:{follower_id}"
await redis.execute("DEL", cache_key)
# Пытаемся подписаться повторно (обходим авторизацию)
mock_info = MockGraphQLResolveInfo(follower_id)
with patch('resolvers.follower.login_required', lambda func: func):
result = await follow(
None,
mock_info,
what="AUTHOR",
slug=target_author_slug
)
# Должна вернуться ошибка "already following"
assert result.get("error") == "already following", "Должна быть ошибка повторной подписки"
# Но список авторов должен содержать целевого автора
returned_authors = result.get("authors", [])
assert len(returned_authors) == 1, "Должен вернуться список с 1 автором"
assert any(
author.get("id") == target_author_id for author in returned_authors
), "Целевой автор должен быть в списке"
finally:
# Очистка
with local_session() as session:
session.query(AuthorFollower).filter(
AuthorFollower.follower == follower_id
).delete()
session.query(Author).filter(Author.id.in_([follower_id, target_author_id])).delete()
session.commit()
await redis.execute("DEL", f"author:follows-authors:{follower_id}")
if __name__ == "__main__":
# Запуск тестов напрямую
asyncio.run(test_follow_cache_consistency())
asyncio.run(test_follow_already_following())
print("Все тесты консистентности кеша прошли успешно!")