mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 21:13:48 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
117 lines
4.3 KiB
Python
117 lines
4.3 KiB
Python
"""Tests for _backups: state.json snapshots taken before destructive commands."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from steam_backlog_enforcer import _backups as backups
|
|
from steam_backlog_enforcer import config
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
_OLD_ID = "20260101T000000Z-aaaaaa"
|
|
_NEW_ID = "20260202T000000Z-bbbbbb"
|
|
|
|
|
|
def _write_state(text: str = '{"current_app_id": 1}') -> None:
|
|
config.STATE_FILE.write_text(text, encoding="utf-8")
|
|
|
|
|
|
def _plant(
|
|
backup_id: str, *, state: str | None = '{"x": 1}', meta: str | None = None
|
|
) -> Path:
|
|
"""Create a backup directory by hand; ``None`` omits that file."""
|
|
directory = backups.BACKUPS_DIR / backup_id
|
|
directory.mkdir(parents=True)
|
|
if state is not None:
|
|
(directory / "state.json").write_text(state, encoding="utf-8")
|
|
if meta is not None:
|
|
(directory / "meta.json").write_text(meta, encoding="utf-8")
|
|
return directory
|
|
|
|
|
|
class TestBackupDir:
|
|
@pytest.mark.parametrize("bad", ["", "..", "../x", "20260101T000000Z", "a/b"])
|
|
def test_refuses_anything_that_is_not_an_id(self, bad: str) -> None:
|
|
with pytest.raises(backups.BackupError, match="Not a backup id"):
|
|
backups._backup_dir(bad)
|
|
|
|
def test_resolves_a_valid_id(self) -> None:
|
|
assert backups._backup_dir(_OLD_ID) == backups.BACKUPS_DIR / _OLD_ID
|
|
|
|
|
|
class TestCreateBackup:
|
|
def test_no_state_file_means_no_backup(self) -> None:
|
|
assert backups.create_backup("why") is None
|
|
assert not backups.BACKUPS_DIR.exists()
|
|
|
|
def test_copies_state_and_records_why(self) -> None:
|
|
_write_state()
|
|
backup = backups.create_backup("before reset")
|
|
assert backup is not None
|
|
assert backup.reason == "before reset"
|
|
assert backup.size_bytes == len('{"current_app_id": 1}')
|
|
copy = backups.BACKUPS_DIR / backup.id / "state.json"
|
|
assert copy.read_text(encoding="utf-8") == '{"current_app_id": 1}'
|
|
assert backup.to_json()["id"] == backup.id
|
|
|
|
|
|
class TestReadBackup:
|
|
def test_missing_meta_is_unreadable(self) -> None:
|
|
assert backups._read_backup(_plant(_OLD_ID)) is None
|
|
|
|
def test_corrupt_meta_is_unreadable(self) -> None:
|
|
assert backups._read_backup(_plant(_OLD_ID, meta="{nope")) is None
|
|
|
|
def test_missing_state_is_unreadable(self) -> None:
|
|
directory = _plant(_OLD_ID, state=None, meta="{}")
|
|
assert backups._read_backup(directory) is None
|
|
|
|
def test_absent_meta_fields_default_to_empty(self) -> None:
|
|
found = backups._read_backup(_plant(_OLD_ID, meta="{}"))
|
|
assert found is not None
|
|
assert (found.created_at, found.reason) == ("", "")
|
|
|
|
|
|
class TestListBackups:
|
|
def test_no_directory_is_empty(self) -> None:
|
|
assert backups.list_backups() == []
|
|
|
|
def test_newest_first_and_skips_junk(self) -> None:
|
|
_plant(_OLD_ID, meta='{"reason": "old"}')
|
|
_plant(_NEW_ID, meta='{"reason": "new"}')
|
|
_plant("20260303T000000Z-cccccc") # incomplete: no meta
|
|
(backups.BACKUPS_DIR / "not-an-id").mkdir()
|
|
assert [b.id for b in backups.list_backups()] == [_NEW_ID, _OLD_ID]
|
|
|
|
|
|
class TestRestoreBackup:
|
|
def test_replaces_state_and_backs_up_the_old_one(self) -> None:
|
|
_plant(_OLD_ID, state='{"current_app_id": 7}', meta="{}")
|
|
_write_state('{"current_app_id": 1}')
|
|
previous = backups.restore_backup(_OLD_ID)
|
|
assert json.loads(config.STATE_FILE.read_text("utf-8")) == {"current_app_id": 7}
|
|
assert previous is not None
|
|
assert previous.reason == f"before restoring {_OLD_ID}"
|
|
|
|
def test_restoring_over_no_state_has_no_previous(self) -> None:
|
|
_plant(_OLD_ID, state="{}", meta="{}")
|
|
assert backups.restore_backup(_OLD_ID) is None
|
|
assert config.STATE_FILE.read_text(encoding="utf-8") == "{}"
|
|
|
|
@pytest.mark.parametrize("state", [None, "{broken"])
|
|
def test_unreadable_backup_raises_and_keeps_state(self, state: str | None) -> None:
|
|
_plant(_OLD_ID, state=state, meta="{}")
|
|
_write_state()
|
|
with pytest.raises(backups.BackupError, match="unreadable"):
|
|
backups.restore_backup(_OLD_ID)
|
|
assert config.STATE_FILE.read_text(encoding="utf-8") == '{"current_app_id": 1}'
|
|
|
|
def test_malformed_id_raises(self) -> None:
|
|
with pytest.raises(backups.BackupError):
|
|
backups.restore_backup("../../etc")
|