test(6849): the restore command previews without --yes and rolls back from the tar with it
CI & Build / Python lint (push) Successful in 1s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / integration (push) Successful in 1m51s
CI & Build / Build & push image (push) Successful in 16s
CI & Build / Python lint (push) Successful in 1s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / integration (push) Successful in 1m51s
CI & Build / Build & push image (push) Successful in 16s
Two integration tests in test_integration_backup_rollback.py drive grimoire.restore.run on the scheduled backup's tar, written from a full export of this instance: - without --yes it exits 4 and nothing changes; - with --yes it takes the safety backup, then puts back the edited and the deleted task and removes the new one. The file-restore test that expected a refusal "because the database has accounts" now names the reason it actually refuses: on a live instance, a backup that records no instance id can't be rolled back to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,8 @@ the real `restore_full_backup`.
|
|||||||
The unit lane already shows the tar hands back the dict it was given. What
|
The unit lane already shows the tar hands back the dict it was given. What
|
||||||
only this lane shows is that the dict it hands back is one the restore
|
only this lane shows is that the dict it hands back is one the restore
|
||||||
accepts — every id remapped to the restored rows, nothing dropped — and that
|
accepts — every id remapped to the restored rows, nothing dropped — and that
|
||||||
the command-line restore refuses a database that already has accounts.
|
the command-line restore refuses, on a live instance, a backup that doesn't
|
||||||
|
record this instance as its source.
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
@@ -97,10 +98,11 @@ async def test_the_tar_restores_with_every_id_remapped(written):
|
|||||||
assert note.status == "todo"
|
assert note.status == "todo"
|
||||||
|
|
||||||
|
|
||||||
async def test_the_command_refuses_a_database_with_accounts(written):
|
async def test_the_command_refuses_a_backup_from_no_known_instance(written):
|
||||||
"""The source user exists, so this database is not empty: the command
|
"""This database is live and the backup names no instance it came from,
|
||||||
must stop before restoring anything."""
|
so it can't be rolled back to: the command stops before restoring
|
||||||
assert await run(written) == 3
|
anything, even with --yes."""
|
||||||
|
assert await run(written, yes=True) == 3
|
||||||
async with async_session() as s:
|
async with async_session() as s:
|
||||||
restored = (await s.execute(select(User).where(User.username == RESTORED))).scalars().all()
|
restored = (await s.execute(select(User).where(User.username == RESTORED))).scalars().all()
|
||||||
assert restored == []
|
assert restored == []
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ The lane shares one database, so every test here takes its backup of the
|
|||||||
whole store, changes its own rows, and rolls back to that backup: what it
|
whole store, changes its own rows, and rolls back to that backup: what it
|
||||||
puts back is exactly what was there when it started.
|
puts back is exactly what was there when it started.
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,8 +15,10 @@ from grimoire.models import async_session
|
|||||||
from grimoire.models.note import Note
|
from grimoire.models.note import Note
|
||||||
from grimoire.models.project import Project
|
from grimoire.models.project import Project
|
||||||
from grimoire.models.user import User
|
from grimoire.models.user import User
|
||||||
from grimoire.services import backup
|
from grimoire.restore import run
|
||||||
|
from grimoire.services import backup, backup_job
|
||||||
from grimoire.services.api_keys import create_api_key, lookup_key
|
from grimoire.services.api_keys import create_api_key, lookup_key
|
||||||
|
from grimoire.services.backup_file import write_archive
|
||||||
from grimoire.services.instance import adopt_instance_id, get_instance_id
|
from grimoire.services.instance import adopt_instance_id, get_instance_id
|
||||||
from tests.helpers import ensure_user
|
from tests.helpers import ensure_user
|
||||||
|
|
||||||
@@ -176,3 +179,36 @@ async def test_a_fresh_instance_adopts_the_backups_instance_id(seeded):
|
|||||||
async with async_session() as s:
|
async with async_session() as s:
|
||||||
await adopt_instance_id(s, own)
|
await adopt_instance_id(s, own)
|
||||||
await s.commit()
|
await s.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_command_only_previews_without_yes(seeded, tmp_path):
|
||||||
|
ids, payload, _ = seeded
|
||||||
|
path = write_archive(payload, tmp_path, datetime.now(timezone.utc), app_version="test")
|
||||||
|
new_id = await _change_everything(ids)
|
||||||
|
|
||||||
|
assert await run(path) == 4
|
||||||
|
|
||||||
|
async with async_session() as s:
|
||||||
|
assert (await s.get(Note, ids["kept"])).title == "after"
|
||||||
|
assert await s.get(Note, new_id) is not None
|
||||||
|
await backup.replace_from_backup(payload)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_command_rolls_back_from_the_tar_with_yes(seeded, tmp_path):
|
||||||
|
"""The scheduled backup's own file, read back through the command: the
|
||||||
|
whole store returns to it, after the safety backup is taken."""
|
||||||
|
ids, payload, _ = seeded
|
||||||
|
path = write_archive(payload, tmp_path, datetime.now(timezone.utc), app_version="test")
|
||||||
|
new_id = await _change_everything(ids)
|
||||||
|
saved = {"file": "grimoire-safety.tar", "size_bytes": 1, "where": str(tmp_path)}
|
||||||
|
|
||||||
|
with patch.object(backup_job, "save_state_before_restore",
|
||||||
|
AsyncMock(return_value=saved)) as save:
|
||||||
|
assert await run(path, yes=True) == 0
|
||||||
|
|
||||||
|
save.assert_awaited_once()
|
||||||
|
async with async_session() as s:
|
||||||
|
kept = await s.get(Note, ids["kept"])
|
||||||
|
assert (kept.title, kept.body) == ("before", "as backed up")
|
||||||
|
assert await s.get(Note, ids["doomed"]) is not None
|
||||||
|
assert await s.get(Note, new_id) is None
|
||||||
|
|||||||
Reference in New Issue
Block a user