Files
bambuddy/backend/tests/integration/test_archive_purge_api.py
T
maziggy 689bc04d7b ● feat(#1008): honour reprint dates in archive purge + clarify purge UX
Fix a silent correctness bug: archive purge used `created_at` which is
  pinned to the first print, so reprinting a two-year-old archive yesterday
  would still make it eligible for a 365-day purge. The preview and purge
  queries now age each archive by `COALESCE(completed_at, started_at,
  created_at)` — reprints refresh the clock.

  Also flesh out both purge modals (File Manager + Archives) with an
  explicit "What happens when you click Purge" effects list so users see
  upfront that library files go to Trash (reversible) while archives are
  hard-deleted (irreversible), plus what disk artefacts get removed.

  Backend:
  - services/archive_purge.py: `_last_activity_expr()` helper used by
    preview, purge, and sample query
  - tests/integration/test_archive_purge_api.py: new test covering the
    reprinted-archive case

  Frontend:
  - PurgeOldFilesModal / PurgeArchivesModal: new effects bullet list
  - i18n: reprint-aware ageLabel/description/warning and effects bullets
    across all 8 locales (en/de fully translated, rest English fallback)

  Docs:
  - wiki/features/archiving.md: "How old is measured" note + effects list
  - wiki/features/file-manager.md: "What happens when you click Purge"
    section + explicit age-rule breakdown
  - CHANGELOG: archive auto-purge entry rewritten to mention reprint
    semantics, `archives:purge` permission backfill, and updated test count
2026-04-23 17:38:41 +02:00

186 lines
7.1 KiB
Python

"""Integration tests for archive auto-purge (#1008 follow-up)."""
from datetime import datetime, timedelta, timezone
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
@pytest.mark.integration
async def test_settings_defaults_when_unset(async_client: AsyncClient):
"""GET /archives/purge/settings returns sensible defaults on a fresh install."""
resp = await async_client.get("/api/v1/archives/purge/settings")
assert resp.status_code == 200
body = resp.json()
assert body["enabled"] is False
assert body["days"] == 365
@pytest.mark.asyncio
@pytest.mark.integration
async def test_settings_roundtrip(async_client: AsyncClient):
"""PUT persists, GET returns the saved values, days is clamped."""
resp = await async_client.put(
"/api/v1/archives/purge/settings",
json={"enabled": True, "days": 180},
)
assert resp.status_code == 200
assert resp.json() == {"enabled": True, "days": 180}
resp = await async_client.get("/api/v1/archives/purge/settings")
assert resp.json() == {"enabled": True, "days": 180}
@pytest.mark.asyncio
@pytest.mark.integration
async def test_settings_rejects_out_of_range_days(async_client: AsyncClient):
"""days below MIN or above MAX is rejected."""
resp = await async_client.put(
"/api/v1/archives/purge/settings",
json={"enabled": True, "days": 1},
)
# Pydantic validation returns 422; explicit bound check returns 400.
assert resp.status_code in (400, 422)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_preview_counts_old_archives(async_client: AsyncClient, archive_factory, printer_factory, db_session):
"""Preview returns the count + total bytes of archives older than the threshold."""
printer = await printer_factory()
old = await archive_factory(printer.id, print_name="Old", file_size=1000)
fresh = await archive_factory(printer.id, print_name="Fresh", file_size=2000)
old.created_at = datetime.now(timezone.utc) - timedelta(days=400)
fresh.created_at = datetime.now(timezone.utc) - timedelta(days=10)
await db_session.commit()
resp = await async_client.get("/api/v1/archives/purge/preview?older_than_days=365")
assert resp.status_code == 200
body = resp.json()
assert body["count"] == 1
assert body["total_bytes"] == 1000
assert "Old" in body["sample_filenames"][0] or old.filename in body["sample_filenames"]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_preview_ignores_recently_reprinted_archives(
async_client: AsyncClient, archive_factory, printer_factory, db_session
):
"""Reprints update completed_at but leave created_at pinned; purge must honour that."""
printer = await printer_factory()
reprinted = await archive_factory(printer.id, print_name="Reprinted", file_size=1000)
# Originally printed 400 days ago, but a reprint last week refreshed completed_at.
reprinted.created_at = datetime.now(timezone.utc) - timedelta(days=400)
reprinted.started_at = datetime.now(timezone.utc) - timedelta(days=7)
reprinted.completed_at = datetime.now(timezone.utc) - timedelta(days=7)
await db_session.commit()
resp = await async_client.get("/api/v1/archives/purge/preview?older_than_days=365")
assert resp.status_code == 200
body = resp.json()
assert body["count"] == 0
@pytest.mark.asyncio
@pytest.mark.integration
async def test_manual_purge_deletes_old_archives(
async_client: AsyncClient, archive_factory, printer_factory, db_session
):
"""POST /archives/purge hard-deletes archives older than the threshold."""
from backend.app.models.archive import PrintArchive
printer = await printer_factory()
old = await archive_factory(printer.id, print_name="Old")
fresh = await archive_factory(printer.id, print_name="Fresh")
old_id = old.id
fresh_id = fresh.id
old.created_at = datetime.now(timezone.utc) - timedelta(days=400)
fresh.created_at = datetime.now(timezone.utc) - timedelta(days=10)
await db_session.commit()
resp = await async_client.post(
"/api/v1/archives/purge",
json={"older_than_days": 365},
)
assert resp.status_code == 200
assert resp.json()["deleted"] == 1
# Old is gone, fresh remains.
db_session.expire_all()
assert await db_session.get(PrintArchive, old_id) is None
assert await db_session.get(PrintArchive, fresh_id) is not None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_auto_purge_runs_when_enabled(async_client: AsyncClient, archive_factory, printer_factory, db_session):
"""With the toggle on, a stale archive is hard-deleted by the sweeper.
``async_client`` is included solely so its fixture activates the module-level
``async_session`` patches that let :meth:`purge_older_than`'s per-row
delete sessions reach the in-memory test database.
"""
from backend.app.models.archive import PrintArchive
from backend.app.services.archive_purge import archive_purge_service
printer = await printer_factory()
stale = await archive_factory(printer.id, print_name="Stale")
stale_id = stale.id
stale.created_at = datetime.now(timezone.utc) - timedelta(days=400)
await db_session.commit()
await archive_purge_service.set_settings(db_session, enabled=True, days=365)
deleted = await archive_purge_service._maybe_run_auto_purge(db_session)
assert deleted >= 1
db_session.expire_all()
assert await db_session.get(PrintArchive, stale_id) is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_auto_purge_throttles_within_24h(async_client: AsyncClient, archive_factory, printer_factory, db_session):
"""A recent last-run timestamp blocks the sweeper for 24h."""
from backend.app.services.archive_purge import archive_purge_service
printer = await printer_factory()
stale = await archive_factory(printer.id, print_name="Stale")
stale.created_at = datetime.now(timezone.utc) - timedelta(days=400)
await db_session.commit()
await archive_purge_service.set_settings(db_session, enabled=True, days=365)
# Stamp a last-run time 1h ago — should block the sweeper for another 23h.
await archive_purge_service._stamp_last_run(db_session, datetime.now(timezone.utc) - timedelta(hours=1))
deleted = await archive_purge_service._maybe_run_auto_purge(db_session)
assert deleted == 0
@pytest.mark.asyncio
@pytest.mark.integration
async def test_auto_purge_skipped_when_disabled(
async_client: AsyncClient, archive_factory, printer_factory, db_session
):
"""When the toggle is off, old archives stay put."""
from backend.app.models.archive import PrintArchive
from backend.app.services.archive_purge import archive_purge_service
printer = await printer_factory()
stale = await archive_factory(printer.id, print_name="Stale")
stale_id = stale.id
stale.created_at = datetime.now(timezone.utc) - timedelta(days=400)
await db_session.commit()
await archive_purge_service.set_settings(db_session, enabled=False, days=365)
deleted = await archive_purge_service._maybe_run_auto_purge(db_session)
assert deleted == 0
db_session.expire_all()
assert await db_session.get(PrintArchive, stale_id) is not None