test: migrate backend suites to pytest with module coverage gate

Split backend tests into unit and integration suites, add isolated fixtures and coverage enforcement, and update CI and test documentation. Cover current authentication, content, notification, payment, media, and admin flows; fix behaviors revealed by the new tests. Validate both suites and the per-module gate against local PostgreSQL.
This commit is contained in:
2026-09-26 16:50:29 +03:30
parent 3ec28638d1
commit ff99130938
76 changed files with 3271 additions and 1357 deletions

View File

@@ -0,0 +1,64 @@
import pytest
import json
from types import SimpleNamespace
from django.contrib import admin
from django.test import RequestFactory
from apps.notifications.api.views import get_notification, list_notifications
from apps.notifications.services import RedisNotificationStore
from apps.notifications.tasks import send_manual_notification
from apps.users.admin import ManualNotificationForm, UserAdmin
from apps.users.models import User
from core.authentication import create_jwt_token
@pytest.mark.unit
class TestNotificationStoreTests:
def test_summary_hides_full_body_and_advertises_it(self):
notification = RedisNotificationStore._normalize_notification({'id': 'one', 'title': 'Title', 'message': 'Digest', 'body_markdown': '# Full body'})
summary = RedisNotificationStore.summary(notification)
assert summary['has_body']
assert 'body_markdown' not in summary
assert notification['body_markdown'] == '# Full body'
def test_read_filter_applies_before_pagination(self, mocker):
redis_client = mocker.patch('apps.notifications.services.redis_client')
redis_client.zrevrange.return_value = ['one', 'two', 'three']
redis_client.pipeline.return_value.execute.return_value = [json.dumps({'id': 'one', 'is_seen': False}), json.dumps({'id': 'two', 'is_seen': True}), json.dumps({'id': 'three', 'is_seen': False})]
unread, count = RedisNotificationStore.list('user', limit=1, offset=1, read_filter='unread')
assert count == 2
assert [item['id'] for item in unread] == ['three']
read, count = RedisNotificationStore.list('user', read_filter='read')
assert count == 1
assert read[0]['id'] == 'two'
def test_detail_is_scoped_to_authenticated_recipient(self, mocker):
store = mocker.patch('apps.notifications.api.views.RedisNotificationStore')
store.get.side_effect = lambda user_id, _: {'id': 'one', 'body_markdown': '# Secret'} if user_id == '1' else None
owner = SimpleNamespace(auth=SimpleNamespace(id=1))
other = SimpleNamespace(auth=SimpleNamespace(id=2))
assert get_notification(owner, 'one')[0] == 200
assert get_notification(other, 'one')[0] == 404
store.get.assert_any_call('2', 'one')
def test_list_returns_summaries_and_rejects_unknown_status(self, mocker):
store = mocker.patch('apps.notifications.api.views.RedisNotificationStore')
request = SimpleNamespace(auth=SimpleNamespace(id=1))
store.list.return_value = ([{'id': 'one', 'body_markdown': '# Secret'}], 1)
store.summary.side_effect = RedisNotificationStore.summary
store.unread_count.return_value = 1
result = list_notifications(request, status='unread')
assert 'body_markdown' not in result['notifications'][0]
assert store.list.call_args.kwargs['read_filter'] == 'unread'
assert list_notifications(request, status='invalid').status_code == 400
def test_marking_old_ephemeral_notification_read_keeps_it_visible(self, mocker):
redis_client = mocker.patch('apps.notifications.services.redis_client')
get = mocker.patch.object(RedisNotificationStore, 'get')
_count = mocker.patch.object(RedisNotificationStore, 'unread_count', return_value=0)
_publish = mocker.patch.object(RedisNotificationStore, '_publish_event')
get.return_value = {'id': 'one', 'is_seen': False, 'delete_on_seen': True, 'body_markdown': '# Body'}
result = RedisNotificationStore.mark_seen('user', 'one')
assert not result['deleted']
assert result['notification']['is_seen']
redis_client.hset.assert_called_once()
redis_client.pipeline.assert_not_called()