"""Real-Postgres check that the full export is ORDERED, so the backup file deduplicates between runs. Only Postgres shows this. An unordered select returns rows in heap order, and an UPDATE writes the row's new version at the end of the heap — so without an ORDER BY in export_full_backup, editing one old note moves it to the bottom of `notes`. Every byte after its old place shifts, and a restic run over the next backup stores most of the table again. A mocked session returns whatever order it was handed and could never catch it. """ import io import tarfile import pytest import pytest_asyncio from sqlalchemy import select from grimoire.models import async_session from grimoire.models.note import Note from grimoire.models.user import User from grimoire.services import backup from grimoire.services.backup_file import write_archive_to from tests.helpers import ensure_user pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")] USERNAME = "backup_file_ordering_owner" async def _purge() -> None: async with async_session() as s: for user in (await s.execute( select(User).where(User.username == USERNAME) )).scalars().all(): for note in (await s.execute( select(Note).where(Note.user_id == user.id) )).scalars().all(): await s.delete(note) await s.delete(user) await s.commit() @pytest_asyncio.fixture(autouse=True) async def _no_leftovers(): """Setup only — see the rule_version roundtrip module for why a teardown here would break the next test's event loop.""" await _purge() @pytest_asyncio.fixture async def notes(): async with async_session() as s: user = await ensure_user(s, USERNAME) rows = [Note(user_id=user.id, title=f"ordering {n}", body="b" * 100) for n in range(5)] s.add_all(rows) await s.commit() ids = [n.id for n in rows] yield ids await _purge() def _sections(payload: dict) -> dict[str, bytes]: buf = io.BytesIO() write_archive_to(payload, buf, app_version="test") buf.seek(0) with tarfile.open(fileobj=buf, mode="r:") as tar: return {m.name: tar.extractfile(m).read() for m in tar.getmembers() if m.name != "manifest.json"} async def _edit_first(ids: list[int]) -> None: async with async_session() as s: note = await s.get(Note, ids[0]) note.body = "edited after the first export" await s.commit() async def test_an_edited_row_keeps_its_place(notes): await _edit_first(notes) payload = await backup.export_full_backup() exported = [row["id"] for row in payload["notes"]] assert exported == sorted(exported) mine = [i for i in exported if i in set(notes)] assert mine == notes async def test_two_exports_of_unchanged_data_are_the_same_bytes(notes): await _edit_first(notes) first = _sections(await backup.export_full_backup()) second = _sections(await backup.export_full_backup()) assert first.keys() == second.keys() for name in first: assert first[name] == second[name], name