93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""Tests for nightly DB backups (T32).
|
|
|
|
The backup service is intentionally simple: a flat ``data/backups/`` dir
|
|
containing timestamped copies of ``chat.db``, with retention of the most
|
|
recent 14. The scheduling decision (``should_take_backup``) is a pure
|
|
function of clock + filesystem state so it can be unit-tested without
|
|
spinning up the BackgroundWorker tick loop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import patch
|
|
|
|
from chat.services.backup import (
|
|
prune_backups,
|
|
should_take_backup,
|
|
take_backup,
|
|
)
|
|
|
|
|
|
def test_take_backup_creates_timestamped_copy(tmp_path):
|
|
db = tmp_path / "chat.db"
|
|
db.write_text("fake db contents")
|
|
backup_path = take_backup(db_path=db, data_dir=tmp_path / "data")
|
|
assert backup_path.exists()
|
|
assert backup_path.name.startswith("chat-")
|
|
assert backup_path.name.endswith(".db")
|
|
# Contents copied
|
|
assert backup_path.read_text() == "fake db contents"
|
|
# Located in data/backups/
|
|
assert backup_path.parent == tmp_path / "data" / "backups"
|
|
|
|
|
|
def test_prune_keeps_last_14(tmp_path):
|
|
backup_dir = tmp_path / "data" / "backups"
|
|
backup_dir.mkdir(parents=True)
|
|
# Create 17 dummy backup files spanning days 1..17 of Jan 2026.
|
|
# Filenames sort lexicographically by the embedded timestamp, so
|
|
# prune_backups should drop the three oldest.
|
|
for i in range(1, 18):
|
|
(backup_dir / f"chat-202601{i:02d}T000000Z.db").write_text(
|
|
f"backup {i}"
|
|
)
|
|
removed = prune_backups(tmp_path / "data", keep=14)
|
|
assert removed == 3
|
|
remaining = sorted(backup_dir.glob("chat-*.db"))
|
|
assert len(remaining) == 14
|
|
# Days 1, 2, 3 removed; day 4 is now the oldest retained backup.
|
|
assert remaining[0].name == "chat-20260104T000000Z.db"
|
|
|
|
|
|
def test_should_take_backup_when_no_prior_and_target_hour_matches(tmp_path):
|
|
from chat.services import backup as backup_mod
|
|
|
|
class FakeDateTime(datetime):
|
|
@classmethod
|
|
def now(cls, tz=None):
|
|
return datetime(2026, 4, 26, 3, 0, 0)
|
|
|
|
with patch.object(backup_mod, "datetime", FakeDateTime):
|
|
assert should_take_backup(tmp_path / "data") is True
|
|
|
|
|
|
def test_should_not_take_backup_outside_target_hour(tmp_path):
|
|
from chat.services import backup as backup_mod
|
|
|
|
class FakeDateTime(datetime):
|
|
@classmethod
|
|
def now(cls, tz=None):
|
|
return datetime(2026, 4, 26, 14, 0, 0)
|
|
|
|
with patch.object(backup_mod, "datetime", FakeDateTime):
|
|
assert should_take_backup(tmp_path / "data") is False
|
|
|
|
|
|
def test_should_not_take_backup_when_recent_backup_exists(tmp_path):
|
|
backup_dir = tmp_path / "data" / "backups"
|
|
backup_dir.mkdir(parents=True)
|
|
recent = backup_dir / "chat-recent.db"
|
|
recent.write_text("x")
|
|
# mtime defaults to "now" — within the 23h freshness window so
|
|
# should_take_backup must return False even at the target hour.
|
|
from chat.services import backup as backup_mod
|
|
|
|
class FakeDateTime(datetime):
|
|
@classmethod
|
|
def now(cls, tz=None):
|
|
return datetime(2026, 4, 26, 3, 0, 0)
|
|
|
|
with patch.object(backup_mod, "datetime", FakeDateTime):
|
|
assert should_take_backup(tmp_path / "data") is False
|