ppt-tool/backend/tests/test_retention_service.py
Vadym Samoilenko bdf6e4b4d0 Fix Docker build, test suite, and runtime issues for local deployment
- Fix UV index strategy: mark PyTorch CPU index as explicit with name
- Add --index-strategy unsafe-best-match to Dockerfile uv pip install
- Fix redis version constraint (>=5.0,<6) for ARQ compatibility
- Fix Anthropic model name (claude-sonnet-4-5-20250929)
- Fix IMAGE_PROVIDER enum value (gemini_flash, not google)
- Resolve middlewares.py vs middlewares/ package conflict
- Fix worker import paths (models.sql.presentation, models.sql.slide, utils split)
- Fix seed script FK resolution by importing all related models
- Fix test suite: async fixture scoping, greenlet dep, regex patterns, fixture params
- Fix frontend TypeScript error (Boolean cast for layout.react_code)
- Regenerate package-lock.json with i18n packages
- Add initial Alembic migration (autogenerated from all models)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 17:56:30 +00:00

127 lines
4.4 KiB
Python

"""Tests for RetentionService: cleanup and purge logic.
Uses mocked async_session_maker since RetentionService creates its own sessions.
"""
import uuid
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from models.sql.client import ClientModel
from models.sql.presentation import PresentationModel
from services.retention_service import RetentionService
@pytest.fixture
def svc():
return RetentionService()
class TestRunCleanup:
async def test_soft_deletes_expired_presentations(
self, svc, session, make_client, make_presentation, make_user
):
"""Presentations older than client.retention_days get soft-deleted."""
client = await make_client(name="RetCorp", slug="retcorp", retention_days=30)
user = await make_user(email="ret@test.com")
# Old presentation (60 days ago) — should be deleted
old = await make_presentation(
owner_id=user.id,
client_id=client.id,
title="Old Deck",
created_at=datetime.now(timezone.utc) - timedelta(days=60),
)
# Recent presentation — should survive
recent = await make_presentation(
owner_id=user.id,
client_id=client.id,
title="New Deck",
created_at=datetime.now(timezone.utc) - timedelta(days=5),
)
# Mock async_session_maker to use our test session
with patch("services.retention_service.async_session_maker") as mock_sm:
mock_sm.return_value.__aenter__ = AsyncMock(return_value=session)
mock_sm.return_value.__aexit__ = AsyncMock(return_value=False)
result = await svc.run_cleanup()
assert result["deleted"] == 1
await session.refresh(old)
await session.refresh(recent)
assert old.deleted_at is not None
assert recent.deleted_at is None
async def test_no_clients_with_retention_policy(self, session, make_client):
"""Clients without retention_days are skipped."""
await make_client(name="NoPol", slug="nopol", retention_days=None)
with patch("services.retention_service.async_session_maker") as mock_sm:
mock_sm.return_value.__aenter__ = AsyncMock(return_value=session)
mock_sm.return_value.__aexit__ = AsyncMock(return_value=False)
result = await RetentionService().run_cleanup()
assert result["deleted"] == 0
class TestPurgeSoftDeleted:
async def test_purges_old_soft_deleted(
self, session, make_client, make_presentation, make_user
):
"""Soft-deleted presentations older than threshold get permanently deleted."""
client = await make_client(name="Purge Corp", slug="purge-corp")
user = await make_user(email="purge@test.com")
# Soft-deleted 45 days ago — should be purged
pres = await make_presentation(
owner_id=user.id,
client_id=client.id,
title="Dead Deck",
deleted_at=datetime.now(timezone.utc) - timedelta(days=45),
)
with patch("services.retention_service.async_session_maker") as mock_sm:
mock_sm.return_value.__aenter__ = AsyncMock(return_value=session)
mock_sm.return_value.__aexit__ = AsyncMock(return_value=False)
result = await RetentionService().purge_soft_deleted(days_after_soft_delete=30)
assert result["purged"] == 1
# Should be gone from DB
deleted = await session.get(PresentationModel, pres.id)
assert deleted is None
class TestCleanupFiles:
def test_cleanup_files_removes_existing_files(self, svc, tmp_path):
"""_cleanup_files removes files listed in presentation.file_paths."""
f = tmp_path / "test.pptx"
f.write_text("dummy")
pres = MagicMock()
pres.id = uuid.uuid4()
pres.file_paths = [str(f)]
svc._cleanup_files(pres)
assert not f.exists()
def test_cleanup_files_handles_missing_files(self, svc):
"""Missing files don't raise errors."""
pres = MagicMock()
pres.id = uuid.uuid4()
pres.file_paths = ["/nonexistent/file.pptx"]
# Should not raise
svc._cleanup_files(pres)
def test_cleanup_files_handles_none(self, svc):
pres = MagicMock()
pres.id = uuid.uuid4()
pres.file_paths = None
svc._cleanup_files(pres)